Compare commits

..

3 Commits

Author SHA1 Message Date
0d7336bce8 refactor(achievement): 稀有度配色收进 constants,文字色适配浅色模式
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
四份重复的 RARITY_COLOR / RARITY_LABEL 合并到 utils/constants。
拆成两套:边框色块继续用饱和的原色,文字改走 useRarityColor,
按明暗主题切换,两套都压到 4.5:1 以上——原色是照深色底调的,
白底上白金只有 1.7:1、黄金 2.2:1,卡片上的稀有度 tag 看不清。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 11:24:04 -06:00
4db0dc7e83 fix(achievement): 抬头稀有度配色适配浅色模式
原色值是照深色底调的,白底上白金 1.7:1、黄金 2.2:1 看不清。
改成按 useDark 切换明暗两套色值。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 11:21:03 -06:00
56d06eb456 style(achievement): 成就页抬头改为环形进度 + 四色稀有度条
左侧环形显示总完成度,右侧四行稀有度各配一条对应颜色的进度条,
配色复用 AchievementCard 的 RARITY_COLOR。窄屏自动塌成一列。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 11:19:57 -06:00
7 changed files with 149 additions and 56 deletions

View File

@@ -7,6 +7,12 @@ import {
type MetricOption,
} from "admin/api"
import AchievementIcon from "shared/components/AchievementIcon.vue"
import { RARITY_LABEL } from "utils/constants"
const rarityOptions = Object.entries(RARITY_LABEL).map(([value, label]) => ({
label,
value,
}))
const props = defineProps<{
show: boolean
@@ -118,15 +124,7 @@ async function save() {
</n-flex>
</n-form-item>
<n-form-item label="稀有度">
<n-select
v-model:value="form.rarity"
:options="[
{ label: '青铜', value: 'bronze' },
{ label: '白银', value: 'silver' },
{ label: '黄金', value: 'gold' },
{ label: '白金', value: 'platinum' },
]"
/>
<n-select v-model:value="form.rarity" :options="rarityOptions" />
</n-form-item>
<n-form-item label="指标" required>
<n-select

View File

@@ -7,6 +7,7 @@ import {
} from "admin/api"
import AchievementIcon from "shared/components/AchievementIcon.vue"
import AchievementModal from "./components/AchievementModal.vue"
import { RARITY_LABEL } from "utils/constants"
const message = useMessage()
const dialog = useDialog()
@@ -16,13 +17,6 @@ const loading = ref(false)
const showModal = ref(false)
const editing = ref<AdminAchievement | null>(null)
const RARITY_LABEL: Record<string, string> = {
bronze: "青铜",
silver: "白银",
gold: "黄金",
platinum: "白金",
}
// 下架的只在有的时候才提,全部上架时标题不啰嗦
const title = computed(() => {
if (!list.value.length) return "成就管理"

View File

@@ -1,22 +1,13 @@
<script setup lang="ts">
import AchievementIcon from "shared/components/AchievementIcon.vue"
import { useRarityColor } from "shared/composables/rarity"
import { RARITY_COLOR, RARITY_LABEL } from "utils/constants"
import type { Achievement } from "utils/types"
const props = defineProps<{ achievement: Achievement }>()
const RARITY_COLOR: Record<string, string> = {
bronze: "#b87333",
silver: "#9fa6b2",
gold: "#e0a300",
platinum: "#7dd3fc",
}
const RARITY_LABEL: Record<string, string> = {
bronze: "青铜",
silver: "白银",
gold: "黄金",
platinum: "白金",
}
// 边框用原色tag 里的文字用跟主题走的那套
const rarityTextColor = useRarityColor()
// 隐藏且未解锁:后端已把名称/描述/图标和条件三件套都遮成 ??? 和 null
// 这里只负责不要把 null 渲染出来,也不要画出会泄露门槛的进度条
@@ -78,7 +69,7 @@ const unlockDate = computed(() => {
size="tiny"
:color="{
borderColor: RARITY_COLOR[achievement.rarity],
textColor: RARITY_COLOR[achievement.rarity],
textColor: rarityTextColor[achievement.rarity],
}"
>
{{ RARITY_LABEL[achievement.rarity] }}

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { getAchievements, getAchievementSummary } from "oj/achievement/api"
import { getUserBadges } from "oj/api"
import { useRarityColor } from "shared/composables/rarity"
import type { Achievement, AchievementSummary } from "utils/types"
import AchievementCard from "./components/AchievementCard.vue"
@@ -18,6 +19,9 @@ interface UserBadge {
const route = useRoute()
const name = computed(() => (route.query.name as string) || undefined)
// 标签和进度条同色,整行读作一个单位
const rarityColor = useRarityColor()
const achievements = ref<Achievement[]>([])
const summary = ref<AchievementSummary | null>(null)
const badges = ref<UserBadge[]>([])
@@ -59,19 +63,44 @@ watch(name, load)
<n-card v-if="summary" class="overview">
<div class="overview-row">
<div class="percent">
<div class="big">{{ summary.percent }}%</div>
<n-progress
type="circle"
:percentage="summary.percent"
:stroke-width="8"
>
<div class="ring-text">
<div class="ring-percent">{{ summary.percent }}%</div>
</div>
</n-progress>
<div class="sub">
{{ summary.unlocked }} / {{ summary.total }} 已获得
已获得 {{ summary.unlocked }} / {{ summary.total }}
</div>
</div>
<div class="rarity">
<div
v-for="r in summary.rarity"
:key="r.rarity"
class="rarity-item"
>
<div class="rarity-label">{{ r.label }}</div>
<div class="rarity-count">{{ r.unlocked }} / {{ r.total }}</div>
<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>
<span class="rarity-count">
<b>{{ r.unlocked }}</b> / {{ r.total }}
</span>
</div>
</div>
</div>
@@ -114,34 +143,70 @@ watch(name, load)
.overview-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
gap: 32px;
flex-wrap: wrap;
}
.big {
font-size: 40px;
.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: 6px;
font-size: 13px;
opacity: 0.7;
margin-top: 8px;
font-size: 12px;
opacity: 0.65;
white-space: nowrap;
}
.rarity {
display: flex;
gap: 20px;
flex: 1;
min-width: 240px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 10px 28px;
}
.rarity-item {
text-align: center;
display: flex;
align-items: center;
gap: 10px;
}
.rarity-label {
font-size: 12px;
opacity: 0.7;
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 {
font-weight: 600;
margin-top: 2px;
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;
}
.tabs {
margin: 16px 0;

View File

@@ -1,18 +1,12 @@
<script setup lang="ts">
import AchievementIcon from "shared/components/AchievementIcon.vue"
import { useAchievementStore } from "shared/store/achievement"
import { RARITY_COLOR } from "utils/constants"
const store = useAchievementStore()
const { current, queue } = storeToRefs(store)
const visible = ref(false)
const RARITY_COLOR: Record<string, string> = {
bronze: "#b87333",
silver: "#9fa6b2",
gold: "#e0a300",
platinum: "#7dd3fc",
}
let timer: ReturnType<typeof setTimeout> | null = null
let gapTimer: ReturnType<typeof setTimeout> | null = null

View File

@@ -0,0 +1,15 @@
import { useDark } from "@vueuse/core"
import { computed } from "vue"
import { RARITY_TEXT_COLOR } from "utils/constants"
/**
* 成就稀有度的文字配色,跟随明暗主题切换。
* 两套色值都压到 4.5:1 以上,不然浅色模式下白金和黄金的小字看不清。
* 边框和色块不用这个,直接用 RARITY_COLOR 的原色。
*/
export function useRarityColor() {
const isDark = useDark()
return computed(() =>
isDark.value ? RARITY_TEXT_COLOR.dark : RARITY_TEXT_COLOR.light,
)
}

View File

@@ -1,4 +1,4 @@
import type { SUBMISSION_RESULT } from "./types"
import type { AchievementRarity, SUBMISSION_RESULT } from "./types"
export enum SubmissionStatus {
compile_error = -2,
@@ -276,6 +276,42 @@ export enum ChartType {
Activity,
}
// 成就稀有度
export const RARITY_LABEL: Record<AchievementRarity, string> = {
bronze: "青铜",
silver: "白银",
gold: "黄金",
platinum: "白金",
}
// 边框、色块用的原色:饱和度够高,明暗底上都醒目
export const RARITY_COLOR: Record<AchievementRarity, string> = {
bronze: "#b87333",
silver: "#9fa6b2",
gold: "#e0a300",
platinum: "#7dd3fc",
}
// 文字要另配一套。上面那组是照深色底调的,搬到白底上白金只有 1.7:1、
// 黄金 2.2:112px 的稀有度标签根本看不清。取色见 useRarityColor
export const RARITY_TEXT_COLOR: Record<
"dark" | "light",
Record<AchievementRarity, string>
> = {
dark: {
bronze: "#d18d4f",
silver: "#b6bcc7",
gold: "#e0a300",
platinum: "#7dd3fc",
},
light: {
bronze: "#9a5b25",
silver: "#6b7280",
gold: "#a37500",
platinum: "#0284c7",
},
}
// 时间范围配置
export const DURATION_OPTIONS = [
{ label: "本节课内", value: "hours:1" },