Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
530e1a3d5d
commit
195069450f
480 files changed
+15507
-24650
No files matched your search
@@ -1,8 +1,8 @@
|
||||
import api from "utils/api"
|
||||
import type { AchievementList } from "utils/types"
|
||||
import api from 'utils/api'
|
||||
import type { AchievementList } from 'utils/types'
|
||||
|
||||
export function getAchievements(name?: string) {
|
||||
return api.get<AchievementList>("achievements", {
|
||||
return api.get<AchievementList>('achievements', {
|
||||
params: name ? { username: name } : {},
|
||||
})
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import UserName from "shared/components/UserName.vue"
|
||||
import { parseTime } from "utils/functions"
|
||||
import type { Achievement, AchievementClassBoard } from "utils/types"
|
||||
import Medal from "./Medal.vue"
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
import UserName from 'shared/components/UserName.vue'
|
||||
import { parseTime } from 'utils/functions'
|
||||
import type { Achievement, AchievementClassBoard } from 'utils/types'
|
||||
import Medal from './Medal.vue'
|
||||
|
||||
/**
|
||||
* 「本班」:最近谁拿到了什么、成就最多的前五名(自己不在里面就钉在底下)。
|
||||
@@ -26,8 +26,7 @@ const recent = computed(() => {
|
||||
const item = byId.value.get(row.achievementId)
|
||||
if (!item) continue
|
||||
const last = rows.at(-1)
|
||||
if (last && last.username === row.username && last.time === row.unlockTime)
|
||||
last.items.push(item)
|
||||
if (last && last.username === row.username && last.time === row.unlockTime) last.items.push(item)
|
||||
else
|
||||
rows.push({
|
||||
key: `${row.username}-${row.unlockTime}-${row.achievementId}`,
|
||||
@@ -42,8 +41,8 @@ const recent = computed(() => {
|
||||
function what(items: Achievement[]) {
|
||||
const known = items.filter((item) => item.metric !== null).map((item) => item.name)
|
||||
const eggs = items.length - known.length
|
||||
if (eggs) known.push(eggs === 1 ? "一个彩蛋" : `${eggs} 个彩蛋`)
|
||||
return known.join("、")
|
||||
if (eggs) known.push(eggs === 1 ? '一个彩蛋' : `${eggs} 个彩蛋`)
|
||||
return known.join('、')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -54,29 +53,19 @@ function what(items: Achievement[]) {
|
||||
<div class="col">
|
||||
<span class="sub">最近拿到</span>
|
||||
<div v-for="row in recent" :key="row.key" class="line">
|
||||
<Medal
|
||||
:icon="row.items[0]!.icon"
|
||||
:rarity="row.items[0]!.rarity"
|
||||
:size="28"
|
||||
:state="row.items[0]!.metric === null ? 'unknown' : 'on'"
|
||||
/>
|
||||
<Medal :icon="row.items[0]!.icon" :rarity="row.items[0]!.rarity" :size="28" :state="row.items[0]!.metric === null ? 'unknown' : 'on'" />
|
||||
<span class="what">
|
||||
<UserName :username="row.username" />
|
||||
<span class="muted">拿到</span>
|
||||
{{ what(row.items) }}
|
||||
</span>
|
||||
<span class="date">{{ parseTime(row.time, "M月D日") }}</span>
|
||||
<span class="date">{{ parseTime(row.time, 'M月D日') }}</span>
|
||||
</div>
|
||||
<span v-if="!recent.length" class="empty">这学期还没人拿到</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<span class="sub">成就最多</span>
|
||||
<div
|
||||
v-for="row in board.most"
|
||||
:key="row.username"
|
||||
class="line rank"
|
||||
:class="{ mine: row.username === me }"
|
||||
>
|
||||
<div v-for="row in board.most" :key="row.username" class="line rank" :class="{ mine: row.username === me }">
|
||||
<span class="no">{{ row.rank }}</span>
|
||||
<UserName :username="row.username" />
|
||||
<b>{{ row.count }}</b>
|
||||
@@ -94,9 +83,9 @@ function what(items: Achievement[]) {
|
||||
<style scoped>
|
||||
.card {
|
||||
padding: 14px 20px 12px;
|
||||
border: 1px solid v-bind("theme.borderColor");
|
||||
border: 1px solid v-bind('theme.borderColor');
|
||||
border-radius: var(--oj-radius);
|
||||
background: v-bind("theme.cardColor");
|
||||
background: v-bind('theme.cardColor');
|
||||
}
|
||||
|
||||
h3 {
|
||||
@@ -120,7 +109,7 @@ h3 {
|
||||
.sub {
|
||||
margin-bottom: 4px;
|
||||
font-size: var(--oj-fs-meta);
|
||||
color: v-bind("theme.textColor3");
|
||||
color: v-bind('theme.textColor3');
|
||||
}
|
||||
|
||||
.line {
|
||||
@@ -143,7 +132,7 @@ h3 {
|
||||
.date,
|
||||
.no,
|
||||
.empty {
|
||||
color: v-bind("theme.textColor3");
|
||||
color: v-bind('theme.textColor3');
|
||||
}
|
||||
|
||||
.muted {
|
||||
@@ -178,7 +167,7 @@ h3 {
|
||||
}
|
||||
|
||||
.pinned {
|
||||
border-top: 1px dashed v-bind("theme.borderColor");
|
||||
border-top: 1px dashed v-bind('theme.borderColor');
|
||||
}
|
||||
|
||||
.compact {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import AchievementIcon from "shared/components/AchievementIcon.vue"
|
||||
import type { UserBadge } from "utils/types"
|
||||
import type { Group } from "../series"
|
||||
import Medal from "./Medal.vue"
|
||||
import SeriesRow from "./SeriesRow.vue"
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
import AchievementIcon from 'shared/components/AchievementIcon.vue'
|
||||
import type { UserBadge } from 'utils/types'
|
||||
import type { Group } from '../series'
|
||||
import Medal from './Medal.vue'
|
||||
import SeriesRow from './SeriesRow.vue'
|
||||
|
||||
/** 一组成就线(做题 / 坚持 / …),「彩蛋」组末尾是还没找到的问号,「题单和比赛」组末尾是拿到的题单奖章 */
|
||||
const props = defineProps<{ group: Group; badges?: UserBadge[]; compact?: boolean }>()
|
||||
|
||||
const theme = useThemeVars()
|
||||
const showBadges = computed(() => props.group.name === "题单和比赛" && !!props.badges?.length)
|
||||
const showBadges = computed(() => props.group.name === '题单和比赛' && !!props.badges?.length)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -31,10 +31,7 @@ const showBadges = computed(() => props.group.name === "题单和比赛" && !!pr
|
||||
<div class="badge-list">
|
||||
<n-tooltip v-for="b in badges" :key="b.id" :delay="150">
|
||||
<template #trigger>
|
||||
<router-link
|
||||
:to="{ name: 'problemset', params: { problemSetId: b.problemSet.id } }"
|
||||
class="badge"
|
||||
>
|
||||
<router-link :to="{ name: 'problemset', params: { problemSetId: b.problemSet.id } }" class="badge">
|
||||
<AchievementIcon :icon="b.badge.icon" :size="24" />
|
||||
<span>{{ b.badge.name }}</span>
|
||||
</router-link>
|
||||
@@ -49,9 +46,9 @@ const showBadges = computed(() => props.group.name === "题单和比赛" && !!pr
|
||||
<style scoped>
|
||||
.card {
|
||||
padding: 14px 20px 20px;
|
||||
border: 1px solid v-bind("theme.borderColor");
|
||||
border: 1px solid v-bind('theme.borderColor');
|
||||
border-radius: var(--oj-radius);
|
||||
background: v-bind("theme.cardColor");
|
||||
background: v-bind('theme.cardColor');
|
||||
}
|
||||
|
||||
h3 {
|
||||
@@ -65,7 +62,7 @@ h3 {
|
||||
h3 small {
|
||||
font-size: var(--oj-fs-meta);
|
||||
font-weight: 400;
|
||||
color: v-bind("theme.textColor3");
|
||||
color: v-bind('theme.textColor3');
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@@ -79,7 +76,7 @@ h3 small {
|
||||
|
||||
.masked-title {
|
||||
font-size: 13px;
|
||||
color: v-bind("theme.textColor2");
|
||||
color: v-bind('theme.textColor2');
|
||||
}
|
||||
|
||||
.masked-list,
|
||||
@@ -99,8 +96,8 @@ h3 small {
|
||||
border-radius: 6px;
|
||||
font-size: var(--oj-fs-sec);
|
||||
white-space: nowrap;
|
||||
color: v-bind("theme.textColor1");
|
||||
background: v-bind("theme.actionColor");
|
||||
color: v-bind('theme.textColor1');
|
||||
background: v-bind('theme.actionColor');
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import AchievementIcon from "shared/components/AchievementIcon.vue"
|
||||
import { RARITY_COLOR } from "utils/constants"
|
||||
import type { AchievementRarity } from "utils/types"
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
import AchievementIcon from 'shared/components/AchievementIcon.vue'
|
||||
import { RARITY_COLOR } from 'utils/constants'
|
||||
import type { AchievementRarity } from 'utils/types'
|
||||
|
||||
/**
|
||||
* 一枚徽章:拿到的是稀有度色的圈,没拿到的灰掉,还没找到的隐藏成就是虚线问号。
|
||||
@@ -13,10 +13,10 @@ const props = withDefaults(
|
||||
icon?: string
|
||||
rarity?: AchievementRarity
|
||||
size?: number
|
||||
state?: "on" | "off" | "unknown"
|
||||
state?: 'on' | 'off' | 'unknown'
|
||||
ring?: number | null
|
||||
}>(),
|
||||
{ icon: "", rarity: "bronze", size: 40, state: "on", ring: null },
|
||||
{ icon: '', rarity: 'bronze', size: 40, state: 'on', ring: null },
|
||||
)
|
||||
|
||||
const theme = useThemeVars()
|
||||
@@ -25,25 +25,13 @@ const r = computed(() => props.size / 2 + 3)
|
||||
const box = computed(() => props.size + 8)
|
||||
const circumference = computed(() => 2 * Math.PI * r.value)
|
||||
// 底色都是半透明的,下面垫一层卡片底色 —— 不然成就线的进度线会从徽章中间透出来
|
||||
const onBg = computed(
|
||||
() => `linear-gradient(${color.value}26, ${color.value}26), ${theme.value.cardColor}`,
|
||||
)
|
||||
const offBg = computed(
|
||||
() =>
|
||||
`linear-gradient(${theme.value.actionColor}, ${theme.value.actionColor}), ${theme.value.cardColor}`,
|
||||
)
|
||||
const onBg = computed(() => `linear-gradient(${color.value}26, ${color.value}26), ${theme.value.cardColor}`)
|
||||
const offBg = computed(() => `linear-gradient(${theme.value.actionColor}, ${theme.value.actionColor}), ${theme.value.cardColor}`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="medal" :style="{ width: `${size}px`, height: `${size}px` }">
|
||||
<svg
|
||||
v-if="ring !== null && state !== 'unknown'"
|
||||
class="arc"
|
||||
:width="box"
|
||||
:height="box"
|
||||
:viewBox="`0 0 ${box} ${box}`"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg v-if="ring !== null && state !== 'unknown'" class="arc" :width="box" :height="box" :viewBox="`0 0 ${box} ${box}`" aria-hidden="true">
|
||||
<circle :cx="box / 2" :cy="box / 2" :r="r" fill="none" class="arc-track" stroke-width="3" />
|
||||
<circle
|
||||
:cx="box / 2"
|
||||
@@ -56,15 +44,8 @@ const offBg = computed(
|
||||
:stroke-dasharray="`${circumference * Math.min(1, ring)} ${circumference}`"
|
||||
/>
|
||||
</svg>
|
||||
<span v-if="state === 'unknown'" class="disc unknown" :style="{ fontSize: `${size * 0.42}px` }">
|
||||
?
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="disc"
|
||||
:class="state"
|
||||
:style="state === 'on' ? { borderColor: color, background: onBg } : undefined"
|
||||
>
|
||||
<span v-if="state === 'unknown'" class="disc unknown" :style="{ fontSize: `${size * 0.42}px` }"> ? </span>
|
||||
<span v-else class="disc" :class="state" :style="state === 'on' ? { borderColor: color, background: onBg } : undefined">
|
||||
<AchievementIcon :icon="icon" :size="Math.round(size * 0.56)" />
|
||||
</span>
|
||||
</span>
|
||||
@@ -85,11 +66,11 @@ const offBg = computed(
|
||||
}
|
||||
|
||||
.arc-track {
|
||||
stroke: v-bind("theme.dividerColor");
|
||||
stroke: v-bind('theme.dividerColor');
|
||||
}
|
||||
|
||||
.arc-fill {
|
||||
stroke: v-bind("theme.primaryColor");
|
||||
stroke: v-bind('theme.primaryColor');
|
||||
}
|
||||
|
||||
.disc {
|
||||
@@ -105,7 +86,7 @@ const offBg = computed(
|
||||
}
|
||||
|
||||
.disc.off {
|
||||
border-color: v-bind("theme.borderColor");
|
||||
border-color: v-bind('theme.borderColor');
|
||||
background: v-bind(offBg);
|
||||
}
|
||||
|
||||
@@ -117,9 +98,9 @@ const offBg = computed(
|
||||
}
|
||||
|
||||
.disc.unknown {
|
||||
border: 1.5px dashed v-bind("theme.borderColor");
|
||||
border: 1.5px dashed v-bind('theme.borderColor');
|
||||
background: v-bind(offBg);
|
||||
color: v-bind("theme.textColor3");
|
||||
color: v-bind('theme.textColor3');
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { RARITY_LABEL } from "utils/constants"
|
||||
import { useRarityColor } from "shared/composables/rarity"
|
||||
import type { Near } from "../series"
|
||||
import Medal from "./Medal.vue"
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
import { RARITY_LABEL } from 'utils/constants'
|
||||
import { useRarityColor } from 'shared/composables/rarity'
|
||||
import type { Near } from '../series'
|
||||
import Medal from './Medal.vue'
|
||||
|
||||
/**
|
||||
* 「快拿到了」:每个只说还差什么,进度条底下是现在几 / 要几、本班几人有。
|
||||
@@ -16,9 +16,7 @@ const rarityText = useRarityColor()
|
||||
|
||||
function now(item: Near) {
|
||||
const { series, achievement } = item
|
||||
return series.streak
|
||||
? `现在连着 ${series.value} ${series.unit}`
|
||||
: `${series.value} / ${achievement.threshold} ${series.unit}`
|
||||
return series.streak ? `现在连着 ${series.value} ${series.unit}` : `${series.value} / ${achievement.threshold} ${series.unit}`
|
||||
}
|
||||
|
||||
const percent = (item: Near) => Math.round((item.series.value / item.achievement.threshold!) * 100)
|
||||
@@ -55,9 +53,7 @@ const percent = (item: Near) => Math.round((item.series.value / item.achievement
|
||||
/>
|
||||
<div class="foot">
|
||||
<span>{{ now(item) }}</span>
|
||||
<span v-if="item.achievement.classHolders !== null">
|
||||
本班 {{ item.achievement.classHolders }} 人有
|
||||
</span>
|
||||
<span v-if="item.achievement.classHolders !== null"> 本班 {{ item.achievement.classHolders }} 人有 </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,9 +64,9 @@ const percent = (item: Near) => Math.round((item.series.value / item.achievement
|
||||
<style scoped>
|
||||
.card {
|
||||
padding: 14px 20px 6px;
|
||||
border: 1px solid v-bind("theme.borderColor");
|
||||
border: 1px solid v-bind('theme.borderColor');
|
||||
border-radius: var(--oj-radius);
|
||||
background: v-bind("theme.cardColor");
|
||||
background: v-bind('theme.cardColor');
|
||||
}
|
||||
|
||||
h3 {
|
||||
@@ -83,7 +79,7 @@ h3 {
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 11px 0;
|
||||
border-top: 1px solid v-bind("theme.dividerColor");
|
||||
border-top: 1px solid v-bind('theme.dividerColor');
|
||||
}
|
||||
|
||||
.body {
|
||||
@@ -109,7 +105,7 @@ h3 {
|
||||
margin-left: auto;
|
||||
font-size: var(--oj-fs-sec);
|
||||
font-weight: 600;
|
||||
color: v-bind("theme.primaryColorPressed");
|
||||
color: v-bind('theme.primaryColorPressed');
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -117,7 +113,7 @@ h3 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: v-bind("theme.textColor3");
|
||||
color: v-bind('theme.textColor3');
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@@ -132,7 +128,7 @@ h3 {
|
||||
}
|
||||
|
||||
.wide .item {
|
||||
border-top: 1px solid v-bind("theme.dividerColor");
|
||||
border-top: 1px solid v-bind('theme.dividerColor');
|
||||
}
|
||||
|
||||
/* 手机:「还差什么」换到名字下面一行 */
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { RARITY_LABEL } from "utils/constants"
|
||||
import { parseTime } from "utils/functions"
|
||||
import type { Achievement } from "utils/types"
|
||||
import type { Series } from "../series"
|
||||
import Medal from "./Medal.vue"
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
import { RARITY_LABEL } from 'utils/constants'
|
||||
import { parseTime } from 'utils/functions'
|
||||
import type { Achievement } from 'utils/types'
|
||||
import type { Series } from '../series'
|
||||
import Medal from './Medal.vue'
|
||||
|
||||
/**
|
||||
* 一条成就线:左边线名和现在的数,右边几档按门槛排在一根进度线上,下一档头上标「差几」。
|
||||
@@ -53,8 +53,8 @@ function gap(item: Achievement) {
|
||||
function when(item: Achievement) {
|
||||
if (!item.unlocked) return `现在 ${props.series.value} ${props.series.unit}`
|
||||
// 补发的不写日期:一次补发会给几百人盖上同一个时间戳
|
||||
if (item.backfilled || !item.unlockTime) return "已拿到"
|
||||
return `${parseTime(item.unlockTime, "M月D日")}拿到`
|
||||
if (item.backfilled || !item.unlockTime) return '已拿到'
|
||||
return `${parseTime(item.unlockTime, 'M月D日')}拿到`
|
||||
}
|
||||
|
||||
function holders(item: Achievement) {
|
||||
@@ -68,35 +68,19 @@ function holders(item: Achievement) {
|
||||
<div class="label">
|
||||
<span class="name">{{ series.label }}</span>
|
||||
<span class="value">
|
||||
<b>{{ series.streak ? "现在 " : "" }}{{ series.value }}</b>
|
||||
<b>{{ series.streak ? '现在 ' : '' }}{{ series.value }}</b>
|
||||
<small>{{ series.unit }}</small>
|
||||
</span>
|
||||
</div>
|
||||
<div class="track" :style="{ height: `${size + 36}px` }">
|
||||
<span
|
||||
class="line"
|
||||
:style="{ top: `${size / 2 - 2}px`, width: `${center(series.items.length - 1)}%` }"
|
||||
></span>
|
||||
<span class="line" :style="{ top: `${size / 2 - 2}px`, width: `${center(series.items.length - 1)}%` }"></span>
|
||||
<span class="line done" :style="{ top: `${size / 2 - 2}px`, width: `${fill}%` }"></span>
|
||||
<div
|
||||
v-for="(item, i) in series.items"
|
||||
:key="item.id"
|
||||
class="tier"
|
||||
:class="{ on: item.unlocked }"
|
||||
:style="{ left: `${center(i)}%` }"
|
||||
>
|
||||
<div v-for="(item, i) in series.items" :key="item.id" class="tier" :class="{ on: item.unlocked }" :style="{ left: `${center(i)}%` }">
|
||||
<n-tooltip :delay="150">
|
||||
<template #trigger>
|
||||
<span class="trigger">
|
||||
<span v-if="gap(item)" class="gap" :style="{ bottom: `${size + 4}px` }">
|
||||
差 {{ gap(item) }}
|
||||
</span>
|
||||
<Medal
|
||||
:icon="item.icon"
|
||||
:rarity="item.rarity"
|
||||
:size="size"
|
||||
:state="item.unlocked ? 'on' : 'off'"
|
||||
/>
|
||||
<span v-if="gap(item)" class="gap" :style="{ bottom: `${size + 4}px` }"> 差 {{ gap(item) }} </span>
|
||||
<Medal :icon="item.icon" :rarity="item.rarity" :size="size" :state="item.unlocked ? 'on' : 'off'" />
|
||||
</span>
|
||||
</template>
|
||||
<div class="tip">
|
||||
@@ -129,7 +113,7 @@ function holders(item: Achievement) {
|
||||
|
||||
.name {
|
||||
font-size: 13px;
|
||||
color: v-bind("theme.textColor2");
|
||||
color: v-bind('theme.textColor2');
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -142,7 +126,7 @@ function holders(item: Achievement) {
|
||||
.value small {
|
||||
margin-left: 3px;
|
||||
font-size: 12px;
|
||||
color: v-bind("theme.textColor3");
|
||||
color: v-bind('theme.textColor3');
|
||||
}
|
||||
|
||||
.track {
|
||||
@@ -154,11 +138,11 @@ function holders(item: Achievement) {
|
||||
left: 0;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: v-bind("theme.dividerColor");
|
||||
background: v-bind('theme.dividerColor');
|
||||
}
|
||||
|
||||
.line.done {
|
||||
background: v-bind("theme.primaryColor");
|
||||
background: v-bind('theme.primaryColor');
|
||||
}
|
||||
|
||||
.tier {
|
||||
@@ -188,7 +172,7 @@ function holders(item: Achievement) {
|
||||
line-height: 16px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
color: v-bind("theme.primaryColorPressed");
|
||||
color: v-bind('theme.primaryColorPressed');
|
||||
background: v-bind(gapBg);
|
||||
}
|
||||
|
||||
@@ -197,18 +181,18 @@ function holders(item: Achievement) {
|
||||
line-height: 15px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
color: v-bind("theme.textColor3");
|
||||
color: v-bind('theme.textColor3');
|
||||
}
|
||||
|
||||
.tier.on .tier-name {
|
||||
color: v-bind("theme.textColor1");
|
||||
color: v-bind('theme.textColor1');
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tier-at {
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
color: v-bind("theme.textColor3");
|
||||
color: v-bind('theme.textColor3');
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { getAchievements } from "oj/achievement/api"
|
||||
import { getUserBadges } from "oj/api"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import UserName from "shared/components/UserName.vue"
|
||||
import AdminLink from "shared/components/AdminLink.vue"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import { RARITY_COLOR, RARITY_LABEL } from "utils/constants"
|
||||
import type { AchievementList, AchievementRarity, UserBadge } from "utils/types"
|
||||
import ClassCard from "./components/ClassCard.vue"
|
||||
import GroupCard from "./components/GroupCard.vue"
|
||||
import NearCard from "./components/NearCard.vue"
|
||||
import { buildGroups, nearest, type GroupName } from "./series"
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
import { getAchievements } from 'oj/achievement/api'
|
||||
import { getUserBadges } from 'oj/api'
|
||||
import { useBreakpoints } from 'shared/composables/breakpoints'
|
||||
import UserName from 'shared/components/UserName.vue'
|
||||
import AdminLink from 'shared/components/AdminLink.vue'
|
||||
import { useUserStore } from 'shared/store/user'
|
||||
import { RARITY_COLOR, RARITY_LABEL } from 'utils/constants'
|
||||
import type { AchievementList, AchievementRarity, UserBadge } from 'utils/types'
|
||||
import ClassCard from './components/ClassCard.vue'
|
||||
import GroupCard from './components/GroupCard.vue'
|
||||
import NearCard from './components/NearCard.vue'
|
||||
import { buildGroups, nearest, type GroupName } from './series'
|
||||
|
||||
/**
|
||||
* 我的成就(设计稿「我的成就重设计」定稿,画布 page ach):
|
||||
@@ -34,23 +34,19 @@ const other = computed(() => !!data.value && data.value.username !== userStore.u
|
||||
const groups = computed(() => (data.value ? buildGroups(data.value.achievements) : []))
|
||||
const near = computed(() => (other.value ? [] : nearest(groups.value)))
|
||||
const got = computed(() => data.value?.achievements.filter((item) => item.unlocked) ?? [])
|
||||
const rarities: AchievementRarity[] = ["platinum", "gold", "silver", "bronze"]
|
||||
const countOf = (rarity: AchievementRarity) =>
|
||||
got.value.filter((item) => item.rarity === rarity).length
|
||||
const rarities: AchievementRarity[] = ['platinum', 'gold', 'silver', 'bronze']
|
||||
const countOf = (rarity: AchievementRarity) => got.value.filter((item) => item.rarity === rarity).length
|
||||
|
||||
// 桌面两栏:左边做题、其他、彩蛋,右边坚持、题单和比赛 —— 两边差不多高
|
||||
const LEFT: GroupName[] = ["做题", "其他", "彩蛋"]
|
||||
const RIGHT: GroupName[] = ["坚持", "题单和比赛"]
|
||||
const LEFT: GroupName[] = ['做题', '其他', '彩蛋']
|
||||
const RIGHT: GroupName[] = ['坚持', '题单和比赛']
|
||||
const pick = (names: GroupName[]) => groups.value.filter((group) => names.includes(group.name))
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
notFound.value = false
|
||||
try {
|
||||
const [list, badgeRes] = await Promise.all([
|
||||
getAchievements(name.value),
|
||||
getUserBadges(name.value).catch(() => []),
|
||||
])
|
||||
const [list, badgeRes] = await Promise.all([getAchievements(name.value), getUserBadges(name.value).catch(() => [])])
|
||||
data.value = list
|
||||
badges.value = badgeRes ?? []
|
||||
} catch {
|
||||
@@ -91,19 +87,8 @@ watch(name, load)
|
||||
|
||||
<!-- 没有班级(老师、自己注册的号)就没有「本班」,「快拿到了」三个横排占满整宽 -->
|
||||
<div v-if="near.length || data.classBoard" class="top" :class="{ alone: !data.classBoard }">
|
||||
<NearCard
|
||||
v-if="near.length"
|
||||
:items="near"
|
||||
:compact="!isDesktop"
|
||||
:wide="isDesktop && !data.classBoard"
|
||||
/>
|
||||
<ClassCard
|
||||
v-if="data.classBoard"
|
||||
:board="data.classBoard"
|
||||
:achievements="data.achievements"
|
||||
:me="data.username"
|
||||
:compact="!isDesktop"
|
||||
/>
|
||||
<NearCard v-if="near.length" :items="near" :compact="!isDesktop" :wide="isDesktop && !data.classBoard" />
|
||||
<ClassCard v-if="data.classBoard" :board="data.classBoard" :achievements="data.achievements" :me="data.username" :compact="!isDesktop" />
|
||||
</div>
|
||||
|
||||
<div v-if="isDesktop" class="columns">
|
||||
@@ -138,12 +123,12 @@ h2 {
|
||||
|
||||
.count {
|
||||
font-size: var(--oj-fs-body);
|
||||
color: v-bind("theme.textColor2");
|
||||
color: v-bind('theme.textColor2');
|
||||
}
|
||||
|
||||
.count b {
|
||||
font-size: 18px;
|
||||
color: v-bind("theme.textColor1");
|
||||
color: v-bind('theme.textColor1');
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@@ -158,7 +143,7 @@ h2 {
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: var(--oj-fs-meta);
|
||||
color: v-bind("theme.textColor2");
|
||||
color: v-bind('theme.textColor2');
|
||||
}
|
||||
|
||||
.rarity i {
|
||||
@@ -168,7 +153,7 @@ h2 {
|
||||
}
|
||||
|
||||
.rarity b {
|
||||
color: v-bind("theme.textColor1");
|
||||
color: v-bind('theme.textColor1');
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@@ -185,7 +170,7 @@ h2 {
|
||||
.mine {
|
||||
margin-left: auto;
|
||||
font-size: var(--oj-fs-sec);
|
||||
color: v-bind("theme.primaryColor");
|
||||
color: v-bind('theme.primaryColor');
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Achievement } from "utils/types"
|
||||
import type { Achievement } from 'utils/types'
|
||||
|
||||
/**
|
||||
* 成就页按「一条条线」摆:同一个指标的几档是一条线(做对 1 / 10 / 50 / 100 / 200 道),
|
||||
@@ -7,7 +7,7 @@ import type { Achievement } from "utils/types"
|
||||
* 指标是后台可配的,这里没登记的指标归到「其他」、拿第一档的名字当线名 —— 不会丢,只是不好看,
|
||||
* 在后台加了新指标记得回来补一行。
|
||||
*/
|
||||
export const GROUPS = ["做题", "坚持", "题单和比赛", "其他", "彩蛋"] as const
|
||||
export const GROUPS = ['做题', '坚持', '题单和比赛', '其他', '彩蛋'] as const
|
||||
export type GroupName = (typeof GROUPS)[number]
|
||||
|
||||
interface MetricInfo {
|
||||
@@ -21,109 +21,109 @@ interface MetricInfo {
|
||||
}
|
||||
|
||||
const METRICS: Record<string, MetricInfo> = {
|
||||
accepted_count: { group: "做题", label: "做对的题", unit: "道", remain: (r) => `再做对 ${r} 道` },
|
||||
accepted_count: { group: '做题', label: '做对的题', unit: '道', remain: (r) => `再做对 ${r} 道` },
|
||||
mid_ac_count: {
|
||||
group: "做题",
|
||||
label: "中等题",
|
||||
unit: "道",
|
||||
group: '做题',
|
||||
label: '中等题',
|
||||
unit: '道',
|
||||
remain: (r) => `再做对 ${r} 道中等题`,
|
||||
},
|
||||
hard_ac_count: {
|
||||
group: "做题",
|
||||
label: "困难题",
|
||||
unit: "道",
|
||||
group: '做题',
|
||||
label: '困难题',
|
||||
unit: '道',
|
||||
remain: (r) => `再做对 ${r} 道困难题`,
|
||||
},
|
||||
max_ac_week_streak: {
|
||||
group: "坚持",
|
||||
label: "连着几周有做对",
|
||||
unit: "周",
|
||||
group: '坚持',
|
||||
label: '连着几周有做对',
|
||||
unit: '周',
|
||||
remain: (r) => `再连着 ${r} 周都有做对`,
|
||||
streak: true,
|
||||
},
|
||||
max_ac_streak_days: {
|
||||
group: "坚持",
|
||||
label: "连着几天有做对",
|
||||
unit: "天",
|
||||
group: '坚持',
|
||||
label: '连着几天有做对',
|
||||
unit: '天',
|
||||
remain: (r) => `再连着 ${r} 天都有做对`,
|
||||
},
|
||||
active_days: {
|
||||
group: "坚持",
|
||||
label: "交过题的日子",
|
||||
unit: "天",
|
||||
group: '坚持',
|
||||
label: '交过题的日子',
|
||||
unit: '天',
|
||||
remain: (r) => `再有 ${r} 天交题`,
|
||||
},
|
||||
submission_count: { group: "坚持", label: "交了几次", unit: "次", remain: (r) => `再交 ${r} 次` },
|
||||
submission_count: { group: '坚持', label: '交了几次', unit: '次', remain: (r) => `再交 ${r} 次` },
|
||||
problemset_completed: {
|
||||
group: "题单和比赛",
|
||||
label: "做完的题单",
|
||||
unit: "个",
|
||||
group: '题单和比赛',
|
||||
label: '做完的题单',
|
||||
unit: '个',
|
||||
remain: (r) => `再做完 ${r} 个题单`,
|
||||
},
|
||||
badge_count: {
|
||||
group: "题单和比赛",
|
||||
label: "题单奖章",
|
||||
unit: "枚",
|
||||
group: '题单和比赛',
|
||||
label: '题单奖章',
|
||||
unit: '枚',
|
||||
remain: (r) => `再拿 ${r} 枚题单奖章`,
|
||||
},
|
||||
contest_joined: {
|
||||
group: "题单和比赛",
|
||||
label: "参加的比赛",
|
||||
unit: "场",
|
||||
group: '题单和比赛',
|
||||
label: '参加的比赛',
|
||||
unit: '场',
|
||||
remain: (r) => `再参加 ${r} 场比赛`,
|
||||
},
|
||||
languages_used: {
|
||||
group: "其他",
|
||||
label: "用过的语言",
|
||||
unit: "种",
|
||||
group: '其他',
|
||||
label: '用过的语言',
|
||||
unit: '种',
|
||||
remain: (r) => `再用 ${r} 种语言`,
|
||||
},
|
||||
achievement_unlocked_count: {
|
||||
group: "其他",
|
||||
label: "拿到的成就",
|
||||
unit: "个",
|
||||
group: '其他',
|
||||
label: '拿到的成就',
|
||||
unit: '个',
|
||||
remain: (r) => `再拿 ${r} 个成就`,
|
||||
},
|
||||
first_try_ac_count: {
|
||||
group: "彩蛋",
|
||||
label: "一交就对",
|
||||
unit: "道",
|
||||
group: '彩蛋',
|
||||
label: '一交就对',
|
||||
unit: '道',
|
||||
remain: (r) => `再有 ${r} 道一交就对`,
|
||||
},
|
||||
max_ac_in_one_day: {
|
||||
group: "彩蛋",
|
||||
label: "一天做对",
|
||||
unit: "道",
|
||||
group: '彩蛋',
|
||||
label: '一天做对',
|
||||
unit: '道',
|
||||
remain: (r) => `一天做对 ${r} 道`,
|
||||
},
|
||||
midnight_submissions: {
|
||||
group: "彩蛋",
|
||||
label: "凌晨交题",
|
||||
unit: "次",
|
||||
group: '彩蛋',
|
||||
label: '凌晨交题',
|
||||
unit: '次',
|
||||
remain: (r) => `再交 ${r} 次`,
|
||||
},
|
||||
early_bird_submissions: {
|
||||
group: "彩蛋",
|
||||
label: "早上交题",
|
||||
unit: "次",
|
||||
group: '彩蛋',
|
||||
label: '早上交题',
|
||||
unit: '次',
|
||||
remain: (r) => `再交 ${r} 次`,
|
||||
},
|
||||
compile_error_count: {
|
||||
group: "彩蛋",
|
||||
label: "编译失败",
|
||||
unit: "次",
|
||||
group: '彩蛋',
|
||||
label: '编译失败',
|
||||
unit: '次',
|
||||
remain: (r) => `还差 ${r} 次`,
|
||||
},
|
||||
max_wa_before_ac: {
|
||||
group: "彩蛋",
|
||||
label: "错了好多次才做对",
|
||||
unit: "次",
|
||||
group: '彩蛋',
|
||||
label: '错了好多次才做对',
|
||||
unit: '次',
|
||||
remain: (r) => `还差 ${r} 次`,
|
||||
},
|
||||
max_code_lines: {
|
||||
group: "彩蛋",
|
||||
label: "做对的代码最长",
|
||||
unit: "行",
|
||||
group: '彩蛋',
|
||||
label: '做对的代码最长',
|
||||
unit: '行',
|
||||
remain: (r) => `还差 ${r} 行`,
|
||||
},
|
||||
}
|
||||
@@ -178,9 +178,9 @@ export function buildGroups(achievements: Achievement[]): Group[] {
|
||||
const info = METRICS[metric]
|
||||
return {
|
||||
metric,
|
||||
group: info?.group ?? "其他",
|
||||
group: info?.group ?? '其他',
|
||||
label: info?.label ?? items[0]!.name,
|
||||
unit: info?.unit ?? "",
|
||||
unit: info?.unit ?? '',
|
||||
streak: info?.streak ?? false,
|
||||
value: items[0]!.progress ?? 0,
|
||||
items,
|
||||
@@ -189,13 +189,13 @@ export function buildGroups(achievements: Achievement[]): Group[] {
|
||||
})
|
||||
return GROUPS.map((name) => {
|
||||
const own = series.filter((item) => item.group === name)
|
||||
const extra = name === "彩蛋" ? masked : 0
|
||||
const extra = name === '彩蛋' ? masked : 0
|
||||
const items = own.flatMap((item) => item.items)
|
||||
return {
|
||||
name,
|
||||
series: own,
|
||||
masked: extra,
|
||||
got: items.filter((item) => item.unlocked).length + (name === "彩蛋" ? maskedGot : 0),
|
||||
got: items.filter((item) => item.unlocked).length + (name === '彩蛋' ? maskedGot : 0),
|
||||
total: items.length + extra,
|
||||
}
|
||||
})
|
||||
@@ -221,7 +221,7 @@ export function nearest(groups: Group[], limit = 3): Near[] {
|
||||
for (const group of groups) {
|
||||
for (const series of group.series) {
|
||||
const next = series.next
|
||||
if (!next || next.operator !== "gte" || !next.threshold) continue
|
||||
if (!next || next.operator !== 'gte' || !next.threshold) continue
|
||||
const r = next.threshold - series.value
|
||||
if (r <= 0) continue
|
||||
const info = METRICS[series.metric]
|
||||
@@ -233,7 +233,5 @@ export function nearest(groups: Group[], limit = 3): Near[] {
|
||||
}
|
||||
}
|
||||
const ratio = (item: Near) => item.series.value / item.achievement.threshold!
|
||||
return candidates
|
||||
.sort((a, b) => ratio(b) - ratio(a) || rankOf(a.series.metric) - rankOf(b.series.metric))
|
||||
.slice(0, limit)
|
||||
return candidates.sort((a, b) => ratio(b) - ratio(a) || rankOf(a.series.metric) - rankOf(b.series.metric)).slice(0, limit)
|
||||
}
|
||||
Reference in new issue
Block a user