From 4ef6b8af050a96e07e226fd7dfa341566119bbf0 Mon Sep 17 00:00:00 2001 From: jocay <1579649885@qq.com> Date: Sun, 9 Aug 2026 22:10:19 +0800 Subject: [PATCH] chore: update project --- CLAUDE.md | 89 ++++++ package-lock.json | 42 --- questions.json | 408 +++++++++++++++++++++++++++- scripts/data/set-flutter-basic.json | 373 +++++++++++++++++++++++++ scripts/import-sets.js | 3 +- server.js | 4 +- web/index.html | 6 +- web/src/styles/theme.css | 14 +- web/src/views/AdminView.vue | 7 +- web/src/views/QuizView.vue | 159 +++++++---- 10 files changed, 998 insertions(+), 107 deletions(-) create mode 100644 CLAUDE.md create mode 100644 scripts/data/set-flutter-basic.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..33c0a84 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,89 @@ +# 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 :3030 + 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 :3030, serving dist/ + API (requires a prior build) +``` + +Server env vars (defaults in parens): `PORT` (3030), `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/ into an ALREADY-RUNNING server on :3030 (logs in as admin/admin123); with no args, re-imports the two original dev sets +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 `:3030`. 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 it **once at boot** into a module-level `data` object; every mutating route calls `saveData()`, which rewrites the whole file synchronously. + +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 and no invalidation mechanism is needed. Keys are versioned (`quiz-cache-v1-sets`, `quiz-cache-v1-set-`) — bump `CACHE_VER` in `QuizView.vue` when the payload shape changes. Answer progress is intentionally *not* persisted: a reload restarts the round. + +`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 full questions and stays behind auth. + +### 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.checkAuth()` validates it by probing `/admin/sets` on mount. + +### 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, …) and escapes everything else, so question authors can paste raw `<` and `>` from code samples without hand-writing `<`. 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 1–6, 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 `