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

+37 -3
View File
@@ -46,6 +46,8 @@ const WINDOW_BEFORE = 5
const WINDOW_AFTER = 7
/** 没有「我」(老师、没上榜)时榜面给多少 */
const BOARD_HEAD_NO_ME = 30
/** 全服名单最多列到第几名,展开也到此为止(一千五百多人全摊开页面会卡死) */
const ALL_CAP = 100
/** 走势图往回看几周 */
const TREND_WEEKS = 4
/** 每周冠军列几周 */
@@ -58,6 +60,7 @@ function toRow(standing: Standing, change: number | null): RankRow {
user: sampleUser(entrant, null),
avatar: entrant.avatar,
className: entrant.className,
mood: entrant.mood,
solved: standing.solved,
reachedAt: standing.reachedAt,
change,
@@ -187,11 +190,13 @@ rankingRoutes.get("/rankings/board", optionalAuth, async (c) => {
const meIndex = user ? now.findIndex((row) => row.entrant.id === user.id && row.solved > 0) : -1
const me = meIndex >= 0 ? now[meIndex] : undefined
const cap = scope.data === "all" ? ALL_CAP : null
const listable = cap ? now.slice(0, cap) : now
const full = includeZero || c.req.query("full") === "1"
let picked = now
let picked = listable
if (!full) {
const head = me ? BOARD_HEAD : BOARD_HEAD_NO_ME
picked = now.filter(
picked = listable.filter(
(_, index) =>
index < head || (me && index >= meIndex - WINDOW_BEFORE && index <= meIndex + WINDOW_AFTER),
)
@@ -230,7 +235,8 @@ rankingRoutes.get("/rankings/board", optionalAuth, async (c) => {
changeSince,
total: now.length,
rows: picked.map((row) => rowOf(row)!),
complete: picked.length === now.length,
complete: picked.length === listable.length,
cap,
me: rowOf(me),
ahead: rowOf(meIndex > 0 ? now[meIndex - 1] : undefined),
behind: rowOf(meIndex >= 0 ? now[meIndex + 1] : undefined),
@@ -340,3 +346,31 @@ rankingRoutes.put("/rankings/hidden/:userId", requireTeacher, async (c) => {
if (!updated.length) return failure(c, 404, "user-not-found", "没有这个学生")
return success(c, null)
})
/**
* 老师清空一个学生的个性签名。签名在排名上挂出来以后更显眼,拿同学开涮、留电话号码的
* 得有人能拿掉(用户 2026-10-10 定的)。只清这一句,他之后还能重新写。
*/
rankingRoutes.delete("/rankings/mood/:userId", requireTeacher, async (c) => {
const userId = Number(c.req.param("userId"))
if (!Number.isInteger(userId)) return failure(c, 400, "invalid-user", "用户不对")
const updated = await db
.update(schema.userProfile)
.set({ mood: null })
.where(
and(
eq(schema.userProfile.userId, userId),
inArray(
schema.userProfile.userId,
db
.select({ id: schema.user.id })
.from(schema.user)
.where(inArray(schema.user.adminType, [...STUDENT_ROLES])),
),
),
)
.returning({ id: schema.userProfile.userId })
if (!updated.length) return failure(c, 404, "user-not-found", "没有这个学生")
return success(c, null)
})
+3
View File
@@ -51,6 +51,7 @@ export interface Entrant {
username: string
className: string | null
avatar: string | null
mood: string | null
/** 每道题第一次做对的时刻,升序;毫秒数用来比较,原文留着给出参 */
times: number[]
stamps: string[]
@@ -90,6 +91,7 @@ export async function loadEntrants(where: SQL, since: string | null): Promise<En
username: schema.user.username,
className: schema.user.className,
avatar: schema.userProfile.avatar,
mood: schema.userProfile.mood,
})
.from(schema.user)
.leftJoin(schema.userProfile, eq(schema.userProfile.userId, schema.user.id))
@@ -125,6 +127,7 @@ export async function loadEntrants(where: SQL, since: string | null): Promise<En
username: person.username,
className: person.className,
avatar: avatarOf(person.avatar),
mood: person.mood?.trim() || null,
times: pairs.map(([time]) => time),
stamps: pairs.map(([, stamp]) => stamp),
}