Compare commits

...

2 Commits

Author SHA1 Message Date
8cfa90ca16 feat(achievement): 奖章标出来自哪个题单 + 进度条动画
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled
奖章卡片只有名字和描述,看不出是哪个题单发的,补上题单名并链到详情页。
进度是异步加载的,卡片挂载时百分比已是终值,naive 自带的过渡触发不了,
先按 0 渲染一帧再填真值,同时把时长从 .3s/.2s 拉到 .9s,
圆环数字换成 n-number-animation 同步滚动。
2026-08-05 13:52:58 -06:00
4b796650ff fix(achievement): 拉开青铜和黄金的配色
青铜和黄金原来都落在橙棕区(色相 30 / 44),扫一眼分不出来。
青铜往红铜偏、黄金往纯黄推,边框色和明暗两套文字色一起调整,
对比度全部保持 4.5:1 以上。顺带修浅色模式白金只有 4.10:1。
2026-08-05 13:52:58 -06:00
2 changed files with 85 additions and 11 deletions

View File

@@ -19,6 +19,11 @@ interface UserBadge {
description: string
icon: string
}
// 奖章来自哪个题单,接口在 UserBadgeSerializer 里带出来
problemset: {
id: number
title: string
} | null
}
const route = useRoute()
@@ -43,6 +48,17 @@ const rarities = computed(() =>
(a, b) => RARITY_RANK[a.rarity] - RARITY_RANK[b.rarity],
),
)
// 数据是异步来的卡片挂上去时百分比就是终值CSS 过渡不会触发。
// 先按 0 渲染一帧,下一帧再填真值,圆环和进度条才有"长出来"的动画
const grown = ref(false)
const percent = computed(() =>
grown.value ? (summary.value?.percent ?? 0) : 0,
)
function rarityPercent(r: { unlocked: number; total: number }) {
if (!grown.value || !r.total) return 0
return (r.unlocked / r.total) * 100
}
const badges = ref<UserBadge[]>([])
const { isDesktop } = useBreakpoints()
const tab = ref("all")
@@ -58,6 +74,7 @@ const filtered = computed(() => {
async function load() {
loading.value = true
grown.value = false
try {
const [list, sum, badgeRes] = await Promise.all([
getAchievements(name.value),
@@ -71,6 +88,9 @@ async function load() {
} finally {
loading.value = false
}
// 等卡片以 0 渲染完这一帧,再切到真值
await nextTick()
requestAnimationFrame(() => requestAnimationFrame(() => (grown.value = true)))
}
onMounted(load)
@@ -84,10 +104,17 @@ watch(name, load)
<n-flex vertical align="center" :size="6">
<n-progress
type="circle"
:percentage="summary.percent"
:percentage="percent"
:stroke-width="8"
class="grow"
>
<n-text strong>{{ summary.percent }}%</n-text>
<n-text strong>
<n-number-animation
:from="0"
:to="summary.percent"
:duration="900"
/>%
</n-text>
</n-progress>
<n-text depth="3" class="nowrap">
已获得 {{ summary.unlocked }} / {{ summary.total }}
@@ -107,8 +134,9 @@ watch(name, load)
</n-text>
<n-progress
style="flex: 1"
class="grow"
type="line"
:percentage="r.total ? (r.unlocked / r.total) * 100 : 0"
:percentage="rarityPercent(r)"
:height="6"
:border-radius="3"
:fill-border-radius="3"
@@ -165,6 +193,17 @@ watch(name, load)
object-fit="contain"
/>
</template>
<n-text v-if="b.problemset" depth="3" class="source">
来自题单
<router-link
:to="{
name: 'problemset',
params: { problemSetId: b.problemset.id },
}"
>
{{ b.problemset.title }}
</router-link>
</n-text>
</n-thing>
</n-card>
</n-gi>
@@ -190,4 +229,37 @@ watch(name, load)
.tabs {
margin: 16px 0;
}
.source {
display: block;
margin-top: 6px;
font-size: 13px;
}
.source a {
color: inherit;
text-decoration: underline;
text-underline-offset: 2px;
}
.source a:hover {
color: var(--n-text-color);
}
/* naive 自带过渡,但圆环 stroke-dasharray 只有 .3s、线条 max-width 只有 .2s
配合 0 → 真值那一下太快看不出来,这里拉长 */
.grow :deep(.n-progress-graph-circle-fill) {
transition:
opacity 0.3s var(--n-bezier),
stroke 0.3s var(--n-bezier),
stroke-dasharray 0.9s cubic-bezier(0.25, 1, 0.5, 1);
}
.grow :deep(.n-progress-graph-line-fill) {
transition:
background-color 0.3s var(--n-bezier),
max-width 0.9s cubic-bezier(0.25, 1, 0.5, 1);
}
@media (prefers-reduced-motion: reduce) {
.grow :deep(.n-progress-graph-circle-fill),
.grow :deep(.n-progress-graph-line-fill) {
transition: none;
}
}
</style>

View File

@@ -284,11 +284,13 @@ export const RARITY_LABEL: Record<AchievementRarity, string> = {
platinum: "白金",
}
// 边框、色块用的原色:饱和度够高,明暗底上都醒目
// 边框、色块用的原色:饱和度够高,明暗底上都醒目
// 青铜往红偏(色相 23黄金往纯黄推色相 47中间隔开两档亮度
// 不然两个都落在橙棕区,扫一眼分不出来
export const RARITY_COLOR: Record<AchievementRarity, string> = {
bronze: "#b87333",
bronze: "#c2703d",
silver: "#9fa6b2",
gold: "#e0a300",
gold: "#f2c012",
platinum: "#7dd3fc",
}
@@ -299,16 +301,16 @@ export const RARITY_TEXT_COLOR: Record<
Record<AchievementRarity, string>
> = {
dark: {
bronze: "#d18d4f",
bronze: "#e08d63",
silver: "#b6bcc7",
gold: "#e0a300",
gold: "#f2c012",
platinum: "#7dd3fc",
},
light: {
bronze: "#9a5b25",
bronze: "#a13d1e",
silver: "#6b7280",
gold: "#a37500",
platinum: "#0284c7",
gold: "#8f6f00",
platinum: "#0369a1",
},
}