diff --git a/client/src/components/task/TaskStatsModal.vue b/client/src/components/task/TaskStatsModal.vue
index 24a2802..e63c0ff 100644
--- a/client/src/components/task/TaskStatsModal.vue
+++ b/client/src/components/task/TaskStatsModal.vue
@@ -3,452 +3,526 @@
:show="show"
@update:show="$emit('update:show', $event)"
preset="card"
- title="提交统计"
- style="max-width: 660px"
+ style="width: 90vw; max-width: 760px"
:bordered="false"
>
-
-
-
-
-
-
-
+
+
+ 课堂总结
+ {{ title }}
+
+
+
+
+
-
-
-
- 班级筛选:
- 全部
- {{ c }}
-
+
+
+
-
-
-
-
-
- {{ metric.value }}
-
-
- {{ metric.label }}
+
+
+
+
+
+
已交
+
+ {{ stats.submitted_count }} / {{ total }} 人
+ {{ pct(stats.submitted_count, total) }}
+
+
+
+
+
+
+
平均分
+
{{ stats.average_score?.toFixed(1) ?? "—" }}
+
每人最新一版
+
+
+
改过再交
+
{{ pct(multiSubmit, stats.submitted_count) }}
+
交了 2 次及以上
+
+
+
待讲评
+
{{ stats.present_count }} 份
+
已加入课上展示
+
+
+
+
+
+
+
+ 未交
+ {{ stats.unsubmitted_count }} 人
+
+
+
+ {{ g.classname || "未分班" }}
+
+ {{ n }}
+
+
+
都交了
+
+
+
+
+
+
+ 交了但还没人评
+ {{ stats.unrated_count }} 人
+
+
+
+ {{ g.classname || "未分班" }}
+
+ {{ n }}
+
+
+
+ {{ stats.submitted_count ? "每份作品都有人评过" : "还没有人交" }}
-
+
-
-
-
-
-
- 未提交({{ stats.unsubmitted_count }}人)
-
-
+
+
+ 交了几次
+
+
+ {{ b.label }}
+
+
+
+ {{ b.value }}
+
-
- {{ displayName(u.username, u.classname) }}
- 暂无
-
-
+
还没有人交
+
-
-
-
-
-
- 未打分({{ stats.unrated_count }}人)
- — 还没给任何提交打过分
-
-
-
-
- {{ displayName(u.username, u.classname) }}
- 暂无
-
-
-
-
-
-
- 提交次数分布
- (已提交的 {{ stats.submitted_count }} 人)
-
-
-
-
- {{ bucket.value }}
-
-
- {{ bucket.label }}
-
-
-
- {{ bucketPct(bucket.value) }}
-
-
-
-
-
-
-
-
- 人气前五
-
-
- 暂无
-
-
-
+
+ 高分作品每人最高的一版
+
+
+ {{ item.score.toFixed(1) }}
+
+
-
-
-
-
+
还没有打过分的作品
+
+
+
+
+
+
+
+
+ 去批改{{ isAll ? "" : " " + classname + " 班" }}
+
+
+
+
+
+
diff --git a/client/src/pages/ChallengeDetail.vue b/client/src/pages/ChallengeDetail.vue
index e90318c..48bb7b3 100644
--- a/client/src/pages/ChallengeDetail.vue
+++ b/client/src/pages/ChallengeDetail.vue
@@ -25,7 +25,7 @@
- 本题统计
+ 课堂总结
@@ -53,7 +53,7 @@
-
+
{
else dist.count_4_plus++
}
- // 标记统计:该任务所有提交(不按学生过滤)
- const flagStats = { red: 0, blue: 0, green: 0, yellow: 0 }
- for (const s of subs) {
- if (s.flag && s.flag in flagStats) flagStats[s.flag as keyof typeof flagStats]++
+ // 高分作品:每人分数最高的一份(同分取较早的),取前五
+ const best = new Map()
+ for (const s of studentSubs) {
+ const cur = best.get(s.user_id)
+ if (s.score > 0 && (!cur || s.score > cur.score || (s.score === cur.score && s.created < cur.created)))
+ best.set(s.user_id, s)
}
-
- const topViewed = classname
- ? all(
- `SELECT s.id, s.view_count, u.username, u.classname ${SUBMISSION_FROM}
- WHERE s.task_id = $id AND u.role = 'normal' AND u.classname = $classname
- ORDER BY s.view_count DESC LIMIT 5`,
- { id: taskId, classname },
- )
- : all(
- `SELECT s.id, s.view_count, u.username, u.classname ${SUBMISSION_FROM}
- WHERE s.task_id = $id AND u.role = 'normal'
- ORDER BY s.view_count DESC LIMIT 5`,
- { id: taskId },
- )
+ const studentById = new Map(students.map((s) => [s.id, s]))
+ const topScored = [...best.values()]
+ .sort((a, b) => b.score - a.score || (a.created < b.created ? -1 : 1))
+ .slice(0, 5)
+ .map((s) => ({ ...tag(studentById.get(s.user_id)!), score: s.score, submission_id: uuidOut(s.id) }))
const unrated = students.filter((s) => submittedIds.has(s.id) && !ratedIds.has(s.id))
return c.json({
@@ -477,14 +470,9 @@ router.get("/stats/:task_id", (c) => {
unsubmitted_users: students.filter((s) => !submittedIds.has(s.id)).map(tag),
unrated_users: unrated.map(tag),
submission_count_distribution: dist,
- flag_stats: flagStats,
+ present_count: studentSubs.filter((s) => s.flag === "present").length,
classes,
- top_viewed: topViewed.map((s) => ({
- username: s.username,
- classname: s.classname,
- view_count: s.view_count,
- submission_id: uuidOut(s.id),
- })),
+ top_scored: topScored,
})
})
diff --git a/server/tests/api.test.ts b/server/tests/api.test.ts
index 7f6300d..d150648 100644
--- a/server/tests/api.test.ts
+++ b/server/tests/api.test.ts
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, test } from "bun:test"
-import { get, all } from "../src/db"
+import { get, all, run } from "../src/db"
import { app } from "../src/app"
import { config } from "../src/config"
import { mkdtemp, readFile, readdir, rm } from "node:fs/promises"
@@ -609,3 +609,40 @@ describe("同学作品与批改", () => {
expect((await teacher("PUT", `/api/submission/${uuid(latest)}/flag`, { flag: "present" })).json.flag).toBe("present")
})
})
+
+describe("课堂总结", () => {
+ test("按班级统计,高分作品每人只取最高的一份", async () => {
+ makeUser("adm", "admin")
+ const task = makeTask("挑战", "challenge", 1)
+ const a = makeUser("weba1小明", "normal", "a1")
+ const b = makeUser("weba1小红", "normal", "a1")
+ makeUser("weba1小刚", "normal", "a1")
+ const other = makeUser("weba2小李", "normal", "a2")
+ makeSubmission(a, task, 3, "2026-01-01 00:00:00.000000")
+ const aBest = makeSubmission(a, task, 4.5, "2026-01-02 00:00:00.000000")
+ makeSubmission(a, task, 4, "2026-01-03 00:00:00.000000")
+ const bSub = makeSubmission(b, task, 0)
+ run(`UPDATE submission_submission SET flag = 'present' WHERE id = $id`, { id: bSub })
+ makeSubmission(other, task, 5)
+
+ const adm = await client("adm")
+ const res = await adm("GET", `/api/submission/stats/${task}?classname=a1`)
+ expect(res.status).toBe(200)
+ expect(res.json).toMatchObject({
+ submitted_count: 2,
+ unsubmitted_count: 1,
+ average_score: 4,
+ unrated_count: 2,
+ present_count: 1,
+ classes: ["a1", "a2"],
+ submission_count_distribution: { count_1: 1, count_2: 0, count_3: 1, count_4_plus: 0 },
+ })
+ expect(res.json.unsubmitted_users).toEqual([{ username: "weba1小刚", classname: "a1" }])
+ expect(res.json.top_scored).toEqual([
+ { username: "weba1小明", classname: "a1", score: 4.5, submission_id: uuid(aBest) },
+ ])
+
+ const all = await adm("GET", `/api/submission/stats/${task}`)
+ expect(all.json.top_scored.map((t: any) => t.username)).toEqual(["weba2小李", "weba1小明"])
+ })
+})