10 KiB
CLAUDE.md — weather-data-ui
Frontend module: Vue 3 / Vite 5 / TypeScript SPA.
Code Style
- No emoji in UI strings. Plain Chinese text for labels, buttons, status.
- No emoji in code comments or docstrings. Plain text only.
- Keep CLAUDE.md current — whenever code is modified, added, deleted, or any file change affects the module structure, build, conventions, or component patterns, update this file (and the root
CLAUDE.mdif cross-cutting) in the same commit to reflect the new state. Stale documentation is a bug.
Commands
npm install # install dependencies
npm run dev # Vite dev server (port 8001, host 0.0.0.0)
npm run build / npm run build:prod # production build
npm run serve # preview production build
npm run lint # lint with autofix (ESLint)
npx vue-tsc --noEmit # type-check (not in pre-commit)
Pre-commit: lint-staged runs eslint --fix on *.ts/*.vue via yorkie git hooks (not husky). No test runner configured.
Stack
- Vite 5 + Vue 3 + TypeScript SPA
- Element Plus + Element Plus Icons (all icons registered globally)
vue-routerwith hash history (createWebHashHistory)- Pinia for state management
- Axios via
src/utils/http.ts+src/service/baseService.ts - API base URL:
VITE_APP_APIenv var, overridable at runtime bywindow.SITE_CONFIG.apiURL
Environment config
- Dev:
VITE_APP_API=http://192.168.2.186:8080/system-admin(hardcoded IP — new devs must change) - Prod:
VITE_APP_API=/system-admin(relative, proxied via Nginx) - Runtime override takes priority:
window.SITE_CONFIG.apiURL
Vite config
base: "./"(relative paths),chunkSizeWarningLimit: 1024- Manual chunks:
lodashandvlib(vue/vue-router/element-plus) - Dev: HMR overlay disabled,
host: "0.0.0.0", port 8001
Axios HTTP pattern
- Success check:
response.data.code === 0(not=== 200) - Request interceptor: adds
tokenheader,X-Requested-With, request timing, cache-busting_ton GET - On
code === 401: auto-redirects to/login - Response unwrapped: callers receive
response.data - File exports: bypass Axios, use
window.location.hrefwith token as query param - Uploads: no
Content-Typeset (browser auto-sets forFormData)
Routing & state
src/router/base.ts: 7 base routes (/,/home,/login,/user/password,/iframe/:id?,/error, 404 catch-all)src/router/index.ts:beforeEachguard — auth check, dynamic route registration from backend menus, tab management. Routes are dynamically added viaaddRoutewith flattened nested routes (keep-alive limitation). View components resolved viaimport.meta.glob("/src/views/**/*.vue").src/store/index.ts(useAppStore): monolithic store — all state nested instate.state(double nesting, e.g.store.state.appIsLogin).initAppfetches menus/permissions/user/dicts in 4 parallel requests.src/store/importTasks.ts: separate store for import task tracking (computed getters:activeTasks,hasActiveTasks,recentTasks).src/utils/router.ts: converts backend menu records → Vue router records, supports iframe/external links withopenStyleflags.
Layout is event-driven: src/layout/ shell + mitt event bus (src/utils/emits.ts). The EMitt enum defines events for sidebar, theme, tabs, layout changes. Trace both the Pinia store and mitt events when changing navigation/sidebar/tabs/theme.
Header right side: notification bell (combined badge) → expand (user menu). The old import-task-indicator.vue has been removed in favor of the notification center drawer.
Common page pattern: useView hook
Admin CRUD pages use src/hooks/useView.ts for shared list-page workflow.
Key behaviors to know before refactoring:
closeCurrentTab(): if tabs enabled, emitsOnCloseCurrTabmitt event; otherwise navigates to/home.exportHandle(): useswindow.location.hrefwith token as query param (NOT Axios).dataListSortChangeHandle(): converts camelCase → snake_case for backend (e.g.stationId→station_id).createdIsNeed: true/activatedIsNeed: falseby default. Pages needing refresh on tab activation must setactivatedIsNeed: true.- Includes workflow helpers (
handleFlowRoute,flowDetailRoute) hardcoded to/flow/task-form.
Cache utility
All cache keys prefixed with v1@ to avoid collisions. Supports localStorage and sessionStorage (token uses sessionStorage). JSON serialization is automatic. getCache supports auto-delete-after-read (isDelete flag).
Weather frontend module
The home dashboard (src/views/home.vue) uses a composable-based architecture:
| Composable | Responsibility |
|---|---|
useWeatherConstants.ts |
Rain levels, temperature thresholds, filter field definitions, fmtVal(), level/class helpers |
useWeatherFilter.ts |
Filter state, toggle/reset/match logic, matchOp() |
useWeatherStats.ts |
computeStats(), buildStatCards(), buildSummary(), rainLevelDistribution, WeatherDataRow type |
useWeatherChart.ts |
ECharts dynamic import, buildChartOption(), ResizeObserver, precise trigger key (not deep watch) |
useWeatherExport.ts |
PNG/PDF export with dynamic html2canvas/jspdf imports, loading indicator |
Supporting utils: src/utils/chartBuilder.ts, src/utils/exportReport.ts.
Critical rules
1. Null ≠ zero — missing data MUST be preserved as null
When mapping backend API responses to frontend models, never default missing numeric values to 0. Rainfall of 0mm means "no rain that day" (valid measurement); null means "no data available" (missing record). Use : null not : 0 in data mapping, and display "—" for null values via fmtVal().
// ✅ Correct
rainfall: row.rain2020 != null ? +row.rain2020 : null,
// ❌ Wrong — confuses "no data" with "measured zero"
rainfall: row.rain2020 != null ? +row.rain2020 : 0,
All helper functions must accept number | null and return "—" or "" for null. Stats computations must skip null values.
2. Heavy libraries must use dynamic imports
html2canvas, jspdf, and echarts are NOT imported at module level. Load them via await import() only when triggered by user action. This saves ~600KB from the initial bundle.
3. Fonts are self-hosted — no external network dependency
Fonts (Noto Sans SC, JetBrains Mono) are bundled via @fontsource/* packages, imported in src/main.ts. Do NOT add Google Fonts <link> tags or @import back — the system runs on intranet where external network may be unavailable. To add a new font weight, import the corresponding fontsource CSS file in main.ts.
4. Export must show user feedback
Always show ElLoading.service fullscreen and ElMessage success/failure. Disable the export button during rendering.
5. Deep watchers on filter objects are banned
Never use watch(filters, callback, { deep: true }). Derive a precise computed trigger key (e.g. dataHash, filteredHash, extremesVersion) and watch that instead.
Notification center
The global notification system (src/components/alert-marquee/index.vue) aggregates two feed types:
| Feed | Source | Clickable? |
|---|---|---|
| System alerts | useAlertMarquee.ts composable (polls /sys/alert/active/since every 30s) |
Yes — opens detail dialog |
| Import tasks | useImportTaskStore Pinia store |
No — shows real-time progress inline |
Components & composables
| File | Role |
|---|---|
src/composables/useAlertMarquee.ts |
Module-level singleton: message queue, drawer toggle, detail dialog state, scrollbar position. Real-time delivery: SSE (/sys/alert/stream) primary + 10s polling fallback. Exports danger-specific computed: dangerMessages, latestDangerMessage, dangerCount, hasDangerMessages |
src/composables/useFloatingDrag.ts |
Pointer Events drag logic: setPointerCapture, viewport clamping, deferred isDragging (activates only on >3px move), dragMoved flag to distinguish drag vs click |
src/components/alert-marquee/index.vue |
Floating scrollbar (640px wide, centered top, draggable) + el-drawer notification center + el-dialog detail popup |
src/layout/header/base-header.vue |
Bell button with combined badge (alerts + active import tasks), toggles drawer |
src/store/importTasks.ts |
Import task CRUD: addTask, updateTask, removeTask, clearCompleted; getters: activeTasks, recentTasks |
Level-based routing
- danger (紧急): triggers the floating scrollbar + appears in notification center drawer + bell badge
- warning / info (警告 / 提示): notification center drawer + bell badge only (no scrollbar)
- The bell badge in base-header always shows total count (all levels + active import tasks)
Floating scrollbar behavior
- Visible only when logged in (
appStore.state.appIsLogin) and has danger-level alerts (hasDangerMessages) - Positioned centered at top (
y: 56below header), draggable to reposition, re-centers on window resize - Shows latest alert headline + count badge
- Close button hides the bar; auto-reappears when new danger alerts arrive (watch on
dangerCount) - 查看详情 button opens
el-dialogwith full alert text - The old
import-task-indicator.vue(bell icon with popover in header) has been removed
Import progress integration
- Upload via
baseService.upload()(FormData, no explicit Content-Type). - On upload start, toast:
"导入已开始,可在通知中心查看进度". - Poll
GET .../import/progress/{backendTaskId}every 3 seconds. - Update Pinia store (
useImportTaskStore) — progress bar + status tag render reactively in the drawer. - On completion: green checkmark; on failure: red cross + error message. Completed/failed tasks show a dismiss button.
Alert management page
Manual alert CRUD at route sys/system-alert:
| File | Role |
|---|---|
src/views/sys/system-alert.vue |
List page: useView-based, search by level/title/sourceType, batch delete, per-row withdraw |
src/views/sys/system-alert-add-or-update.vue |
Add/edit dialog: level (info/warning/danger), title, content (textarea), sourceType, expireTime (datetime picker) |
Backend endpoints under /sys/alert:
GET /page— paginated list (sys:alert:page)GET /{id}— detail (sys:alert:info)POST /— create (sys:alert:save)PUT /— update (sys:alert:update)DELETE /— batch delete (sys:alert:delete)PUT /{id}/withdraw— soft-withdraw (sys:alert:update)GET /active,GET /active/since— frontend polling (no permission required)