Files
2026-08-17 23:57:43 +08:00

92 lines
7.8 KiB
Markdown
Raw Permalink 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.
## What this is
Despite the repo name, this is **not** a Flutter project — there is no Dart toolchain here. It is a Vue 3 + Express web app that serves multiple-choice quizzes *about* Dart & Flutter, with an admin backend for managing question sets.
All UI copy, comments, and API error messages are Simplified Chinese. Keep new strings in Chinese to match.
## Commands
```bash
npm install
npm run dev # concurrently: Express on :3031 + Vite dev server on :5173 — develop at http://localhost:5173
npm run build # Vite builds web/ → dist/ at the repo root
npm start # Express only on :3031, serving dist/ + API (requires a prior build)
```
Server env vars (defaults in parens): `PORT` (3031), `ADMIN_USER` (admin), `ADMIN_PASS` (admin123).
### Data scripts
No test runner is configured. `verify-questions.js` is the closest thing to a test suite — run it after any bulk edit of `questions.json`.
```bash
node scripts/verify-questions.js # structural validation of every question + hardcoded answer-index spot checks
node scripts/fix-escaping.js # re-normalizes HTML escaping across questions.json in place; also asserts the transform is idempotent
node scripts/import-sets.js [file...] # POSTs scripts/data/<file> into an ALREADY-RUNNING server on :3031; BASE_URL/ADMIN_USER/ADMIN_PASS can override connection settings
node scripts/extract-questions.js # DESTRUCTIVE one-shot legacy migration; see "Legacy files" below
```
`scripts/` and `server.js` are CommonJS (no `"type": "module"` in package.json); everything under `web/` is ESM.
## Architecture
### Two processes, one origin
The frontend never hardcodes an API host — `web/src/api.js` always fetches `/api/...`. In dev, Vite's proxy (`vite.config.js`) forwards `/api` to `:3031`. In production, Express serves `dist/` and the API from the same port, with a catch-all (`/^\/(?!api\/).*/`) sending everything non-`/api` to `index.html`. Preserve that same-origin assumption when adding endpoints.
### Data store: one JSON file, held in memory
`questions.json` at the repo root is the entire database. `server.js` reads and validates it **once at boot** into a module-level `data` object; every mutating route calls `saveData()`, which writes a temporary file and atomically replaces the database.
Consequences to keep in mind:
- Hand-editing `questions.json` while the server is running will be silently clobbered by the next write. Restart after external edits (including after running `fix-escaping.js`).
- `loadData()` auto-migrates a legacy top-level array into `{sets, nextSetId, nextQId}` and rewrites the file on first read.
### IDs are global, not per-set
`nextSetId` / `nextQId` are counters in the JSON. Question IDs are unique **across all sets**, which is why `findQuestion(qid)` scans every set and why the admin routes for questions are `/api/admin/questions/:qid` with no set in the path. Don't reintroduce per-set question numbering.
### Grading is client-side, and the quiz page works offline
`GET /api/questions` returns whole questions through `toPublic()`, **including `ans` and `exp`**. That is deliberate: `QuizView` caches the full set in `localStorage` and grades locally, so answering keeps working with the network down — and because grading is synchronous, a correct pick renders green immediately instead of flashing red while a round trip completes. The trade-off is that answers are visible in the network payload; don't reintroduce answer-stripping without also rethinking offline grading. `toPublic()` is still an explicit whitelist, so new internal fields (authoring notes, review flags) stay server-side unless you add them there.
Caching is **network-first, cache-fallback** in both `loadSets()` and `loadQuestions()`: an online client always gets fresh questions, so edits made in the admin UI are never masked by a stale cache. Keys are versioned (`quiz-cache-v2-sets`, `quiz-cache-v2-set-<id>`) — bump `CACHE_VER` in `QuizView.vue` when the payload shape changes. Answer progress is persisted per set and restored only when the server-provided content `version` still matches.
`loadQuestions()` bails out with a visible error if the first question has no `ans`, which catches the dev-mode trap of editing `server.js` without restarting it (`npm run dev` runs Express and Vite as separate processes) — otherwise local grading would silently mark every answer wrong.
`POST /api/check` still exists and still grades server-side, but the quiz page no longer calls it. `GET /api/admin/sets` returns lightweight summaries; fetch one set and its questions from `GET /api/admin/sets/:id`.
New and duplicated sets are drafts (`published: false`). Public endpoints only expose sets that are published and contain at least one question. Use `PATCH /api/admin/sets/:id/publish` to publish or unpublish; deleting the last question automatically returns a set to draft state.
### Auth
`POST /api/admin/login` compares against the env credentials and returns a random hex token stored in an in-memory `Set`. Tokens have no expiry but are lost on restart, so a server restart logs every admin out. The client keeps the token in `localStorage` under `quiz_token` and sends it as `Authorization: Bearer`; `AdminView` loads `/admin/sets` once on mount to validate the token and bootstrap the list.
### HTML escaping contract
`web/src/htmlUtil.mjs` is shared by the browser bundle and the Node scripts — that is why it is `.mjs` (CommonJS scripts `await import()` it). Don't rename it to `.js`.
`escapeSmartHtml()` preserves an allowlist of formatting tags (`b`, `code`, `pre`, lists, headings, …), strips all tag attributes, and escapes everything else, so question authors can paste raw `<` and `>` from code samples without hand-writing `&lt;`. The server applies the same attribute-free allowlist when questions are created, imported, or updated. Stored content in `questions.json` is therefore already-escaped HTML.
The round trip in `AdminView` is: `decodeEntities()` when loading into the editor → edit as plain text → `escapeSmartHtml(decodeEntities(...))` on save. This is deliberately idempotent, and `fix-escaping.js` verifies that property. Keep any new field that accepts authored markup on the same path.
Rendering is asymmetric on purpose: question text (`q`) and explanations (`exp`) render through `v-html`; **options render as plain text** via `stripHtmlAndDecode()`. Options don't support markup.
### Difficulty levels
`lv` is an integer 16, validated server-side. Each set may carry `lvNames`, which must be exactly 6 non-empty strings (`validateLvNames`); sets without it fall back to `DEFAULT_LV_NAMES`. `QuizView` prepends an empty string to the array so `lvNames[lv]` indexes directly — the constant there is 7 elements, the one in `AdminView` and the API is 6. The `.seal-lv1``.seal-lv6` badge colors live in `web/src/styles/theme.css`.
### Routing and styling
`vue-router` uses **hash history**, so real URLs are `/#/` (quiz) and `/#/admin`. Set selection is a query param on the quiz route: `/#/quiz?set=9`. The startup banner in `server.js` still advertises `/admin.html`, which no longer exists.
`theme.css` is a global design system (CSS custom properties + `.card` / `.btn-*` / `.input` / `.seal` / `.table` classes). Component `<style>` blocks are `scoped` and only add what's specific to that view — reach for the existing tokens and classes before writing new ones.
### Legacy files
`index.html` at the repo root is the original standalone single-file version, with its 31 questions inlined as a JS array. Nothing serves it (Vite's root is `web/`, and the live entry is `web/index.html`). `scripts/extract-questions.js` exists only to parse that array out of it; running it overwrites `questions.json` with a single legacy set, discarding every set added since. Treat both as historical artifacts.