# CLAUDE.md — weather-data-ui Frontend module: Vue 3 / Vite 5 / TypeScript SPA. --- ## Commands ```bash 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-router` with **hash history** (`createWebHashHistory`) - Pinia for state management - Axios via `src/utils/http.ts` + `src/service/baseService.ts` - API base URL: `VITE_APP_API` env var, overridable at runtime by `window.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: `lodash` and `vlib` (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 `token` header, `X-Requested-With`, request timing, cache-busting `_t` on GET - On `code === 401`: auto-redirects to `/login` - Response unwrapped: callers receive `response.data` - File exports: bypass Axios, use `window.location.href` with token as query param - Uploads: no `Content-Type` set (browser auto-sets for `FormData`) ## Routing & state - `src/router/base.ts`: 7 base routes (`/`, `/home`, `/login`, `/user/password`, `/iframe/:id?`, `/error`, 404 catch-all) - `src/router/index.ts`: `beforeEach` guard — auth check, dynamic route registration from backend menus, tab management. Routes are dynamically added via `addRoute` with **flattened nested routes** (keep-alive limitation). View components resolved via `import.meta.glob("/src/views/**/*.vue")`. - `src/store/index.ts` (`useAppStore`): monolithic store — all state nested in `state.state` (double nesting, e.g. `store.state.appIsLogin`). `initApp` fetches 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 with `openStyle` flags. 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, emits `OnCloseCurrTab` mitt event; otherwise navigates to `/home`. - `exportHandle()`: uses `window.location.href` with token as query param (NOT Axios). - `dataListSortChangeHandle()`: converts camelCase → snake_case for backend (e.g. `stationId` → `station_id`). - `createdIsNeed: true` / `activatedIsNeed: false` by default. Pages needing refresh on tab activation must set `activatedIsNeed: 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()`. ```typescript // ✅ 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 `` + `` 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 1. Upload via `baseService.upload()` (FormData, no explicit Content-Type). 2. On success, poll `GET .../import/progress/{backendTaskId}` every 3 seconds. 3. Update Pinia store (`useImportTaskStore`) with percentage/state. 4. Emit `refreshDataList` on completion. Header indicator (`import-task-indicator.vue`) shows active tasks with spinning badge + popover.