Files
flutter_learning/CLAUDE.md
T
2026-08-09 22:10:19 +08:00

7.3 KiB
Raw Blame History

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

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.

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 :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-<id>) — 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 &lt;. 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.