Files
OJ2/apps/web/CLAUDE.md
T
xuyueandClaude Opus 5.5 0bcc981e6f fix(提交列表/统计): 按方向键会顺手切掉「代码/流程图」、重判后右栏还是旧内容、没人交时统计说「全都做完了」
提交列表:
- 焦点在单选框、下拉框上时方向键不再一边切控件一边换条;跨页没回来前不再连跳一整页
- 重判、重新评分之后右栏重新拉,不再命中旧缓存;同一条新旧请求不再互相覆盖
- 从「我的提交」点顶栏「提交」、浏览器后退时,地址里没有的筛选回到默认
- 找回点用户名开个人主页;「只看他 / 只看这题」学生也能用;重判不用等代码拉下来
- 手机上老师也能重判、能按语言筛,流程图行有手机排版;学生界面带着的班级筛选显示成能点掉的标签
- mermaid 画不出来时先说中文、原始报错折起来

统计页:
- 没人交时说「这段时间没有人交」,不再说「全都做完了」;没填题号时各处统一叫「做对过题」
- 班级按 class_name 单独传;查询失败清掉旧数据并说清原因;标签跟着查出来的那一份走
- 「语法未过」的格子单独标「语」;正确率不算还在判的;截断提示说清哪些数字受影响
- 看板的请假隐藏只藏还没交过、没在举手的人,「恢复」只恢复这个班的
- 手机上筛选折行、上下排,不再挤出屏幕

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:52:58 +08:00

