diff --git a/apps/web/src/oj/api.ts b/apps/web/src/oj/api.ts index 6cee907..df610b5 100644 --- a/apps/web/src/oj/api.ts +++ b/apps/web/src/oj/api.ts @@ -14,9 +14,10 @@ import { type ClassBoard, type ClassLesson, type KnowledgeMap, - type ClassComparisonResponse, type ClassBattleItem, type ClassDetail, + type ClassPk, + type ClassPkPeriod, type RankBoard, type RankPeriod, type RankScope, @@ -304,11 +305,10 @@ export function getLastVisit() { return api.get("me/last-visit") } -export function getClassPK(classNames: string[], startTime?: string, endTime?: string) { - return api.post("classes/comparison", { - classNames, - ...(startTime ? { startTime } : {}), - ...(endTime ? { endTime } : {}), +/** 班级 PK:不给班就用自己的班(老师用默认的班),只给一个班就配一个同年级的对手 */ +export function getClassPk(classes: string[], period: ClassPkPeriod) { + return api.get("rankings/pk", { + params: { period, ...(classes.length ? { classes: classes.join(",") } : {}) }, }) } diff --git a/apps/web/src/oj/class/components/PkDuel.vue b/apps/web/src/oj/class/components/PkDuel.vue new file mode 100644 index 0000000..ddec7e3 --- /dev/null +++ b/apps/web/src/oj/class/components/PkDuel.vue @@ -0,0 +1,808 @@ + + + + + diff --git a/apps/web/src/oj/class/components/PkGrid.vue b/apps/web/src/oj/class/components/PkGrid.vue new file mode 100644 index 0000000..96a48f3 --- /dev/null +++ b/apps/web/src/oj/class/components/PkGrid.vue @@ -0,0 +1,625 @@ + + + + + diff --git a/apps/web/src/oj/class/components/PkHist.vue b/apps/web/src/oj/class/components/PkHist.vue new file mode 100644 index 0000000..215f45f --- /dev/null +++ b/apps/web/src/oj/class/components/PkHist.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/apps/web/src/oj/class/components/PkRace.vue b/apps/web/src/oj/class/components/PkRace.vue new file mode 100644 index 0000000..8a9f2e2 --- /dev/null +++ b/apps/web/src/oj/class/components/PkRace.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/apps/web/src/oj/class/pk.vue b/apps/web/src/oj/class/pk.vue index 85b6a64..21903f0 100644 --- a/apps/web/src/oj/class/pk.vue +++ b/apps/web/src/oj/class/pk.vue @@ -1,915 +1,543 @@ + AI 分析 + + +
+ + + + +
+
+ + +
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/apps/web/src/oj/class/utils.ts b/apps/web/src/oj/class/utils.ts new file mode 100644 index 0000000..2ab0a62 --- /dev/null +++ b/apps/web/src/oj/class/utils.ts @@ -0,0 +1,59 @@ +import type { ClassPk, ClassPkCell } from "@oj2/contract" +import { classLabel } from "oj/submission/utils" + +/** + * 每个班一种颜色,按选班的顺序给。蓝 / 橙打头:两个班对决时最常见,这两个色盲也分得开; + * 后面几个亮度错开,对照格的深浅才看得出来。 + */ +const COLORS = [ + "#2f6fd0", + "#e0702f", + "#18a058", + "#8e5bd0", + "#b8901a", + "#d0457a", + "#0f8f9f", + "#5f6b7a", +] + +export function pkColor(index: number) { + return COLORS[index % COLORS.length]! +} + +/** 颜色加透明度:对照格按做对比例取深浅,比分卡两头的底色也用它(暗色下一样能用) */ +export function tint(hex: string, alpha: number) { + const r = parseInt(hex.slice(1, 3), 16) + const g = parseInt(hex.slice(3, 5), 16) + const b = parseInt(hex.slice(5, 7), 16) + return `rgba(${r}, ${g}, ${b}, ${alpha})` +} + +/** 同一个年级的几个班只说「3班」,跨年级说全名 */ +export function shortLabels(pk: ClassPk) { + const grades = new Set(pk.classes.map((item) => item.className.slice(0, 2))) + return pk.classes.map((item) => + grades.size === 1 ? `${item.className.slice(2)}班` : classLabel(item.className), + ) +} + +/** + * 你们班再有几个人做对,这道就能和 `target` 打平。比的是显示出来的整数百分比 + * (后端 `percent` 就是这么比输赢的),所以按同样的取整往上加。 + */ +export function needToTie(cell: ClassPkCell, members: number, target: number) { + let more = 0 + while ( + Math.round(((cell.solved + more) / members) * 100) < target && + cell.solved + more < members + ) + more++ + return more +} + +/** 一档同分人最多、且至少 5 个人,才说「N 个人停在 x 道」 */ +export function plateau(values: number[]) { + const counts = new Map() + for (const value of values) if (value > 0) counts.set(value, (counts.get(value) ?? 0) + 1) + const [top] = [...counts].filter(([, n]) => n >= 5).sort((a, b) => b[1] - a[1] || b[0] - a[0]) + return top ? { solved: top[0], count: top[1] } : null +} diff --git a/apps/web/src/oj/rank/components/ClassBattle.vue b/apps/web/src/oj/rank/components/ClassBattle.vue index 3512eae..c877733 100644 --- a/apps/web/src/oj/rank/components/ClassBattle.vue +++ b/apps/web/src/oj/rank/components/ClassBattle.vue @@ -14,7 +14,7 @@ const props = defineProps<{ teacher?: boolean /** 放在手机页签卡里:不要自己的边框和标题 */ bare?: boolean - /** 显示「班级 PK」入口(原来排名页上那个按钮,管理员角色才有) */ + /** 显示「班级 PK」入口(登录了就有:学生也能比) */ pk?: boolean }>() const emit = defineEmits<{ pick: [className: string] }>() @@ -56,7 +56,12 @@ function shortLabel(className: string) {
班级对抗人均做对 · 全服 · 点班名看详情 - 班级 PK › + 班级 PK ›
diff --git a/apps/web/src/oj/rank/list.vue b/apps/web/src/oj/rank/list.vue index ec410c1..996451e 100644 --- a/apps/web/src/oj/rank/list.vue +++ b/apps/web/src/oj/rank/list.vue @@ -480,7 +480,17 @@ async function restore(userId: number) { > 班级详情 · AI 分析 - 班级 PK + 班级 PK
不计入排名的人 @@ -506,7 +516,7 @@ async function restore(userId: number) { :items="battle" :mine="classContext" :teacher="teacher" - :pk="userStore.isAdminRole" + :pk="userStore.isAuthed" @pick="openDetail" /> @@ -560,7 +570,7 @@ async function restore(userId: number) { :items="battle" :mine="classContext" :teacher="teacher" - :pk="userStore.isAdminRole" + :pk="userStore.isAuthed" @pick="openDetail" />
diff --git a/apps/web/src/routes.ts b/apps/web/src/routes.ts index 70aa19e..2309735 100644 --- a/apps/web/src/routes.ts +++ b/apps/web/src/routes.ts @@ -93,6 +93,7 @@ export const ojs: RouteRecordRaw = { path: "class", name: "class", component: () => import("oj/class/pk.vue"), + meta: { requiresAuth: true }, }, { path: "announcement", diff --git a/apps/web/src/utils/types.ts b/apps/web/src/utils/types.ts index 0a0441f..39653e8 100644 --- a/apps/web/src/utils/types.ts +++ b/apps/web/src/utils/types.ts @@ -275,8 +275,6 @@ export type Rank = RankProfile /** 本周进步榜:`rank` 是周榜名次,跟存量总榜的名次没有关系 */ export type { WeeklyRank, WeeklyRankItem } from "@oj2/contract" -export type { ClassComparison } from "@oj2/contract" - /** 班里最近一次一起做的题(课上老师点的那几道),见后端 `/me/class-activity` */ export type { ClassActivity, ClassActivityProblem, LastVisit } from "@oj2/contract"