revert(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

PSN 那版奖杯陈列观感不佳,回退到环形 + 四色进度条。稀有度顺序
沿用 PSN 的做法:白金、黄金、白银、青铜,稀有的排前面。

顺带保留卡片网格的 min() 修复,避免 320 宽机型横向滚动。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 11:40:55 -06:00
parent 4072abe199
commit 05ce9cbb9e

View File

@@ -1,8 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from "@iconify/vue"
import { getAchievements, getAchievementSummary } from "oj/achievement/api" import { getAchievements, getAchievementSummary } from "oj/achievement/api"
import { getUserBadges } from "oj/api" import { getUserBadges } from "oj/api"
import { useBreakpoints } from "shared/composables/breakpoints"
import { useRarityColor } from "shared/composables/rarity" import { useRarityColor } from "shared/composables/rarity"
import type { import type {
Achievement, Achievement,
@@ -25,19 +23,13 @@ interface UserBadge {
const route = useRoute() const route = useRoute()
const name = computed(() => (route.query.name as string) || undefined) const name = computed(() => (route.query.name as string) || undefined)
// 断点走全站那套tailwind md = 768px不另开一套 // 标签和进度条同色,整行读作一个单位
const { isMobile } = useBreakpoints() const rarityColor = useRarityColor()
const achievements = ref<Achievement[]>([]) const achievements = ref<Achievement[]>([])
const summary = ref<AchievementSummary | null>(null) const summary = ref<AchievementSummary | null>(null)
const badges = ref<UserBadge[]>([])
const tab = ref("all")
const loading = ref(true)
// 奖杯和数字同色,一列读作一个单位 // 白金排最前,青铜垫底:稀有的先亮相,接口给的顺序是反的
const rarityColor = useRarityColor()
// 白金在最左,青铜在最右——奖杯陈列按稀有度倒序,接口给的顺序反过来
const RARITY_RANK: Record<AchievementRarity, number> = { const RARITY_RANK: Record<AchievementRarity, number> = {
platinum: 0, platinum: 0,
gold: 1, gold: 1,
@@ -45,23 +37,14 @@ const RARITY_RANK: Record<AchievementRarity, number> = {
bronze: 3, bronze: 3,
} }
const trophies = computed(() => const rarities = computed(() =>
[...(summary.value?.rarity ?? [])].sort( [...(summary.value?.rarity ?? [])].sort(
(a, b) => RARITY_RANK[a.rarity] - RARITY_RANK[b.rarity], (a, b) => RARITY_RANK[a.rarity] - RARITY_RANK[b.rarity],
), ),
) )
const badges = ref<UserBadge[]>([])
// 进度条按稀有度分段,每段宽度是该档已获得占总数的比例, const tab = ref("all")
// 拼起来正好是总完成度:条形的长度和构成都是真的 const loading = ref(true)
function segmentWidth(unlocked: number) {
const total = summary.value?.total ?? 0
return total ? `${(unlocked / total) * 100}%` : "0%"
}
const remaining = computed(() => {
if (!summary.value) return 0
return summary.value.total - summary.value.unlocked
})
const filtered = computed(() => { const filtered = computed(() => {
if (tab.value === "unlocked") if (tab.value === "unlocked")
@@ -93,58 +76,47 @@ watch(name, load)
</script> </script>
<template> <template>
<div class="hall" :class="{ mobile: isMobile }"> <div class="hall">
<n-spin :show="loading"> <n-spin :show="loading">
<n-card v-if="summary" class="overview"> <n-card v-if="summary" class="overview">
<div class="overview-row"> <div class="overview-row">
<div class="level"> <div class="percent">
<div class="plate"> <n-progress
<span class="plate-num">{{ summary.percent }}</span> type="circle"
<span class="plate-unit">%</span> :percentage="summary.percent"
</div> :stroke-width="8"
<div class="plate-label">完成度</div>
</div>
<div class="trophies">
<div
v-for="r in trophies"
:key="r.rarity"
class="trophy"
:class="{ none: !r.unlocked }"
> >
<Icon <div class="ring-text">
icon="mdi:trophy" <div class="ring-percent">{{ summary.percent }}%</div>
:width="30"
:height="30"
:style="{ color: rarityColor[r.rarity] }"
/>
<div class="trophy-count">
<b :style="{ color: rarityColor[r.rarity] }">{{
r.unlocked
}}</b>
<span>/ {{ r.total }}</span>
</div> </div>
<div class="trophy-label">{{ r.label }}</div> </n-progress>
<div class="sub">
已获得 {{ summary.unlocked }} / {{ summary.total }}
</div> </div>
</div> </div>
</div>
<div class="track"> <div class="rarity">
<span <div v-for="r in rarities" :key="r.rarity" class="rarity-item">
v-for="r in trophies" <span
:key="r.rarity" class="rarity-label"
class="seg" :style="{ color: rarityColor[r.rarity] }"
:style="{ >
width: segmentWidth(r.unlocked), {{ r.label }}
background: rarityColor[r.rarity], </span>
}" <span class="rarity-bar">
/> <span
</div> class="rarity-fill"
:style="{
<div class="foot"> width: r.total ? (r.unlocked / r.total) * 100 + '%' : '0%',
<span>已获得 {{ summary.unlocked }} / {{ summary.total }}</span> background: rarityColor[r.rarity],
<span v-if="remaining">还差 {{ remaining }} 个全收集</span> }"
<span v-else class="done">全部获得</span> />
</span>
<span class="rarity-count">
<b>{{ r.unlocked }}</b> / {{ r.total }}
</span>
</div>
</div>
</div> </div>
</n-card> </n-card>
@@ -185,156 +157,71 @@ watch(name, load)
.overview-row { .overview-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 28px; gap: 32px;
flex-wrap: wrap; flex-wrap: wrap;
} }
.percent {
/* 等级铭牌:六边形靠 clip-path老 Chrome 也支持 */
.level {
flex: none; flex: none;
width: 110px;
text-align: center; text-align: center;
} }
.plate { .percent :deep(.n-progress) {
width: 84px; width: 110px;
height: 92px;
display: flex;
align-items: baseline;
justify-content: center;
clip-path: polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%);
background: linear-gradient(
180deg,
rgba(128, 128, 128, 0.24),
rgba(128, 128, 128, 0.1)
);
} }
.plate-num { .ring-percent {
font-size: 26px;
font-weight: 700;
line-height: 92px;
font-variant-numeric: tabular-nums;
}
.plate-unit {
font-size: 13px;
opacity: 0.6;
margin-left: 1px;
}
.plate-label {
margin-top: 6px;
font-size: 11px;
letter-spacing: 3px;
opacity: 0.55;
padding-left: 3px; /* 抵消字距在末字后面留的空 */
}
/* 奖杯陈列 */
.trophies {
flex: 1;
min-width: 260px;
display: flex;
justify-content: space-around;
gap: 8px;
}
.trophy {
text-align: center;
min-width: 56px;
}
.trophy.none {
opacity: 0.35;
filter: grayscale(1);
}
.trophy-count {
margin-top: 4px;
font-variant-numeric: tabular-nums;
line-height: 1.1;
}
.trophy-count b {
font-size: 22px; font-size: 22px;
font-weight: 700; font-weight: 700;
line-height: 1;
} }
.trophy-count span { .sub {
margin-top: 8px;
font-size: 12px; font-size: 12px;
opacity: 0.5; opacity: 0.65;
margin-left: 3px; white-space: nowrap;
} }
.trophy-label { .rarity {
margin-top: 2px; flex: 1;
font-size: 11px; min-width: 240px;
letter-spacing: 2px; display: grid;
opacity: 0.55; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
padding-left: 2px; gap: 10px 28px;
} }
.rarity-item {
/* 总进度条:四段拼起来正好是总完成度 */
.track {
display: flex; display: flex;
align-items: center;
gap: 10px;
}
.rarity-label {
flex: none;
width: 30px;
font-size: 13px;
font-weight: 600;
}
.rarity-bar {
flex: 1;
height: 6px; height: 6px;
margin-top: 20px; min-width: 40px;
border-radius: 3px; border-radius: 3px;
overflow: hidden; overflow: hidden;
background: rgba(128, 128, 128, 0.2); /* 灰底在明暗两套主题下都成立 */ background: rgba(128, 128, 128, 0.2); /* 灰底在明暗两套主题下都成立 */
} }
.seg { .rarity-fill {
display: block;
height: 100%; height: 100%;
transition: width 0.5s ease; border-radius: 3px;
transition: width 0.4s ease;
} }
.foot { .rarity-count {
display: flex; flex: none;
justify-content: space-between;
margin-top: 8px;
font-size: 12px; font-size: 12px;
opacity: 0.6; opacity: 0.7;
white-space: nowrap;
font-variant-numeric: tabular-nums;
} }
.done { .rarity-count b {
font-size: 13px;
opacity: 0.9; opacity: 0.9;
} }
/* 移动端改成竖排:铭牌居中一行,四个奖杯等分下面一行 */
.mobile .overview-row {
flex-direction: column;
align-items: center;
gap: 14px;
}
.mobile .plate {
width: 72px;
height: 80px;
}
.mobile .plate-num {
font-size: 22px;
line-height: 80px;
}
.mobile .trophies {
width: 100%;
min-width: 0;
gap: 4px;
}
.mobile .trophy {
flex: 1 1 0;
min-width: 0;
}
.mobile .trophy svg {
width: 26px;
height: 26px;
}
.mobile .trophy-count b {
font-size: 19px;
}
.mobile .trophy-label {
letter-spacing: 1px;
padding-left: 1px;
}
.mobile .track {
margin-top: 16px;
}
.mobile .foot {
font-size: 11px;
}
/* 这个留给系统偏好,和断点无关 */
@media (prefers-reduced-motion: reduce) {
.seg {
transition: none;
}
}
.tabs { .tabs {
margin: 16px 0; margin: 16px 0;
} }