185 lines
10 KiB
Markdown
185 lines
10 KiB
Markdown
# 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.md` if cross-cutting) in the same commit to reflect the new state. Stale documentation is a bug.
|
|
|
|
---
|
|
|
|
## 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 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, 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. 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: 56` below 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-dialog` with full alert text
|
|
- The old `import-task-indicator.vue` (bell icon with popover in header) has been **removed**
|
|
|
|
### Import progress integration
|
|
|
|
1. Upload via `baseService.upload()` (FormData, no explicit Content-Type).
|
|
2. On upload start, toast: `"导入已开始,可在通知中心查看进度"`.
|
|
3. Poll `GET .../import/progress/{backendTaskId}` every 3 seconds.
|
|
4. Update Pinia store (`useImportTaskStore`) — progress bar + status tag render reactively in the drawer.
|
|
5. 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)
|