style(格式化): 改成单引号、一行最长 160,全仓重新格式化
Deploy / deploy (push) Canceled after 0s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 13:52:24 +08:00
1 parent 530e1a3d5d
commit 195069450f
480 files changed
+15507 -24650

No files matched your search

+3 -3
View File
@@ -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;
}
+25 -40
View File
@@ -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;
}
+61 -63
View File
@@ -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)
}