revert(achievement): 抬头回到环形进度版,保留白金优先的排序
PSN 那版奖杯陈列观感不佳,回退到环形 + 四色进度条。稀有度顺序 沿用 PSN 的做法:白金、黄金、白银、青铜,稀有的排前面。 顺带保留卡片网格的 min() 修复,避免 320 宽机型横向滚动。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user