From 75e20a3ae1e71f024ae04f6c8528967aca753878 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 27 Oct 2026 09:22:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=8C=E5=AD=A6=E4=BD=9C=E5=93=81?= =?UTF-8?q?=E5=92=8C=E6=89=B9=E6=94=B9=E9=83=BD=E8=83=BD=E7=9C=8B=E5=85=A8?= =?UTF-8?q?=E9=83=A8=E7=8F=AD=E7=BA=A7=EF=BC=8C=E6=89=B9=E6=94=B9=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=A4=A7=E5=9B=BE=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 学生“同学作品”加“本班 / 全部班级”切换(peers?scope=all),作品标班级,仍是交过才开放、打分前匿名; 服务端放开:交过某题后能看任何班同学在这道题上的提交 - 批改页班级下拉加“全部班级”:一道题全校作品按班级分组,没人交的班收起成一行; /review/class、/review/progress 不传 classname 就是全部班级 - 点开作品默认大图:作品墙收起,预览占满中间,打分 / 课上展示 / 提示词放右栏; Esc 返回作品墙,可切回小面板(记住选择);成绩册弹出的详情也用大图布局 Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 6 +- client/src/api.ts | 10 +-- client/src/components/review/PeersDrawer.vue | 46 +++++++++--- client/src/components/review/ReviewPanel.vue | 75 +++++++++++++++++-- client/src/icons.ts | 4 ++ client/src/pages/Gradebook.vue | 4 +- client/src/pages/Review.vue | 76 +++++++++++++++++--- client/src/utils/type.ts | 2 + server/src/routes/review.ts | 68 +++++++++++------- server/src/routes/submission.ts | 9 +-- server/tests/api.test.ts | 33 +++++++-- 11 files changed, 265 insertions(+), 68 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b0f6938..6c84c85 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,8 +15,8 @@ The backend was rewritten from Django in 2026-10 to cut memory use (~25 MB vs. 1 - `src/app.ts` — mounts routers under `/api/`, CORS, auth middleware, error → `{ detail }` JSON (ninja-compatible; validation errors are 422) - `src/db.ts` — schema (`CREATE TABLE IF NOT EXISTS`, same DDL as Django), `all/get/run/tx` helpers using `$name` params, time (`now()`, `iso()`) and UUID (`uuidHex()`, `uuidOut()`) conversions - `src/auth.ts` — Django-compatible password hashing; JWT login, no cookies: `POST /account/login` returns a 15-min HS256 `access_token` (sent as `Authorization: Bearer`, signed with `JWT_SECRET`, random per process if unset) and a 14-day opaque `refresh_token` (only its sha256 is stored, in `auth_refresh_token`; `POST /account/refresh` trades it for a new access token, logout / password change / deactivation delete it). An invalid or expired Bearer token is a 401 even on public endpoints so the client knows to refresh. `requireUser/requireAdmin/requireSuper` (401 / 403) - - `src/routes/` — `account`, `tasks` (tutorial / challenge / creation), `files` (task assets + image upload), `submission`, `showcase` (mounted under `/submission/showcase`), `review` (mounted under `/submission/review`: teacher class × task wall, students' same-class peer works), `prompt` - - Who can read a submission (`canView` in `routes/submission.ts`): staff, the owner, anyone if it's in the showcase, or a same-class student who has submitted the same task. Students' `GET /submission/` only lists their own. Peer works and the random-rating item are anonymous until the viewer rates them (`PUT /:id/score` returns the author). `flag` now only carries `present` (teacher's in-class presentation queue); the old four colors are no longer shown + - `src/routes/` — `account`, `tasks` (tutorial / challenge / creation), `files` (task assets + image upload), `submission`, `showcase` (mounted under `/submission/showcase`), `review` (mounted under `/submission/review`: teacher class × task wall — no `classname` means all classes —, students' peer works with `scope=class|all`), `prompt` + - Who can read a submission (`canView` in `routes/submission.ts`): staff, the owner, anyone if it's in the showcase, or any student who has submitted the same task (any class). Students' `GET /submission/` only lists their own. Peer works and the random-rating item are anonymous until the viewer rates them (`PUT /:id/score` returns the author). `flag` now only carries `present` (teacher's in-class presentation queue); the old four colors are no longer shown - `src/services/` — `score.ts` (rating weights super×0.5, admin×0.3, normal×0.2, then Bayesian smoothing with C=3 and zones), `gradebook.ts`, `llm.ts` (OpenAI-compatible streaming via fetch; DeepSeek default, ARK for doubao models; chat context is the last 20 messages with older code blocks elided), `classifier.ts` (Bloom-level L1–L6 prompt tagging, runs in background), `cascade.ts` (Django's CASCADE deletes done by hand, plus the shared delete rules: `hasRatingsOrAwards`, `deleteRound`), `pyround.ts` (`pyRound`/`pySum` reproduce Python's `round()` and `sum()` so scores match the old system bit-for-bit) - `src/ws.ts` — `/ws/prompt//?token=` (code generation chat; the AI context and the page's chat include `conversation`, `edit` and `manual` messages) and `/ws/guidance//?token=...` (prompt coaching); one message per connection at a time (a message sent while the AI is still replying gets an error, payloads capped at 64 KB); on disconnect the pending user message is deleted and the LLM request aborted - `scripts/` — one-off CLIs: `create-superuser.ts`, `classify-prompts.ts`, `recalculate-scores.ts` @@ -32,7 +32,7 @@ The backend was rewritten from Django in 2026-10 to cut memory use (~25 MB vs. 1 - `src/icons.ts` — `` renders bundled `lucide-vue-next` icons (works offline); register new names in its map. Fonts are bundled too (`@fontsource/jetbrains-mono`) - `components/workbench/Workbench.vue` — shared challenge / creation page: left = brief + AI chat (`PromptPanel`), right = preview / code (`CodeTabs`) + `VersionBar`. Every assistant message with code is a version; hand edits and pasted external-AI code are saved as versions via `POST /prompt/versions` (`source` `edit` / `manual`, stored as a user+assistant pair). Challenges are submitted only when the student clicks “交这一版” (`POST /submission/` with `message_id`), never automatically. Editor code is per task (`composables/useTaskCode.ts`, localStorage `web-code::`); the tutorial page edits the shared `store/editors.ts` refs but `Workspace.vue` saves them under the same per-task key and `TutorialContent` restores them (example code only the first time) - `pages/ChallengeHome.vue` — challenge list page; `pages/TaskEditor.vue` (prop `kind`) is the single tutorial / challenge authoring page - - `pages/Review.vue` (“批改”, default teaching page) + `components/review/ReviewPanel.vue` (also opened from the gradebook) — keys: J/K next/prev student, 1–5 rate, P present; `components/review/PeersDrawer.vue` is the students' “同学作品” + - `pages/Review.vue` (“批改”, default teaching page) + `components/review/ReviewPanel.vue` (also opened from the gradebook) — opens as a large preview (wall hidden, Esc returns; “小面板” switches to a side panel, remembered); keys: J/K next/prev student, 1–5 rate, P present; “全部班级” groups the wall by class; `components/review/PeersDrawer.vue` is the students' “同学作品” (本班 / 全部班级) - `components/layout/AppShell.vue` — top bar for every page; `pages/Dashboard.vue` is the teaching sidebar (“教学”) under `/dashboard` - `src/store/` — Reactive state (plain Vue refs, not Vuex/Pinia); `prompt.ts` / `guidance.ts` own the WebSockets - `src/pages/`, `src/components/` — user management uses `components/dashboard/UserFormModal.vue` (there is no Django Admin anymore) diff --git a/client/src/api.ts b/client/src/api.ts index b86a40d..142ab91 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -465,18 +465,20 @@ export const Review = { async options(): Promise { return (await http.get("/submission/review/options")).data }, - async progress(classname: string): Promise { + // classname 不传就是全部班级 + async progress(classname?: string): Promise { return (await http.get("/submission/review/progress", { params: { classname } })).data }, - async classWall(classname: string, taskId: number): Promise { + async classWall(classname: string | undefined, taskId: number): Promise { return ( await http.get("/submission/review/class", { params: { classname, task_id: taskId }, }) ).data }, - async peers(taskId: number): Promise { - return (await http.get(`/submission/review/peers/${taskId}`)).data + // scope:class 只看本班,all 看所有班 + async peers(taskId: number, scope: "class" | "all" = "class"): Promise { + return (await http.get(`/submission/review/peers/${taskId}`, { params: { scope } })).data }, } diff --git a/client/src/components/review/PeersDrawer.vue b/client/src/components/review/PeersDrawer.vue index aaace7a..45feee3 100644 --- a/client/src/components/review/PeersDrawer.vue +++ b/client/src/components/review/PeersDrawer.vue @@ -10,9 +10,15 @@ style="margin-top: 40px" />