From b9ece7d38f24325d2081d3f103fdf112c84fa875 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Mon, 12 Oct 2026 21:31:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=BB=9F=E8=AE=A1):=20=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E5=9B=BE=E7=BB=9F=E8=AE=A1=E5=88=97=E5=87=BA=E3=80=8C=E6=9C=80?= =?UTF-8?q?=E5=B8=B8=E8=A7=81=E7=9A=84=E9=94=99=E3=80=8D=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E6=98=BE=E7=A4=BA=E6=96=B0=E8=AF=84=E6=B3=95=E4=B8=8B?= =?UTF-8?q?=E6=B2=A1=E6=84=8F=E4=B9=89=E7=9A=84=E5=B9=B3=E5=9D=87=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新评法(走例子)没有四项分、几乎没有评语,雷达图、各项平均分、词云都拼不出来, 页面上只剩一个等级圆环。改成: - 「最常见的错」:每张图第一组没走对的原因原样数一下(「『一级果』多输出了一行 『一级果』2 张」),最多 8 条;全是新数据时放在等级分布旁边,新老混着时顶替词云 - 只有新数据时「每人最好的一次」铺满一行、分几栏 - 去掉「平均分」:新评法的分数只是走对的比例,看它不如看「拿到 A / S」; 「每人最好的一次」也只写等级 老数据(2026-10 之前)照旧是四项分和词云。 Co-Authored-By: Claude Opus 5.5 --- apps/api/src/routes/flowchart.ts | 15 +++- .../oj/statistics/components/FlowStats.vue | 79 +++++++++++++++---- packages/contract/src/flowchart.ts | 6 ++ 3 files changed, 85 insertions(+), 15 deletions(-) diff --git a/apps/api/src/routes/flowchart.ts b/apps/api/src/routes/flowchart.ts index eadd4ee..3202008 100644 --- a/apps/api/src/routes/flowchart.ts +++ b/apps/api/src/routes/flowchart.ts @@ -610,6 +610,7 @@ flowchartRoutes.get("/flowcharts/statistics", requireTeacher, async (c) => { personCount: roster.length, completedCount: 0, wordFrequencies: [], + commonMistakes: [], // 一条提交都没有时,花名册上的人**全都**是「没做」—— 原来这里写死空数组, // 于是一节课刚开始、最该点名的时候,教师面板反而一个名字都不给 dataUnaccepted: roster.map((row) => ({ @@ -645,8 +646,16 @@ flowchartRoutes.get("/flowcharts/statistics", requireTeacher, async (c) => { const pushText = (value: string) => { if (texts.length < WORDCLOUD_TEXT_LIMIT) texts.push(value) } + const mistakes = new Map() for (const row of textRows) { - for (const [key, value] of Object.entries(asRecord(row.criteria))) { + const report = asRecord(row.criteria) + if (report.version === 2 && Array.isArray(report.cases)) { + const first = report.cases.map((item) => asRecord(item)).find((item) => item.ok === false) + if (typeof first?.why === "string" && first.why) { + mistakes.set(first.why, (mistakes.get(first.why) ?? 0) + 1) + } + } + for (const [key, value] of Object.entries(report)) { const detail = asRecord(value) // 和上面那条聚合同一道闸:分数不是数字的项当没配过,满分和评语也都不收 if (typeof detail.score !== "number") continue @@ -676,6 +685,10 @@ flowchartRoutes.get("/flowcharts/statistics", requireTeacher, async (c) => { personCount: roster.length, completedCount: totals.completedCount, wordFrequencies: await buildWordFrequencies(texts), + commonMistakes: [...mistakes] + .sort((a, b) => b[1] - a[1]) + .slice(0, 8) + .map(([text, n]) => ({ text, count: n })), dataUnaccepted: roster .filter((row) => !submitted.has(row.id)) .map((row) => ({ diff --git a/apps/web/src/oj/statistics/components/FlowStats.vue b/apps/web/src/oj/statistics/components/FlowStats.vue index 8c6464a..c4d6099 100644 --- a/apps/web/src/oj/statistics/components/FlowStats.vue +++ b/apps/web/src/oj/statistics/components/FlowStats.vue @@ -200,13 +200,11 @@ const people = computed(() => [ ...props.data.dataUnaccepted.map((row) => ({ username: row.username, grade: null as string | null, - score: null as number | null, count: 0, })), ...props.data.people.map((row) => ({ username: row.username, grade: row.bestGrade, - score: row.bestScore, count: row.count, })), ]) @@ -229,7 +227,6 @@ function gradeStyle(grade: string | null) {
-
-
-
四项评分(平均得分率)
-
- + + +
+
最常见的错(最近交的图里)
+
    +
  1. + {{ item.text }} + {{ item.count }} 张 +
  2. +
+
交上来的图都走对了
-
+ +
+
最常见的错(最近交的图里)
+
    +
  1. + {{ item.text }} + {{ item.count }} 张 +
  2. +
+
+
常见问题高频词(最近的评语里)
评语太少,还拼不出词云
-
+
每人最好的一次(没交的在前)
人太多,只列了成绩靠后的 500 位
- {{ row.grade ? `${row.grade} 级 ${row.score ?? 0} 分` : "没交" }} + {{ row.grade ? `${row.grade} 级` : "没交" }} {{ row.count }} 次
@@ -489,4 +509,35 @@ function gradeStyle(grade: string | null) { max-height: 360px; } } + +.mistakes { + margin: 0; + padding-left: 20px; + display: flex; + flex-direction: column; + gap: 8px; + font-size: 14px; + line-height: 1.6; +} + +.mistakes li { + padding-left: 2px; +} + +.mistake-text { + margin-right: 8px; +} + +/* 没有词云、四项分的时候,每人一格铺满一行 */ +.card.people.wide { + flex: 1 1 0; + width: auto; +} + +.card.people.wide .people-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + align-content: start; + column-gap: 24px; +} diff --git a/packages/contract/src/flowchart.ts b/packages/contract/src/flowchart.ts index d1cac84..0f2edf1 100644 --- a/packages/contract/src/flowchart.ts +++ b/packages/contract/src/flowchart.ts @@ -97,6 +97,12 @@ export const flowchartStatisticsSchema = z.object({ personCount: z.number().int(), completedCount: z.number().int(), wordFrequencies: z.array(z.object({ word: z.string(), count: z.number().int() })), + /** + * 2026-10 起「走例子」评的图里最常见的错:每张图取第一组没走对的原因(「『一级果』多输出了 + * 一行『一级果』」),同一句话数一下,最多 8 条。新评法几乎没有评语,词云拼不出来, + * 而同一个错在一个班里往往原样出现几十次 —— 老师要看的就是它 + */ + commonMistakes: z.array(z.object({ text: z.string(), count: z.number().int() })), // 与提交统计共用「未完成学生」的形状,见 submission.ts 的 unacceptedStudentSchema dataUnaccepted: z.array(z.object({ username: z.string(), realName: z.string() })), /**