update
This commit is contained in:
@@ -58,13 +58,14 @@ const unlockDate = computed(() => {
|
|||||||
:class="{ locked: !achievement.unlocked, rare: isRare }"
|
:class="{ locked: !achievement.unlocked, rare: isRare }"
|
||||||
:style="{ borderColor: RARITY_COLOR[achievement.rarity] }"
|
:style="{ borderColor: RARITY_COLOR[achievement.rarity] }"
|
||||||
>
|
>
|
||||||
<div class="row">
|
<n-thing>
|
||||||
<div class="icon">
|
<template #avatar>
|
||||||
<AchievementIcon :icon="achievement.icon" :size="32" />
|
<AchievementIcon :icon="achievement.icon" :size="32" />
|
||||||
</div>
|
</template>
|
||||||
<div class="body">
|
|
||||||
<div class="title">
|
<template #header>
|
||||||
<span class="name">{{ achievement.name }}</span>
|
<n-flex align="center" :size="8">
|
||||||
|
<n-text strong>{{ achievement.name }}</n-text>
|
||||||
<n-tag
|
<n-tag
|
||||||
size="tiny"
|
size="tiny"
|
||||||
:color="{
|
:color="{
|
||||||
@@ -74,80 +75,53 @@ const unlockDate = computed(() => {
|
|||||||
>
|
>
|
||||||
{{ RARITY_LABEL[achievement.rarity] }}
|
{{ RARITY_LABEL[achievement.rarity] }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
</div>
|
</n-flex>
|
||||||
<div class="desc">{{ achievement.description }}</div>
|
</template>
|
||||||
|
|
||||||
<div v-if="achievement.unlocked" class="meta">
|
<template #description>
|
||||||
<span>{{ unlockDate }}</span>
|
<n-text depth="3">{{ achievement.description }}</n-text>
|
||||||
<span class="rate">仅 {{ achievement.unlock_rate }}% 的人获得</span>
|
</template>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="showProgressBar" class="meta">
|
<n-flex align="center" :size="8" :wrap="false">
|
||||||
|
<template v-if="achievement.unlocked">
|
||||||
|
<n-text depth="3" class="nowrap">{{ unlockDate }}</n-text>
|
||||||
|
<n-text depth="3" class="nowrap">
|
||||||
|
仅 {{ achievement.unlock_rate }}% 的人获得
|
||||||
|
</n-text>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="showProgressBar">
|
||||||
<n-progress
|
<n-progress
|
||||||
|
style="flex: 1"
|
||||||
type="line"
|
type="line"
|
||||||
:percentage="percent"
|
:percentage="percent"
|
||||||
:height="6"
|
:height="6"
|
||||||
:show-indicator="false"
|
:show-indicator="false"
|
||||||
/>
|
/>
|
||||||
<span class="progress-text">
|
<n-text depth="3" class="nowrap">
|
||||||
{{ achievement.progress ?? 0 }} / {{ achievement.threshold }}
|
{{ achievement.progress ?? 0 }} / {{ achievement.threshold }}
|
||||||
</span>
|
</n-text>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<div v-else-if="showBestSoFar" class="meta">
|
<template v-else-if="showBestSoFar">
|
||||||
<span class="progress-text">
|
<n-text depth="3" class="nowrap">
|
||||||
目标 ≤ {{ achievement.threshold }}
|
目标 ≤ {{ achievement.threshold }}
|
||||||
</span>
|
</n-text>
|
||||||
<span v-if="achievement.progress !== null" class="rate">
|
<n-text v-if="achievement.progress !== null" depth="3" class="nowrap">
|
||||||
当前最好 {{ achievement.progress }}
|
当前最好 {{ achievement.progress }}
|
||||||
</span>
|
</n-text>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<div v-else class="meta">
|
<n-text v-else depth="3" class="nowrap">
|
||||||
<span class="rate">仅 {{ achievement.unlock_rate }}% 的人获得</span>
|
仅 {{ achievement.unlock_rate }}% 的人获得
|
||||||
</div>
|
</n-text>
|
||||||
</div>
|
</n-flex>
|
||||||
</div>
|
</n-thing>
|
||||||
</n-card>
|
</n-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.row {
|
.nowrap {
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.icon {
|
|
||||||
font-size: 32px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
.body {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.desc {
|
|
||||||
margin-top: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
.meta {
|
|
||||||
margin-top: 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.65;
|
|
||||||
}
|
|
||||||
.meta :deep(.n-progress) {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.progress-text {
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.locked {
|
.locked {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
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,
|
||||||
@@ -43,6 +44,7 @@ const rarities = computed(() =>
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
const badges = ref<UserBadge[]>([])
|
const badges = ref<UserBadge[]>([])
|
||||||
|
const { isDesktop } = useBreakpoints()
|
||||||
const tab = ref("all")
|
const tab = ref("all")
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
|
||||||
@@ -77,47 +79,48 @@ watch(name, load)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="hall">
|
<div class="hall">
|
||||||
<n-spin :show="loading">
|
<n-card v-if="summary">
|
||||||
<n-card v-if="summary" class="overview">
|
<!-- 窄屏也不折行:圆环缩小,间距收窄,抬头始终一行 -->
|
||||||
<div class="overview-row">
|
<n-flex align="center" :wrap="false" :size="isDesktop ? 32 : 16">
|
||||||
<div class="percent">
|
<n-flex vertical align="center" :size="6">
|
||||||
<n-progress
|
<n-progress
|
||||||
type="circle"
|
type="circle"
|
||||||
:percentage="summary.percent"
|
:percentage="summary.percent"
|
||||||
:stroke-width="8"
|
:stroke-width="8"
|
||||||
>
|
:style="{ width: isDesktop ? '110px' : '76px' }"
|
||||||
<div class="ring-text">
|
|
||||||
<div class="ring-percent">{{ summary.percent }}%</div>
|
|
||||||
</div>
|
|
||||||
</n-progress>
|
|
||||||
<div class="sub">
|
|
||||||
已获得 {{ summary.unlocked }} / {{ summary.total }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rarity">
|
|
||||||
<div v-for="r in rarities" :key="r.rarity" class="rarity-item">
|
|
||||||
<span
|
|
||||||
class="rarity-label"
|
|
||||||
:style="{ color: rarityColor[r.rarity] }"
|
|
||||||
>
|
|
||||||
{{ r.label }}
|
|
||||||
</span>
|
|
||||||
<span class="rarity-bar">
|
|
||||||
<span
|
|
||||||
class="rarity-fill"
|
|
||||||
:style="{
|
|
||||||
width: r.total ? (r.unlocked / r.total) * 100 + '%' : '0%',
|
|
||||||
background: rarityColor[r.rarity],
|
|
||||||
}"
|
|
||||||
/>
|
/>
|
||||||
</span>
|
<n-text depth="3" class="nowrap">
|
||||||
<span class="rarity-count">
|
已获得 {{ summary.unlocked }} / {{ summary.total }}
|
||||||
<b>{{ r.unlocked }}</b> / {{ r.total }}
|
</n-text>
|
||||||
</span>
|
</n-flex>
|
||||||
</div>
|
|
||||||
</div>
|
<n-flex vertical :size="8" class="rarity">
|
||||||
</div>
|
<n-flex
|
||||||
|
v-for="r in rarities"
|
||||||
|
:key="r.rarity"
|
||||||
|
align="center"
|
||||||
|
:wrap="false"
|
||||||
|
:size="10"
|
||||||
|
>
|
||||||
|
<n-text strong :style="{ color: rarityColor[r.rarity] }">
|
||||||
|
{{ r.label }}
|
||||||
|
</n-text>
|
||||||
|
<n-progress
|
||||||
|
style="flex: 1"
|
||||||
|
type="line"
|
||||||
|
:percentage="r.total ? (r.unlocked / r.total) * 100 : 0"
|
||||||
|
:height="6"
|
||||||
|
:border-radius="3"
|
||||||
|
:fill-border-radius="3"
|
||||||
|
:color="rarityColor[r.rarity]"
|
||||||
|
:show-indicator="false"
|
||||||
|
/>
|
||||||
|
<n-text depth="3" class="nowrap">
|
||||||
|
{{ r.unlocked }} / {{ r.total }}
|
||||||
|
</n-text>
|
||||||
|
</n-flex>
|
||||||
|
</n-flex>
|
||||||
|
</n-flex>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<n-tabs v-model:value="tab" type="line" class="tabs">
|
<n-tabs v-model:value="tab" type="line" class="tabs">
|
||||||
@@ -127,24 +130,47 @@ watch(name, load)
|
|||||||
<n-tab name="badges">题单奖章</n-tab>
|
<n-tab name="badges">题单奖章</n-tab>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
|
|
||||||
<div v-if="tab !== 'badges'" class="grid">
|
<template v-if="tab !== 'badges'">
|
||||||
<AchievementCard v-for="a in filtered" :key="a.id" :achievement="a" />
|
<n-grid
|
||||||
<n-empty v-if="!filtered.length" description="这里还什么都没有" />
|
v-if="filtered.length"
|
||||||
</div>
|
responsive="screen"
|
||||||
|
cols="1 s:2 l:3"
|
||||||
|
:x-gap="12"
|
||||||
|
:y-gap="12"
|
||||||
|
>
|
||||||
|
<n-gi v-for="a in filtered" :key="a.id">
|
||||||
|
<AchievementCard :achievement="a" />
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
<!-- 加载中不显示空态,不然首屏会闪一下"什么都没有" -->
|
||||||
|
<n-empty v-else-if="!loading" description="这里还什么都没有" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<div v-else class="grid">
|
<template v-else>
|
||||||
<n-card v-for="b in badges" :key="b.id" size="small">
|
<n-grid
|
||||||
<div class="badge-row">
|
v-if="badges.length"
|
||||||
<img v-if="b.badge?.icon" :src="b.badge.icon" class="badge-icon" />
|
responsive="screen"
|
||||||
<div>
|
cols="1 s:2 l:3"
|
||||||
<div class="badge-name">{{ b.badge?.name }}</div>
|
:x-gap="12"
|
||||||
<div class="badge-desc">{{ b.badge?.description }}</div>
|
:y-gap="12"
|
||||||
</div>
|
>
|
||||||
</div>
|
<n-gi v-for="b in badges" :key="b.id">
|
||||||
|
<n-card size="small">
|
||||||
|
<n-thing :title="b.badge?.name" :description="b.badge?.description">
|
||||||
|
<template #avatar v-if="b.badge?.icon">
|
||||||
|
<n-avatar
|
||||||
|
:size="40"
|
||||||
|
:src="b.badge.icon"
|
||||||
|
color="transparent"
|
||||||
|
object-fit="contain"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</n-thing>
|
||||||
</n-card>
|
</n-card>
|
||||||
<n-empty v-if="!badges.length" description="还没有获得任何题单奖章" />
|
</n-gi>
|
||||||
</div>
|
</n-grid>
|
||||||
</n-spin>
|
<n-empty v-else-if="!loading" description="还没有获得任何题单奖章" />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -154,135 +180,15 @@ watch(name, load)
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
.overview-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 32px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.percent {
|
|
||||||
flex: none;
|
|
||||||
width: 110px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.percent :deep(.n-progress) {
|
|
||||||
width: 110px;
|
|
||||||
}
|
|
||||||
.ring-percent {
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
.sub {
|
|
||||||
margin-top: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.65;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.rarity {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 240px;
|
|
||||||
/* 卡片有 1100px 宽,不封顶的话 6px 高的细条会被拉到近 900px,读起来很空 */
|
/* 卡片有 1100px 宽,不封顶的话 6px 高的细条会被拉到近 900px,读起来很空 */
|
||||||
max-width: 420px;
|
|
||||||
display: grid;
|
|
||||||
/* 固定单列,四条各占一行:折成两列会让进度条短得看不出差别 */
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.rarity-item {
|
|
||||||
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;
|
|
||||||
min-width: 40px;
|
|
||||||
border-radius: 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: rgba(128, 128, 128, 0.2); /* 灰底在明暗两套主题下都成立 */
|
|
||||||
}
|
|
||||||
.rarity-fill {
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 3px;
|
|
||||||
transition: width 0.4s ease;
|
|
||||||
}
|
|
||||||
.rarity-count {
|
|
||||||
flex: none;
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.7;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
.rarity-count b {
|
|
||||||
font-size: 13px;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
/* 移动端抬头挤成一行:圆环缩小、间距收窄,稀有度条不再被挤到第二行 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.overview-row {
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
.percent {
|
|
||||||
/* 宽度交给底下那行"已获得 x / y",圆环缩小后不至于把文字挤出去 */
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
.percent :deep(.n-progress) {
|
|
||||||
width: 76px;
|
|
||||||
}
|
|
||||||
.ring-percent {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.sub {
|
|
||||||
margin-top: 6px;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
.rarity {
|
.rarity {
|
||||||
min-width: 0;
|
flex: 1;
|
||||||
|
max-width: 420px;
|
||||||
}
|
}
|
||||||
.rarity-label {
|
.nowrap {
|
||||||
width: auto;
|
white-space: nowrap;
|
||||||
font-size: 12px;
|
|
||||||
}
|
}
|
||||||
.rarity-count {
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
.rarity-count b {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
}
|
}
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
/* min() 兜住窄屏:不加的话 300px 的下限会把 320 宽的手机撑出横向滚动 */
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.badge-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.badge-icon {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
.badge-name {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.badge-desc {
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user