项目优化,构建文件Lombok异常问题修复
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
NODE_ENV=development
|
||||
VITE_APP_API=http://192.168.2.186:8080/system-admin
|
||||
VITE_APP_API=http://192.168.2.151:48080/system-admin
|
||||
|
||||
|
||||
+122
-158
@@ -1,184 +1,148 @@
|
||||
# CLAUDE.md — weather-data-ui
|
||||
# CLAUDE.md - weather-data-ui
|
||||
|
||||
Frontend module: Vue 3 / Vite 5 / TypeScript SPA.
|
||||
This file provides guidance to Claude Code when working in the weather-data-ui module.
|
||||
|
||||
---
|
||||
## Purpose
|
||||
|
||||
## Code Style
|
||||
Vue 3 + TypeScript frontend for the weather data management system. Single-page application built with Vite 5.
|
||||
|
||||
- **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.
|
||||
## Tech Stack
|
||||
|
||||
---
|
||||
- Vue 3.5 (Composition API), TypeScript 5.7, Vite 5.4
|
||||
- Element Plus 2.10 (UI library)
|
||||
- Pinia 2.3 (state management)
|
||||
- Vue Router 4.2 (hash mode routing)
|
||||
- ECharts 5 + vue-echarts 6 (charts)
|
||||
- Axios 1.11 (HTTP client)
|
||||
- Less/Sass for styling
|
||||
|
||||
## Commands
|
||||
## Build 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)
|
||||
npm install
|
||||
npm run dev # dev server on port 8001, hot reload, proxies /api to backend
|
||||
npm run build # production build (vite build --mode production), outputs to dist/
|
||||
npm run lint # ESLint on src/**/*.{vue,ts} with --fix
|
||||
npm run serve # build + vite preview
|
||||
```
|
||||
|
||||
Pre-commit: `lint-staged` runs `eslint --fix` on `*.ts`/`*.vue` via `yorkie` git hooks (not husky). No test runner configured.
|
||||
## Project Structure
|
||||
|
||||
## 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,
|
||||
```
|
||||
src/
|
||||
├── main.ts # App bootstrap, Pinia init
|
||||
├── App.vue # Root component
|
||||
├── assets/ # Static assets (css, icons, images, theme)
|
||||
├── components/ # Reusable components
|
||||
│ ├── alert-marquee/ # Alert scrolling marquee (SSE-driven)
|
||||
│ ├── base/ # Base table/form/dialog wrappers
|
||||
│ ├── sys-dept-tree/ # Department tree selector
|
||||
│ ├── sys-radio-group/ # Radio group with dict-driven options
|
||||
│ ├── sys-region-tree/ # Region tree selector
|
||||
│ ├── sys-select/ # Dict-driven select dropdown
|
||||
│ └── wang-editor/ # Rich text editor wrapper
|
||||
├── composables/ # Vue 3 composables
|
||||
│ ├── useAlertMarquee.ts # SSE alert streaming logic
|
||||
│ ├── useFloatingDrag.ts # Draggable floating panel logic
|
||||
│ ├── useWeatherChart.ts # ECharts chart configuration
|
||||
│ ├── useWeatherConstants.ts # Weather domain constants
|
||||
│ ├── useWeatherExport.ts # Export to Excel/PDF
|
||||
│ ├── useWeatherFilter.ts # Query filter state management
|
||||
│ └── useWeatherStats.ts # Statistical computation
|
||||
├── constants/ # Application constants
|
||||
│ ├── app.ts # API base URL, request timeout
|
||||
│ ├── cacheKey.ts # Cache key enums
|
||||
│ ├── config.ts # App config
|
||||
│ └── enum.ts # Enums (EMitt events, etc.)
|
||||
├── hooks/ # Legacy hooks
|
||||
│ └── useView.ts # View loader for dynamic routes
|
||||
├── layout/ # Layout components
|
||||
│ ├── index.vue # Main layout with sidebar + header
|
||||
│ ├── layout.vue # Alternative layout
|
||||
│ ├── fullscreen-layout.vue # Fullscreen page layout
|
||||
│ ├── header/ # Top header bar
|
||||
│ ├── sidebar/ # Left sidebar navigation
|
||||
│ └── view/ # Content view wrapper (tabs)
|
||||
├── router/
|
||||
│ ├── index.ts # Router instance + dynamic route registration
|
||||
│ └── base.ts # Static base routes (login, home, error, iframe)
|
||||
├── service/
|
||||
│ └── baseService.ts # HTTP helpers: get/post/put/delete/upload
|
||||
├── store/
|
||||
│ ├── index.ts # useAppStore (user, permissions, menus, routes, tabs)
|
||||
│ └── importTasks.ts # Import task progress store
|
||||
├── types/ # TypeScript type definitions
|
||||
├── utils/
|
||||
│ ├── cache.ts # Cookie/localStorage cache helpers (token storage)
|
||||
│ ├── chartBuilder.ts # ECharts option builder
|
||||
│ ├── emits.ts # Event bus (mitt)
|
||||
│ ├── exportReport.ts # PDF/Excel export utilities
|
||||
│ ├── http.ts # Axios instance with interceptors
|
||||
│ ├── router.ts # Route merging and registration helpers
|
||||
│ ├── theme.ts # Theme switching logic
|
||||
│ └── utils.ts # General utility functions
|
||||
└── views/ # Feature views
|
||||
├── dailyweather/ # Daily weather data query/import/charts
|
||||
├── home.vue # Dashboard home page
|
||||
├── iframe.vue # Iframe wrapper for external pages
|
||||
├── job/ # Scheduled job management
|
||||
├── login.vue # Login page
|
||||
├── oss/ # Cloud file storage management
|
||||
├── region/ # Region management
|
||||
├── station/ # Weather station management
|
||||
├── sys/ # System management (users, roles, menus, depts, dicts, alerts)
|
||||
├── tools/ # Utility tools
|
||||
└── weather/ # Weather data views
|
||||
```
|
||||
|
||||
All helper functions must accept `number | null` and return `"—"` or `""` for null. Stats computations must skip null values.
|
||||
## Router Architecture
|
||||
|
||||
### 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.
|
||||
- **Mode**: `createWebHashHistory()` (hash mode)
|
||||
- **Dynamic routing**: On login, `useAppStore.initApp()` fetches `/sys/menu/nav` (menu tree), `/sys/menu/permissions` (perms), `/sys/user/info` (user), `/sys/dict/type/all` (dicts). Menu data is merged with `src/views/**/*.vue` component map via `mergeServerRoute()` to build the full route table.
|
||||
- **Auto-registration**: `registerDynamicToRouterAndNext()` can register a new route at runtime by matching path to a view component file.
|
||||
- **Tab tracking**: Router `beforeEach` guard emits tab push events; tabs are tracked in store state.
|
||||
- **404 fallback**: Unmatched routes redirect to `/error` with `to=404` query param.
|
||||
|
||||
### 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`.
|
||||
## State Management (Pinia)
|
||||
|
||||
### 4. Export must show user feedback
|
||||
Always show `ElLoading.service` fullscreen and `ElMessage` success/failure. Disable the export button during rendering.
|
||||
`useAppStore` holds all application state:
|
||||
- `appIsLogin`, `appIsReady`, `appIsRender` -- lifecycle flags
|
||||
- `permissions[]` -- user permission set (strings)
|
||||
- `user` -- current user object
|
||||
- `dicts[]` -- dictionary data array
|
||||
- `routes[]` -- resolved route records
|
||||
- `routeToMeta` -- path-to-metadata mapping for tab titles
|
||||
- `tabs[]`, `activeTabName`, `closedTabs` -- tab management
|
||||
|
||||
### 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.
|
||||
`initApp()` is called once on login -- returns merged routes that are then registered with the router.
|
||||
|
||||
## Notification center
|
||||
## HTTP Layer
|
||||
|
||||
The global notification system (`src/components/alert-marquee/index.vue`) aggregates two feed types:
|
||||
`utils/http.ts` creates an Axios instance:
|
||||
- **Request interceptor**: Injects `token` header from cache, adds `_t` timestamp to GET requests, handles form-urlencoded serialization
|
||||
- **Response interceptor**: `code === 0` means success; `code === 401` triggers redirect to `/login`; other codes show ElMessage error
|
||||
|
||||
| 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 |
|
||||
`service/baseService.ts` wraps HTTP methods:
|
||||
- `get(path, params, headers)` -- adds cache-busting `_t`
|
||||
- `post(path, body, headers)` -- JSON content-type
|
||||
- `put(path, params, headers)` -- JSON content-type
|
||||
- `delete(path, params)` -- sends body
|
||||
- `upload(path, formData, headers)` -- multipart form upload
|
||||
|
||||
### Components & composables
|
||||
## SSE Alert Integration
|
||||
|
||||
| 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` |
|
||||
`composables/useAlertMarquee.ts` manages SSE connection lifecycle. It connects to the backend SSE endpoint and dispatches `alert`, `alert-withdrawn`, and `alert-deleted` events to the `alert-marquee` component for real-time notification display.
|
||||
|
||||
### Level-based routing
|
||||
## Environment Variables
|
||||
|
||||
- **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)
|
||||
- `VITE_APP_API`: Backend API base URL (injected into `constants/app.ts` at build time)
|
||||
- `.env.development` / `.env.production`: Environment-specific configs
|
||||
|
||||
### Floating scrollbar behavior
|
||||
## Key Dependencies
|
||||
|
||||
- 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)
|
||||
- `@vueuse/core`: Vue composition utilities
|
||||
- `mitt`: Lightweight event emitter
|
||||
- `nprogress`: Page load progress bar
|
||||
- `html2canvas` + `jspdf`: Client-side PDF export
|
||||
- `js-cookie`: Cookie management (token storage)
|
||||
- `qs`: Query string parsing/serialization
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# weather-data-ui
|
||||
|
||||
气象数据分析平台前端项目。
|
||||
|
||||
> 项目说明、快速启动、部署文档 → [../README.md](../README.md)
|
||||
> 开发者文档(架构、规范) → [../CLAUDE.md](../CLAUDE.md)
|
||||
Generated
+141
-125
File diff suppressed because it is too large
Load Diff
@@ -127,7 +127,7 @@
|
||||
margin: 8px 8px 0 0;
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
box-shadow: 0 1px 3px rgba(0 0 0, 0.1);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
vertical-align: top;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -17,7 +17,7 @@ export default defineComponent({
|
||||
const store = useAppStore();
|
||||
return {
|
||||
value: computed(() => `${props.modelValue}`),
|
||||
dataList: getDictDataList(store.state.dicts, props.dictType)
|
||||
dataList: computed(() => getDictDataList(store.state.dicts, props.dictType))
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ export default defineComponent({
|
||||
const store = useAppStore();
|
||||
return {
|
||||
value: computed(() => `${props.modelValue}`),
|
||||
dataList: getDictDataList(store.state.dicts, props.dictType)
|
||||
dataList: computed(() => getDictDataList(store.state.dicts, props.dictType))
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// 消息队列 / 通知中心抽屉 / 详情弹窗 / 悬浮滚动条位置
|
||||
// 获取:SSE 主通道 + 10s 轮询降级 + 每60s全量对账
|
||||
|
||||
import { ref, computed } from "vue";
|
||||
import { ref, computed, onBeforeUnmount } from "vue";
|
||||
import baseService from "@/service/baseService";
|
||||
import app from "@/constants/app";
|
||||
import { getToken } from "@/utils/cache";
|
||||
@@ -329,6 +329,10 @@ function setPosition(pos: { x: number; y: number }): void {
|
||||
// ---------- composable 导出 ----------
|
||||
|
||||
export function useAlertMarquee() {
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling();
|
||||
});
|
||||
|
||||
return {
|
||||
// 消息
|
||||
messages,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// composables/useFloatingDrag.ts
|
||||
// 浮窗拖动逻辑 — Pointer Events 统一处理鼠标和触屏
|
||||
|
||||
import { ref, type Ref } from "vue";
|
||||
import { ref, onBeforeUnmount, type Ref } from "vue";
|
||||
|
||||
export interface DragPosition {
|
||||
x: number;
|
||||
@@ -16,6 +16,24 @@ export function useFloatingDrag(
|
||||
const dragMoved = ref(false);
|
||||
let dragResetTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
/** 清理拖拽期间注册的事件监听器(组件卸载时使用) */
|
||||
function cleanupDragListeners(): void {
|
||||
const el = elementRef.value;
|
||||
if (el) {
|
||||
el.removeEventListener("pointermove", onPointerMove);
|
||||
el.removeEventListener("pointerup", onPointerUp);
|
||||
el.removeEventListener("pointercancel", onPointerUp);
|
||||
}
|
||||
if (dragResetTimer) {
|
||||
clearTimeout(dragResetTimer);
|
||||
dragResetTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanupDragListeners();
|
||||
});
|
||||
|
||||
/** pointerdown 时记录的元素尺寸和指针在元素内的偏移 */
|
||||
let dragMeta: {
|
||||
width: number;
|
||||
|
||||
@@ -9,7 +9,7 @@ export const RAIN_LEVELS = [
|
||||
{ label: "大雨", min: 25, max: 50 },
|
||||
{ label: "暴雨", min: 50, max: 100 },
|
||||
{ label: "大暴雨", min: 100, max: 250 },
|
||||
{ label: "特大暴雨", min: 250, max: 9999 }
|
||||
{ label: "特大暴雨", min: 250, max: Infinity }
|
||||
] as const;
|
||||
|
||||
/** 可筛选的降雨等级(排除"无雨",因为无雨通常单独判断) */
|
||||
|
||||
@@ -58,7 +58,7 @@ export default defineComponent({
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<el-container :class="`rr ${containerClassNames}`" v-loading="state.loading" element-loading-background="#0000" element-loading-lock="true" element-loading-custom-class="rr-loading">
|
||||
<el-container :class="`rr ${containerClassNames}`" v-loading="state.loading" element-loading-background="transparent" element-loading-lock="true" element-loading-custom-class="rr-loading">
|
||||
<el-header class="rr-header" height="50px">
|
||||
<base-header></base-header>
|
||||
</el-header>
|
||||
|
||||
@@ -7,7 +7,7 @@ import emits from "@/utils/emits";
|
||||
import { toValidRoutes } from "@/utils/router";
|
||||
import { getThemeConfigCacheByKey } from "@/utils/theme";
|
||||
import { useWindowSize } from "@vueuse/core";
|
||||
import { defineComponent, onMounted, reactive, ref, watch } from "vue";
|
||||
import { defineComponent, onMounted, onBeforeUnmount, reactive, ref, watch } from "vue";
|
||||
import { RouteRecordRaw, useRoute, useRouter } from "vue-router";
|
||||
import { useAppStore } from "@/store";
|
||||
import SidebarMenusItems from "./sidebar-menus-items.vue";
|
||||
@@ -84,16 +84,25 @@ export default defineComponent({
|
||||
state.menus = ms;
|
||||
}
|
||||
);
|
||||
emits.on(EMitt.OnSwitchLeftSidebar, () => {
|
||||
const onSwitchLeftSidebar = () => {
|
||||
state.collapseSidebar = !state.collapseSidebar;
|
||||
});
|
||||
emits.on(EMitt.OnSetThemeNotUniqueOpened, (vl) => {
|
||||
};
|
||||
const onSetThemeNotUniqueOpened = (vl: boolean) => {
|
||||
state.uniqueOpened = vl;
|
||||
});
|
||||
emits.on(EMitt.OnSetTheme, ([vl]) => {
|
||||
};
|
||||
const onSetTheme = ([vl]: [string, string]) => {
|
||||
if (vl === EThemeSetting.Sidebar) {
|
||||
state.popClassName = getPopClassName();
|
||||
}
|
||||
};
|
||||
emits.on(EMitt.OnSwitchLeftSidebar, onSwitchLeftSidebar);
|
||||
emits.on(EMitt.OnSetThemeNotUniqueOpened, onSetThemeNotUniqueOpened);
|
||||
emits.on(EMitt.OnSetTheme, onSetTheme);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
emits.off(EMitt.OnSwitchLeftSidebar, onSwitchLeftSidebar);
|
||||
emits.off(EMitt.OnSetThemeNotUniqueOpened, onSetThemeNotUniqueOpened);
|
||||
emits.off(EMitt.OnSetTheme, onSetTheme);
|
||||
});
|
||||
watch(
|
||||
() => route.path,
|
||||
|
||||
@@ -15,7 +15,7 @@ export default defineComponent({
|
||||
setup(props) {
|
||||
const getStyle = (index: number): string => {
|
||||
const styles: Array<any> = [];
|
||||
const isHidden = props.hiddenIndex ? props.hiddenIndex > -1 && index > props.hiddenIndex : false;
|
||||
const isHidden = props.hiddenIndex != null ? props.hiddenIndex > -1 && index > props.hiddenIndex : false;
|
||||
styles.push("display:" + (isHidden ? "none" : "block"));
|
||||
return styles.join(";");
|
||||
};
|
||||
|
||||
@@ -27,6 +27,8 @@ const router = createRouter({
|
||||
routes: baseRoutes
|
||||
});
|
||||
|
||||
let initAppPromise: Promise<any> | null = null;
|
||||
|
||||
// 路由加载前
|
||||
router.beforeEach((to, from, next) => {
|
||||
//外链
|
||||
@@ -65,23 +67,28 @@ router.beforeEach((to, from, next) => {
|
||||
}
|
||||
} else {
|
||||
if (token) {
|
||||
store.initApp().then((res: Array<RouteRecordRaw>) => {
|
||||
const mergeRoute = baseRoutes.concat(res);
|
||||
router.options.routes = mergeRoute;
|
||||
registerToRouter(router, mergeRoute);
|
||||
if (!to.matched.length) {
|
||||
registerDynamicToRouterAndNext({ path: to.path, query: to.query });
|
||||
}
|
||||
store.updateState({
|
||||
appIsReady: true,
|
||||
routes: mergeRoute,
|
||||
routeToMeta: { ...store.state.routeToMeta, ...getBaseRouteToMeta(baseRoutes) }
|
||||
if (!initAppPromise) {
|
||||
initAppPromise = store.initApp().then((res: Array<RouteRecordRaw>) => {
|
||||
const mergeRoute = baseRoutes.concat(res);
|
||||
router.options.routes = mergeRoute;
|
||||
registerToRouter(router, mergeRoute);
|
||||
if (!to.matched.length) {
|
||||
registerDynamicToRouterAndNext({ path: to.path, query: to.query });
|
||||
}
|
||||
store.updateState({
|
||||
appIsReady: true,
|
||||
routes: mergeRoute,
|
||||
routeToMeta: { ...store.state.routeToMeta, ...getBaseRouteToMeta(baseRoutes) }
|
||||
});
|
||||
setTimeout(() => {
|
||||
store.updateState({ appIsRender: true, appIsLogin: true });
|
||||
}, 600);
|
||||
next({ ...to, replace: true });
|
||||
}).finally(() => {
|
||||
initAppPromise = null;
|
||||
});
|
||||
setTimeout(() => {
|
||||
store.updateState({ appIsRender: true, appIsLogin: true });
|
||||
}, 600);
|
||||
next({ ...to, replace: true });
|
||||
});
|
||||
}
|
||||
initAppPromise.then(() => {});
|
||||
} else {
|
||||
if (isPop) {
|
||||
if (!to.matched.length) {
|
||||
@@ -126,7 +133,7 @@ export const getSysRouteMap = (): IObject => {
|
||||
* @returns
|
||||
*/
|
||||
export const toSysViewComponentPath = (path: string): string => {
|
||||
path = path.replace("_", "-");
|
||||
path = path.replace(/_/g, "-");
|
||||
return `/src/views${path}.vue`;
|
||||
};
|
||||
/**
|
||||
|
||||
@@ -1,36 +1,2 @@
|
||||
// utils/chartBuilder.ts
|
||||
import type { WeatherRecord } from "../service/weatherDataService";
|
||||
import type { WeatherExtremes } from "../service/weatherStatsService";
|
||||
|
||||
export function buildWeatherOption(rows: WeatherRecord[], stats: WeatherExtremes) {
|
||||
return {
|
||||
tooltip: { trigger: "axis" },
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: rows.map((r) => r.year)
|
||||
},
|
||||
yAxis: {
|
||||
type: "value"
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "最高温",
|
||||
type: "bar",
|
||||
data: rows.map((r) => {
|
||||
const isMax = r.year === stats.maxTmaxYear;
|
||||
return {
|
||||
value: r.tmax,
|
||||
itemStyle: {
|
||||
color: isMax ? "#ff4d4f" : "#5b8ff9"
|
||||
},
|
||||
label: {
|
||||
show: isMax,
|
||||
position: "top",
|
||||
fontWeight: 600
|
||||
}
|
||||
};
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
// utils/chartBuilder.ts — 已废弃,请使用 composables/useWeatherChart.ts
|
||||
export {}
|
||||
|
||||
@@ -87,9 +87,9 @@ export const getThemeCluster = (theme: string): string[] => {
|
||||
green += Math.round(tint * (255 - green));
|
||||
blue += Math.round(tint * (255 - blue));
|
||||
|
||||
red = red.toString(16);
|
||||
green = green.toString(16);
|
||||
blue = blue.toString(16);
|
||||
red = red.toString(16).padStart(2, "0");
|
||||
green = green.toString(16).padStart(2, "0");
|
||||
blue = blue.toString(16).padStart(2, "0");
|
||||
|
||||
return `#${red}${green}${blue}`;
|
||||
}
|
||||
@@ -104,9 +104,9 @@ export const getThemeCluster = (theme: string): string[] => {
|
||||
green = Math.round((1 - shade) * green);
|
||||
blue = Math.round((1 - shade) * blue);
|
||||
|
||||
red = red.toString(16);
|
||||
green = green.toString(16);
|
||||
blue = blue.toString(16);
|
||||
red = red.toString(16).padStart(2, "0");
|
||||
green = green.toString(16).padStart(2, "0");
|
||||
blue = blue.toString(16).padStart(2, "0");
|
||||
|
||||
return `#${red}${green}${blue}`;
|
||||
};
|
||||
|
||||
@@ -237,19 +237,21 @@ export const treeDataTranslate = (data: IObject[], id?: string, pid?: string): I
|
||||
const temp: IObject = {};
|
||||
id = id || "id";
|
||||
pid = pid || "pid";
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
temp[data[i][id]] = data[i];
|
||||
// 浅拷贝避免修改输入数组元素
|
||||
const cloned = data.map((item) => ({ ...item }));
|
||||
for (let i = 0; i < cloned.length; i++) {
|
||||
temp[cloned[i][id]] = cloned[i];
|
||||
}
|
||||
for (let k = 0; k < data.length; k++) {
|
||||
if (!temp[data[k][pid]] || data[k][id] === data[k][pid]) {
|
||||
res.push(data[k]);
|
||||
for (let k = 0; k < cloned.length; k++) {
|
||||
if (!temp[cloned[k][pid]] || cloned[k][id] === cloned[k][pid]) {
|
||||
res.push(cloned[k]);
|
||||
continue;
|
||||
}
|
||||
if (!temp[data[k][pid]]["children"]) {
|
||||
temp[data[k][pid]]["children"] = [];
|
||||
if (!temp[cloned[k][pid]]["children"]) {
|
||||
temp[cloned[k][pid]]["children"] = [];
|
||||
}
|
||||
temp[data[k][pid]]["children"].push(data[k]);
|
||||
data[k]["_level"] = (temp[data[k][pid]]._level || 0) + 1;
|
||||
temp[cloned[k][pid]]["children"].push(cloned[k]);
|
||||
cloned[k]["_level"] = (temp[cloned[k][pid]]._level || 0) + 1;
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
<el-divider content-position="left">温湿度与气压</el-divider>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="相对湿度" prop="avgTemp" label-width="80px">
|
||||
<el-form-item label="相对湿度" prop="relativeHumidity" label-width="80px">
|
||||
<el-input-number v-model="dataForm.relativeHumidity" :precision="1" :step="0.1" controls-position="right" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="气压" prop="avgTemp" label-width="80px">
|
||||
<el-form-item label="气压" prop="atmospheres" label-width="80px">
|
||||
<el-input-number v-model="dataForm.atmospheres" :precision="1" :step="0.1" controls-position="right" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -80,13 +80,13 @@
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="日平均风速" prop="maxWindSpeed">
|
||||
<el-form-item label="日平均风速" prop="dayAvgWindSpeed">
|
||||
<el-input-number v-model="dataForm.dayAvgWindSpeed" :precision="1" :min="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="日平均风向" prop="maxWindSpeed">
|
||||
<el-form-item label="日平均风向" prop="dayAvgWindDirection">
|
||||
<el-input-number v-model="dataForm.dayAvgWindDirection" :precision="1" :min="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -103,23 +103,23 @@
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="极大风速" prop="maxWindSpeed">
|
||||
<el-form-item label="极大风速" prop="extremeWindSpeed">
|
||||
<el-input-number v-model="dataForm.extremeWindSpeed" :precision="1" :min="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="极大风速风向" prop="maxWindSpeed">
|
||||
<el-form-item label="极大风速风向" prop="extremeWindDirection">
|
||||
<el-input-number v-model="dataForm.extremeWindDirection" :precision="1" :min="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="极大风速出现时间" prop="minTempTime" label-width="125px">
|
||||
<el-form-item label="极大风速出现时间" prop="extremeWindTime" label-width="125px">
|
||||
<el-input v-model="dataForm.extremeWindTime" placeholder="HHmm"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最大风速出现时间" prop="minTempTime" label-width="125px">
|
||||
<el-form-item label="最大风速出现时间" prop="maxWindTime" label-width="125px">
|
||||
<el-input v-model="dataForm.maxWindTime" placeholder="HHmm"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
@@ -18,12 +18,20 @@ export default defineComponent({
|
||||
title: "404",
|
||||
message: "访问页面不存在"
|
||||
},
|
||||
403: {
|
||||
title: "403",
|
||||
message: "无权限访问"
|
||||
},
|
||||
500: {
|
||||
title: "500",
|
||||
message: "服务器错误"
|
||||
},
|
||||
error: {
|
||||
title: "错误",
|
||||
message: "访问出错了"
|
||||
}
|
||||
};
|
||||
const tip: ITip = tips[to?.toString() ?? "error"];
|
||||
const tip: ITip = tips[to?.toString()] || tips["error"];
|
||||
const onBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
@@ -273,7 +273,8 @@
|
||||
</section>
|
||||
|
||||
<!-- ==================== 主体 ==================== -->
|
||||
<div class="main-grid">
|
||||
<el-empty v-if="!loading && pageState.weatherData.length === 0" description="暂无历史气象数据" :image-size="120" style="margin-top: 60px" />
|
||||
<div class="main-grid" v-else>
|
||||
<div class="col-main">
|
||||
<el-card class="mod-card" shadow="never">
|
||||
<template #header>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<aside class="file-list-panel">
|
||||
<div v-for="(item, index) in state.fileItems" :key="item.anchorId" class="file-list-item" :class="{ 'is-active': state.selectedFileIndex === index }" @click="selectFile(index)">
|
||||
<div class="file-list-item__thumb">
|
||||
<img v-if="blobUrlCache[item.fileId]" :src="blobUrlCache[item.fileId]" class="file-list-item__img" alt="{{item.displayName}}}" />
|
||||
<img v-if="blobUrlCache[item.fileId]" :src="blobUrlCache[item.fileId]" class="file-list-item__img" :alt="item.displayName" />
|
||||
<span v-else-if="isImageFile(item.type)" class="file-list-item__icon file-list-item__icon--img">GIF</span>
|
||||
<span v-else class="file-list-item__icon file-list-item__icon--other">—</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user