feat: 同学作品和批改都能看全部班级,批改默认大图预览
Deploy / deploy (build, debian, 22) (push) Canceled after 0s

- 学生“同学作品”加“本班 / 全部班级”切换(peers?scope=all),作品标班级,仍是交过才开放、打分前匿名;
  服务端放开:交过某题后能看任何班同学在这道题上的提交
- 批改页班级下拉加“全部班级”:一道题全校作品按班级分组,没人交的班收起成一行;
  /review/class、/review/progress 不传 classname 就是全部班级
- 点开作品默认大图:作品墙收起,预览占满中间,打分 / 课上展示 / 提示词放右栏;
  Esc 返回作品墙,可切回小面板(记住选择);成绩册弹出的详情也用大图布局

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-27 09:22:41 +08:00
1 parent 8653d411a6
commit 75e20a3ae1
11 files changed
+265 -68

No files matched your search

+44 -24
View File
@@ -14,6 +14,15 @@ function requireStaff(user: User) {
// 和成绩册一致:班里的学生包括当助教的 admin
const CLASS_MEMBERS = `SELECT id FROM account_user
WHERE classname = $classname AND role IN ('normal', 'admin') AND is_active = 1`
// 不传班级时:所有有班级的学生
const ALL_MEMBERS = `SELECT id FROM account_user
WHERE classname != '' AND role IN ('normal', 'admin') AND is_active = 1`
function members(classname: string | undefined) {
return classname
? { sql: CLASS_MEMBERS, params: { classname } as Record<string, unknown> }
: { sql: ALL_MEMBERS, params: {} as Record<string, unknown> }
}
// 每个人在这个任务上最新的一次提交,以及提交了几次。
// 先只在 slim 覆盖索引上找,再按 rowid 读这几行的完整内容(代码是宽列,见 CLAUDE.md)
@@ -64,17 +73,16 @@ router.get("/options", (c) => {
})
// 一个班每个任务交了几个人
// 不传 classname 就是全部班级
router.get("/progress", (c) => {
requireStaff(requireUser(c))
const { classname } = query(c, z.object({ classname: z.string().min(1) }))
const studentCount = get<{ n: number }>(
`SELECT COUNT(*) AS n FROM (${CLASS_MEMBERS})`,
{ classname },
)!.n
const { classname } = query(c, z.object({ classname: z.string().min(1).optional() }))
const m = members(classname)
const studentCount = get<{ n: number }>(`SELECT COUNT(*) AS n FROM (${m.sql})`, m.params)!.n
const rows = all<{ task_id: number; n: number }>(
`SELECT s.task_id, COUNT(DISTINCT s.user_id) AS n FROM submission_submission s
WHERE s.user_id IN (${CLASS_MEMBERS}) GROUP BY s.task_id`,
{ classname },
WHERE s.user_id IN (${m.sql}) GROUP BY s.task_id`,
m.params,
)
return c.json({
student_count: studentCount,
@@ -86,15 +94,18 @@ router.get("/progress", (c) => {
router.get("/class", (c) => {
const user = requireUser(c)
requireStaff(user)
const q = query(c, z.object({ classname: z.string().min(1), task_id: z.coerce.number().int() }))
orNotFound(get(`SELECT 1 FROM task_task WHERE id = $id`, { id: q.task_id }), "任务不存在")
const students = all<{ id: number; username: string }>(
`SELECT id, username FROM account_user
WHERE classname = $classname AND role IN ('normal', 'admin') AND is_active = 1
ORDER BY username, id`,
{ classname: q.classname },
const q = query(
c,
z.object({ classname: z.string().min(1).optional(), task_id: z.coerce.number().int() }),
)
const latest = latestByUser(q.task_id, CLASS_MEMBERS, { classname: q.classname })
orNotFound(get(`SELECT 1 FROM task_task WHERE id = $id`, { id: q.task_id }), "任务不存在")
const m = members(q.classname)
const students = all<{ id: number; username: string; classname: string }>(
`SELECT id, username, classname FROM account_user WHERE id IN (${m.sql})
ORDER BY classname, username, id`,
m.params,
)
const latest = latestByUser(q.task_id, m.sql, m.params)
const rows = loadRows([...latest.values()].map((l) => l.rid), user.id)
return c.json(
students.map((s) => {
@@ -102,6 +113,7 @@ router.get("/class", (c) => {
return {
user_id: s.id,
username: s.username,
classname: s.classname,
submission: r
? {
id: uuidOut(r.id),
@@ -122,33 +134,41 @@ router.get("/class", (c) => {
)
})
// 学生看本班同学在这道题上的作品:自己交过才开放;自己打分之前不显示作者和得分
// 学生看同学在这道题上的作品:自己交过才开放;自己打分之前不显示作者和得分。
// scope=class 只看本班(默认,一个班几十份),scope=all 看全校所有班
router.get("/peers/:task_id", (c) => {
const user = requireUser(c)
const taskId = intParam(c, "task_id")
const { scope } = query(c, z.object({ scope: z.enum(["class", "all"]).default("class") }))
orNotFound(get(`SELECT 1 FROM task_task WHERE id = $id`, { id: taskId }), "任务不存在")
const submitted = get(
`SELECT 1 FROM submission_submission WHERE user_id = $me AND task_id = $task_id LIMIT 1`,
{ me: user.id, task_id: taskId },
)
if (!submitted || !user.classname) return c.json({ locked: true, items: [] })
if (!submitted) return c.json({ locked: true, items: [] })
const byClass = scope === "class" && !!user.classname
const members = `SELECT id FROM account_user
WHERE classname = $classname AND role = 'normal' AND is_active = 1 AND id != $me`
const params = { classname: user.classname, me: user.id }
WHERE role = 'normal' AND is_active = 1 AND id != $me
${byClass ? "AND classname = $classname" : ""}`
const params = byClass ? { classname: user.classname, me: user.id } : { me: user.id }
const latest = latestByUser(taskId, members, params)
const rows = loadRows([...latest.values()].map((l) => l.rid), user.id)
const names = new Map(
all<{ id: number; username: string }>(`SELECT id, username FROM account_user WHERE id IN (${members})`, params).map(
(u) => [u.id, u.username],
),
const people = new Map(
all<{ id: number; username: string; classname: string }>(
`SELECT id, username, classname FROM account_user WHERE id IN (${members})`,
params,
).map((u) => [u.id, u]),
)
const items = [...rows.values()]
.sort((a, b) => (a.created < b.created ? 1 : -1))
.map((r) => {
const rated = (r.my_score || 0) > 0
const p = people.get(r.user_id)
return {
id: uuidOut(r.id),
username: rated ? names.get(r.user_id) : null,
username: rated ? p?.username : null,
// 班级不算身份信息,打分前也显示
classname: p?.classname ?? "",
score: rated ? r.score : null,
my_score: r.my_score || 0,
created: iso(r.created),
+3 -6
View File
@@ -37,16 +37,13 @@ const router = new Hono<Env>()
router.route("/showcase", showcase)
router.route("/review", review)
// 学生能看的提交:自己的、进了作品展的、本班同学在自己也交过的题上的。老师都能看
// 学生能看的提交:自己的、进了作品展的、自己也交过的那道题上同学(任何班)的。老师都能看
function canView(user: User, sub: { id: string; user_id: number; task_id: number }) {
if (isStaff(user) || sub.user_id === user.id) return true
if (get(`SELECT 1 FROM submission_submissionaward WHERE submission_id = $id`, { id: sub.id })) return true
if (!user.classname) return false
return !!get(
`SELECT 1 FROM account_user u
WHERE u.id = $owner AND u.classname = $classname
AND EXISTS (SELECT 1 FROM submission_submission s WHERE s.user_id = $me AND s.task_id = $task_id)`,
{ owner: sub.user_id, classname: user.classname, me: user.id, task_id: sub.task_id },
`SELECT 1 FROM submission_submission s WHERE s.user_id = $me AND s.task_id = $task_id LIMIT 1`,
{ me: user.id, task_id: sub.task_id },
)
}