Files
OJ2/apps/web/src/oj/problemset/components/BadgeLadder.vue
T
xuyueandClaude Opus 5.5 c17f88cdbb
Deploy / deploy (push) Canceled after 0s
feat(题单): 题单页重做——布置中的放最上面,详情顶上一条进度和奖章阶梯,没加入看得见题点不进;老师看全班、后台一屏编辑
- 列表:布置中的大卡片(做对几道、布置到哪天、再做对几道拿哪个奖章),其余小卡片,加入过的在前
- 详情:进度 + 奖章阶梯(下一个金圈「再做对 n 道」),题目两列写几点做对 / 交过几次;
  以前做对过的标「旧代码先藏着」;做完整条变绿,烟花只在做完那一刻放
- 老师多一个「全班情况」页签;题目页从链接直接进来没加入时,条上给「加入题单」
- 后台一屏编辑:布置一周 / 两周 / 四周 / 选日期,公开开关,粘贴多个题号一次加,拖动排序,奖章只剩两种条件
- 旧代码藏着的地方都说清楚是哪个题单、哪天之后能看
- 修掉列表卡片把选做题数说成「新加了 N 题」

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:01:39 +08:00

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>