feat(排名): 排名页重做成领奖台加全班赛道,一眼看出自己第几、该追谁、谁在追自己
Deploy / deploy (push) Canceled after 0s

- 一张榜两个开关:本班 / 本年级 / 全服 × 这周 / 这学期 / 全部,默认本班 · 这学期。
  原来最大那张全服 Top100 里,这学期在用的 25 级学生一个都进不去
- 做对一样多的不再按「提交少的在前」排,改成先做到的在前;名次旁边写比周一升降了几名
- 「你」那张卡:第几名、再做对几道上领奖台,要追的人早你几秒、追你的人晚你几秒
- 本年级 / 全服只列前面一段和你附近一段,中间折起来,点开再看
- 班级对抗按人均做对比,另有我和对手的名次走势、这周本班、每周冠军
- 老师不上榜,选班看;每行「⋯」能把怀疑抄代码的学生设成不计入排名(新增 user.rank_hidden_at,
  首页周榜同样不算他),随时能恢复
- 去掉全服 Top10、日活两张图、在线人数和班级详情弹框里的统计词,以及它们背后的旧接口

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-10 07:26:15 +08:00
1 parent 0cee4ef779
commit 1c6e2b9d82
31 files changed
+6633 -1392

No files matched your search

@@ -0,0 +1,164 @@
<script setup lang="ts">
import type { ClassBattleItem } from "@oj2/contract"
import { useThemeVars } from "naive-ui"
import { classLabel } from "oj/submission/utils"
import { useRankPalette } from "../palette"
/**
* 班级对抗:全服各班这学期人均做对,绿字是这周人均涨了多少。按人均比,因为班级人数从
* 十来个到五十几个都有。列前 7 个班,自己的班不在里面就补在最后。
*/
const props = defineProps<{ items: ClassBattleItem[]; mine: string | null; teacher?: boolean }>()
const theme = useThemeVars()
const palette = useRankPalette()
const LIMIT = 7
const shown = computed(() => {
const head = props.items.slice(0, LIMIT)
const own = props.items.find((item) => item.className === props.mine)
return own && own.rank > LIMIT ? [...head, own] : head
})
const max = computed(() => Math.max(1, ...props.items.map((item) => item.perCapita)))
const summary = computed(() => {
const own = props.items.find((item) => item.className === props.mine)
if (!own) return ""
const who = props.teacher ? "这个班" : "你们班"
const faster = props.items.filter((item) => item.weekGain > own.weekGain).length + 1
const speed = own.weekGain > 0 ? ` · 这周涨得第 ${faster} 快` : ""
if (own.rank === 1) return `${who}第 1${speed}`
const prev = props.items[own.rank - 2]!
const need = Math.floor(prev.perCapita - own.perCapita) + 1
return `${who}第 ${own.rank} · 每人再做对 ${need} 道就超过${shortLabel(prev.className)}${speed}`
})
/** 同年级的班只说「3 班」,别的年级说全名 */
function shortLabel(className: string) {
if (props.mine && className.slice(0, 2) === props.mine.slice(0, 2))
return ` ${className.slice(2)} 班`
return ` ${classLabel(className)}`
}
</script>
<template>
<div class="battle">
<div class="title">
<b>班级对抗</b><span class="muted">人均做对 · 全服 · 绿字是这周涨的</span>
</div>
<div
v-for="item in shown"
:key="item.className"
class="row"
:class="{ mine: item.className === mine }"
>
<span class="rank">{{ item.rank }}</span>
<span class="name">{{ classLabel(item.className) }}</span>
<span class="bar-box">
<span
class="bar"
:style="{
width: `${(item.perCapita / max) * 100}%`,
background: item.className === mine ? palette.me : palette.bar,
}"
/>
</span>
<span class="value">{{ item.perCapita }}</span>
<span class="gain">{{ item.weekGain > 0 ? `+${item.weekGain}` : "" }}</span>
</div>
<span v-if="!items.length" class="muted">这学期还没有班做对题</span>
<span v-if="summary" class="summary">{{ summary }}</span>
</div>
</template>
<style scoped>
.battle {
padding: 12px 16px;
display: flex;
flex-direction: column;
gap: 1px;
border: 1px solid v-bind("theme.borderColor");
border-radius: 6px;
background: v-bind("theme.cardColor");
}
.title {
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 4px;
}
.muted {
font-size: 12px;
color: v-bind("theme.textColor3");
}
.row {
height: 22px;
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.row.mine {
margin: 0 -8px;
padding: 0 8px;
border-radius: 4px;
background: rgba(24, 160, 88, 0.12);
}
.rank {
width: 14px;
font-size: 12px;
color: v-bind("theme.textColor3");
font-variant-numeric: tabular-nums;
}
.name {
width: 86px;
white-space: nowrap;
}
.mine .name {
font-weight: 700;
color: #18a058;
}
.bar-box {
flex-grow: 1;
height: 10px;
position: relative;
}
.bar {
position: absolute;
left: 0;
top: 0;
bottom: 0;
border-radius: 2px 5px 5px 2px;
}
.value {
width: 32px;
text-align: right;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.gain {
width: 34px;
font-size: 11px;
color: #18a058;
font-variant-numeric: tabular-nums;
}
.summary {
margin-top: 6px;
font-size: 12px;
color: #18a058;
}
</style>