feat(排名): 个性签名挂到排名上,领奖台上每人一句、「你」卡里就地改;全服排名只列到第 100 名,展开不再卡死

- 领奖台:头像旁边一个签名气泡(手机在头像上面),只露两行,长的指上去看全文
- 「你」卡:自己的签名带「改」,就地改不用跳设置页;没写的提示「写一句个性签名」;前一名、后一名名字下面带他们的签名
- 赛道:桌面有签名的名字后面一个引号,指上去看;手机直接写在名字后面
- 个性签名以后限 30 个字(老的长签名不动,榜上只露两行)
- 老师在名单「⋯」或台上气泡的「×」能清空不合适的签名(DELETE /rankings/mood/:userId)
- 全服名单最多列到第 100 名:原来展开会一次摊开一千五百多人把页面卡死

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-10 09:52:06 +08:00
1 parent 540d88355c
commit 2553c6ddfe
10 files changed
+588 -69

No files matched your search

+38 -4
View File
@@ -8,7 +8,7 @@ import type {
WeeklyChampion,
} from "@oj2/contract"
import { useThemeVars } from "naive-ui"
import { getClassBattle, getRankBoard, getWeeklyChampions, setRankHidden } from "oj/api"
import { clearMood, getClassBattle, getRankBoard, getWeeklyChampions, setRankHidden } from "oj/api"
import { classLabel } from "oj/submission/utils"
import UserName from "shared/components/UserName.vue"
import { useBreakpoints } from "shared/composables/breakpoints"
@@ -199,11 +199,14 @@ const subtitle = computed(() => {
const b = board.value
if (!b) return ""
const head = b.scope === "class" ? `${b.total} 人` : `${b.total} 人做对过`
const part = b.complete
? ""
: b.me
const capped = !!b.cap && b.total > b.cap
const part = !b.complete
? b.me
? " · 前面一段 + 你附近一段"
: ` · 只列前面 ${b.rows.length} 名`
: capped
? ` · 只列前 ${b.cap} 名`
: ""
return `${head} · 一样多的,先做到的在前${part}`
})
/** 手机上标题旁边那句:只说从哪天起、跟什么时候比 */
@@ -342,6 +345,32 @@ function hide(row: RankRow) {
})
}
/** 签名改了 / 清了:榜上这个人的每一处(赛道、领奖台、「你」卡、前后一名)就地换掉,不重取 */
function setMood(userId: number, mood: string | null) {
const b = board.value
if (!b) return
for (const row of [...b.rows, b.me, b.ahead, b.behind])
if (row?.user.id === userId) row.mood = mood
}
function onMoodSaved(mood: string | null) {
if (meId.value) setMood(meId.value, mood)
}
function confirmClearMood(row: RankRow) {
dialog.warning({
title: "清空个性签名",
content: `${row.user.username} 的签名「${row.mood}」会被清空。只清这一句,他之后还能重新写。`,
positiveText: "清空",
negativeText: "取消",
onPositiveClick: async () => {
await clearMood(row.user.id)
setMood(row.user.id, null)
message.success("签名已清空")
},
})
}
async function restore(userId: number) {
await setRankHidden(userId, false)
message.success("已恢复")
@@ -414,7 +443,9 @@ async function restore(userId: number) {
:me-id="meId"
:chase-id="board.ahead?.user.id"
:threat-id="board.behind?.user.id"
:teacher="teacher"
@open="open"
@clear-mood="confirmClearMood"
/>
<RankTrack
:key="`${board.scope}|${board.period}|${board.className}`"
@@ -422,6 +453,7 @@ async function restore(userId: number) {
:whole-label="board.scope === 'class' ? `看全班 ${board.total} 人` : undefined"
:total="board.total"
:complete="board.complete"
:cap="board.cap"
:scale="scale"
:me-id="meId"
:chase-id="board.ahead?.user.id"
@@ -435,6 +467,7 @@ async function restore(userId: number) {
@submissions="openSubmissions"
@analysis="openAnalysis"
@hide="hide"
@clear-mood="confirmClearMood"
/>
</template>
<div v-else-if="!loading" class="empty">排名没取到,刷新一下试试</div>
@@ -452,6 +485,7 @@ async function restore(userId: number) {
:label="`${scopeLabel(board.scope)} · ${periodLabel(board.period)}`"
:minis="minis"
:compact="!isDesktop"
@mood-saved="onMoodSaved"
/>
<div v-else-if="board.hidden" class="note-card">
<b>你现在不计入排名</b>