6.6 KiB
CLAUDE.md — weather-data-ui
Frontend module: Vue 3 / Vite 5 / TypeScript SPA.
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 13 events for sidebar, theme, tabs, layout changes. Trace both the Pinia store and mitt events when changing navigation/sidebar/tabs/theme.
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. Google Fonts go in index.html, not scoped styles
Never use @import url("https://fonts.googleapis.com/...") inside Vue scoped styles. Use <link rel="preconnect"> + <link rel="stylesheet"> in index.html.
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.
Import progress polling pattern
- Upload via
baseService.upload()(FormData, no explicit Content-Type). - On success, poll
GET .../import/progress/{backendTaskId}every 3 seconds. - Update Pinia store (
useImportTaskStore) with percentage/state. - Emit
refreshDataListon completion. Header indicator (import-task-indicator.vue) shows active tasks with spinning badge + popover.