refactor(前端): shared / utils 不再 import oj 和 admin 下的任何东西

依赖方向原来是反的:顶栏、登录框、举手列表这些 shared 组件从 oj/ 里拿东西。
- 跨端的接口挪进 shared/api.ts(按 apps/web/CLAUDE.md 的分法):学生端题目列表、
  出题人列表、这节课的题、待弹的成就
- 这节课的 store → shared/store/lesson.ts;头像 RankAvatar → shared/components
- 纯函数挪进 utils/:举手列表的错法归类 errorKind、它依赖的答案错误分类 wrongAnswer
  和 Python 报错翻译 pythonError、题目列表行的 toProblemRow
- 只有后台在用的出题人下拉 AuthorSelect 挪回 admin/components

纯搬家。首页、题目列表、题目页、排名、课堂看板、后台题目列表打开无报错,
顶栏题号框敲 1001 回车直达题目页。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-16 15:11:21 +08:00
1 parent d4aadc7722
commit e8a20d12f9
38 files changed
+78 -78

No files matched your search

+107
View File
@@ -0,0 +1,107 @@
import { classifyDiff, type DiffKind } from "utils/wrongAnswer"
import { JUDGE_STATUS, SubmissionStatus } from "utils/constants"
import type { ClassBoardCell } from "utils/types"
/**
* 课堂看板:一道题错着的人「按错法归堆」(设计稿「课堂看板重设计」A+B)。老师讲错误时
* 要的是「这道题全班错在哪」:同一种错讲一遍就够,所以按最后一次提交归类,堆名要短,
* 一眼能读完 —— 长的中文解释在点开之后的弹层里(和提交详情同一份)。
*
* 依据是提交里存的 runtime_error / sample_check(判题机写的),和提交详情、题目页结果区同源。
*/
export type BoardLatest = NonNullable<ClassBoardCell["latest"]>
export interface ErrorKind {
/** 归堆用:同一个 key 是同一种错 */
key: string
label: string
/** 报错指到的行,答案错误没有 */
line: number | null
}
const DIFF_LABEL: Record<DiffKind, string> = {
"no-output": "什么都没输出",
whitespace: "只差空格、换行",
punctuation: "中英文标点不对",
case: "大小写不对",
extra: "多输出了东西",
missing: "少输出了东西",
"number-format": "小数位数不对",
values: "算出来的数不对",
other: "输出和例子不一样",
}
/** runtime-diagnosis.ts 细分出来的 kind 里常见的几种;没列的按异常类型归 */
const KIND_LABEL: Record<string, string> = {
"int-parse": "int() 转换失败",
"float-parse": "float() 转换失败",
unpack: "拆出来的个数对不上",
"unpack-number": "一个数拆给好几个变量",
"str-concat": "文字和数字用 + 连",
operand: "文字和数字一起算",
compare: "文字和数字比大小",
"not-int": "该给整数的地方不是整数",
"format-spec": "保留小数的格式写错",
"not-callable": "把不是函数的当函数用",
"func-value": "函数名后面忘了括号",
"none-value": "拿 None 接着用",
"map-value": "map() 的结果没转成列表",
}
const TYPE_LABEL: Record<string, string> = {
EOFError: "input() 用多了",
IndexError: "下标越界",
ZeroDivisionError: "除以 0",
KeyError: "字典里没有这个键",
RecursionError: "递归停不下来",
UnboundLocalError: "变量还没赋值就用",
ModuleNotFoundError: "导入了没有的模块",
ImportError: "导入了没有的模块",
ValueError: "值不符合要求",
TypeError: "类型不对",
}
/** C / C++ 只有信号:11 段错误、8 除零 */
const SIGNAL_LABEL: Record<number, string> = {
11: "数组越界或指针出错",
8: "除以 0",
}
export function errorKind(latest: BoardLatest): ErrorKind {
const result = latest.result
if (result === SubmissionStatus.wrong_answer) {
const check = latest.sampleCheck
if (!check) return { key: "wa", label: "答案错误", line: null }
if (check.passed) return { key: "wa-hidden", label: "例子都对,错在没公开的测试点", line: null }
const diff = classifyDiff(check.expected ?? "", check.output ?? "")
return { key: `wa-${diff}`, label: DIFF_LABEL[diff], line: null }
}
if (result === SubmissionStatus.runtime_error) {
const rt = latest.runtimeError
const line = rt?.line ?? null
if (!rt) return { key: "re", label: "运行时出错", line }
if (rt.signal && SIGNAL_LABEL[rt.signal]) {
return { key: `re-signal-${rt.signal}`, label: SIGNAL_LABEL[rt.signal]!, line }
}
if (rt.type === "NameError") {
return rt.name
? { key: `re-name-${rt.name}`, label: `「${rt.name}」没有定义`, line }
: { key: "re-name", label: "用了没定义的名字", line }
}
if (rt.kind && KIND_LABEL[rt.kind]) {
return { key: `re-${rt.kind}`, label: KIND_LABEL[rt.kind]!, line }
}
if (rt.type && TYPE_LABEL[rt.type]) {
return { key: `re-${rt.type}`, label: TYPE_LABEL[rt.type]!, line }
}
return { key: "re", label: "运行时出错", line }
}
if (result === SubmissionStatus.compile_error) {
return { key: "ce", label: "编译没通过", line: null }
}
return {
key: `r${result}`,
label: JUDGE_STATUS[result as keyof typeof JUDGE_STATUS]?.name ?? "没通过",
line: null,
}
}