依赖方向原来是反的:顶栏、登录框、举手列表这些 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>
108 lines
4.1 KiB
TypeScript
108 lines
4.1 KiB
TypeScript
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,
|
|
}
|
|
}
|