Deploy / deploy (push) Canceled after 0s
- 列表:布置中的大卡片(做对几道、布置到哪天、再做对几道拿哪个奖章),其余小卡片,加入过的在前 - 详情:进度 + 奖章阶梯(下一个金圈「再做对 n 道」),题目两列写几点做对 / 交过几次; 以前做对过的标「旧代码先藏着」;做完整条变绿,烟花只在做完那一刻放 - 老师多一个「全班情况」页签;题目页从链接直接进来没加入时,条上给「加入题单」 - 后台一屏编辑:布置一周 / 两周 / 四周 / 选日期,公开开关,粘贴多个题号一次加,拖动排序,奖章只剩两种条件 - 旧代码藏着的地方都说清楚是哪个题单、哪天之后能看 - 修掉列表卡片把选做题数说成「新加了 N 题」 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
144 lines
3.3 KiB
Vue
144 lines
3.3 KiB
Vue
<script setup lang="ts">
|
|
import { useThemeVars } from "naive-ui"
|
|
import { useTone } from "oj/submission/composables/tone"
|
|
import { parseTime } from "utils/functions"
|
|
import type { ProblemSet } from "utils/types"
|
|
import { badgeCondition, badgeShortfall, ladder, nextBadge } from "../badges"
|
|
|
|
/**
|
|
* 奖章阶梯(设计稿「题单重设计」详情 B):一排奖章连成一条线,拿到的亮、写哪天拿的;
|
|
* 下一个要拿的套金圈、写「再做对 n 道」;更远的灰着、写条件。
|
|
*/
|
|
const props = defineProps<{ set: ProblemSet }>()
|
|
const theme = useThemeVars()
|
|
const tone = useTone()
|
|
|
|
const steps = computed(() => {
|
|
const next = nextBadge(props.set)?.badge.id
|
|
return ladder(props.set).map((badge) => ({
|
|
badge,
|
|
next: badge.id === next,
|
|
sub: badge.isEarned
|
|
? badge.earnedTime
|
|
? parseTime(badge.earnedTime, "M月D日")
|
|
: "拿到了"
|
|
: badge.id === next && props.set.userProgress.isJoined
|
|
? `再做对 ${badgeShortfall(badge, props.set)} 道`
|
|
: badgeCondition(badge),
|
|
}))
|
|
})
|
|
|
|
/** 连线亮到最后一枚拿到的奖章那里 */
|
|
const filled = computed(() => {
|
|
const last = steps.value.map((step) => step.badge.isEarned).lastIndexOf(true)
|
|
return steps.value.length > 1 && last > 0 ? last / (steps.value.length - 1) : 0
|
|
})
|
|
|
|
const warning = computed(() => tone("warning"))
|
|
</script>
|
|
|
|
<template>
|
|
<div class="ladder" :style="{ width: `${steps.length * 92}px` }">
|
|
<div v-if="steps.length > 1" class="track">
|
|
<div class="track-fill" :style="{ width: `${filled * 100}%` }"></div>
|
|
</div>
|
|
<div
|
|
v-for="step in steps"
|
|
:key="step.badge.id"
|
|
class="step"
|
|
:title="`${step.badge.name} · ${badgeCondition(step.badge)}${step.badge.description ? `\n${step.badge.description}` : ''}`"
|
|
>
|
|
<span class="medal" :class="{ next: step.next }">
|
|
<img
|
|
:src="step.badge.icon"
|
|
:alt="step.badge.name"
|
|
:class="{ locked: !step.badge.isEarned }"
|
|
/>
|
|
</span>
|
|
<b class="name" :class="{ dim: !step.badge.isEarned }">{{ step.badge.name }}</b>
|
|
<span class="sub" :class="{ urge: step.next && !step.badge.isEarned }">{{ step.sub }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.ladder {
|
|
position: relative;
|
|
display: flex;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.track {
|
|
position: absolute;
|
|
left: 46px;
|
|
right: 46px;
|
|
top: 21px;
|
|
height: 3px;
|
|
background: v-bind("theme.dividerColor");
|
|
}
|
|
|
|
.track-fill {
|
|
height: 100%;
|
|
background: v-bind("theme.successColor");
|
|
}
|
|
|
|
.step {
|
|
position: relative;
|
|
width: 92px;
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 3px;
|
|
}
|
|
|
|
.medal {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 50%;
|
|
background: v-bind("theme.cardColor");
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.medal.next {
|
|
box-shadow: 0 0 0 2px #f0b44c;
|
|
}
|
|
|
|
.medal img {
|
|
width: 36px;
|
|
height: 36px;
|
|
}
|
|
|
|
.locked {
|
|
filter: grayscale(1);
|
|
opacity: 0.35;
|
|
}
|
|
|
|
.name {
|
|
max-width: 88px;
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.name.dim {
|
|
color: v-bind("theme.textColor3");
|
|
font-weight: 400;
|
|
}
|
|
|
|
.sub {
|
|
font-size: 11px;
|
|
font-variant-numeric: tabular-nums;
|
|
color: v-bind("theme.textColor3");
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.sub.urge {
|
|
color: v-bind("warning.color");
|
|
font-weight: 600;
|
|
}
|
|
</style>
|