197 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
**OJ2 的前端**(`OJ2/apps/web`),代码从上一代 `ojnext/` 原样搬来、只替换了 API 层
(`ojnext` 与 `../OnlineJudge` 都已下线且**完全冻结,一行都不改**)。Vue 3 + TypeScript,
Vite(Rolldown 内核)、Naive UI、Pinia、Vue Router。
**浏览器基线是 Chrome 105**(机房部分电脑那一档):
`vite.config.ts` 的 `@vitejs/plugin-legacy` 和写死的 polyfill 清单不能删 —— vite 8
默认 target 是 chrome111,比机房高。
理由写在该文件的注释里,详见 `../../CLAUDE.md`。
## Commands
前端一般不单独起,`OJ2/` 根目录 `bun run dev` 会把 api + worker + web 一起拉起来。
只跑前端或要验证时:
```bash
bun run dev # 只起前端 dev server(5173),后端得另外起
bun run type-check # 类型检查。改完 .vue / .ts 必须跑这个
bun run build # 生产构建
```
⚠️ **验证只认 `bun run type-check`。** `vue-tsc --noEmit -p tsconfig.json` 会**静默
通过**——那个 tsconfig 是 `files: []` + references 的壳,真正的配置在
`tsconfig.app.json`(0.2 秒跑完就是没在检查的信号);`vite build` 也不做类型检查。
不写测试(沿用项目约定),验证靠实跑。格式化和 lint 走 Vite+,**脚本在仓库根目录**
(`cd ../.. && bun run fmt` / `bun run lint`,一把把后端、契约、前端全过一遍),配置在
根目录的 `vite.config.ts`,不是本目录那份(本目录的 `vite.config.ts` 只管前端构建)。
## Architecture
### Directory Structure
```
src/
├── shared/ # Cross-cutting concerns: layout, stores, composables, API
├── oj/ # User-facing features (problems, submissions, contests, etc.)
├── admin/ # Admin panel features
├── utils/ # Constants, types, HTTP client, helpers
├── routes.ts # Route definitions (two top-level: ojs, admins)
├── main.ts # App entry point
└── App.vue # Root component with Naive UI theme setup
```
### Module Pattern
Each feature module (under `oj/` or `admin/`) typically has:
- 页面组件直接放模块根下(`problem/list.vue`、`problem/detail.vue`),**没有 `views/` 这一层**
- `components/` — feature-specific components
- `composables/` / `utils/` — 模块自己的组合式函数与纯函数(按需,不是每个模块都有)
API 调用不按模块分:学生端全在 `oj/api.ts`、后台全在 `admin/api.ts`、
跨端的(登录、资料、标签、验证码)在 `shared/api.ts`。
Shared logic lives in `shared/`:
- `store/` — Pinia stores: `user` (auth/roles), `config` (site-wide settings), `authModal` (login/signup form state), `loginSummary` (AI activity summary), `collab` (help-request queue + collab room), `achievement` (解锁弹窗队列), `myFlowchart` (题面「你画的流程图」那张 A/S 图的 mermaid 源码)
- `composables/` — `aiStream`, `pagination` (URL-synced), `websocket` (reconnect + heartbeat), `collabDoc` (Yjs binding for the collab channel), `configUpdate` (WS-pushed config sync), `useMermaid` (lazy Mermaid render), `darkTransition` (View Transitions,111 以下走降级分支), `hiddenStudents` (课堂看板的「请假隐藏」), `chartTheme`, `breakpoints`, `maxkb`, `learnProgress`, `rarity`
- `layout/` — `default.vue` and `admin.vue` layout wrappers
- `api.ts` — shared API calls (auth, profile, tags, captcha)
### Auto-Imports
Configured via `unplugin-auto-import` and `unplugin-vue-components`. You do **not** need to manually import:
- Vue APIs (`ref`, `computed`, `watch`, etc.)
- Vue Router (`useRouter`, `useRoute`)
- Pinia (`defineStore`, `storeToRefs`)
- VueUse composables
- Naive UI composables (`useDialog`, `useMessage`, `useNotification`, `useLoadingBar`)
- Naive UI components (all `N*` components)
- Naive UI types (`DataTableColumn`, `FormRules`, `FormItemRule`, `SelectOption`, `UploadCustomRequestOptions`, `UploadFileInfo`, `MenuOption`, `DropdownOption`)
Generated type declaration files: `src/auto-imports.d.ts`, `src/components.d.ts`.
### Path Aliases
```
utils → ./src/utils
oj → ./src/oj
admin → ./src/admin
shared → ./src/shared
```
只在 `tsconfig.app.json` 的 `paths` 里写一份,`vite.config.ts` 用 `resolve.tsconfigPaths`
直接读它 —— 加别名只改 tsconfig。
### HTTP Client
`utils/api.ts` — Axios instance with interceptors (`baseURL: "/api"`,
`withCredentials`). It unwraps both the axios envelope and the backend's
`{ data }` envelope, so callers get the payload directly. All API calls proxy
through the dev server (see `vite.config.ts`).
### Contract guard (`utils/contract.ts`)
`@oj2/contract` 的 zod schema 是**前后端唯一的形状来源**,`utils/types.ts` 只做
`z.infer` 派生与少量前端专有的收窄(都写了理由)。
运行时闸门**只挂三处**:题目详情、提交详情、`shared/api.ts` 的用户资料 ——
原本就写了 `.parse()` 的那三条。留着它们的理由是**别抛错**,不是校验:
```ts
// 原来是 problemDetailSchema.parse(v) as Problem —— `as` 让校验白做,
// 而 parse 抛错会让整个题目页白屏
return contract("GET /problems/:id", problemDetailSchema, value)
```
失败时记一条控制台日志再**放行原始数据**,页面照常渲染。
**不要把它铺到更多端点上。** 试过一次(41 个),收益是 41 次 safeParse 加一条
没人读的 console.error:前后端同仓、共享同一份 schema,「后端改字段前端不知道」
`tsc` 已经抓了。
### 什么该收紧,什么不该
**JSONB 原文(`submission.info` / `statistic_info` / `exercise.data`)不在读出侧
校验。** 它们的形状真相在写入侧 —— 判题机、`services/exercise.ts`。在读出侧再收
一遍的结果实测过两次:
- `info` 按采样键集收紧后,124192 条提交里 9163 条(RE、TLE、MLE 全中)对不上,
被 union 的空对象分支**静默剥成 `{}`**,管理员的测试点表格无声消失;
- `exercise.data` 按题型收紧后,后端读路径(`routes/content.ts` 硬 parse)变成
一道闸,一行脏数据能让整条练习列表 500。
**后端出参已经不 `parse` 了**(原来 136 处,全部改成 `satisfies`;撤的时候炸出两个
一直存在的线上 500,见 `../../CLAUDE.md` 的「出参不 `parse`,用 `satisfies`」)。
所以现在收紧一个字段的直接后果落在 **`tsc` 编译期**,而不再是运行时 500 —— 这是好事,
但别因此就放心大胆收:契约里的形状仍然要对得上库里的存量数据,前端拿到对不上的值
一样会渲染错。收紧任何字段之前,拿根目录那份生产备份把全量数据跑一遍,
尤其要看**空值**而不只是键集合。
### 时间一律按东八区展示,不跟浏览器走
**显示时间走 `utils/functions.ts` 的 `parseTime()`;要日历部件走 `zonedParts()` /
`zonedYear()`。** 不要在组件里写 `new Date(x).getFullYear()` / `getMonth()` /
`getDate()` / `toLocaleDateString()` / `toLocaleTimeString()` —— 那些取的是
**浏览器本地**时区。机房电脑、学生手机平时都在东八区所以看不出来,但只要有人
(比如时区没设对的机房机器、或在外地的老师)从别的时区打开,同一张提交记录表就会
显示成另一个时间,和榜单、统计、成就里的日期对不上。
时区常量在契约 `@oj2/contract` 的 `TIME_ZONE_OFFSET_MINUTES`,和后端 `time.ts` 共用。
实现是「平移固定偏移 + 读 `getUTC*`」,不用 `Intl` 的时区选项:东八区没有夏令时,
纯算术在表格里逐格调用也不费事,和后端 `time.ts` 算得一模一样。
**`n-date-picker` 要平移**(`admin/contest/detail.vue`、`admin/problemset/edit.vue`):
Naive 的日期选择器按浏览器本地时区渲染、没有 `timezone` 属性,所以绑定值走
`toPickerValue()`,取回来走 `fromPickerValue()`。显示时间不要用这对函数。
### Key Utilities
- `utils/constants.ts` — Judge status codes, language IDs, difficulty levels, contest types
- `utils/types.ts` — 契约类型的派生与前端专有收窄(不是手写的一份平行类型)
- `utils/contract.ts` — 运行时契约闸门,见上
- `utils/functions.ts` — `parseTime` / `zonedParts` / `zonedYear`(东八区时间口径,见上)、
`duration`、压缩与剪贴板等杂项
- `utils/judge.ts` — Judge-related utilities
### Environment Variables
Variables prefixed with `PUBLIC_` are injected at build time,声明在 `src/env.d.ts`。
Env files: `.env`(本机)、`.env.production`(服务器)、`.env.staging` / `.env.test`(机房)。
| Variable | Purpose |
|---|---|
| `PUBLIC_ENV` | 环境角标:`test` → 「测试版」,`dev` → 「开发版」,其余不显示 |
| `PUBLIC_CODE_URL` | 代码分享服务(提交详情、题目页的「分享」) |
| `PUBLIC_MAXKB_URL` | 知识库问答挂件 |
| `PUBLIC_ICONIFY_URL` | 自建 Iconify 图标源,不设则走公共 CDN |
后端地址**不在这里**:`utils/api.ts` 写死 `baseURL: "/api"`,dev 由 `vite.config.ts` 的
proxy 转给 3000,线上由 Caddy 同源伺服。
### Routing
Routes are defined in `src/routes.ts` with two root routes: `ojs` (user-facing) and `admins` (admin panel). Route meta fields used:
- `requiresAuth` — redirect to login if not authenticated
- `requiresSuperAdmin` — super admin only
- `requiresProblemPermission` — problem management access
### Real-time Features
- WebSocket via composable in `shared/composables/` for submission status updates
- Yjs over the `/ws/collab` channel for classroom help requests and collaborative
code editing (students raise a hand, teachers join their editor). The server is a
dumb relay — it authenticates, assigns rooms, and forwards frames without parsing
them. See `docs/specs/2026-08-28-collab-help-request-design.md`.
## Related Repository
后端就在同一个仓库的 `../api`(Bun + Hono + Drizzle,编译成单二进制),
契约在 `../../packages/contract`。**不要再去看 `OnlineJudge/`** —— 那是已下线的
Django 后端,只作参照、完全冻结。详见 `../../CLAUDE.md`。