前端部分更新
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
Subdirectories have their own `CLAUDE.md` with module-specific details — see:
|
||||||
|
- `system-admin/CLAUDE.md` — backend build, patterns, conventions, weather domain
|
||||||
|
- `system-common/CLAUDE.md` — shared base classes, i18n
|
||||||
|
- `weather-data-ui/CLAUDE.md` — frontend stack, patterns, critical rules
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Project overview
|
## Project overview
|
||||||
@@ -12,8 +17,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
weather-data/
|
weather-data/
|
||||||
├── system-common/ → shared Java lib
|
├── system-common/ → shared Java lib
|
||||||
├── system-admin/ → admin backend (port 8080, /system-admin)
|
├── system-admin/ → admin backend (port 8080, /system-admin)
|
||||||
├── system-api/ → public API service (port 8081, /renren-api)
|
├── system-dynamic-datasource → multi-DS routing via @DataSource annotation + AOP
|
||||||
├── system-dynamic-datasource → multi-DS support (stub, not populated)
|
|
||||||
├── renren-generator/ → code generator (commented out of build)
|
├── renren-generator/ → code generator (commented out of build)
|
||||||
└── weather-data-ui/ → Vue 3 SPA frontend
|
└── weather-data-ui/ → Vue 3 SPA frontend
|
||||||
```
|
```
|
||||||
@@ -25,257 +29,22 @@ weather-data/
|
|||||||
| Service | Port | Context Path | App Class |
|
| Service | Port | Context Path | App Class |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Admin | 8080 | `/system-admin` | `AdminApplication` |
|
| Admin | 8080 | `/system-admin` | `AdminApplication` |
|
||||||
| API | 8081 | `/renren-api` | `ApiApplication` |
|
|
||||||
| Frontend (dev) | 8001 | `/` | Vite dev server |
|
| Frontend (dev) | 8001 | `/` | Vite dev server |
|
||||||
| Frontend (prod) | 80 | `/` | Nginx via gateway |
|
| Frontend (prod) | 80 | `/` | Nginx via gateway |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Backend
|
## Code Style
|
||||||
|
|
||||||
### Build & Run
|
- **No emoji in UI strings.** Plain Chinese text for labels, buttons, status. Functional Unicode symbols are allowed and used: status dots `●`/`○`, checkmark `✓`, and box-drawing chars (`╔ ═ ╗ ━`) in log banners.
|
||||||
|
- **No emoji in code comments or docstrings.** Plain text only.
|
||||||
```bash
|
- **Keep CLAUDE.md current** — whenever code is modified, added, deleted, or any file change affects the project structure, build process, architecture, or conventions, update the relevant `CLAUDE.md` in the same commit to reflect the new state. This file and its children are the source of truth for both humans and Claude; stale documentation is a bug.
|
||||||
mvn clean install -DskipTests # full build (tests skipped by default)
|
|
||||||
mvn clean install -DskipTests=false # build with tests
|
|
||||||
mvn -pl system-admin -DskipTests=false -Dtest=YourTestClass test # single test
|
|
||||||
```
|
|
||||||
|
|
||||||
Tests use **JUnit 4** (`@RunWith(SpringRunner.class)`), not JUnit 5. Only 2 test files exist (Redis, dynamic datasource). No Maven wrapper (`mvnw`). Maven remote repository: Aliyun mirror.
|
|
||||||
|
|
||||||
Launch from IntelliJ:
|
|
||||||
- `AdminApplication` (`system-admin/`) → port 8080, context `/system-admin`
|
|
||||||
- `ApiApplication` (`system-api/`) → port 8081, context `/renren-api`
|
|
||||||
- `GeneratorApplication` (`renren-generator/`) — commented out of build
|
|
||||||
|
|
||||||
### Service layer pattern
|
|
||||||
|
|
||||||
Two base classes in `system-common`:
|
|
||||||
|
|
||||||
| Base | Purpose |
|
|
||||||
|---|---|
|
|
||||||
| `CrudService<Dao, Entity, DTO>` | Generic CRUD: `page()`, `get()`, `save()`, `update()`, `delete()` |
|
|
||||||
| `BaseService<Dao>` | Lighter base without DTO generic |
|
|
||||||
|
|
||||||
Module convention:
|
|
||||||
```
|
|
||||||
modules/<name>/
|
|
||||||
├── controller/ → @RestController, returns Result
|
|
||||||
├── dao/ → extends BaseMapper<Entity> (MyBatis-Plus)
|
|
||||||
├── dto/ → request/query DTOs (often extends BaseEntity)
|
|
||||||
├── entity/ → @TableName JPA entity
|
|
||||||
├── service/ → interface extends CrudService/BaseService
|
|
||||||
│ └── impl/ → @Service implementation
|
|
||||||
├── excel/ → EasyExcel VO classes (optional)
|
|
||||||
└── vo/ → response VO classes (optional)
|
|
||||||
```
|
|
||||||
|
|
||||||
Mapper XMLs: `src/main/resources/mapper/<domain>/**/*.xml`
|
|
||||||
|
|
||||||
### Conventions
|
|
||||||
|
|
||||||
- Lombok used throughout: `@Data`, `@AllArgsConstructor`, `@Slf4j` are standard on entity/service classes.
|
|
||||||
- DTO/Entity/VO separation per module — request DTOs often extend `BaseEntity`.
|
|
||||||
|
|
||||||
### PK & Auth
|
|
||||||
|
|
||||||
- PK: `ASSIGN_ID` (Snowflake) via `IdUtil.getSnowflakeNextId()`. All entities extend `BaseEntity`.
|
|
||||||
- Auth: Apache Shiro 1.12 (**Jakarta classifier**) + OAuth2 token. Login → `token` header.
|
|
||||||
- API module: `@Login` annotation + `AuthorizationInterceptor` (token in header or param), backed by a `token` table.
|
|
||||||
- **Do not introduce Spring Security** — the project uses Shiro exclusively.
|
|
||||||
|
|
||||||
### Key cross-cutting mechanisms
|
|
||||||
|
|
||||||
| Mechanism | How |
|
|
||||||
|---|---|
|
|
||||||
| **Data permissions** | `@DataFilter` on controller → `DataFilterAspect` → MyBatis interceptor injects dept-based SQL |
|
|
||||||
| **Auto-fill** | `FieldMetaObjectHandler` fills creator/date via MyBatis-Plus. **Only works with `insert()`/`updateById()`** — batch inserts (e.g. `insertBatchMultiRow`) bypass auto-fill; fields must be set manually. |
|
|
||||||
| **Scheduled jobs** | Quartz. `schedule_job` table, implements `ITask`, `@Component("beanName")`. Jobs auto-register at startup via `JobCommandLineRunner`. Seed data includes `testTask` (paused by default). |
|
|
||||||
| **File scanning** | `WatchService` (primary, background thread) + Quartz fallback (`FileScanTask`) + startup runner (`FileScanStartupRunner`). Files identified by MD5 hash. |
|
|
||||||
| **Excel import** | EasyExcel + async dual-pass via `WeatherDataImportManager`. Progress tracked in-memory (`ConcurrentHashMap`), lost on restart. |
|
|
||||||
| **API responses** | Always wrapped in `Result`. Frontend expects `code === 0` for success. |
|
|
||||||
| **Validation** | Hibernate Validator. XSS filter via `XssFilter`. |
|
|
||||||
|
|
||||||
### Exception handling
|
|
||||||
|
|
||||||
Two separate `@RestControllerAdvice` handlers, one per module:
|
|
||||||
|
|
||||||
| Handler | Catches | Persists errors? |
|
|
||||||
|---|---|---|
|
|
||||||
| `system-admin`: `CustomExceptionHandler` | `CommonException`, `DuplicateKeyException`, `UnauthorizedException`, generic `Exception` | **Yes** — saves to `SysLogErrorService` (IP, user-agent, URI, params, stack trace) |
|
|
||||||
| `system-api`: `RenExceptionHandler` | `CommonException`, `DuplicateKeyException`, generic `Exception` | **No** — returns `Result` only |
|
|
||||||
|
|
||||||
Both return a generic error for caught `Exception` (not the exception message). `CommonException` uses i18n message lookup via `MessageUtils.getMessage(code)`. Error codes follow `int` scheme: 5 digits, first 2 = module, last 3 = business (e.g. `10001`-`10029`).
|
|
||||||
|
|
||||||
### Logging
|
|
||||||
|
|
||||||
- Logback config differs per module. Logger names use `io.renren` (fork legacy), **not** `com.weather`.
|
|
||||||
- Admin dev profile enables MyBatis SQL stdout logging (`StdOutImpl`); API does not.
|
|
||||||
- When adding `@Slf4j` to `com.weather.*` classes, add a `com.weather` level override or change the existing `io.renren` logger scope.
|
|
||||||
|
|
||||||
### Redis & Docs
|
|
||||||
|
|
||||||
- Admin: `project-options.redis.open: true` in dev YAML. API: inherits `RedisAspect` default of `false` (no override in its YAML).
|
|
||||||
- Knife4j: disabled by default in admin (`knife4j.enable: false`), **enabled** in API (`knife4j.enable: true`). Docs at `/doc.html`.
|
|
||||||
- `RedisAspect` wraps `@RedisCache` annotations with channel publish for cache invalidation.
|
|
||||||
|
|
||||||
### MyBatis-Plus gotchas
|
|
||||||
|
|
||||||
- **Batch inserts bypass auto-fill** — `FieldMetaObjectHandler` only fires on `insert()`/`updateById()`. Custom batch methods must manually set `creator`, `createDate`, `updater`, `updateDate`, `deptId`.
|
|
||||||
- Column names with special characters (e.g. `rain_20_20`) require explicit `@TableField` annotations — MyBatis-Plus cannot auto-map them from camelCase.
|
|
||||||
- Admin `typeAliasesPackage: com.weather.modules.*.entity`; API still uses `io.renren.entity` (legacy).
|
|
||||||
|
|
||||||
### Weather domain (backend)
|
|
||||||
|
|
||||||
Three sub-modules under `system-admin/.../modules/weather/`:
|
|
||||||
|
|
||||||
| Module | Purpose |
|
|
||||||
|---|---|
|
|
||||||
| `dailydata/` | Daily observations, Excel batch import (async dual-pass), EasyExcel listener, summary export |
|
|
||||||
| `station/` | Weather station CRUD, linked to dept via `dept_id` |
|
|
||||||
| `filescan/` | File monitoring + serving. Format: `<地区>地区-<指标>.png` / `<地区>地区631信息.txt` |
|
|
||||||
|
|
||||||
#### Weather data import flow
|
|
||||||
|
|
||||||
1. **First pass**: `AnalysisEventListener` counts total rows.
|
|
||||||
2. **Second pass**: `WeatherDataListener` processes with batch insert (2000 records/batch).
|
|
||||||
3. Progress tracked in-memory via `ConcurrentHashMap<String, ImportProgress>` (`volatile` fields + `AtomicInteger`).
|
|
||||||
4. Runs on `CompletableFuture` with manual `UserContextHolder` propagation for security context.
|
|
||||||
5. On completion, clears Redis summary cache (`weather:summarize:*`).
|
|
||||||
|
|
||||||
#### Weather summarize cache
|
|
||||||
|
|
||||||
`WeatherSummarizeCacheTask` (Quartz job) pre-computes daily historical summaries into Redis. Uses **MySQL-specific** SQL functions (`MONTH()`, `DAY()` on `observe_date`). Cache key: `weather:summarize:{month}:{day}`, non-expiring. Service checks cache first for queries spanning same month/day across years.
|
|
||||||
|
|
||||||
#### Station priority ordering
|
|
||||||
|
|
||||||
`WeatherDailyDataServiceImpl.page()` uses custom `CASE WHEN` SQL to order stations belonging to the user's department + sub-departments first.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Frontend (`weather-data-ui/`)
|
|
||||||
|
|
||||||
### 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 (must follow)
|
|
||||||
|
|
||||||
#### 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 `<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
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Docker deployment
|
## Docker deployment
|
||||||
|
|
||||||
Six services in `docker-compose.yml`: mysql (8.0), redis (7 Alpine + AOF), admin JAR, API JAR, UI (Nginx + built frontend), gateway (Nginx reverse proxy on port 80).
|
Five services in `docker-compose.yml`: mysql (8.0), redis (7 Alpine + AOF), admin JAR, UI (Nginx + built frontend), gateway (Nginx reverse proxy on port 80). The `api` service definition still references the deleted `system-api` module — remove it before deploying.
|
||||||
|
|
||||||
**Important**: The `deploy/` directory referenced by Docker Compose (`deploy/mysql/init/`, `deploy/nginx/`) **does not exist locally** — it must be created for deployment.
|
**Important**: The `deploy/` directory referenced by Docker Compose (`deploy/mysql/init/`, `deploy/nginx/`) **does not exist locally** — it must be created for deployment.
|
||||||
|
|
||||||
@@ -284,8 +53,7 @@ Environment variables from `.env` at project root. Two frontend Dockerfiles: sta
|
|||||||
## Repository notes
|
## Repository notes
|
||||||
|
|
||||||
- `README.md` does not contain substantive guidance — this file is the primary operational reference.
|
- `README.md` does not contain substantive guidance — this file is the primary operational reference.
|
||||||
- `weather-data-ui/CLAUDE.md` is superseded by this merged root file.
|
|
||||||
- No CI configuration exists. Only pre-commit is frontend lint-staged via yarn git hooks.
|
- No CI configuration exists. Only pre-commit is frontend lint-staged via yarn git hooks.
|
||||||
- `renren-generator` module exists but is commented out of the root POM build.
|
- `renren-generator` module exists but is commented out of the root POM build.
|
||||||
- `system-dynamic-datasource` is a stub — multi-DS config in `application-dev.yml` is commented out.
|
- `system-dynamic-datasource` provides `@DataSource` annotation-driven multi-DS routing; the slave DS config in `application-dev.yml` is commented out.
|
||||||
- `.gitignore` excludes `.idea/` but `.idea/` is tracked (committed IDE config; `.idea/.gitignore` only excludes local files like `workspace.xml`).
|
- `.gitignore` excludes `.idea/` but `.idea/` is tracked (committed IDE config; `.idea/.gitignore` only excludes local files like `workspace.xml`).
|
||||||
|
|||||||
@@ -9,15 +9,26 @@ import com.weather.common.utils.Result;
|
|||||||
import com.weather.modules.log.entity.SysLogErrorEntity;
|
import com.weather.modules.log.entity.SysLogErrorEntity;
|
||||||
import com.weather.modules.log.service.SysLogErrorService;
|
import com.weather.modules.log.service.SysLogErrorService;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.validation.ConstraintViolation;
|
||||||
|
import jakarta.validation.ConstraintViolationException;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.authz.UnauthorizedException;
|
import org.apache.shiro.authz.UnauthorizedException;
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||||
|
import org.springframework.validation.FieldError;
|
||||||
|
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,6 +67,57 @@ public class CustomExceptionHandler {
|
|||||||
return new Result().error(ErrorCode.UNAUTHORIZED);
|
return new Result().error(ErrorCode.UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 @Valid/@Validated 校验失败(@RequestBody 参数)
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
|
||||||
|
FieldError fieldError = ex.getBindingResult().getFieldError();
|
||||||
|
String msg = fieldError != null ? fieldError.getDefaultMessage() : ex.getMessage();
|
||||||
|
return new Result().error(ErrorCode.PARAMS_GET_ERROR, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理类级别 @Validated 校验失败(路径/查询参数)
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(ConstraintViolationException.class)
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
public Result handleConstraintViolationException(ConstraintViolationException ex) {
|
||||||
|
Set<ConstraintViolation<?>> violations = ex.getConstraintViolations();
|
||||||
|
String msg = violations.stream()
|
||||||
|
.map(ConstraintViolation::getMessage)
|
||||||
|
.collect(Collectors.joining("; "));
|
||||||
|
return new Result().error(ErrorCode.PARAMS_GET_ERROR, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理请求体解析失败(JSON格式错误等)
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
public Result handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) {
|
||||||
|
return new Result().error(ErrorCode.PARAMS_GET_ERROR, "请求参数格式错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理缺少必要请求参数
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
public Result handleMissingServletRequestParameterException(MissingServletRequestParameterException ex) {
|
||||||
|
return new Result().error(ErrorCode.NOT_NULL, "参数[" + ex.getParameterName() + "]不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理请求方法不支持(GET/POST方法错误)
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||||
|
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
|
||||||
|
public Result handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException ex) {
|
||||||
|
return new Result().error(ErrorCode.INTERNAL_SERVER_ERROR, "请求方法不支持: " + ex.getMethod());
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public Result handleException(Exception ex) {
|
public Result handleException(Exception ex) {
|
||||||
log.error(ex.getMessage(), ex);
|
log.error(ex.getMessage(), ex);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package com.weather.modules.security.dto;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@@ -20,6 +21,7 @@ public class LoginDTO implements Serializable {
|
|||||||
|
|
||||||
@Schema(title = "用户名", required = true)
|
@Schema(title = "用户名", required = true)
|
||||||
@NotBlank(message="{sysuser.username.require}")
|
@NotBlank(message="{sysuser.username.require}")
|
||||||
|
@Pattern(regexp="^[a-zA-Z0-9]+$", message="{sysuser.username.format}")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@Schema(title = "密码")
|
@Schema(title = "密码")
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import jakarta.validation.constraints.Email;
|
|||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Null;
|
import jakarta.validation.constraints.Null;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.hibernate.validator.constraints.Range;
|
import org.hibernate.validator.constraints.Range;
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ public class SysUserDTO implements Serializable {
|
|||||||
|
|
||||||
@Schema(title = "用户名", required = true)
|
@Schema(title = "用户名", required = true)
|
||||||
@NotBlank(message="{sysuser.username.require}", groups = DefaultGroup.class)
|
@NotBlank(message="{sysuser.username.require}", groups = DefaultGroup.class)
|
||||||
|
@Pattern(regexp="^[a-zA-Z0-9]+$", message="{sysuser.username.format}", groups = DefaultGroup.class)
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@Schema(title = "密码")
|
@Schema(title = "密码")
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package com.weather.modules.weather.filescan;
|
|||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.weather.common.constant.Constant;
|
import com.weather.common.constant.Constant;
|
||||||
import com.weather.modules.sys.dao.SysDeptDao;
|
|
||||||
import com.weather.modules.sys.entity.SysDeptEntity;
|
|
||||||
import com.weather.modules.sys.service.SysParamsService;
|
import com.weather.modules.sys.service.SysParamsService;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -12,11 +10,6 @@ import org.springframework.context.event.EventListener;
|
|||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -30,7 +23,6 @@ public class FileScanStartupRunner {
|
|||||||
private static final long STARTUP_DELAY_SECONDS = 30;
|
private static final long STARTUP_DELAY_SECONDS = 30;
|
||||||
|
|
||||||
private final SysParamsService sysParamsService;
|
private final SysParamsService sysParamsService;
|
||||||
private final SysDeptDao sysDeptDao;
|
|
||||||
private final FileWatchServiceManager fileWatchServiceManager;
|
private final FileWatchServiceManager fileWatchServiceManager;
|
||||||
|
|
||||||
@Async
|
@Async
|
||||||
@@ -45,38 +37,12 @@ public class FileScanStartupRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String rootPath = sysParamsService.getValue(Constant.FILE_SCAN_ROOT_PATH);
|
String rootPath = sysParamsService.getValue(Constant.FILE_SCAN_ROOT_PATH);
|
||||||
if (StrUtil.isBlank(rootPath)) {
|
if (StrUtil.isBlank(rootPath) || "0".equals(rootPath)) {
|
||||||
log.warn("FILE_SCAN_ROOT_PATH 未配置,跳过文件扫描目录初始化");
|
log.warn("FILE_SCAN_ROOT_PATH 未配置,跳过文件扫描目录初始化");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保根层级 receive / display / archive 目录存在(用于模型预报等非部门文件)
|
fileWatchServiceManager.ensureDirectoriesExist();
|
||||||
List<String> rootDirs = List.of("receive", "display", "archive");
|
|
||||||
for (String sub : rootDirs) {
|
|
||||||
Path dir = Paths.get(rootPath, sub);
|
|
||||||
try {
|
|
||||||
Files.createDirectories(dir);
|
|
||||||
log.debug("创建根目录: {}", dir);
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("创建根目录失败: {}", dir, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建各部门子目录
|
|
||||||
List<SysDeptEntity> deptList = sysDeptDao.selectList(null);
|
|
||||||
for (SysDeptEntity dept : deptList) {
|
|
||||||
if (StrUtil.isBlank(dept.getName())) continue;
|
|
||||||
for (String sub : rootDirs) {
|
|
||||||
Path dir = Paths.get(rootPath, sub, dept.getName());
|
|
||||||
try {
|
|
||||||
Files.createDirectories(dir);
|
|
||||||
log.debug("创建目录: {}", dir);
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("创建目录失败: {}", dir, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileWatchServiceManager.registerDeptDirectories();
|
fileWatchServiceManager.registerDeptDirectories();
|
||||||
fileWatchServiceManager.scanAllDirectories();
|
fileWatchServiceManager.scanAllDirectories();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package com.weather.modules.weather.filescan;
|
package com.weather.modules.weather.filescan;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.weather.common.constant.Constant;
|
||||||
import com.weather.modules.job.task.ITask;
|
import com.weather.modules.job.task.ITask;
|
||||||
|
import com.weather.modules.sys.service.SysParamsService;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -10,11 +13,27 @@ import org.springframework.stereotype.Component;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class FileScanTask implements ITask {
|
public class FileScanTask implements ITask {
|
||||||
|
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
private final FileWatchServiceManager fileWatchServiceManager;
|
private final FileWatchServiceManager fileWatchServiceManager;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run(String params) {
|
public void run(String params) {
|
||||||
log.info("文件扫描定时任务开始执行");
|
String rootPath = sysParamsService.getValue(Constant.FILE_SCAN_ROOT_PATH);
|
||||||
|
if (StrUtil.isBlank(rootPath) || "0".equals(rootPath)) {
|
||||||
|
log.warn("FILE_SCAN_ROOT_PATH 未配置,跳过文件扫描定时任务");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("文件扫描定时任务开始执行, 根路径: {}", rootPath);
|
||||||
|
|
||||||
|
// 确保目录结构存在(路径可能在启动后才配置)
|
||||||
|
fileWatchServiceManager.ensureDirectoriesExist();
|
||||||
|
|
||||||
|
// 如果 WatchService 尚未注册部门目录则注册
|
||||||
|
if (fileWatchServiceManager.deptDirMap.isEmpty()) {
|
||||||
|
fileWatchServiceManager.registerDeptDirectories();
|
||||||
|
}
|
||||||
|
|
||||||
fileWatchServiceManager.scanAllDirectories();
|
fileWatchServiceManager.scanAllDirectories();
|
||||||
log.info("文件扫描定时任务执行完毕");
|
log.info("文件扫描定时任务执行完毕");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public class FileWatchServiceManager {
|
|||||||
private static final Set<String> SKIP_DIR_NAMES = Set.of("新建文件夹");
|
private static final Set<String> SKIP_DIR_NAMES = Set.of("新建文件夹");
|
||||||
|
|
||||||
private WatchService watchService;
|
private WatchService watchService;
|
||||||
private final Map<String, Long> deptDirMap = new LinkedHashMap<>();
|
final Map<String, Long> deptDirMap = new LinkedHashMap<>();
|
||||||
private String rootReceiveDir;
|
private String rootReceiveDir;
|
||||||
private volatile boolean running = false;
|
private volatile boolean running = false;
|
||||||
|
|
||||||
@@ -74,10 +74,43 @@ public class FileWatchServiceManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 确保文件扫描目录结构存在(receive/display/archive 及各部门子目录) */
|
||||||
|
public void ensureDirectoriesExist() {
|
||||||
|
String rootPath = sysParamsService.getValue(Constant.FILE_SCAN_ROOT_PATH);
|
||||||
|
if (StrUtil.isBlank(rootPath) || "0".equals(rootPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> rootDirs = List.of("receive", "display", "archive");
|
||||||
|
for (String sub : rootDirs) {
|
||||||
|
Path dir = Paths.get(rootPath, sub);
|
||||||
|
try {
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
log.debug("确保根目录存在: {}", dir);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("创建根目录失败: {}", dir, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SysDeptEntity> deptList = sysDeptDao.selectList(null);
|
||||||
|
for (SysDeptEntity dept : deptList) {
|
||||||
|
if (StrUtil.isBlank(dept.getName())) continue;
|
||||||
|
for (String sub : rootDirs) {
|
||||||
|
Path dir = Paths.get(rootPath, sub, dept.getName());
|
||||||
|
try {
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
log.debug("确保部门目录存在: {}", dir);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("创建部门目录失败: {}", dir, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 注册部门目录 + 根目录的 WatchService 监控 */
|
/** 注册部门目录 + 根目录的 WatchService 监控 */
|
||||||
public void registerDeptDirectories() {
|
public void registerDeptDirectories() {
|
||||||
String rootPath = sysParamsService.getValue(Constant.FILE_SCAN_ROOT_PATH);
|
String rootPath = sysParamsService.getValue(Constant.FILE_SCAN_ROOT_PATH);
|
||||||
if (StrUtil.isBlank(rootPath)) {
|
if (StrUtil.isBlank(rootPath) || "0".equals(rootPath)) {
|
||||||
log.warn("文件扫描根路径未配置,请检查 sys_params 中的 FILE_SCAN_ROOT_PATH");
|
log.warn("文件扫描根路径未配置,请检查 sys_params 中的 FILE_SCAN_ROOT_PATH");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -114,7 +147,7 @@ public class FileWatchServiceManager {
|
|||||||
/** 全量扫描:部门子目录 + 根目录直接文件 */
|
/** 全量扫描:部门子目录 + 根目录直接文件 */
|
||||||
public void scanAllDirectories() {
|
public void scanAllDirectories() {
|
||||||
String rootPath = sysParamsService.getValue(Constant.FILE_SCAN_ROOT_PATH);
|
String rootPath = sysParamsService.getValue(Constant.FILE_SCAN_ROOT_PATH);
|
||||||
if (StrUtil.isBlank(rootPath)) return;
|
if (StrUtil.isBlank(rootPath) || "0".equals(rootPath)) return;
|
||||||
|
|
||||||
Path receiveRoot = Paths.get(rootPath, "receive");
|
Path receiveRoot = Paths.get(rootPath, "receive");
|
||||||
if (!Files.exists(receiveRoot)) return;
|
if (!Files.exists(receiveRoot)) return;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ sysparams.paramcode.require=\u53C2\u6570\u7F16\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
|||||||
sysparams.paramvalue.require=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
|
sysparams.paramvalue.require=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
|
|
||||||
sysuser.username.require=\u7528\u6237\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
sysuser.username.require=\u7528\u6237\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
|
sysuser.username.format=\u7528\u6237\u540D\u53EA\u80FD\u4E3A\u82F1\u6587\u6216\u6570\u5B57\u7EC4\u5408
|
||||||
sysuser.password.require=\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
sysuser.password.require=\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
sysuser.realname.require=\u59D3\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
sysuser.realname.require=\u59D3\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
sysuser.gender.range=\u6027\u522B\u53D6\u503C\u8303\u56F40~2
|
sysuser.gender.range=\u6027\u522B\u53D6\u503C\u8303\u56F40~2
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not dead
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Test
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
NODE_ENV=development
|
||||||
|
VITE_APP_API=http://192.168.2.186:8080/system-admin
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
NODE_ENV=production
|
||||||
|
# 内网部署:通过 Nginx 反向代理,使用相对路径避免硬编码 IP
|
||||||
|
VITE_APP_API=/system-admin
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
"vue/setup-compiler-macros": true
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
"plugin:vue/vue3-essential",
|
||||||
|
"eslint:recommended",
|
||||||
|
"@vue/typescript/recommended",
|
||||||
|
"@vue/prettier"
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"no-console": "off",
|
||||||
|
"no-debugger": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": ["off"],
|
||||||
|
"@typescript-eslint/no-var-requires": 0,
|
||||||
|
"vue/multi-word-component-names": "off"
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": false,
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"endOfLine": "auto",
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ".prettierrc",
|
||||||
|
"options": { "parser": "json" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": "*.vue",
|
||||||
|
"options": {
|
||||||
|
"parser": "vue",
|
||||||
|
"printWidth": 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# 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 `<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
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# ============================================================
|
||||||
|
# 多阶段构建 (开发机有网络时使用)
|
||||||
|
# docker build -t weather-data-ui .
|
||||||
|
# ============================================================
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm ci --registry=https://registry.npmmirror.com
|
||||||
|
|
||||||
|
# 复制源码并构建
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build:prod
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 运行阶段 - Nginx 提供静态文件服务
|
||||||
|
# ============================================================
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
LABEL maintainer="weather-data"
|
||||||
|
LABEL description="Weather Data System - Frontend UI"
|
||||||
|
|
||||||
|
# 删除默认配置
|
||||||
|
RUN rm -f /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# 复制自定义 nginx 配置
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# 复制构建产物
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||||
|
CMD wget -qO- http://localhost/ || exit 1
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# ============================================================
|
||||||
|
# 离线部署 Dockerfile (内网服务器使用)
|
||||||
|
# 前提:已将 dist/ 目录拷贝到当前路径
|
||||||
|
# docker build -f Dockerfile.offline -t weather-data-ui .
|
||||||
|
# ============================================================
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
LABEL maintainer="weather-data"
|
||||||
|
LABEL description="Weather Data System - Frontend UI (offline)"
|
||||||
|
|
||||||
|
RUN rm -f /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||||
|
CMD wget -qO- http://localhost/ || exit 1
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# weather-data-ui
|
||||||
|
|
||||||
|
气象数据分析平台前端项目。
|
||||||
|
|
||||||
|
> 项目说明、快速启动、部署文档 → [../README.md](../README.md)
|
||||||
|
> 开发者文档(架构、规范) → [../CLAUDE.md](../CLAUDE.md)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: ["@vue/cli-plugin-babel/preset"],
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>气象数据 - 管理系统</title>
|
||||||
|
<!-- Preload fonts to avoid blocking render (moved from home.vue @import) -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700;900&family=JetBrains+Mono:wght@400;600&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<script>
|
||||||
|
//全局钩子
|
||||||
|
window.SITE_CONFIG = {
|
||||||
|
//api
|
||||||
|
apiURL: "<%=apiURL%>"
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="./src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
charset utf-8;
|
||||||
|
|
||||||
|
# 访问日志 (生产环境可关闭 access_log 减少 IO)
|
||||||
|
access_log /var/log/nginx/access.log;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
|
||||||
|
# 站点根目录
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# SPA 路由处理:所有非静态资源请求返回 index.html
|
||||||
|
# 因为前端使用 hash router,这里主要是兜底
|
||||||
|
# ============================================================
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 静态资源缓存策略
|
||||||
|
# /assets/ 下的文件经过 Vite 处理带有 content hash,可长期缓存
|
||||||
|
# ============================================================
|
||||||
|
location /assets/ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# index.html 禁止缓存(确保前端更新后立即生效)
|
||||||
|
# ============================================================
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
add_header Pragma "no-cache";
|
||||||
|
add_header Expires 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Gzip 压缩
|
||||||
|
# ============================================================
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/xml
|
||||||
|
text/javascript
|
||||||
|
application/javascript
|
||||||
|
application/json
|
||||||
|
application/xml
|
||||||
|
image/svg+xml;
|
||||||
|
|
||||||
|
# 隐藏 Nginx 版本号
|
||||||
|
server_tokens off;
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"name": "weather-data-ui",
|
||||||
|
"version": "5.5.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "npm run build:prod",
|
||||||
|
"build:prod": "vite build --mode production",
|
||||||
|
"serve": "npm run build && vite preview",
|
||||||
|
"lint": "eslint \"src/**/*.{vue,ts}\" --fix"
|
||||||
|
},
|
||||||
|
"gitHooks": {
|
||||||
|
"pre-commit": "lint-staged"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"src/**/*.{ts,vue}": [
|
||||||
|
"eslint --fix",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "2.3.1",
|
||||||
|
"@vueuse/core": "9.1.1",
|
||||||
|
"@wangeditor/editor": "5.1.1",
|
||||||
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
|
"axios": "1.11.0",
|
||||||
|
"classnames": "^2.3.1",
|
||||||
|
"core-js": "^3.14.0",
|
||||||
|
"echarts": "^5.2.2",
|
||||||
|
"element-plus": "2.10.5",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
|
"jspdf": "^4.2.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"mitt": "^2.1.0",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"pinia": "2.3.1",
|
||||||
|
"qs": "^6.10.1",
|
||||||
|
"vue": "^3.5.18",
|
||||||
|
"vue-echarts": "^6.0.0",
|
||||||
|
"vue-router": "4.2.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
|
"@types/lodash": "^4.14.172",
|
||||||
|
"@types/nprogress": "^0.2.0",
|
||||||
|
"@types/papaparse": "^5.5.2",
|
||||||
|
"@types/qs": "^6.9.6",
|
||||||
|
"@types/sortablejs": "^1.10.6",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.23.0",
|
||||||
|
"@typescript-eslint/parser": "^5.23.0",
|
||||||
|
"@vitejs/plugin-vue": "5.1.5",
|
||||||
|
"@vue/compiler-sfc": "^3.5.18",
|
||||||
|
"@vue/eslint-config-prettier": "^7.0.0",
|
||||||
|
"@vue/eslint-config-typescript": "^10.0.0",
|
||||||
|
"eslint": "^8.13.0",
|
||||||
|
"eslint-plugin-vue": "^8.6.0",
|
||||||
|
"less": "^4.1.1",
|
||||||
|
"less-loader": "^10.0.0",
|
||||||
|
"lint-staged": "^11.0.0",
|
||||||
|
"papaparse": "^5.5.3",
|
||||||
|
"prettier": "^2.6.2",
|
||||||
|
"sass": "^1.50.1",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "5.4.19",
|
||||||
|
"vite-plugin-html": "^3.2.2",
|
||||||
|
"vite-plugin-svg-icons": "2.0.1",
|
||||||
|
"vite-tsconfig-paths": "3.4.0",
|
||||||
|
"vue-tsc": "2.1.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 83 KiB |
@@ -0,0 +1,57 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import "@/assets/css/app.less";
|
||||||
|
import "@/assets/theme/index.less";
|
||||||
|
import "@/assets/theme/mobile.less";
|
||||||
|
import AlertMarquee from "@/components/alert-marquee/index.vue";
|
||||||
|
import FullscreenLayout from "@/layout/fullscreen-layout.vue";
|
||||||
|
import Layout from "@/layout/index.vue";
|
||||||
|
import { ElConfigProvider } from "element-plus";
|
||||||
|
import { defineComponent, onMounted, reactive, watch } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { useAppStore } from "@/store";
|
||||||
|
import app from "./constants/app";
|
||||||
|
import { EPageLayoutEnum, EThemeColor, EThemeSetting } from "./constants/enum";
|
||||||
|
import { IObject } from "./types/interface";
|
||||||
|
import { getThemeConfigCache, setThemeColor, updateTheme } from "./utils/theme";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "App",
|
||||||
|
components: { AlertMarquee, Layout, FullscreenLayout, [ElConfigProvider.name]: ElConfigProvider },
|
||||||
|
setup() {
|
||||||
|
const store = useAppStore();
|
||||||
|
const route = useRoute();
|
||||||
|
const state = reactive({
|
||||||
|
layout: location.href.includes("pop=true") ? EPageLayoutEnum.fullscreen : EPageLayoutEnum.page
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
//读取主题色缓存
|
||||||
|
const themeCache = getThemeConfigCache();
|
||||||
|
const themeColor = themeCache[EThemeSetting.ThemeColor];
|
||||||
|
setThemeColor(EThemeColor.ThemeColor, themeColor);
|
||||||
|
updateTheme(themeColor);
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => [route.path, route.query, route.fullPath],
|
||||||
|
([path, query, fullPath]) => {
|
||||||
|
store.updateState({ activeTabName: fullPath });
|
||||||
|
state.layout = app.fullscreenPages.includes(path as string) || (query as IObject)["pop"] ? EPageLayoutEnum.fullscreen : EPageLayoutEnum.page;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
store,
|
||||||
|
state,
|
||||||
|
pageTag: EPageLayoutEnum.page
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<el-config-provider>
|
||||||
|
<div v-if="!store.state.appIsRender" v-loading="true" :element-loading-fullscreen="true" :element-loading-lock="true" style="width: 100vw; height: 100vh; position: absolute; top: 0; left: 0; z-index: 99999; background: #fff"></div>
|
||||||
|
<template v-if="store.state.appIsReady">
|
||||||
|
<alert-marquee />
|
||||||
|
<layout v-if="state.layout === pageTag"> </layout>
|
||||||
|
<fullscreen-layout v-else></fullscreen-layout>
|
||||||
|
</template>
|
||||||
|
</el-config-provider>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,567 @@
|
|||||||
|
@import "../theme/base.less";
|
||||||
|
|
||||||
|
*,
|
||||||
|
:after,
|
||||||
|
:before {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: #595959;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
|
||||||
|
"微软雅黑", Arial, sans-serif;
|
||||||
|
color: #595959;
|
||||||
|
background: #f0f2f5;
|
||||||
|
|
||||||
|
//字体
|
||||||
|
.text {
|
||||||
|
&-2 {
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: @--color-primary;
|
||||||
|
text-decoration: none;
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
color: @--color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
cursor: pointer;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-variant: normal;
|
||||||
|
text-transform: none;
|
||||||
|
line-height: 1;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
display: inline-block;
|
||||||
|
fill: currentColor;
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
}
|
||||||
|
.icon-svg {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
fill: currentColor;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-badge__content {
|
||||||
|
height: 16px;
|
||||||
|
line-height: 16px;
|
||||||
|
padding: 0 5px;
|
||||||
|
border: none;
|
||||||
|
background: #ff4d4f !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ele-badge-static {
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ele-badge-static .el-badge__content {
|
||||||
|
position: static;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
//alert
|
||||||
|
.ele-alert-border.is-light.el-alert--warning {
|
||||||
|
border: 1px solid #faad144d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-alert--warning.is-light {
|
||||||
|
background-color: #fff7e8 !important;
|
||||||
|
color: #faad14 !important;
|
||||||
|
}
|
||||||
|
.ele-alert-border.is-light .el-alert__title {
|
||||||
|
color: #262626 !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
.el-alert__content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
//menu
|
||||||
|
.el-menu-item a,
|
||||||
|
.el-menu-item span,
|
||||||
|
.el-sub-menu > .el-sub-menu__title a,
|
||||||
|
.el-sub-menu > .el-sub-menu__title span {
|
||||||
|
color: @dark-text;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-left: 5px;
|
||||||
|
display: inline-flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.rr-sidebar-menu.el-menu--horizontal > .el-menu-item {
|
||||||
|
padding: 0 12px;
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
|
.rr-sidebar-menu-pop-dark,
|
||||||
|
.rr-sidebar-menu-pop-light {
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-width: 0 !important;
|
||||||
|
}
|
||||||
|
.el-sub-menu__icon-arrow {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
//pop
|
||||||
|
.el-popper.is-dark a {
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.el-popover.el-popper {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
//表格
|
||||||
|
.el-table thead {
|
||||||
|
color: #303133 !important;
|
||||||
|
th {
|
||||||
|
background-color: #f5f7fa !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-table__fixed-right::before {
|
||||||
|
background: transparent !important; //element-plus表格高度动态计算bug,强制下划线不显示颜色
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form--inline .el-form-item{
|
||||||
|
margin-right: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
//分页
|
||||||
|
.el-pagination {
|
||||||
|
margin-top: 15px !important;
|
||||||
|
justify-content: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
//tinymce
|
||||||
|
.tox-tinymce-aux {
|
||||||
|
z-index: 3000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
//弹窗popover
|
||||||
|
.popover-pop {
|
||||||
|
padding: 10px 0 5px 5px !important;
|
||||||
|
&-body {
|
||||||
|
max-height: 255px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//弹窗
|
||||||
|
.rr-dialog {
|
||||||
|
min-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rr {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&-loading {
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
//全屏页面渲染
|
||||||
|
&-fullscreen {
|
||||||
|
width: 100vw;
|
||||||
|
|
||||||
|
&.new-pop-window > div {
|
||||||
|
padding: 15px;
|
||||||
|
margin: 15px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-error {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: #fff;
|
||||||
|
z-index: 1200;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-drawer {
|
||||||
|
.el-drawer__header {
|
||||||
|
color: #595959;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 13px 16px;
|
||||||
|
border-bottom: 1px solid #f4f4f4;
|
||||||
|
}
|
||||||
|
.el-drawer__body {
|
||||||
|
padding: 15px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//顶部
|
||||||
|
&-header {
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 !important;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 200;
|
||||||
|
&-ctx {
|
||||||
|
display: flex;
|
||||||
|
height: 50px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
&-logo {
|
||||||
|
display: flex;
|
||||||
|
color: #ffffffe6;
|
||||||
|
background-color: #191a23;
|
||||||
|
font-size: 19px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
width: 230px;
|
||||||
|
height: 50px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: Avenir, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue,
|
||||||
|
Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol,
|
||||||
|
Noto Color Emoji;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
transition: width 0.3s;
|
||||||
|
padding: 0 15px;
|
||||||
|
|
||||||
|
&-img {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: inline-block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
&-wrap {
|
||||||
|
display: flex;
|
||||||
|
&.enabled-logo {
|
||||||
|
&-false {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-line {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
&-text {
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei,
|
||||||
|
"微软雅黑", Arial, sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
//左侧侧边栏
|
||||||
|
&-sidebar {
|
||||||
|
margin-top: 50px;
|
||||||
|
width: 230px !important;
|
||||||
|
min-height: calc(100vh - 50px);
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
transition: width 0.3s;
|
||||||
|
z-index: 120;
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
&-menu {
|
||||||
|
transition: width 0.3s;
|
||||||
|
overflow: hidden;
|
||||||
|
&.el-menu--horizontal {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
.el-menu-item {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu {
|
||||||
|
width: 230px !important;
|
||||||
|
border-right: 0 !important;
|
||||||
|
&-item {
|
||||||
|
height: 45px;
|
||||||
|
line-height: 45px;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
&-item,
|
||||||
|
.el-sub-menu__title {
|
||||||
|
background: transparent !important;
|
||||||
|
&:focus {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-item,
|
||||||
|
.el-sub-menu__title,
|
||||||
|
&-item-group__title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.el-sub-menu {
|
||||||
|
.el-sub-menu__title {
|
||||||
|
i {
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu-item,
|
||||||
|
.el-sub-menu .el-sub-menu__title {
|
||||||
|
margin: 0;
|
||||||
|
height: 48px;
|
||||||
|
line-height: 48px;
|
||||||
|
}
|
||||||
|
.el-sub-menu {
|
||||||
|
.el-menu-item {
|
||||||
|
height: 45px;
|
||||||
|
line-height: 45px;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu-item [class^="el-icon"],
|
||||||
|
.el-sub-menu > .el-sub-menu__title [class^="el-icon"] {
|
||||||
|
font-size: 17px;
|
||||||
|
margin-right: 0;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu-item a,
|
||||||
|
.el-menu-item span,
|
||||||
|
.el-sub-menu > .el-sub-menu__title a,
|
||||||
|
.el-sub-menu > .el-sub-menu__title span {
|
||||||
|
margin-left: 10px;
|
||||||
|
> a {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//页面内容区域外层
|
||||||
|
&-view {
|
||||||
|
flex: 1;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0 !important;
|
||||||
|
border-top: 1px solid #f4f4f4 !important;
|
||||||
|
&-container {
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
&-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
&-ctx {
|
||||||
|
margin-top: 39px;
|
||||||
|
padding: 15px !important;
|
||||||
|
flex: 1;
|
||||||
|
//页面内容区域
|
||||||
|
&-card {
|
||||||
|
min-height: calc(100% - 5px);
|
||||||
|
border-width: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//页面内容顶部tab标签栏
|
||||||
|
&-tab {
|
||||||
|
background: #fff;
|
||||||
|
width: 100%;
|
||||||
|
height: 39px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
&:hover {
|
||||||
|
background: inherit !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-wrap {
|
||||||
|
position: fixed;
|
||||||
|
top: 50px;
|
||||||
|
left: 230px;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
z-index: 100;
|
||||||
|
transition: left 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-ops {
|
||||||
|
width: 40px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #fff;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-left: 1px solid #f4f4f4;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
color: #8c8c8c !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
margin-right: 5px; //element-plus el-dropdown自动定位bug bottom-end指令不生效,临时采用偏移5px
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__active-bar {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__nav {
|
||||||
|
&-prev,
|
||||||
|
&-next {
|
||||||
|
.el-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-tabs__item {
|
||||||
|
padding: 0 15px !important;
|
||||||
|
border-right: 1px solid #f4f4f4;
|
||||||
|
user-select: none;
|
||||||
|
color: #8c8c8c;
|
||||||
|
&:hover {
|
||||||
|
color: #262626;
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
.is-icon-close {
|
||||||
|
transition: none !important;
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
margin-right: 8px;
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #ddd;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
color: @primary-bg-light;
|
||||||
|
background-color: @primary-bg-light !important;
|
||||||
|
&:before {
|
||||||
|
background-color: @primary-bg-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
|
&::before {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__nav-wrap {
|
||||||
|
padding: 0px 39px 0 40px !important;
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 44px;
|
||||||
|
text-align: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
font-family: element-icons !important;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-variant: normal;
|
||||||
|
text-transform: none;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
content: url('data:image/svg+xml;charset=utf-8,<svg width="16" height="16" color="rgb(140 140 140)" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-042ca774=""><path fill="currentColor" d="M609.408 149.376L277.76 489.6a32 32 0 000 44.672l331.648 340.352a29.12 29.12 0 0041.728 0 30.592 30.592 0 000-42.752L339.264 511.936l311.872-319.872a30.592 30.592 0 000-42.688 29.12 29.12 0 00-41.728 0z"></path></svg>');
|
||||||
|
border-right: 1px solid #f4f4f4;
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
content: url('data:image/svg+xml;charset=utf-8,<svg width="16" height="16" color="rgb(140 140 140)" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-042ca774=""><path fill="currentColor" d="M340.864 149.312a30.592 30.592 0 000 42.752L652.736 512 340.864 831.872a30.592 30.592 0 000 42.752 29.12 29.12 0 0041.728 0L714.24 534.336a32 32 0 000-44.672L382.592 149.376a29.12 29.12 0 00-41.728 0z"></path></svg>');
|
||||||
|
right: 0;
|
||||||
|
left: auto;
|
||||||
|
bottom: auto;
|
||||||
|
height: auto;
|
||||||
|
background-color: transparent;
|
||||||
|
border-left: 1px solid #f4f4f4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__nav-next,
|
||||||
|
.el-tabs__nav-prev {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
i {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-top: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__nav-prev {
|
||||||
|
border-right: 1px solid #f4f4f4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ql-toolbar.ql-snow{
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form--inline {
|
||||||
|
.el-form-item {
|
||||||
|
& > .el-input, .el-cascader, .el-select, .el-date-editor, .el-autocomplete {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
.rr-header-ctx {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
font-size: 18px;
|
||||||
|
width: auto;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
.rr-header-right {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: space-between;
|
||||||
|
overflow: hidden;
|
||||||
|
align-items: center;
|
||||||
|
> div {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
&-items {
|
||||||
|
display: flex;
|
||||||
|
padding: 0 8px 0 0;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
padding: 0 12px;
|
||||||
|
height: 50px;
|
||||||
|
line-height: 56px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
&-icon {
|
||||||
|
height: 50px;
|
||||||
|
line-height: 56px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.el-badge {
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
.el-dropdown {
|
||||||
|
vertical-align: inherit;
|
||||||
|
.el-icon {
|
||||||
|
.icon {
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-left {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&-br {
|
||||||
|
padding: 0 10px;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
flex: 1;
|
||||||
|
.el-breadcrumb {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.el-breadcrumb__inner,
|
||||||
|
.el-breadcrumb__inner a,
|
||||||
|
.el-breadcrumb__item:last-child .el-breadcrumb__inner,
|
||||||
|
.el-breadcrumb__item:last-child .el-breadcrumb__inner:hover,
|
||||||
|
.el-breadcrumb__item:last-child .el-breadcrumb__inner a,
|
||||||
|
.el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover {
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
.el-breadcrumb__item {
|
||||||
|
float: none !important;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.el-breadcrumb__inner.is-link {
|
||||||
|
color: #595959;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-space__item {
|
||||||
|
&:last-child {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rr-sidebar-menu.el-menu--horizontal {
|
||||||
|
display: flex;
|
||||||
|
span {
|
||||||
|
width: inherit;
|
||||||
|
}
|
||||||
|
.el-sub-menu {
|
||||||
|
.el-sub-menu__icon-arrow {
|
||||||
|
margin-left: 3px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.el-sub-menu__title {
|
||||||
|
padding: 0 10px 0 12px;
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
.rr-setting {
|
||||||
|
padding: 20px;
|
||||||
|
.el-divider {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
&-wrap {
|
||||||
|
.el-drawer__header {
|
||||||
|
color: #595959;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 13px 16px;
|
||||||
|
border-bottom: 1px solid #f4f4f4;
|
||||||
|
}
|
||||||
|
.el-drawer__body {
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-title {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
// 主题
|
||||||
|
.rr-theme {
|
||||||
|
.card {
|
||||||
|
width: 50px;
|
||||||
|
height: 35px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 0 20px 20px 0;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
//侧边栏
|
||||||
|
&.side {
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
width: 15px;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
border-top-left-radius: 3px;
|
||||||
|
border-bottom-left-radius: 3px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
&.dark {
|
||||||
|
&::before {
|
||||||
|
background-color: #2e3549;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//顶栏
|
||||||
|
&.header {
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
border-top-left-radius: 3px;
|
||||||
|
border-bottom-left-radius: 3px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
width: 100%;
|
||||||
|
height: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-top-right-radius: 3px;
|
||||||
|
}
|
||||||
|
&.light {
|
||||||
|
&::before {
|
||||||
|
width: 100%;
|
||||||
|
height: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-top-right-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.dark {
|
||||||
|
&::before {
|
||||||
|
background-color: #2e3549;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.primary {
|
||||||
|
&::before {
|
||||||
|
background-color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mix {
|
||||||
|
background-color: #2e3549;
|
||||||
|
&.dark {
|
||||||
|
&::before {
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
width: 35px;
|
||||||
|
height: 25px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.side,
|
||||||
|
&.header,
|
||||||
|
&.mix {
|
||||||
|
&.active {
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #1ea4ff;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: -15px;
|
||||||
|
margin-left: -3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//主色调
|
||||||
|
.color {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin: 8px 8px 0 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
box-shadow: 0 1px 3px rgba(0 0 0, 0.1);
|
||||||
|
vertical-align: top;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
&.active {
|
||||||
|
&::after {
|
||||||
|
content: url('data:image/svg+xml;charset=utf-8,<svg width="14" height="14" color="rgb(255 255 255)" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-042ca774=""><path fill="currentColor" d="M406.656 706.944L195.84 496.256a32 32 0 10-45.248 45.248l256 256 512-512a32 32 0 00-45.248-45.248L406.592 706.944z"></path></svg>');
|
||||||
|
font-family: element-icons !important;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin: -7px 0 0 -7px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rr-theme,
|
||||||
|
.rr-other {
|
||||||
|
width: 100%;
|
||||||
|
> .el-space__item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rr-switch {
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1641477417057" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6229" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M512 887.466667C303.786667 887.466667 136.533333 720.213333 136.533333 512S303.786667 136.533333 512 136.533333s375.466667 167.253333 375.466667 375.466667-167.253333 375.466667-375.466667 375.466667z m0-716.8C324.266667 170.666667 170.666667 324.266667 170.666667 512s153.6 341.333333 341.333333 341.333333 341.333333-153.6 341.333333-341.333333S699.733333 170.666667 512 170.666667z" fill="" p-id="6230"></path><path d="M512 1024C228.693333 1024 0 795.306667 0 512S228.693333 0 512 0s512 228.693333 512 512-228.693333 512-512 512z m0-989.866667C249.173333 34.133333 34.133333 249.173333 34.133333 512s215.04 477.866667 477.866667 477.866667 477.866667-215.04 477.866667-477.866667S774.826667 34.133333 512 34.133333z" fill="" p-id="6231"></path><path d="M375.466667 409.6m-34.133334 0a34.133333 34.133333 0 1 0 68.266667 0 34.133333 34.133333 0 1 0-68.266667 0Z" fill="" p-id="6232"></path><path d="M648.533333 409.6m-34.133333 0a34.133333 34.133333 0 1 0 68.266667 0 34.133333 34.133333 0 1 0-68.266667 0Z" fill="" p-id="6233"></path><path d="M375.466667 648.533333m-34.133334 0a34.133333 34.133333 0 1 0 68.266667 0 34.133333 34.133333 0 1 0-68.266667 0Z" fill="" p-id="6234"></path><path d="M648.533333 648.533333m-34.133333 0a34.133333 34.133333 0 1 0 68.266667 0 34.133333 34.133333 0 1 0-68.266667 0Z" fill="" p-id="6235"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1576153230908" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="971" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M772.87036133 734.06115723c-43.34106445 0-80.00793458 27.93273926-93.76831055 66.57714843H475.90991211c-56.60705567 0-102.66723633-46.06018067-102.66723633-102.66723633V600.82446289h305.859375c13.76037598 38.64440918 50.42724609 66.57714844 93.76831055 66.57714844 55.12390137 0 99.94812012-44.82421875 99.94812012-99.94812012S827.9942627 467.50537109 772.87036133 467.50537109c-43.34106445 0-80.00793458 27.93273926-93.76831055 66.57714844H373.24267578V401.01062011h321.92687989c55.12390137 0 99.94812012-44.82421875 99.94812011-99.94812011V190.07312011C795.11767578 134.94921875 750.29345703 90.125 695.16955567 90.125H251.12963867C196.0057373 90.125 151.18151855 134.94921875 151.18151855 190.07312011V301.0625c0 55.12390137 44.82421875 99.94812012 99.94812012 99.94812012h55.53588867v296.96044921c0 93.35632325 75.97045898 169.32678223 169.32678224 169.32678223h203.19213866c13.76037598 38.64440918 50.42724609 66.57714844 93.76831055 66.57714844 55.12390137 0 99.94812012-44.82421875 99.94812012-99.94812012s-44.90661622-99.86572266-100.03051758-99.86572265z m0-199.89624024c18.37463379 0 33.28857422 14.91394043 33.28857422 33.28857423s-14.91394043 33.28857422-33.28857422 33.28857421-33.28857422-14.91394043-33.28857422-33.28857421 14.91394043-33.28857422 33.28857422-33.28857422zM217.75866699 301.0625V190.07312011c0-18.37463379 14.91394043-33.28857422 33.28857423-33.28857421h444.03991698c18.37463379 0 33.28857422 14.91394043 33.28857422 33.28857422V301.0625c0 18.37463379-14.91394043 33.28857422-33.28857422 33.28857422H251.12963867c-18.37463379 0-33.37097168-14.91394043-33.37097168-33.28857422z m555.11169434 566.23535156c-18.37463379 0-33.28857422-14.91394043-33.28857422-33.28857422 0-18.37463379 14.91394043-33.28857422 33.28857422-33.28857422s33.28857422 14.91394043 33.28857422 33.28857422c0.08239747 18.29223633-14.91394043 33.28857422-33.28857422 33.28857422z" p-id="972"></path></svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575982282951" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="902" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M828.40625 90.125H195.59375C137.375 90.125 90.125 137.375 90.125 195.59375v632.8125c0 58.21875 47.25 105.46875 105.46875 105.46875h632.8125c58.21875 0 105.46875-47.25 105.46875-105.46875V195.59375c0-58.21875-47.25-105.46875-105.46875-105.46875z m52.734375 738.28125c0 29.16-23.57015625 52.734375-52.734375 52.734375H195.59375c-29.109375 0-52.734375-23.574375-52.734375-52.734375V195.59375c0-29.109375 23.625-52.734375 52.734375-52.734375h632.8125c29.16 0 52.734375 23.625 52.734375 52.734375v632.8125z" p-id="903"></path><path d="M421.52890625 709.55984375a36.28125 36.28125 0 0 1-27.55265625-12.66890625L205.17453125 476.613125a36.28546875 36.28546875 0 0 1 55.10109375-47.22890625l164.986875 192.4846875 342.16171875-298.48078125a36.2896875 36.2896875 0 0 1 47.70984375 54.68765625L445.3859375 700.6203125a36.3234375 36.3234375 0 0 1-23.85703125 8.93953125z" p-id="904"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1577252187056" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2508" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M747.59340925 691.12859384c11.51396329 0.25305413 22.43746719-0.21087818 40.74171707-1.51832482 29.35428085-2.10878421 35.84933734-2.36183835 46.47761114-0.8856895 24.71495444 3.37405491 41.12129828 21.76265671 32.47528161 47.95376084-85.57447632 258.19957947-442.00123984 249.76444099-628.67084683 50.73735554-153.47733892-159.33976008-153.09775772-414.41833795 0.92786545-573.42069196 159.71934128-162.67163983 424.03439521-166.59397897 565.78689185 0.63263534 80.38686649 94.81095318 108.34934958 169.16669549 89.11723508 230.57450162-15.01454608 47.99593598-50.61082928 77.68762207-119.77896259 114.63352789-4.89237973 2.65706845-29.35428085 15.52065436-35.84933652 19.02123633-46.94154346 25.30541465-63.51659033 41.20565021-62.20914449 58.45550757 2.95229856 39.13904114 24.16667102 52.7196135 70.98168823 53.81618115z m44.41100207 50.10472101c-19.82257471 1.43397372-32.05352527 1.940082-45.63409763 1.6448519-70.34905207-1.60267593-115.98314969-30.91478165-121.38163769-101.64341492-3.45840683-46.05585397 24.7571304-73.13264758 89.24376132-107.96976837 6.7902866-3.66928501 31.37871396-16.57504688 36.06021551-19.06341229 57.69634516-30.83042972 85.15271997-53.73183005 94.76877722-84.47790866 12.77923398-40.78389304-9.10994898-98.94417051-79.24812286-181.6507002-121.17075953-142.97559219-350.14258521-139.60153647-489.2380134 2.06660824-134.49827774 138.84237405-134.79350784 362.12048163-0.42175717 501.637667 158.53842169 168.99799328 451.9968783 181.18676788 534.57688175-11.80919339-4.68150156 0.2952301-10.71262573 0.67481131-18.72600705 1.26527069z" p-id="2509"></path><path d="M346.03865637 637.18588562a78.82636652 78.82636652 0 0 0 78.32025825-79.29029883c0-43.69401562-35.005823-79.29029883-78.32025825-79.29029882a78.82636652 78.82636652 0 0 0-78.36243338 79.29029882c0 43.69401562 35.005823 79.29029883 78.36243338 79.29029883z m0-51.7495729a27.07679361 27.07679361 0 0 1-26.5706845-27.54072593c0-15.30977536 11.97789643-27.54072593 26.5706845-27.54072592 14.55061295 0 26.57068533 12.23095057 26.57068533 27.54072592a27.07679361 27.07679361 0 0 1-26.57068533 27.54072593zM475.7289063 807.11174353a78.82636652 78.82636652 0 0 0 78.3624334-79.29029882c0-43.69401562-34.96364785-79.29029883-78.32025825-79.29029883a78.82636652 78.82636652 0 0 0-78.32025742 79.29029883c0 43.69401562 34.96364785 79.29029883 78.32025742 79.29029882z m0-51.74957208a27.07679361 27.07679361 0 0 1-26.57068532-27.54072674c0-15.30977536 12.06224753-27.54072593 26.57068532-27.54072593 14.59278892 0 26.57068533 12.23095057 26.57068453 27.54072593a27.07679361 27.07679361 0 0 1-26.57068453 27.54072674zM601.24376214 377.21492718a78.82636652 78.82636652 0 0 0 78.32025742-79.29029883c0-43.69401562-34.96364785-79.29029883-78.32025742-79.29029882a78.82636652 78.82636652 0 0 0-78.32025823 79.29029883c0 43.69401562 34.96364785 79.29029883 78.32025824 79.29029883z m1e-8-51.74957208a27.07679361 27.07679361 0 0 1-26.57068534-27.54072675c0-15.30977536 11.97789643-27.54072593 26.57068534-27.54072591 14.55061295 0 26.57068533 12.23095057 26.57068451 27.54072592a27.07679361 27.07679361 0 0 1-26.57068451 27.54072674zM378.80916809 433.85687983a78.82636652 78.82636652 0 0 0 78.32025824-79.29029883c0-43.69401562-34.96364785-79.29029883-78.32025824-79.29029802a78.82636652 78.82636652 0 0 0-78.32025742 79.29029802c0 43.69401562 34.96364785 79.29029883 78.32025742 79.29029883z m0-51.74957209a27.07679361 27.07679361 0 0 1-26.57068451-27.54072674c0-15.30977536 11.97789643-27.54072593 26.57068451-27.54072593 14.55061295 0 26.57068533 12.23095057 26.57068533 27.54072593a27.07679361 27.07679361 0 0 1-26.57068533 27.54072674z" p-id="2510"></path></svg>
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575804206892" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3145" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M826.56 470.016c-32.896 0-64.384 12.288-89.984 35.52l0-104.96c0-62.208-50.496-112.832-112.64-113.088L623.936 287.04 519.552 287.104C541.824 262.72 554.56 230.72 554.56 197.12c0-73.536-59.904-133.44-133.504-133.44-73.472 0-133.376 59.904-133.376 133.44 0 32.896 12.224 64.256 35.52 89.984L175.232 287.104l0 0.576C113.728 288.704 64 338.88 64 400.576l0.32 0 0.32 116.48C60.864 544.896 70.592 577.728 100.8 588.48c12.736 4.608 37.632 7.488 60.864-25.28 12.992-18.368 34.24-29.248 56.64-29.248 38.336 0 69.504 31.104 69.504 69.312 0 38.4-31.168 69.504-69.504 69.504-22.656 0-44.032-11.264-57.344-30.4C138.688 610.112 112.576 615.36 102.464 619.136c-29.824 10.752-39.104 43.776-38.144 67.392l0 160.384L64 846.912C64 909.248 114.752 960 177.216 960l446.272 0c62.4 0 113.152-50.752 113.152-113.152l0-145.024c24.384 22.272 56.384 35.008 89.984 35.008 73.536 0 133.44-59.904 133.44-133.504C960 529.92 900.096 470.016 826.56 470.016zM826.56 672.896c-22.72 0-44.032-11.264-57.344-30.4-22.272-32.384-48.448-27.136-58.56-23.36-29.824 10.752-39.04 43.776-38.08 67.392l0 160.384c0 27.136-22.016 49.152-49.152 49.152L177.216 896.064C150.08 896 128 873.984 128 846.848l0.32 0 0-145.024c24.384 22.272 56.384 35.008 89.984 35.008 73.6 0 133.504-59.904 133.504-133.504 0-73.472-59.904-133.376-133.504-133.376-32.896 0-64.32 12.288-89.984 35.52l0-104.96L128 400.512c0-27.072 22.08-49.152 49.216-49.152L177.216 351.04 334.656 350.72c3.776 0.512 7.616 0.832 11.52 0.832 24.896 0 50.752-10.816 60.032-37.056 4.544-12.736 7.424-37.568-25.344-60.736C362.624 240.768 351.68 219.52 351.68 197.12c0-38.272 31.104-69.44 69.376-69.44 38.336 0 69.504 31.168 69.504 69.44 0 22.72-11.264 44.032-30.528 57.472C427.968 276.736 433.088 302.784 436.8 313.024c10.752 29.888 43.072 39.232 67.392 38.08l119.232 0 0 0.384c27.136 0 49.152 22.08 49.152 49.152l0.256 116.48c-3.776 27.84 6.016 60.736 36.224 71.488 12.736 4.608 37.632 7.488 60.8-25.28 13.056-18.368 34.24-29.248 56.704-29.248C864.832 534.016 896 565.12 896 603.392 896 641.728 864.832 672.896 826.56 672.896z" p-id="3146"></path></svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1579774833889" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1376" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M887.466667 192.853333h-100.693334V119.466667c0-10.24-6.826667-17.066667-17.066666-17.066667s-17.066667 6.826667-17.066667 17.066667v73.386666H303.786667V119.466667c0-10.24-6.826667-17.066667-17.066667-17.066667s-17.066667 6.826667-17.066667 17.066667v73.386666H168.96c-46.08 0-85.333333 37.546667-85.333333 85.333334V836.266667c0 46.08 37.546667 85.333333 85.333333 85.333333H887.466667c46.08 0 85.333333-37.546667 85.333333-85.333333V278.186667c0-47.786667-37.546667-85.333333-85.333333-85.333334z m-718.506667 34.133334h100.693333v66.56c0 10.24 6.826667 17.066667 17.066667 17.066666s17.066667-6.826667 17.066667-17.066666v-66.56h450.56v66.56c0 10.24 6.826667 17.066667 17.066666 17.066666s17.066667-6.826667 17.066667-17.066666v-66.56H887.466667c27.306667 0 51.2 22.186667 51.2 51.2v88.746666H117.76v-88.746666c0-29.013333 22.186667-51.2 51.2-51.2zM887.466667 887.466667H168.96c-27.306667 0-51.2-22.186667-51.2-51.2V401.066667H938.666667V836.266667c0 27.306667-22.186667 51.2-51.2 51.2z" p-id="1377"></path><path d="M858.453333 493.226667H327.68c-10.24 0-17.066667 6.826667-17.066667 17.066666v114.346667h-116.053333c-10.24 0-17.066667 6.826667-17.066667 17.066667v133.12c0 10.24 6.826667 17.066667 17.066667 17.066666H460.8c10.24 0 17.066667-6.826667 17.066667-17.066666v-114.346667h380.586666c10.24 0 17.066667-6.826667 17.066667-17.066667v-133.12c0-10.24-6.826667-17.066667-17.066667-17.066666z m-413.013333 34.133333v97.28h-98.986667v-97.28h98.986667z m-230.4 131.413333h98.986667v98.986667h-98.986667v-98.986667z m131.413333 97.28v-97.28h98.986667v97.28h-98.986667z m133.12-228.693333h97.28v98.986667h-97.28v-98.986667z m131.413334 0h98.986666v98.986667h-98.986666v-98.986667z m230.4 97.28h-98.986667v-98.986667h98.986667v98.986667z" p-id="1378"></path></svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1577186573535" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1068" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M479.85714249 608.42857168h64.28571502c19.28571417 0 32.14285751-12.85714249 32.14285664-32.14285751s-12.85714249-32.14285751-32.14285664-32.14285664h-64.28571504c-19.28571417 0-32.14285751 12.85714249-32.14285664 32.14285662s12.85714249 32.14285751 32.14285664 32.14285753z m-2e-8 122.14285665h64.28571504c19.28571417 0 32.14285751-12.85714249 32.14285664-32.14285665s-12.85714249-32.14285751-32.14285664-32.14285751h-64.28571504c-19.28571417 0-32.14285751 12.85714249-32.14285664 32.14285751s12.85714249 32.14285751 32.14285664 32.14285664z m353.57142921-559.28571416h-128.57142921v-32.14285664c0-19.28571417-12.85714249-32.14285751-32.14285664-32.14285753s-32.14285751 12.85714249-32.14285751 32.14285753v32.14285664h-257.14285665v-32.14285664c0-19.28571417-12.85714249-32.14285751-32.14285752-32.14285753s-32.14285751 12.85714249-32.14285664 32.14285753v32.14285664h-128.57142919c-70.71428585 0-128.57142832 57.85714249-128.57142832 122.14285751v501.42857081c0 70.71428585 57.85714249 128.57142832 128.57142832 122.14285751h642.85714335c70.71428585 0 128.57142832-57.85714249 128.57142833-122.14285751v-501.42857081c0-70.71428585-57.85714249-122.14285753-128.57142833-122.14285751z m64.28571415 623.57142832c0 32.14285751-32.14285751 64.28571415-64.28571416 64.28571504h-642.85714335c-32.14285751 0-64.28571415-25.71428583-64.28571417-64.28571504v-372.85714249h771.42857168v372.85714249z m0-437.14285664h-771.42857168v-64.28571417c0-32.14285751 32.14285751-64.28571415 64.28571417-64.28571415h128.57142919v32.14285664c0 19.28571417 12.85714249 32.14285751 32.14285664 32.14285751s32.14285751-12.85714249 32.14285753-32.14285751v-32.14285664h257.14285665v32.14285664c0 19.28571417 12.85714249 32.14285751 32.1428575 32.14285751s32.14285751-12.85714249 32.14285664-32.14285751v-32.14285664h128.57142921c32.14285751 0 64.28571415 25.71428583 64.28571415 64.28571415v64.28571417z m-610.71428583 372.85714247h64.28571415c19.28571417 0 32.14285751-12.85714249 32.14285753-32.14285664s-12.85714249-32.14285751-32.14285753-32.14285751h-64.28571415c-19.28571417 0-32.14285751 12.85714249-32.14285751 32.14285751s12.85714249 32.14285751 32.14285751 32.14285665z m385.71428583-122.14285664h64.28571417c19.28571417 0 32.14285751-12.85714249 32.14285751-32.14285751s-12.85714249-32.14285751-32.14285751-32.14285664h-64.28571415c-19.28571417 0-32.14285751 12.85714249-32.14285753 32.14285664s12.85714249 32.14285751 32.14285753 32.14285751z m-385.71428583 0h64.28571415c19.28571417 0 32.14285751-12.85714249 32.14285753-32.14285751s-12.85714249-32.14285751-32.14285753-32.14285664h-64.28571415c-19.28571417 0-32.14285751 12.85714249-32.14285751 32.14285664s12.85714249 32.14285751 32.14285751 32.14285751z m385.71428583 122.14285665h64.28571417c19.28571417 0 32.14285751-12.85714249 32.14285751-32.14285665s-12.85714249-32.14285751-32.14285751-32.14285751h-64.28571415c-19.28571417 0-32.14285751 12.85714249-32.14285753 32.14285751s12.85714249 32.14285751 32.14285753 32.14285665z" p-id="1069"></path></svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1622124729495" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="653" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M854.4 800.9c0.2-0.3 0.5-0.6 0.7-0.9C920.6 722.1 960 621.7 960 512s-39.4-210.1-104.8-288c-0.2-0.3-0.5-0.5-0.7-0.8-1.1-1.3-2.1-2.5-3.2-3.7-0.4-0.5-0.8-0.9-1.2-1.4-1.4-1.6-2.7-3.1-4.1-4.7l-0.1-0.1c-1.5-1.7-3.1-3.4-4.6-5.1l-0.1-0.1c-3.2-3.4-6.4-6.8-9.7-10.1l-0.1-0.1-4.8-4.8-0.3-0.3c-1.5-1.5-3-2.9-4.5-4.3-0.5-0.5-1-1-1.6-1.5-1-1-2-1.9-3-2.8-0.3-0.3-0.7-0.6-1-1C736.4 109.2 629.5 64 512 64s-224.4 45.2-304.3 119.2c-0.3 0.3-0.7 0.6-1 1-1 0.9-2 1.9-3 2.9-0.5 0.5-1 1-1.6 1.5-1.5 1.4-3 2.9-4.5 4.3l-0.3 0.3-4.8 4.8-0.1 0.1c-3.3 3.3-6.5 6.7-9.7 10.1l-0.1 0.1c-1.6 1.7-3.1 3.4-4.6 5.1l-0.1 0.1c-1.4 1.5-2.8 3.1-4.1 4.7-0.4 0.5-0.8 0.9-1.2 1.4-1.1 1.2-2.1 2.5-3.2 3.7-0.2 0.3-0.5 0.5-0.7 0.8C103.4 301.9 64 402.3 64 512s39.4 210.1 104.8 288c0.2 0.3 0.5 0.6 0.7 0.9 1 1.2 2.1 2.5 3.1 3.7 0.4 0.5 0.8 0.9 1.2 1.4 1.4 1.6 2.7 3.1 4.1 4.7 0 0.1 0.1 0.1 0.1 0.2 1.5 1.7 3 3.4 4.6 5l0.1 0.1c3.2 3.4 6.4 6.8 9.6 10.1l0.1 0.1c1.6 1.6 3.1 3.2 4.7 4.7l0.3 0.3c3.3 3.3 6.7 6.5 10.1 9.6 80.1 74 187 119.2 304.5 119.2s224.4-45.2 304.3-119.2c3.4-3.1 6.7-6.3 10-9.6l0.3-0.3c1.6-1.6 3.2-3.1 4.7-4.7l0.1-0.1c3.3-3.3 6.5-6.7 9.6-10.1l0.1-0.1c1.5-1.7 3.1-3.3 4.6-5 0-0.1 0.1-0.1 0.1-0.2 1.4-1.5 2.8-3.1 4.1-4.7 0.4-0.5 0.8-0.9 1.2-1.4 1.2-1.3 2.3-2.5 3.3-3.7z m4.1-142.6c-13.8 32.6-32 62.8-54.2 90.2-24.9-21.5-52.2-40.3-81.5-55.9 11.6-46.9 18.8-98.4 20.7-152.6H887c-3 40.9-12.6 80.6-28.5 118.3zM887 484H743.5c-1.9-54.2-9.1-105.7-20.7-152.6 29.3-15.6 56.6-34.4 81.5-55.9 22.2 27.4 40.4 57.6 54.2 90.2C874.4 403.4 884 443.1 887 484zM658.3 165.5c39.7 16.8 75.8 40 107.6 69.2-18.5 15.8-38.4 29.7-59.4 41.8-15.7-45-35.8-84.1-59.2-115.4 3.7 1.4 7.4 2.9 11 4.4z m-90.6 700.6c-9.2 7.2-18.4 12.7-27.7 16.4V697c39.9 2.8 78.6 11.6 115.7 26.2-8.3 24.6-17.9 47.3-29 67.8-17.4 32.4-37.8 58.3-59 75.1z m59-633.1c11 20.6 20.7 43.3 29 67.8-37.1 14.6-75.8 23.4-115.7 26.2V141.6c9.2 3.7 18.5 9.1 27.7 16.4 21.2 16.7 41.6 42.6 59 75zM540 640.9V540h147.5c-1.6 44.2-7.1 87.1-16.3 127.8l-0.3 1.2c-41.1-15.6-85.1-25.3-130.9-28.1z m0-156.9V383.1c45.8-2.8 89.8-12.5 130.9-28.1l0.3 1.2c9.2 40.7 14.7 83.5 16.3 127.8H540z m-56 56v100.9c-45.8 2.8-89.8 12.5-130.9 28.1l-0.3-1.2c-9.2-40.7-14.7-83.5-16.3-127.8H484z m-147.5-56c1.6-44.2 7.1-87.1 16.3-127.8l0.3-1.2c41.1 15.6 85 25.3 130.9 28.1V484H336.5zM484 697v185.4c-9.2-3.7-18.5-9.1-27.7-16.4-21.2-16.7-41.7-42.7-59.1-75.1-11-20.6-20.7-43.3-29-67.8 37.2-14.6 75.9-23.3 115.8-26.1z m0-370c-39.9-2.8-78.6-11.6-115.7-26.2 8.3-24.6 17.9-47.3 29-67.8 17.4-32.4 37.8-58.4 59.1-75.1 9.2-7.2 18.4-12.7 27.7-16.4V327zM365.7 165.5c3.7-1.5 7.3-3 11-4.4-23.4 31.3-43.5 70.4-59.2 115.4-21-12-40.9-26-59.4-41.8 31.8-29.2 67.9-52.4 107.6-69.2zM165.5 365.7c13.8-32.6 32-62.8 54.2-90.2 24.9 21.5 52.2 40.3 81.5 55.9-11.6 46.9-18.8 98.4-20.7 152.6H137c3-40.9 12.6-80.6 28.5-118.3zM137 540h143.5c1.9 54.2 9.1 105.7 20.7 152.6-29.3 15.6-56.6 34.4-81.5 55.9-22.2-27.4-40.4-57.6-54.2-90.2C149.6 620.6 140 580.9 137 540z m228.7 318.5c-39.7-16.8-75.8-40-107.6-69.2 18.5-15.8 38.4-29.7 59.4-41.8 15.7 45 35.8 84.1 59.2 115.4-3.7-1.4-7.4-2.9-11-4.4z m292.6 0c-3.7 1.5-7.3 3-11 4.4 23.4-31.3 43.5-70.4 59.2-115.4 21 12 40.9 26 59.4 41.8-31.8 29.2-67.9 52.4-107.6 69.2z" p-id="654"></path></svg>
|
||||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1622128472748" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1971" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M128 128h256v64H192v192H128V128zM896 640v256h-256v-64h192v-192h64zM384 686.496L174.496 896 128 849.504 337.504 640H192v-64h256v256h-64v-145.504zM849.504 128L640 337.504V192h-64v256h256v-64h-145.504L896 174.496 849.504 128z" p-id="1972"></path></svg>
|
||||||
|
After Width: | Height: | Size: 626 B |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1622128325471" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1563" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M608 416h288c35.36 0 64 28.48 64 64v416c0 35.36-28.48 64-64 64H480c-35.36 0-64-28.48-64-64v-288H128c-35.36 0-64-28.48-64-64V128c0-35.36 28.48-64 64-64h416c35.36 0 64 28.48 64 64v288z m0 64v64c0 35.36-28.48 64-64 64h-64v256.032c0 17.664 14.304 31.968 31.968 31.968H864a31.968 31.968 0 0 0 31.968-31.968V512a31.968 31.968 0 0 0-31.968-31.968H608zM128 159.968V512c0 17.664 14.304 31.968 31.968 31.968H512a31.968 31.968 0 0 0 31.968-31.968V160A31.968 31.968 0 0 0 512.032 128H160A31.968 31.968 0 0 0 128 159.968z m64 244.288V243.36h112.736V176h46.752c6.4 0.928 9.632 1.824 9.632 2.752a10.56 10.56 0 0 1-1.376 4.128c-2.752 7.328-4.128 16.032-4.128 26.112v34.368h119.648v156.768h-50.88v-20.64h-68.768v118.272H306.112v-118.272H238.752v24.768H192z m46.72-122.368v60.48h67.392V281.92H238.752z m185.664 60.48V281.92h-68.768v60.48h68.768z m203.84 488H576L668.128 576h64.64l89.344 254.4h-54.976l-19.264-53.664h-100.384l-19.232 53.632z m33.024-96.256h72.864l-34.368-108.608h-1.376l-37.12 108.608zM896 320h-64a128 128 0 0 0-128-128V128a192 192 0 0 1 192 192zM128 704h64a128 128 0 0 0 128 128v64a192 192 0 0 1-192-192z" p-id="1564"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1695003505593" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4091" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 1024C229.222 1024 0 794.778 0 512S229.222 0 512 0s512 229.222 512 512-229.222 512-512 512z m259.149-568.883h-290.74a25.293 25.293 0 0 0-25.292 25.293l-0.026 63.206c0 13.952 11.315 25.293 25.267 25.293h177.024c13.978 0 25.293 11.315 25.293 25.267v12.646a75.853 75.853 0 0 1-75.853 75.853h-240.23a25.293 25.293 0 0 1-25.267-25.293V417.203a75.853 75.853 0 0 1 75.827-75.853h353.946a25.293 25.293 0 0 0 25.267-25.292l0.077-63.207a25.293 25.293 0 0 0-25.268-25.293H417.152a189.62 189.62 0 0 0-189.62 189.645V771.15c0 13.977 11.316 25.293 25.294 25.293h372.94a170.65 170.65 0 0 0 170.65-170.65V480.384a25.293 25.293 0 0 0-25.293-25.267z" p-id="4092"></path></svg>
|
||||||
|
After Width: | Height: | Size: 995 B |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1622128506901" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2107" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zM400 646c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zM904 160H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM904 792H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519c4.5-3.5 4.5-10.3 0-13.9L142.4 381.9c-5.8-4.6-14.4-0.5-14.4 6.9v246.3c0 7.4 8.5 11.6 14.4 7z" p-id="2108"></path></svg>
|
||||||
|
After Width: | Height: | Size: 874 B |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575802859706" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3102" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M896 224H128c-35.2 0-64 28.8-64 64v448c0 35.2 28.8 64 64 64h768c35.2 0 64-28.8 64-64V288c0-35.2-28.8-64-64-64z m0 480c0 19.2-12.8 32-32 32H160c-19.2 0-32-12.8-32-32V320c0-19.2 12.8-32 32-32h704c19.2 0 32 12.8 32 32v384z" p-id="3103"></path><path d="M224 352c-19.2 0-32 12.8-32 32v256c0 16 12.8 32 32 32s32-12.8 32-32V384c0-16-12.8-32-32-32z" p-id="3104"></path></svg>
|
||||||
|
After Width: | Height: | Size: 744 B |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1622128442421" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1835" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M426.666667 512a85.333333 85.333333 0 1 1 170.709333 0.042667A85.333333 85.333333 0 0 1 426.666667 512z m0 298.666667a85.333333 85.333333 0 1 1 170.709333 0.042666A85.333333 85.333333 0 0 1 426.666667 810.666667z m0-597.333334a85.333333 85.333333 0 1 1 170.709333 0.042667A85.333333 85.333333 0 0 1 426.666667 213.333333z" p-id="1836"></path></svg>
|
||||||
|
After Width: | Height: | Size: 725 B |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575802851180" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2867" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M279.272727 791.272727h512a46.545455 46.545455 0 0 1 0 93.090909H279.272727a46.545455 46.545455 0 0 1 0-93.090909z m33.838546-617.984V651.636364H193.722182V395.170909c0-37.003636-0.884364-59.298909-2.653091-66.746182a24.948364 24.948364 0 0 0-14.615273-16.989091c-8.005818-3.863273-25.786182-5.771636-53.341091-5.771636h-11.822545v-55.854545c57.716364-12.381091 101.562182-37.888 131.490909-76.520728h70.283636z m303.709091 396.8V651.636364H354.164364v-68.235637c77.777455-127.255273 124.043636-206.010182 138.705454-236.218182 14.661818-30.254545 22.016-53.853091 22.016-70.74909 0-13.032727-2.234182-22.714182-6.656-29.137455-4.421818-6.376727-11.170909-9.588364-20.247273-9.588364a22.248727 22.248727 0 0 0-20.200727 10.612364c-4.468364 7.121455-6.656 21.178182-6.656 42.263273v45.521454H354.164364v-17.454545c0-26.763636 1.396364-47.941818 4.142545-63.348364 2.746182-15.499636 9.541818-30.72 20.386909-45.661091 10.798545-14.987636 24.901818-26.298182 42.216727-33.978182 17.361455-7.68 38.167273-11.543273 62.37091-11.543272 47.476364 0 83.316364 11.776 107.706181 35.328 24.296727 23.552 36.445091 53.341091 36.445091 89.367272 0 27.368727-6.842182 56.32-20.48 86.853819-13.730909 30.533818-54.039273 95.325091-121.018182 194.420363h130.885819z m270.615272-189.393454c18.152727 6.097455 31.650909 16.104727 40.494546 29.975272 8.843636 13.917091 13.312 46.452364 13.312 97.652364 0 38.027636-4.328727 67.490909-13.032727 88.529455-8.657455 20.945455-23.598545 36.910545-44.869819 47.848727-21.271273 10.938182-48.593455 16.384-81.873454 16.384-37.794909 0-67.490909-6.330182-89.088-19.083636-21.550545-12.660364-35.746909-28.253091-42.542546-46.638546-6.795636-18.432-10.193455-50.362182-10.193454-95.883636v-37.841455h119.389091v77.730909c0 20.666182 1.210182 33.838545 3.723636 39.424 2.420364 5.585455 7.912727 8.424727 16.337455 8.424728 9.309091 0 15.36-3.537455 18.338909-10.612364 2.932364-7.121455 4.421818-25.6 4.421818-55.575273v-33.047273c0-18.338909-2.048-31.744-6.190546-40.215272a30.72 30.72 0 0 0-18.338909-16.709818c-8.052364-2.653091-23.738182-4.189091-46.964363-4.561455V357.050182c28.392727 0 45.893818-1.070545 52.596363-3.258182a22.946909 22.946909 0 0 0 14.475637-14.149818c2.932364-7.307636 4.421818-18.711273 4.421818-34.257455v-26.624c0-16.756364-1.722182-27.741091-5.12-33.047272-3.490909-5.352727-8.843636-8.005818-16.151273-8.005819-8.285091 0-13.963636 2.792727-16.989091 8.378182-3.025455 5.632-4.561455 17.640727-4.561454 35.933091v39.284364h-119.389091v-40.773818c0-45.661091 10.472727-76.567273 31.325091-92.625455 20.898909-16.058182 54.085818-24.064 99.607272-24.064 56.878545 0 95.511273 11.170909 115.805091 33.373091 20.293818 22.248727 30.394182 53.201455 30.394182 92.765091 0 26.810182-3.630545 46.173091-10.891636 58.088727-7.307636 11.915636-20.107636 22.807273-38.446546 32.628364z" p-id="2868"></path></svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1622128519822" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2243" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zM400 646c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zM904 160H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM904 792H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4 0.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1c-4.5 3.5-4.5 10.3 0 13.8z" p-id="2244"></path></svg>
|
||||||
|
After Width: | Height: | Size: 876 B |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575802846045" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2750" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M868.593046 403.832442c-30.081109-28.844955-70.037123-44.753273-112.624057-44.753273L265.949606 359.079168c-42.554188 0-82.510202 15.908318-112.469538 44.690852-30.236652 28.782533-46.857191 67.222007-46.857191 108.198258l0 294.079782c0 40.977273 16.619516 79.414701 46.702672 108.136859 29.959336 28.844955 70.069869 44.814672 112.624057 44.814672l490.019383 0c42.585911 0 82.696444-15.969717 112.624057-44.814672 30.082132-28.844955 46.579875-67.222007 46.579875-108.136859L915.172921 511.968278C915.171897 471.053426 898.675178 432.677397 868.593046 403.832442zM841.821309 806.049083c0 22.098297-8.882298 42.772152-25.099654 58.306964-16.154935 15.661701-37.81935 24.203238-60.752666 24.203238L265.949606 888.559285c-22.934339 0-44.567032-8.54256-60.877509-24.264637-16.186657-15.474436-25.067932-36.148291-25.067932-58.246589L180.004165 511.968278c0-22.035876 8.881274-42.772152 25.192775-58.307987 16.186657-15.536858 37.81935-24.139793 60.753689-24.139793l490.019383 0c22.933315 0 44.597731 8.602935 60.752666 24.139793 16.21838 15.535835 25.099654 36.272112 25.099654 58.307987L841.822332 806.049083zM510.974136 135.440715c114.914216 0 208.318536 89.75214 208.318536 200.055338l73.350588 0c0-149.113109-126.366036-270.496667-281.669124-270.496667-155.333788 0-281.699824 121.383558-281.699824 270.496667l73.350588 0C302.623877 225.193879 396.059919 135.440715 510.974136 135.440715zM474.299865 747.244792l73.350588 0L547.650453 629.576859l-73.350588 0L474.299865 747.244792z" p-id="2751"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575966775973" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="879" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M507.39346659 71.84873358c241.53533667 0 437.39770766 195.85422109 437.39770767 437.37442191 0 241.53766571-195.86237099 437.38955776-437.39770767 437.38955776-241.50040803 0-437.34997219-195.85189205-437.34997219-437.38955776C70.0434944 267.70295467 265.89189347 71.84873358 507.39346659 71.84873358L507.39346659 71.84873358zM507.39346659 282.81899805c-125.00686734 0-226.37039389 101.38914133-226.37039388 226.41813048 0 125.01268821 101.36352768 226.39717262 226.37039388 226.39717262 125.04295993 0 226.42395136-101.38448441 226.42395136-226.39717262C733.81625401 384.20813938 632.43642653 282.81899805 507.39346659 282.81899805L507.39346659 282.81899805zM507.39346659 120.78172615c-214.46664192 0-388.42047261 173.95150279-388.4204726 388.44026539 0 214.51204949 173.95499463 388.46122325 388.4204726 388.46122325 214.52369237 0 388.46005817-173.94800981 388.46005818-388.46122325C895.85236082 294.73322894 721.91715897 120.78172615 507.39346659 120.78172615z" p-id="880"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1577246781606" class="icon" viewBox="0 0 1069 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1098" xmlns:xlink="http://www.w3.org/1999/xlink" width="84.5595703125" height="81"><defs><style type="text/css"></style></defs><path d="M633.72929961 378.02038203l9.49872568 18.68789795 20.78025469 2.79745225 206.61592412 27.33248408a11.46496817 11.46496817 0 0 1 6.6095543 19.47324902l-147.2675168 147.35350284-14.89299345 14.89299345 3.8006376 20.68280244 37.84585956 204.89044571a11.46496817 11.46496817 0 0 1-16.4808914 12.2961788L554.68980898 751.84713388l-18.68789794-9.49299345-18.48726123 9.99171915-183.23885392 99.34968163a11.46496817 11.46496817 0 0 1-16.78471347-11.8662416l32.5433127-205.79617881 3.29617793-20.78598692-15.19108243-14.49172002-151.03375839-143.48407587a11.46496817 11.46496817 0 0 1 6.09936328-19.63949062l205.79617881-32.63503185 20.78598691-3.2961788L428.87898125 380.72038203 518.59235674 192.64331182a11.46496817 11.46496817 0 0 1 20.56815264-0.26369385l94.56879023 185.63503183zM496.64840732 85.52038203l-121.75796162 254.98089229L95.76433145 384.76178369A34.3949045 34.3949045 0 0 0 77.46050938 443.66879023l204.87324901 194.66369385-44.16879023 279.1146498a34.3949045 34.3949045 0 0 0 50.36560489 35.61592325l248.4-134.67898038 251.84522285 128.27579591a34.3949045 34.3949045 0 0 0 49.43694287-36.89426777l-51.30573223-277.85350284 199.73120977-199.90891758a34.3949045 34.3949045 0 0 0-19.82866201-58.40827998l-280.11783428-37.03184736L558.32993633 84.71210205a34.3949045 34.3949045 0 0 0-61.68152901 0.80254775z" p-id="1099"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1588552949749" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1802" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M834.2654461 933.87476599H189.7345539A99.37494442 99.37494442 0 0 1 90.12523401 834.2654461V189.7345539A99.37494442 99.37494442 0 0 1 189.7345539 90.12523401h644.53089221A99.37494442 99.37494442 0 0 1 933.87476599 189.7345539v644.53089221A99.37494442 99.37494442 0 0 1 834.2654461 933.87476599zM189.7345539 140.04708127a49.68747262 49.68747262 0 0 0-49.68747263 49.68747263v644.53089221a49.68747262 49.68747262 0 0 0 49.68747262 49.68747262h644.53089221a49.68747262 49.68747262 0 0 0 49.68747263-49.68747262V189.7345539a49.68747262 49.68747262 0 0 0-49.68747263-49.68747263z" p-id="1803"></path><path d="M561.68747262 239.18765188h247.73423676a23.43748728 23.43748728 0 0 1 24.84373673 24.84373591 23.43748728 23.43748728 0 0 1-24.84373673 24.84373589H561.68747262a23.43748728 23.43748728 0 0 1-24.84373672-24.84373589 23.43748728 23.43748728 0 0 1 24.84373672-24.84373591z m0 123.9843057h247.73423676a24.84373591 24.84373591 0 0 1 0 49.68747262H561.68747262a24.84373591 24.84373591 0 1 1 0-49.68747262z m0 123.98430652h247.73423676a24.84373591 24.84373591 0 0 1 0 49.68747181H561.68747262a24.84373591 24.84373591 0 0 1 0-49.68747181zM214.57829062 611.1405698h594.84341876a24.84373591 24.84373591 0 0 1 0 49.68747263H214.57829062a24.84373591 24.84373591 0 0 1 0-49.68747263z m0 123.98430652h594.84341876a24.84373591 24.84373591 0 0 1 0 49.6874718H214.57829062a24.84373591 24.84373591 0 1 1 0-49.6874718z m52.03122061-280.07797001h133.82805103l32.10935696 81.79682959h46.87497372l-123.51555642-297.65608402H311.14073697l-121.40618308 297.65608403h46.87497373z m61.87496594-156.32803812a171.56240497 171.56240497 0 0 0 4.92187226-19.68748901 72.18745972 72.18745972 0 0 1 5.15624688 19.68748901l49.45309717 123.98430652H279.03137918z" p-id="1804"></path></svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1579339929870" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1182" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M152 854.856875h325.7146875V237.715625H134.856875v600q0 6.99375 5.0746875 12.0684375T152 854.856875z m737.143125-17.1421875v-600H546.284375v617.1421875H872q6.99375 0 12.0684375-5.07375t5.0746875-12.0684375z m68.5715625-651.429375V837.715625q0 35.3821875-25.16625 60.5484375T872 923.4284375H152q-35.383125 0-60.5484375-25.1653125T66.284375 837.7146875V186.284375q0-35.3821875 25.16625-60.5484375T152 100.5715625h720q35.383125 0 60.5484375 25.1653125t25.16625 60.5484375z" p-id="1183"></path></svg>
|
||||||
|
After Width: | Height: | Size: 873 B |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575803481213" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="804" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M62 511.97954521C62 263.86590869 263.90681826 62 511.97954521 62s449.97954521 201.825 449.97954521 449.97954521c0 248.19545479-201.90681826 449.97954521-449.97954521 449.97954521C263.90681826 962 62 760.175 62 511.97954521M901.98636348 511.97954521c0-215.24318174-175.00909131-390.41590869-390.00681827-390.41590869-215.03863652 0-389.96590869 175.17272695-389.96590868 390.41590869 0 215.28409131 175.00909131 390.45681826 389.96590868 390.45681826C727.01818174 902.47727305 901.98636348 727.30454521 901.98636348 511.97954521M264.17272695 430.28409131c0-5.76818174 2.12727305-11.51590869 6.64772696-15.87272696 8.71363652-8.75454521 22.88863652-8.75454521 31.725 0l209.4340913 208.22727305L721.45454521 414.53409131c8.75454521-8.71363652 22.97045479-8.71363652 31.90909132 0 8.71363652 8.75454521 8.71363652 22.88863652 0 31.60227304L511.97954521 685.74090869 270.71818174 446.01363653C266.27954521 441.77954521 264.17272695 436.05227305 264.17272695 430.28409131" p-id="805"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1577185310368" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1238" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M951.453125 476.84375H523.671875a131.8359375 131.8359375 0 0 0-254.1796875 0H72.546875v70.3125h196.9453125a131.8359375 131.8359375 0 0 0 254.1796875 0H951.453125z" p-id="1239"></path></svg>
|
||||||
|
After Width: | Height: | Size: 564 B |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1576042673958" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1110" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M692 792H332c-150 0-270-120-270-270s120-270 270-270h360c150 0 270 120 270 270 0 147-120 270-270 270zM332 312c-117 0-210 93-210 210s93 210 210 210h360c117 0 210-93 210-210s-93-210-210-210H332z" p-id="1111"></path><path d="M341 522m-150 0a150 150 0 1 0 300 0 150 150 0 1 0-300 0Z" p-id="1112"></path></svg>
|
||||||
|
After Width: | Height: | Size: 679 B |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595774196464" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4269" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M856.187 65.711H167.215c-56.054 0-101.554 45.5-101.554 101.554v688.972c0 56.054 45.5 101.554 101.554 101.554h688.972c56.054 0 101.554-45.5 101.554-101.554V167.265c0-56.054-45.5-101.554-101.554-101.554z m-677.024 51.773H844.24c34.05 0 61.729 27.678 61.729 61.728v183.594H117.434V179.212c0-34.05 27.678-61.728 61.729-61.728z m217.046 297.094H634.66v219.934H396.209V414.578z m-51.773 219.834H117.434V414.578h227.002v219.834z m341.997-219.834H905.97v219.934H686.433V414.578z m157.807 491.44H179.163c-34.05 0-61.73-27.678-61.73-61.728V686.185h227.003v219.833h51.773V686.185H634.66v219.833h51.772V686.185H905.97V844.29c0 34.05-27.679 61.728-61.729 61.728z" p-id="4270"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575802855098" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2984" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M896 160H128c-35.2 0-64 28.8-64 64v576c0 35.2 28.8 64 64 64h768c35.2 0 64-28.8 64-64V224c0-35.2-28.8-64-64-64z m0 608c0 16-12.8 32-32 32H160c-19.2 0-32-12.8-32-32V256c0-16 12.8-32 32-32h704c19.2 0 32 12.8 32 32v512z" p-id="2985"></path><path d="M224 288c-19.2 0-32 12.8-32 32v256c0 16 12.8 32 32 32s32-12.8 32-32V320c0-16-12.8-32-32-32z m608 480c19.2 0 32-12.8 32-32V608L704 768h128z" p-id="2986"></path></svg>
|
||||||
|
After Width: | Height: | Size: 787 B |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1579774825624" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1248" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M498.595712 482.290351 345.420077 482.290351l0 57.307194 210.477712 0L555.897789 274.196942l-57.301054 0L498.596735 482.290351zM498.595712 482.290351" p-id="1249"></path><path d="M577.685002 644.98478l379.879913 0 0 57.302077L577.685002 702.286858 577.685002 644.98478 577.685002 644.98478zM577.685002 644.98478" p-id="1250"></path><path d="M577.685002 773.764795l379.879913 0 0 57.307194L577.685002 831.071989 577.685002 773.764795 577.685002 773.764795zM577.685002 773.764795" p-id="1251"></path><path d="M577.685002 902.549927l379.879913 0 0 57.307194L577.685002 959.857121 577.685002 902.549927 577.685002 902.549927zM577.685002 902.549927" p-id="1252"></path><path d="M102.523001 382.290823c4.450359 2.615571 9.470699 3.954055 14.530948 3.954055 2.969635 0 5.952572-0.461511 8.836249-1.394766l190.809767-61.886489c15.052834-4.882194 23.297612-21.040199 18.415418-36.08894-4.882194-15.052834-21.040199-23.297612-36.093033-18.415418L175.676092 308.458257c15.994276-26.115797 35.170011-50.537 57.370639-72.743768 73.767074-73.767074 171.845857-114.388237 276.16783-114.388237 104.32095 0 202.39564 40.622186 276.16169 114.388237s114.393353 171.845857 114.393353 276.16783c0 26.427906-2.615571 52.449559-7.709589 77.780481l58.302871 0c4.464685-25.499767 6.708795-51.470255 6.708795-77.780481 0-60.449767-11.845793-119.102608-35.204803-174.336584-22.559808-53.334719-54.850236-101.226472-95.968725-142.349055-41.122583-41.122583-89.017406-73.408917-142.348032-95.968725C628.317169 75.866898 569.659211 64.021106 509.215584 64.021106c-60.448744 0-119.106702 11.845793-174.336584 35.207873-53.334719 22.559808-101.230566 54.846142-142.349055 95.968725-23.980157 23.980157-44.934398 50.278103-62.727647 78.601172l-20.738323-105.655342c-3.043313-15.527648-18.105357-25.642007-33.631982-22.599717-15.527648 3.048429-25.64303 18.105357-22.599717 33.637098l36.102243 183.932126C90.51348 371.153158 95.460142 378.13313 102.523001 382.290823L102.523001 382.290823zM102.523001 382.290823" p-id="1253"></path><path d="M126.020158 587.9416 67.768453 587.9416c5.759167 33.679054 15.368012 66.544579 28.789697 98.278327 22.559808 53.333696 54.850236 101.225449 95.971795 142.348032 41.122583 41.122583 89.014336 73.408917 142.349055 95.968725 54.112432 22.88829 111.517863 34.71157 170.668031 35.18229L505.547031 902.395408c-102.94972-0.941442-199.594851-41.445948-272.499277-114.349351C177.545672 732.543975 140.810003 663.275355 126.020158 587.9416L126.020158 587.9416zM126.020158 587.9416" p-id="1254"></path></svg>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1577099827399" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1008" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M520 559h204c17.673 0 32 14.327 32 32 0 17.673-14.327 32-32 32H488c-17.673 0-32-14.327-32-32 0-0.167 0.001-0.334 0.004-0.5a32.65 32.65 0 0 1-0.004-0.5V277c0-17.673 14.327-32 32-32 17.673 0 32 14.327 32 32v282z m-8 401C264.576 960 64 759.424 64 512S264.576 64 512 64s448 200.576 448 448-200.576 448-448 448z m0-64c212.077 0 384-171.923 384-384S724.077 128 512 128 128 299.923 128 512s171.923 384 384 384z" p-id="1009"></path></svg>
|
||||||
|
After Width: | Height: | Size: 805 B |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1623495517222" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19363" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M128 128h256v64H192v192H128V128zM896 640v256h-256v-64h192v-192h64zM384 686.496L174.496 896 128 849.504 337.504 640H192v-64h256v256h-64v-145.504zM849.504 128L640 337.504V192h-64v256h256v-64h-145.504L896 174.496 849.504 128z" p-id="19364"></path></svg>
|
||||||
|
After Width: | Height: | Size: 628 B |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1577540289643" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7922" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M530.944 458.24l4.8 3.456 122.176 106.816a32 32 0 0 1-37.44 51.584l-4.672-3.392L546.56 556.16v280.704a32 32 0 0 1-26.24 31.488l-5.76 0.512a32 32 0 0 1-31.424-26.24l-0.512-5.76-0.064-280.704-69.12 60.48a32 32 0 0 1-40.96 0.896l-4.16-3.968a32 32 0 0 1-0.96-40.96l4.032-4.16 122.176-106.816a32 32 0 0 1 37.312-3.456zM497.92 128c128.128 0 239.168 82.304 275.52 199.04 123.968 11.264 221.312 113.088 221.312 237.44 0 128.128-103.68 232.96-234.88 238.272h-5.888l-35.52 0.192a32 32 0 0 1-0.192-64l35.264-0.128 4.672-0.064c96.384-3.84 172.544-80.896 172.544-174.272 0-96.128-80.512-174.464-179.584-174.464h-1.984a32 32 0 0 1-32-25.28C695.872 264.96 604.736 192 497.92 192 381.824 192 285.44 277.76 274.816 388.48a32 32 0 0 1-28.352 28.8c-83.968 9.152-147.84 78.208-147.84 159.552l0.192 7.936c3.84 85.76 77.056 154.112 166.592 154.112h45.632a32 32 0 0 1 0 64h-45.632C142.016 802.944 40.32 708.032 34.88 586.88l-0.192-9.28c0-106.88 76.352-197.184 179.968-219.904C239.488 226.112 357.76 128 497.856 128z" p-id="7923"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,32 @@
|
|||||||
|
//定义基础色
|
||||||
|
|
||||||
|
//主色
|
||||||
|
|
||||||
|
body {
|
||||||
|
--color-primary: #409eff;
|
||||||
|
--color-primary-light: rgb(64 158 255 / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@--color-primary:~ 'var(--color-primary)';
|
||||||
|
@--color-primary-light:~ 'var(--color-primary-light)';
|
||||||
|
|
||||||
|
@text: #595959;
|
||||||
|
@text-2: #8c8c8c;
|
||||||
|
|
||||||
|
//导航菜单
|
||||||
|
@dark-text: rgb(255 255 255 / 66%);
|
||||||
|
@dark-text-active: #eee;
|
||||||
|
@dark-bg: #263238;
|
||||||
|
@dark-bg-active: @--color-primary;
|
||||||
|
|
||||||
|
@light-text: @text;
|
||||||
|
@light-text-active: @--color-primary;
|
||||||
|
@light-bg: #fff;
|
||||||
|
@light-bg-active: @--color-primary-light;
|
||||||
|
|
||||||
|
@primary-text: rgb(255 255 255 / 66%);
|
||||||
|
@primary-text-2: rgb(255 255 255 / 65%);
|
||||||
|
@primary-text-active: #fff;
|
||||||
|
@primary-bg: @--color-primary;
|
||||||
|
@primary-bg-light: @--color-primary-light;
|
||||||
|
@primary-bg-active: @--color-primary-light;
|
||||||
@@ -0,0 +1,935 @@
|
|||||||
|
@import "./base.less";
|
||||||
|
|
||||||
|
//主题样式
|
||||||
|
|
||||||
|
//=================
|
||||||
|
.el-menu--vertical.rr-sidebar-menu-pop-light,
|
||||||
|
.el-menu--vertical.rr-sidebar-menu-pop-dark {
|
||||||
|
border-radius: 4px !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
.el-menu.el-menu--popup {
|
||||||
|
min-width: 160px;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
}
|
||||||
|
.el-menu-item,
|
||||||
|
.el-sub-menu__title {
|
||||||
|
height: 45px;
|
||||||
|
line-height: 45px;
|
||||||
|
}
|
||||||
|
.is-active {
|
||||||
|
&.el-menu-item {
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//深色侧边栏
|
||||||
|
.ui-sidebar-dark .rr-sidebar,
|
||||||
|
.rr-sidebar-menu-pop-dark {
|
||||||
|
background: @dark-bg !important;
|
||||||
|
box-shadow: 0 4px 4px rgba(0, 21, 41, 0.35);
|
||||||
|
.el-menu {
|
||||||
|
background: @dark-bg !important;
|
||||||
|
.el-menu-item,
|
||||||
|
.el-sub-menu__title {
|
||||||
|
&:hover {
|
||||||
|
i,
|
||||||
|
a {
|
||||||
|
color: @dark-text-active !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i,
|
||||||
|
a {
|
||||||
|
color: @dark-text !important;
|
||||||
|
}
|
||||||
|
&:not(.is-active):hover {
|
||||||
|
background: inherit !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.is-active {
|
||||||
|
&.el-menu-item {
|
||||||
|
border-right: none !important;
|
||||||
|
background: @dark-bg-active !important;
|
||||||
|
}
|
||||||
|
&.el-menu-item,
|
||||||
|
> .el-sub-menu__title:first-child {
|
||||||
|
i,
|
||||||
|
a {
|
||||||
|
color: @dark-text-active !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//浅色侧边栏
|
||||||
|
.ui-sidebar-light .rr-sidebar,
|
||||||
|
.rr-sidebar-menu-pop-light {
|
||||||
|
background: @light-bg !important;
|
||||||
|
box-shadow: 0 4px 4px rgba(0, 21, 41, 0.25);
|
||||||
|
.el-menu {
|
||||||
|
background: @light-bg !important;
|
||||||
|
.el-menu-item,
|
||||||
|
.el-sub-menu__title {
|
||||||
|
&:hover {
|
||||||
|
i,
|
||||||
|
a {
|
||||||
|
color: @light-text-active !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i,
|
||||||
|
a {
|
||||||
|
color: @light-text !important;
|
||||||
|
}
|
||||||
|
&:not(.is-active):hover {
|
||||||
|
background: inherit !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.is-active {
|
||||||
|
&.el-menu-item {
|
||||||
|
border-right: 2px solid @light-text-active !important;
|
||||||
|
background: @light-bg-active !important;
|
||||||
|
}
|
||||||
|
&.el-menu-item,
|
||||||
|
> .el-sub-menu__title:first-child {
|
||||||
|
i,
|
||||||
|
a {
|
||||||
|
color: @light-text-active !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//================================
|
||||||
|
.el-menu--horizontal.rr-sidebar-menu-pop-light,
|
||||||
|
.el-menu--horizontal.rr-sidebar-menu-pop-dark {
|
||||||
|
border-radius: 4px !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
background-color: @light-bg !important;
|
||||||
|
border: none !important;
|
||||||
|
margin-top: -5px;
|
||||||
|
margin-left: 0;
|
||||||
|
.el-popper {
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
.el-menu--horizontal {
|
||||||
|
margin-left: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu.el-menu--popup {
|
||||||
|
min-width: 160px;
|
||||||
|
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu-item,
|
||||||
|
.el-sub-menu__title {
|
||||||
|
height: 45px;
|
||||||
|
line-height: 45px;
|
||||||
|
}
|
||||||
|
.is-active {
|
||||||
|
&.el-menu-item {
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//浅色顶栏
|
||||||
|
.ui-topHeader-light {
|
||||||
|
.rr-header-ctx {
|
||||||
|
box-shadow: 0 1px 1px #f1f1f1;
|
||||||
|
&-logo {
|
||||||
|
background: @light-bg !important;
|
||||||
|
color: #000000bf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ui-sidebar-dark {
|
||||||
|
.rr-header-ctx {
|
||||||
|
box-shadow: 0 1px 3px rgb(0 0 0 / 8%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rr-header-right {
|
||||||
|
background: @light-bg !important;
|
||||||
|
.rr-header-right-items {
|
||||||
|
* {
|
||||||
|
color: @light-text !important;
|
||||||
|
}
|
||||||
|
> div {
|
||||||
|
&:hover {
|
||||||
|
color: #262626 !important;
|
||||||
|
background: rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-badge__content {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rr-sidebar-menu {
|
||||||
|
&.el-menu {
|
||||||
|
background: @light-bg !important;
|
||||||
|
.el-menu-item,
|
||||||
|
.el-sub-menu__title {
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.1) !important;
|
||||||
|
i,
|
||||||
|
a {
|
||||||
|
color: @light-text-active !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i,
|
||||||
|
a {
|
||||||
|
color: @light-text !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
i:not(.el-sub-menu__icon-arrow) {
|
||||||
|
width: 17px !important;
|
||||||
|
height: 17px !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
margin-top: -4px;
|
||||||
|
line-height: 17px;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.is-active {
|
||||||
|
&.el-menu-item {
|
||||||
|
border-bottom: 2px solid @light-text-active !important;
|
||||||
|
background: @light-bg !important;
|
||||||
|
}
|
||||||
|
&.el-menu-item,
|
||||||
|
.el-sub-menu__title {
|
||||||
|
i,
|
||||||
|
a {
|
||||||
|
color: @light-text-active !important;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.isLink {
|
||||||
|
border-bottom: 0 !important;
|
||||||
|
i,
|
||||||
|
a {
|
||||||
|
color: @light-text !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//深色顶栏
|
||||||
|
.ui-topHeader-dark {
|
||||||
|
.rr-header-ctx {
|
||||||
|
&-logo {
|
||||||
|
background: @dark-bg !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rr-header-right {
|
||||||
|
background: @dark-bg !important;
|
||||||
|
.rr-header-right-items {
|
||||||
|
* {
|
||||||
|
color: @dark-text !important;
|
||||||
|
&:hover {
|
||||||
|
color: @dark-text-active !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-badge__content {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rr-sidebar-menu {
|
||||||
|
&.el-menu {
|
||||||
|
background: @dark-bg !important;
|
||||||
|
.el-menu-item,
|
||||||
|
.el-sub-menu__title {
|
||||||
|
&:hover {
|
||||||
|
background: @dark-bg !important;
|
||||||
|
i,
|
||||||
|
a {
|
||||||
|
color: @dark-text-active !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i,
|
||||||
|
a {
|
||||||
|
color: @dark-text !important;
|
||||||
|
}
|
||||||
|
&:not(.is-active):hover {
|
||||||
|
background: inherit !important;
|
||||||
|
}
|
||||||
|
i:not(.el-sub-menu__icon-arrow) {
|
||||||
|
width: 17px !important;
|
||||||
|
height: 17px !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
margin-top: -4px;
|
||||||
|
line-height: 17px;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.is-active {
|
||||||
|
&.el-menu-item {
|
||||||
|
border-bottom: 2px solid @dark-text-active !important;
|
||||||
|
background: @dark-bg !important;
|
||||||
|
}
|
||||||
|
&.el-menu-item,
|
||||||
|
.el-sub-menu__title {
|
||||||
|
border-bottom: 2px solid @dark-text-active !important;
|
||||||
|
i,
|
||||||
|
a {
|
||||||
|
color: @dark-text-active !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.isLink {
|
||||||
|
border-bottom: 0 !important;
|
||||||
|
i,
|
||||||
|
a {
|
||||||
|
color: @dark-text !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//主题色
|
||||||
|
.ui-topHeader-primary {
|
||||||
|
.rr-header-ctx {
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08) !important;
|
||||||
|
position: relative;
|
||||||
|
z-index: 102;
|
||||||
|
}
|
||||||
|
.rr-header-ctx-logo {
|
||||||
|
background: @primary-bg !important;
|
||||||
|
}
|
||||||
|
.rr-header-right {
|
||||||
|
background: @primary-bg !important;
|
||||||
|
.rr-header-right-items,
|
||||||
|
.rr-header-right-left-br {
|
||||||
|
div,
|
||||||
|
span,
|
||||||
|
svg,
|
||||||
|
i {
|
||||||
|
color: @primary-text !important;
|
||||||
|
&:hover {
|
||||||
|
color: @primary-text-active !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> div:not(.el-breadcrumb) {
|
||||||
|
&:hover {
|
||||||
|
color: #262626 !important;
|
||||||
|
background: rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-badge__content {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
.el-breadcrumb {
|
||||||
|
.el-breadcrumb__item {
|
||||||
|
&:not(:first-child) {
|
||||||
|
* {
|
||||||
|
color: @primary-text-2 !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rr-sidebar-menu {
|
||||||
|
&.el-menu {
|
||||||
|
background: @primary-bg !important;
|
||||||
|
.el-menu-item,
|
||||||
|
.el-sub-menu__title {
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
background: rgba(0, 0, 0, 0.1) !important;
|
||||||
|
i,
|
||||||
|
a {
|
||||||
|
color: @primary-text-active !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i,
|
||||||
|
a {
|
||||||
|
color: @primary-text !important;
|
||||||
|
}
|
||||||
|
i:not(.el-sub-menu__icon-arrow) {
|
||||||
|
width: 17px !important;
|
||||||
|
height: 17px !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
margin-top: -4px;
|
||||||
|
line-height: 17px;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.is-active {
|
||||||
|
&.el-menu-item {
|
||||||
|
border-bottom: 2px solid @primary-text-active !important;
|
||||||
|
}
|
||||||
|
&.el-menu-item,
|
||||||
|
.el-sub-menu__title {
|
||||||
|
border-bottom: 2px solid @primary-text-active !important;
|
||||||
|
i,
|
||||||
|
a {
|
||||||
|
color: @primary-text-active !important;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.isLink {
|
||||||
|
border-bottom: 0 !important;
|
||||||
|
i,
|
||||||
|
a {
|
||||||
|
color: @primary-text !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============
|
||||||
|
//导航模式
|
||||||
|
.ui-navLayout-left {
|
||||||
|
&.ui-sidebar-light {
|
||||||
|
.rr-sidebar {
|
||||||
|
box-shadow: 1px 2px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
z-index: 101;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-navLayout-top {
|
||||||
|
&.ui-topHeader-light {
|
||||||
|
.rr-header-right {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rr-header-ctx-logo {
|
||||||
|
max-width: inherit !important;
|
||||||
|
&-text {
|
||||||
|
max-width: inherit !important;
|
||||||
|
overflow: inherit !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rr-view-tab-wrap {
|
||||||
|
left: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-navLayout-mix {
|
||||||
|
.rr-header-ctx-logo {
|
||||||
|
max-width: inherit !important;
|
||||||
|
&-text {
|
||||||
|
max-width: inherit !important;
|
||||||
|
overflow: inherit !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ui-sidebar-light {
|
||||||
|
.rr-sidebar {
|
||||||
|
box-shadow: 1px 2px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
z-index: 101;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rr-sidebar {
|
||||||
|
box-shadow: 1px 2px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
z-index: 101;
|
||||||
|
}
|
||||||
|
.rr-header-right-left-br {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//========
|
||||||
|
//内容不铺满
|
||||||
|
.ui-contentFull-false {
|
||||||
|
.rr-view-ctx {
|
||||||
|
width: 1200px !important;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//=======
|
||||||
|
//tab标签栏开关
|
||||||
|
.ui-openTabsPage {
|
||||||
|
&-false {
|
||||||
|
.rr-view-ctx {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//=======
|
||||||
|
//logo自动
|
||||||
|
//导航模式在顶部时logo自动要取消
|
||||||
|
.ui-logoAuto-true,
|
||||||
|
.ui-navLayout-top {
|
||||||
|
.rr-header-ctx-logo {
|
||||||
|
width: inherit !important;
|
||||||
|
padding: 0 15px 0 20px;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
&.ui-topHeader-primary .rr-header-ctx-logo {
|
||||||
|
background: @primary-bg !important;
|
||||||
|
color: #ffffffd9 !important;
|
||||||
|
}
|
||||||
|
&.ui-topHeader-dark .rr-header-ctx-logo {
|
||||||
|
background: @dark-bg !important;
|
||||||
|
color: #ffffffd9 !important;
|
||||||
|
}
|
||||||
|
&.ui-topHeader-light .rr-header-ctx-logo {
|
||||||
|
background: @light-bg !important;
|
||||||
|
color: #000000bf;
|
||||||
|
box-shadow: 1px 0 3px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//侧边栏多彩图标
|
||||||
|
.ui-colorIcon-true {
|
||||||
|
.rr-sidebar {
|
||||||
|
.el-menu {
|
||||||
|
.el-sub-menu__title,
|
||||||
|
.el-menu-item,
|
||||||
|
.isLink {
|
||||||
|
margin-left: -5px !important;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
&:first-child {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: rgb(97, 178, 252);
|
||||||
|
border-radius: 50%;
|
||||||
|
text-align: center;
|
||||||
|
color: rgb(255, 255, 255) !important;
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:nth-child(2n) {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
&:first-child {
|
||||||
|
background-color: rgb(125, 215, 51);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:nth-child(3) {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
&:first-child {
|
||||||
|
background-color: rgb(50, 162, 212);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:nth-child(4) {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
&:first-child {
|
||||||
|
background-color: rgb(115, 131, 207);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:nth-child(5) {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
&:first-child {
|
||||||
|
background-color: rgb(245, 104, 111);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:nth-child(6) {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
&:first-child {
|
||||||
|
background-color: rgb(43, 204, 206);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:nth-child(7) {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
&:first-child {
|
||||||
|
background-color: rgb(125, 215, 51);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:nth-child(8) {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
&:first-child {
|
||||||
|
background-color: rgb(250, 173, 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//--
|
||||||
|
.el-sub-menu {
|
||||||
|
.el-menu {
|
||||||
|
li,
|
||||||
|
.el-sub-menu__title {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
&:first-child:not(.el-sub-menu__icon-arrow) {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
line-height: 8px;
|
||||||
|
font-size: 30px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 0 0 10px;
|
||||||
|
background: @dark-text !important;
|
||||||
|
color: @dark-text !important;
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
margin-left: -11px;
|
||||||
|
font-family: element-icons !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-menu-item,
|
||||||
|
.el-sub-menu.is-active .el-sub-menu__title {
|
||||||
|
i:first-child {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ui-sidebar-light {
|
||||||
|
.rr-sidebar {
|
||||||
|
.el-sub-menu .el-menu {
|
||||||
|
.el-sub-menu {
|
||||||
|
.el-sub-menu__title {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
&:first-child:not(.el-sub-menu__icon-arrow) {
|
||||||
|
color: @light-text !important;
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
&:first-child:not(.el-sub-menu__icon-arrow) {
|
||||||
|
color: @light-text-active !important;
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.is-active .el-sub-menu__title [class^="el-icon"] {
|
||||||
|
&:first-child:not(.el-sub-menu__icon-arrow) {
|
||||||
|
color: @light-text-active !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-menu-item {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
&:first-child:not(.el-sub-menu__icon-arrow) {
|
||||||
|
background: @light-text !important;
|
||||||
|
color: @light-text !important;
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.is-active,
|
||||||
|
&:hover {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
&:first-child:not(.el-sub-menu__icon-arrow) {
|
||||||
|
background: @light-text-active !important;
|
||||||
|
color: @light-text-active !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover:not(.is-active) {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
&:first-child:not(.el-sub-menu__icon-arrow) {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ui-sidebar-dark {
|
||||||
|
.rr-sidebar {
|
||||||
|
.el-sub-menu {
|
||||||
|
.el-sub-menu.is-opened {
|
||||||
|
&.is-active {
|
||||||
|
.el-sub-menu__title {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
&:first-child:not(.el-sub-menu__icon-arrow) {
|
||||||
|
background: @dark-text-active !important;
|
||||||
|
color: @dark-text-active !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-menu .el-menu-item,
|
||||||
|
.el-sub-menu.is-opened .el-sub-menu__title {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
&:first-child:not(.el-sub-menu__icon-arrow) {
|
||||||
|
background: @dark-text !important;
|
||||||
|
color: @dark-text !important;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.is-active,
|
||||||
|
&:hover {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
&:first-child:not(.el-sub-menu__icon-arrow) {
|
||||||
|
background: @dark-text-active !important;
|
||||||
|
color: @dark-text-active !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover:not(.is-active) {
|
||||||
|
[class^="el-icon"] {
|
||||||
|
&:first-child:not(.el-sub-menu__icon-arrow) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//侧边栏收缩状态
|
||||||
|
.rr.ui-sidebarCollapse {
|
||||||
|
&-true {
|
||||||
|
.rr-view-tab-wrap {
|
||||||
|
left: 60px;
|
||||||
|
}
|
||||||
|
.rr-header-ctx-logo-line {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
.enabled-logo-false {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
&.ui-logoAuto {
|
||||||
|
&-false {
|
||||||
|
.rr-header-ctx-logo {
|
||||||
|
width: 60px !important;
|
||||||
|
&-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-true {
|
||||||
|
.enabled-logo-false {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.rr-header-ctx-logo-line {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ui-navLayout-top {
|
||||||
|
//导航模式为顶部时自动展开logo状态
|
||||||
|
.rr-header-ctx-logo {
|
||||||
|
width: inherit !important;
|
||||||
|
padding: 0 15px 0 20px;
|
||||||
|
box-shadow: none !important;
|
||||||
|
&-text {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.enabled-logo-false {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rr-sidebar:not(.rr-sidebar-mobile) {
|
||||||
|
width: 60px !important;
|
||||||
|
.el-menu {
|
||||||
|
width: 60px !important;
|
||||||
|
}
|
||||||
|
// 收起效果
|
||||||
|
.rr-sidebar-menu {
|
||||||
|
.el-menu-item,
|
||||||
|
.el-sub-menu__title,
|
||||||
|
.el-sub-menu {
|
||||||
|
a,
|
||||||
|
.el-menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-false {
|
||||||
|
.rr-header-ctx-logo {
|
||||||
|
&-text {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//tabStyle
|
||||||
|
.ui-tabStyle-default {
|
||||||
|
.rr-view-tab {
|
||||||
|
.el-tabs__item {
|
||||||
|
border-right: none !important;
|
||||||
|
padding: 0 15px 0 !important;
|
||||||
|
&.is-active {
|
||||||
|
color: @--color-primary !important;
|
||||||
|
}
|
||||||
|
&:before {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
height: 3px;
|
||||||
|
width: 0;
|
||||||
|
background-color: @--color-primary !important;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
&.is-active:after,
|
||||||
|
&:hover:after {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-tabs__nav-wrap {
|
||||||
|
&:before,
|
||||||
|
&:after,
|
||||||
|
.el-tabs__nav-next,
|
||||||
|
.el-tabs__nav-prev {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ui-tabStyle-dot {
|
||||||
|
.rr-view-tab-wrap {
|
||||||
|
.rr-view-tab {
|
||||||
|
.el-tabs__item {
|
||||||
|
&.is-active {
|
||||||
|
color: @--color-primary !important;
|
||||||
|
&:before {
|
||||||
|
background-color: @--color-primary !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ui-tabStyle-card {
|
||||||
|
.rr-view-tab-wrap {
|
||||||
|
background: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
padding-top: 10px;
|
||||||
|
.rr-view-tab {
|
||||||
|
height: 30px;
|
||||||
|
background: transparent !important;
|
||||||
|
&-ops {
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
background-color: #fff;
|
||||||
|
margin-right: 10px;
|
||||||
|
.el-icon--right {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-tabs__item {
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 0 15px 0 !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
background-color: #fff;
|
||||||
|
&:nth-child(2) {
|
||||||
|
margin-left: 0;
|
||||||
|
padding: 0 15px !important;
|
||||||
|
}
|
||||||
|
&.is-active {
|
||||||
|
background-color: @--color-primary !important;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
&:before {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-tabs__nav-wrap {
|
||||||
|
&:before,
|
||||||
|
&:after,
|
||||||
|
.el-tabs__nav-next,
|
||||||
|
.el-tabs__nav-prev {
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
background: #eff2f5 !important;
|
||||||
|
}
|
||||||
|
.el-tabs__nav-next,
|
||||||
|
.el-tabs__nav-prev {
|
||||||
|
&:hover {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//外链
|
||||||
|
.rr-sidebar-menu.el-menu .el-menu-item.is-active.isLink {
|
||||||
|
background: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
//不同语言下的差异
|
||||||
|
[lang="en-US"] {
|
||||||
|
.rr-header-ctx-logo-text {
|
||||||
|
letter-spacing: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
:not(html):not(body)::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(html):not(body)::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(html):not(body)::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: hsla(0, 0%, 54.9%, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(html):not(body)::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: hsla(0, 0%, 54.9%, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ele-scrollbar-mini::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ele-scrollbar-mini::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ele-scrollbar-hide::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
@import "./base.less";
|
||||||
|
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
.rr-header-action {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
.show-xs-only {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: 768px) {
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: 768px) and (max-width: 992px) {
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 992px) {
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: 992px) {
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: 992px) and (max-width: 1200px) {
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 1200px) {
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: 1200px) {
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: 1200px) and (max-width: 1920px) {
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 1920px) {
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: 1920px) {
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
.ui-mobile {
|
||||||
|
.rr-view-tab-wrap {
|
||||||
|
left: 0 !important;
|
||||||
|
transition: left 0s !important;
|
||||||
|
}
|
||||||
|
.rr-header-ctx-logo-img-wrap {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rr-sidebar-mobile {
|
||||||
|
z-index: 9999 !important;
|
||||||
|
&-inner {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
scrollbar-width: none;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ui-sidebar-light .rr-sidebar-mobile {
|
||||||
|
.el-drawer__body,
|
||||||
|
.rr-header-ctx-logo-mobile {
|
||||||
|
background: @light-bg !important;
|
||||||
|
}
|
||||||
|
.rr-header-ctx-logo-mobile {
|
||||||
|
color: @light-text !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ui-sidebar-dark .rr-sidebar-mobile {
|
||||||
|
.el-drawer__body,
|
||||||
|
.rr-header-ctx-logo-mobile {
|
||||||
|
background: @dark-bg !important;
|
||||||
|
}
|
||||||
|
.rr-header-ctx-logo-mobile {
|
||||||
|
color: @dark-text !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ui-sidebarCollapse-true,
|
||||||
|
.ui-sidebarCollapse-false {
|
||||||
|
.rr-sidebar-mobile {
|
||||||
|
width: initial !important;
|
||||||
|
.el-menu.rr-sidebar-menu {
|
||||||
|
width: 230px !important;
|
||||||
|
.el-menu-item,
|
||||||
|
.el-sub-menu__title {
|
||||||
|
a {
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rr-header-ctx-logo.rr-header-ctx-logo-mobile {
|
||||||
|
width: auto !important;
|
||||||
|
.rr-header-ctx-logo-text {
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-drawer,
|
||||||
|
.el-drawer__body {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,660 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||||
|
import {
|
||||||
|
WarningFilled,
|
||||||
|
Close,
|
||||||
|
Bell,
|
||||||
|
UploadFilled,
|
||||||
|
CircleCheckFilled,
|
||||||
|
CircleCloseFilled,
|
||||||
|
Loading
|
||||||
|
} from "@element-plus/icons-vue";
|
||||||
|
import { useAlertMarquee, type AlertMessage } from "@/composables/useAlertMarquee";
|
||||||
|
import { useImportTaskStore, type ImportTask } from "@/store/importTasks";
|
||||||
|
import { useFloatingDrag } from "@/composables/useFloatingDrag";
|
||||||
|
|
||||||
|
const {
|
||||||
|
messages,
|
||||||
|
messagesReversed,
|
||||||
|
latestMessage,
|
||||||
|
messageCount,
|
||||||
|
hasMessages,
|
||||||
|
drawerVisible,
|
||||||
|
detailTarget,
|
||||||
|
detailVisible,
|
||||||
|
position,
|
||||||
|
showDetail,
|
||||||
|
closeDetail,
|
||||||
|
removeAlert,
|
||||||
|
clearAll,
|
||||||
|
startPolling,
|
||||||
|
stopPolling
|
||||||
|
} = useAlertMarquee();
|
||||||
|
|
||||||
|
const importStore = useImportTaskStore();
|
||||||
|
|
||||||
|
// ---- 悬浮滚动条拖拽 ----
|
||||||
|
|
||||||
|
const scrollbarRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const { isDragging, dragMoved, onPointerDown } = useFloatingDrag(position, scrollbarRef);
|
||||||
|
|
||||||
|
// ---- 滚动条手动关闭后,有新通知时自动弹出 ----
|
||||||
|
|
||||||
|
const scrollbarDismissed = ref(false);
|
||||||
|
|
||||||
|
function dismissScrollbar(): void {
|
||||||
|
scrollbarDismissed.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onViewDetail(): void {
|
||||||
|
if (latestMessage.value) {
|
||||||
|
showDetail(latestMessage.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听消息数量变化:有新通知时重新显示滚动条
|
||||||
|
watch(messageCount, (now, prev) => {
|
||||||
|
if (now > prev) {
|
||||||
|
scrollbarDismissed.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const showScrollbar = computed(() => hasMessages.value && !scrollbarDismissed.value);
|
||||||
|
|
||||||
|
// ---- 拖拽时跳过按钮点击 ----
|
||||||
|
|
||||||
|
const INTERACTIVE_SEL = "button, a, [role=button]";
|
||||||
|
|
||||||
|
function onScrollbarPointerDown(e: PointerEvent): void {
|
||||||
|
if (e.target instanceof HTMLElement && e.target.closest(INTERACTIVE_SEL)) return;
|
||||||
|
onPointerDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 初始位置 ----
|
||||||
|
|
||||||
|
const initialized = ref(false);
|
||||||
|
|
||||||
|
function initPosition(): void {
|
||||||
|
if (initialized.value) return;
|
||||||
|
initialized.value = true;
|
||||||
|
if (position.value.x === 0 && position.value.y === 0) {
|
||||||
|
position.value = {
|
||||||
|
x: (window.innerWidth - 640) / 2,
|
||||||
|
y: 56
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 窗口大小变化时钳制位置 ----
|
||||||
|
|
||||||
|
function clampPosition(): void {
|
||||||
|
const w = 640;
|
||||||
|
const h = 43;
|
||||||
|
position.value = {
|
||||||
|
x: Math.max(0, Math.min(position.value.x, window.innerWidth - w)),
|
||||||
|
y: Math.max(52, Math.min(position.value.y, window.innerHeight - h - 20))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 窗口 resize 时保持居中 ----
|
||||||
|
|
||||||
|
function recenter(): void {
|
||||||
|
const w = 640;
|
||||||
|
position.value = {
|
||||||
|
x: (window.innerWidth - w) / 2,
|
||||||
|
y: position.value.y
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
startPolling(30000);
|
||||||
|
initPosition();
|
||||||
|
window.addEventListener("resize", recenter);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopPolling();
|
||||||
|
window.removeEventListener("resize", recenter);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- 位置样式 ----
|
||||||
|
|
||||||
|
const positionStyle = computed(() => ({
|
||||||
|
left: position.value.x + "px",
|
||||||
|
top: position.value.y + "px"
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ---- 统一时间线:警告 + 导入任务,按时间倒序 ----
|
||||||
|
|
||||||
|
type TimelineItem =
|
||||||
|
| { kind: "alert"; data: AlertMessage; ts: string }
|
||||||
|
| { kind: "import"; data: ImportTask; ts: number };
|
||||||
|
|
||||||
|
const timeline = computed<TimelineItem[]>(() => {
|
||||||
|
const items: TimelineItem[] = [];
|
||||||
|
|
||||||
|
for (const msg of messages.value) {
|
||||||
|
items.push({ kind: "alert", data: msg, ts: msg.publishTime });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const task of importStore.recentTasks) {
|
||||||
|
items.push({ kind: "import", data: task, ts: task.createdAt });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按时间倒序:alert 用 publishTime 字符串不好比,放前面;
|
||||||
|
// import 用 createdAt 数字好排序
|
||||||
|
items.sort((a, b) => {
|
||||||
|
const ta = a.kind === "import" ? (a.data as ImportTask).createdAt : 0;
|
||||||
|
const tb = b.kind === "import" ? (b.data as ImportTask).createdAt : 0;
|
||||||
|
// alert 没有精确时间戳,统一用 0 兜底;实际排序以 createdAt 为主
|
||||||
|
return tb - ta;
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasTimeline = computed(() => timeline.value.length > 0);
|
||||||
|
|
||||||
|
// ---- 级别标签类型映射 ----
|
||||||
|
|
||||||
|
function tagType(level: string): "info" | "warning" | "danger" {
|
||||||
|
if (level === "danger") return "danger";
|
||||||
|
if (level === "warning") return "warning";
|
||||||
|
return "info";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 导入任务状态辅助 ----
|
||||||
|
|
||||||
|
function importStatusText(status: ImportTask["status"]): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
pending: "等待中",
|
||||||
|
uploading: "上传中",
|
||||||
|
processing: "处理中",
|
||||||
|
success: "已完成",
|
||||||
|
error: "失败"
|
||||||
|
};
|
||||||
|
return map[status] ?? status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function importProgressPercent(task: ImportTask): number {
|
||||||
|
if (task.totalRows <= 0) return 0;
|
||||||
|
return Math.round((task.processedRows / task.totalRows) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatImportTime(ts: number): string {
|
||||||
|
const d = new Date(ts);
|
||||||
|
return d.toLocaleTimeString("zh-CN", { hour12: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 清空全部(仅清空警告,导入任务由各自逻辑管理) ----
|
||||||
|
|
||||||
|
function handleClearAll(): void {
|
||||||
|
clearAll();
|
||||||
|
importStore.clearCompleted();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- ===== 悬浮滚动条(仅展示最新警告) ===== -->
|
||||||
|
<div
|
||||||
|
v-show="showScrollbar"
|
||||||
|
ref="scrollbarRef"
|
||||||
|
class="alert-scrollbar"
|
||||||
|
:class="{ 'is-dragging': isDragging }"
|
||||||
|
:style="positionStyle"
|
||||||
|
@pointerdown="onScrollbarPointerDown"
|
||||||
|
>
|
||||||
|
<span class="alert-scrollbar__icon">
|
||||||
|
<el-icon :size="16"><WarningFilled /></el-icon>
|
||||||
|
</span>
|
||||||
|
<div class="alert-scrollbar__text-wrap">
|
||||||
|
<span
|
||||||
|
class="alert-scrollbar__text"
|
||||||
|
:class="{ 'is-scroll': latestMessage && latestMessage.content.length > 60 }"
|
||||||
|
>{{ latestMessage?.content ?? "" }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="alert-scrollbar__badge">{{ messageCount }}</span>
|
||||||
|
<button class="alert-scrollbar__btn-detail" @click.stop="onViewDetail">查看详情</button>
|
||||||
|
<button class="alert-scrollbar__btn-close" @click.stop="dismissScrollbar">关闭</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== 通知中心抽屉 ===== -->
|
||||||
|
<el-drawer
|
||||||
|
v-model="drawerVisible"
|
||||||
|
direction="rtl"
|
||||||
|
size="420px"
|
||||||
|
:with-header="true"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="drawer-header">
|
||||||
|
<span class="drawer-header__title">
|
||||||
|
<el-icon :size="18"><Bell /></el-icon>
|
||||||
|
通知中心
|
||||||
|
</span>
|
||||||
|
<el-button
|
||||||
|
v-if="hasTimeline"
|
||||||
|
text
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="handleClearAll"
|
||||||
|
>
|
||||||
|
清空全部
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 统一时间线 -->
|
||||||
|
<div v-if="hasTimeline" class="drawer-list">
|
||||||
|
<template v-for="item in timeline" :key="item.kind === 'alert' ? item.data.id : item.data.id">
|
||||||
|
<!-- ===== 警告消息 ===== -->
|
||||||
|
<div
|
||||||
|
v-if="item.kind === 'alert'"
|
||||||
|
class="drawer-item drawer-item--clickable"
|
||||||
|
@click="showDetail(item.data)"
|
||||||
|
>
|
||||||
|
<div class="drawer-item__left">
|
||||||
|
<span class="drawer-item__dot" :class="'dot--' + item.data.level"></span>
|
||||||
|
<div class="drawer-item__body">
|
||||||
|
<p class="drawer-item__content">{{ item.data.content }}</p>
|
||||||
|
<span class="drawer-item__time">{{ item.data.publishTime }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="drawer-item__close"
|
||||||
|
title="移除"
|
||||||
|
@click.stop="removeAlert(item.data.id)"
|
||||||
|
>
|
||||||
|
<el-icon :size="12"><Close /></el-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== 导入任务 ===== -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="drawer-item drawer-item--import"
|
||||||
|
>
|
||||||
|
<div class="drawer-item__left">
|
||||||
|
<!-- 状态图标 -->
|
||||||
|
<span class="drawer-item__import-icon" :class="'import--' + item.data.status">
|
||||||
|
<el-icon :size="16">
|
||||||
|
<CircleCheckFilled v-if="item.data.status === 'success'" />
|
||||||
|
<CircleCloseFilled v-else-if="item.data.status === 'error'" />
|
||||||
|
<Loading v-else />
|
||||||
|
</el-icon>
|
||||||
|
</span>
|
||||||
|
<div class="drawer-item__body">
|
||||||
|
<p class="drawer-item__content">
|
||||||
|
{{ item.data.fileName }}
|
||||||
|
<span class="import-status-tag" :class="'import-status--' + item.data.status">
|
||||||
|
{{ importStatusText(item.data.status) }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<!-- 进度条(仅处理中显示) -->
|
||||||
|
<div
|
||||||
|
v-if="item.data.status === 'processing' && item.data.totalRows > 0"
|
||||||
|
class="import-progress"
|
||||||
|
>
|
||||||
|
<el-progress
|
||||||
|
:percentage="importProgressPercent(item.data)"
|
||||||
|
:stroke-width="4"
|
||||||
|
:show-text="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 失败消息 -->
|
||||||
|
<p
|
||||||
|
v-if="item.data.status === 'error' && item.data.message"
|
||||||
|
class="import-error-msg"
|
||||||
|
>{{ item.data.message }}</p>
|
||||||
|
<span class="drawer-item__time">{{ formatImportTime(item.data.createdAt) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 仅已完成/失败的任务可关闭 -->
|
||||||
|
<button
|
||||||
|
v-if="item.data.status === 'success' || item.data.status === 'error'"
|
||||||
|
class="drawer-item__close"
|
||||||
|
title="移除"
|
||||||
|
@click.stop="importStore.removeTask(item.data.id)"
|
||||||
|
>
|
||||||
|
<el-icon :size="12"><Close /></el-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-else class="drawer-empty">
|
||||||
|
<el-icon :size="48" color="#c0c4cc"><Bell /></el-icon>
|
||||||
|
<p>暂无通知</p>
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
|
|
||||||
|
<!-- ===== 详情弹窗(仅警告消息) ===== -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailVisible"
|
||||||
|
title="通知详情"
|
||||||
|
width="520px"
|
||||||
|
:close-on-click-modal="true"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<template v-if="detailTarget">
|
||||||
|
<div class="detail-level">
|
||||||
|
<el-tag :type="tagType(detailTarget.level)" size="small">
|
||||||
|
{{ detailTarget.level === "danger" ? "紧急" : detailTarget.level === "warning" ? "警告" : "提示" }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="detail-time">
|
||||||
|
发布时间:{{ detailTarget.publishTime }}
|
||||||
|
</div>
|
||||||
|
<div class="detail-content">
|
||||||
|
{{ detailTarget.content }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="closeDetail">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* ================================================
|
||||||
|
悬浮滚动条
|
||||||
|
================================================ */
|
||||||
|
.alert-scrollbar {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1990;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
height: 43px;
|
||||||
|
width: 640px;
|
||||||
|
max-width: calc(100vw - 32px);
|
||||||
|
padding: 0 14px;
|
||||||
|
background: rgba(245, 108, 108, 0.85);
|
||||||
|
border-radius: 18px;
|
||||||
|
box-shadow: 0 2px 10px rgba(245, 108, 108, 0.35);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
-webkit-backdrop-filter: blur(4px);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
touch-action: none;
|
||||||
|
transition: opacity 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-scrollbar:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(245, 108, 108, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-scrollbar.is-dragging {
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: none !important;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-scrollbar__icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-scrollbar__text-wrap {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-scrollbar__text {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 43px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-scrollbar__text.is-scroll {
|
||||||
|
animation: scrollbar-marquee 12s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scrollbar-marquee {
|
||||||
|
0% { transform: translateX(0); }
|
||||||
|
80% { transform: translateX(calc(-100% + 530px)); }
|
||||||
|
100% { transform: translateX(calc(-100% + 530px)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-scrollbar__badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0 5px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-scrollbar__btn-detail,
|
||||||
|
.alert-scrollbar__btn-close {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 26px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 13px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-scrollbar__btn-detail {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
color: #fff;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-scrollbar__btn-detail:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-scrollbar__btn-close {
|
||||||
|
background: rgba(0, 0, 0, 0.15);
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-scrollbar__btn-close:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================
|
||||||
|
通知中心抽屉
|
||||||
|
================================================ */
|
||||||
|
.drawer-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-header__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 基础条目 */
|
||||||
|
.drawer-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-item--clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-item--clickable:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
margin: 0 -20px;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-item__left {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 警告色点 */
|
||||||
|
.drawer-item__dot {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot--danger { background: #f56c6c; }
|
||||||
|
.dot--warning { background: #e6a23c; }
|
||||||
|
.dot--info { background: #409eff; }
|
||||||
|
|
||||||
|
/* 导入状态图标 */
|
||||||
|
.drawer-item__import-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import--success { color: #67c23a; }
|
||||||
|
.import--error { color: #f56c6c; }
|
||||||
|
.import--uploading,
|
||||||
|
.import--processing,
|
||||||
|
.import--pending { color: #409eff; }
|
||||||
|
|
||||||
|
.drawer-item__body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-item__content {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 1.5;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 导入状态标签 */
|
||||||
|
.import-status-tag {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-status--uploading,
|
||||||
|
.import-status--processing,
|
||||||
|
.import-status--pending { color: #409eff; }
|
||||||
|
.import-status--success { color: #67c23a; }
|
||||||
|
.import-status--error { color: #f56c6c; }
|
||||||
|
|
||||||
|
/* 导入进度条 */
|
||||||
|
.import-progress {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 导入失败消息 */
|
||||||
|
.import-error-msg {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #f56c6c;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-item__time {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-item__close {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: #c0c4cc;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-item__close:hover {
|
||||||
|
background: #f56c6c;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 0;
|
||||||
|
color: #c0c4cc;
|
||||||
|
font-size: 14px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================
|
||||||
|
详情弹窗
|
||||||
|
================================================ */
|
||||||
|
.detail-level {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 1.7;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { withInstall } from "@/utils/utils";
|
||||||
|
import SvgIcon from "./index.vue";
|
||||||
|
|
||||||
|
export default withInstall(SvgIcon);
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<svg aria-hidden="true" :class="`iconfont ${className}`" :style="`width:${width};height:${height};color:${color};${style}`">
|
||||||
|
<use :xlink:href="symbolId" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent } from "vue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义svg图标,可自行将svg图标下载后存放在/src/assets/icons/svg目录下
|
||||||
|
* `使用方法:<svg-icon name="earth" color="red"></svg-icon>`
|
||||||
|
*/
|
||||||
|
export default defineComponent({
|
||||||
|
name: "SvgIcon",
|
||||||
|
props: {
|
||||||
|
prefix: {
|
||||||
|
type: String,
|
||||||
|
default: "icon"
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
width: String,
|
||||||
|
height: String,
|
||||||
|
className: { type: String, default: "" },
|
||||||
|
style: { type: String, default: "" }
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const symbolId = computed(() => `#${props.prefix}-${props.name.replace("icon-", "")}`);
|
||||||
|
return { symbolId };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { withInstall } from "@/utils/utils";
|
||||||
|
import SysDeptTree from "./src/sys-dept-tree.vue";
|
||||||
|
|
||||||
|
SysDeptTree.name = "SysDeptTree";
|
||||||
|
export default withInstall(SysDeptTree);
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-input v-model="showDeptName" :placeholder="placeholder" @click="deptDialog">
|
||||||
|
<template v-slot:append>
|
||||||
|
<el-button icon="search" @click="deptDialog"></el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-dialog v-model="visibleDept" width="30%" :modal="false" :title="placeholder" :close-on-click-modal="false" :close-on-press-escape="false">
|
||||||
|
<el-form size="small" :inline="true">
|
||||||
|
<el-form-item label="关键字:">
|
||||||
|
<el-input v-model="filterText" :style="{ width: '150px' }"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="default">查询</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-tree class="filter-tree" :data="deptList" :default-expanded-keys="expandedKeys" :props="{ label: 'name', children: 'children' }" :expand-on-click-node="false" :filter-node-method="filterNode" :highlight-current="true" node-key="id" ref="treeRef"> </el-tree>
|
||||||
|
<template v-slot:footer>
|
||||||
|
<el-button type="default" @click="cancelHandle()">取消</el-button>
|
||||||
|
<el-button v-if="query" type="info" @click="clearHandle()">清除</el-button>
|
||||||
|
<el-button type="primary" @click="commitHandle()">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { nextTick, ref, watch } from "vue";
|
||||||
|
import { IObject } from "@/types/interface";
|
||||||
|
import baseService from "@/service/baseService";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
|
const filterText = ref("");
|
||||||
|
const visibleDept = ref(false);
|
||||||
|
const deptList = ref<any[]>([]);
|
||||||
|
const showDeptName = ref("");
|
||||||
|
const expandedKeys = ref<any[]>([]);
|
||||||
|
const treeRef = ref();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: String,
|
||||||
|
deptName: String,
|
||||||
|
query: Boolean,
|
||||||
|
placeholder: String
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => filterText.value,
|
||||||
|
(val) => {
|
||||||
|
treeRef.value.filter(val);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.deptName,
|
||||||
|
(val) => {
|
||||||
|
showDeptName.value = val as string;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const deptDialog = () => {
|
||||||
|
expandedKeys.value = [];
|
||||||
|
visibleDept.value = true;
|
||||||
|
getDeptList(props.modelValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterNode = (value: string, data: IObject) => {
|
||||||
|
if (!value) return true;
|
||||||
|
return data.name.indexOf(value) !== -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDeptList = (id?: string) => {
|
||||||
|
return baseService.get("/sys/dept/list").then((res) => {
|
||||||
|
deptList.value = res.data;
|
||||||
|
nextTick(() => {
|
||||||
|
if (id) {
|
||||||
|
treeRef.value.setCurrentKey(id);
|
||||||
|
expandedKeys.value = [id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelHandle = () => {
|
||||||
|
visibleDept.value = false;
|
||||||
|
deptList.value = [];
|
||||||
|
filterText.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "update:deptName"]);
|
||||||
|
|
||||||
|
const clearHandle = () => {
|
||||||
|
emit("update:modelValue", "");
|
||||||
|
emit("update:deptName", "");
|
||||||
|
showDeptName.value = "";
|
||||||
|
visibleDept.value = false;
|
||||||
|
deptList.value = [];
|
||||||
|
filterText.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitHandle = () => {
|
||||||
|
const node = treeRef.value.getCurrentNode();
|
||||||
|
if (!node) {
|
||||||
|
ElMessage.error("请选择部门");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit("update:modelValue", node.id);
|
||||||
|
emit("update:deptName", node.name);
|
||||||
|
showDeptName.value = node.name;
|
||||||
|
visibleDept.value = false;
|
||||||
|
deptList.value = [];
|
||||||
|
filterText.value = "";
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { withInstall } from "@/utils/utils";
|
||||||
|
import SysRadioGroup from "./src/sys-radio-group.vue";
|
||||||
|
|
||||||
|
export default withInstall(SysRadioGroup);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<el-radio-group v-model="value" @change="$emit('update:modelValue', $event)">
|
||||||
|
<el-radio :label="data.dictValue" v-for="data in dataList" :key="data.dictValue">{{ data.dictLabel }}</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { getDictDataList } from "@/utils/utils";
|
||||||
|
import { computed, defineComponent } from "vue";
|
||||||
|
import { useAppStore } from "@/store";
|
||||||
|
export default defineComponent({
|
||||||
|
name: "SysRadioGroup",
|
||||||
|
props: {
|
||||||
|
modelValue: [Number, String],
|
||||||
|
dictType: String
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const store = useAppStore();
|
||||||
|
return {
|
||||||
|
value: computed(() => `${props.modelValue}`),
|
||||||
|
dataList: getDictDataList(store.state.dicts, props.dictType)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { withInstall } from "@/utils/utils";
|
||||||
|
import SysRegionTree from "./src/sys-region-tree.vue";
|
||||||
|
|
||||||
|
SysRegionTree.name = "SysRegionTree";
|
||||||
|
export default withInstall(SysRegionTree);
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<div class="sys-region">
|
||||||
|
<el-input v-model="showName" :placeholder="placeholder" @click="treeDialog">
|
||||||
|
<template v-slot:append>
|
||||||
|
<el-button icon="search" @click="treeDialog"></el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-dialog v-model="visibleTree" width="360px" :modal="false" :title="placeholder" :close-on-click-modal="false" :close-on-press-escape="false">
|
||||||
|
<el-form size="small" :inline="true">
|
||||||
|
<el-form-item label="关键字">
|
||||||
|
<el-input v-model="filterText" :style="{ width: '150px' }"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="default">查询</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-tree class="filter-tree" :data="dataList" :default-expanded-keys="expandedKeys" :props="{ label: 'name', children: 'children' }" :expand-on-click-node="false" :filter-node-method="filterNode" :highlight-current="true" node-key="id" ref="treeRef"> </el-tree>
|
||||||
|
<template v-slot:footer>
|
||||||
|
<el-button type="default" @click="cancelHandle()">取消</el-button>
|
||||||
|
<el-button type="info" @click="clearHandle()">清除</el-button>
|
||||||
|
<el-button type="primary" @click="commitHandle()">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { nextTick, ref, watch } from "vue";
|
||||||
|
import { treeDataTranslate } from "@/utils/utils";
|
||||||
|
import { IObject } from "@/types/interface";
|
||||||
|
import baseService from "@/service/baseService";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
|
const filterText = ref("");
|
||||||
|
const visibleTree = ref(false);
|
||||||
|
const dataList = ref<any[]>([]);
|
||||||
|
const showName = ref("");
|
||||||
|
const expandedKeys = ref<any[]>([]);
|
||||||
|
const treeRef = ref();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: [Number, String],
|
||||||
|
parentName: String,
|
||||||
|
placeholder: String
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => filterText.value,
|
||||||
|
(val) => {
|
||||||
|
treeRef.value.filter(val);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.parentName,
|
||||||
|
(val) => {
|
||||||
|
showName.value = val as string;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const treeDialog = () => {
|
||||||
|
expandedKeys.value = [];
|
||||||
|
if (treeRef.value) {
|
||||||
|
treeRef.value.setCurrentKey(null);
|
||||||
|
}
|
||||||
|
visibleTree.value = true;
|
||||||
|
getDataList(props.modelValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterNode = (value: string, data: IObject) => {
|
||||||
|
if (!value) return true;
|
||||||
|
return data.name.indexOf(value) !== -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDataList = (id: any) => {
|
||||||
|
return baseService.get("/sys/region/tree").then((res) => {
|
||||||
|
dataList.value = treeDataTranslate(res.data);
|
||||||
|
nextTick(() => {
|
||||||
|
treeRef.value.setCurrentKey(id);
|
||||||
|
expandedKeys.value = [id];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelHandle = () => {
|
||||||
|
visibleTree.value = false;
|
||||||
|
dataList.value = [];
|
||||||
|
filterText.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "update:parentName"]);
|
||||||
|
|
||||||
|
const clearHandle = () => {
|
||||||
|
emit("update:modelValue", "0");
|
||||||
|
emit("update:parentName", "");
|
||||||
|
showName.value = "";
|
||||||
|
visibleTree.value = false;
|
||||||
|
dataList.value = [];
|
||||||
|
filterText.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitHandle = () => {
|
||||||
|
const node = treeRef.value.getCurrentNode();
|
||||||
|
if (!node) {
|
||||||
|
ElMessage.error("请选择");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit("update:modelValue", node.id);
|
||||||
|
emit("update:parentName", node.name);
|
||||||
|
|
||||||
|
showName.value = node.name;
|
||||||
|
visibleTree.value = false;
|
||||||
|
dataList.value = [];
|
||||||
|
filterText.value = "";
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.sys-region {
|
||||||
|
.filter-tree {
|
||||||
|
max-height: 230px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 0 0 0 20px;
|
||||||
|
}
|
||||||
|
.el-dialog__footer {
|
||||||
|
padding: 10px 20px 8px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { withInstall } from "@/utils/utils";
|
||||||
|
import SysSelect from "./src/sys-select.vue";
|
||||||
|
|
||||||
|
export default withInstall(SysSelect);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<el-select v-model="value" @change="$emit('update:modelValue', $event)" :placeholder="placeholder" clearable>
|
||||||
|
<el-option :label="data.dictLabel" v-for="data in dataList" :key="data.dictValue" :value="data.dictValue">{{ data.dictLabel }}</el-option>
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent } from "vue";
|
||||||
|
import { getDictDataList } from "@/utils/utils";
|
||||||
|
import { useAppStore } from "@/store";
|
||||||
|
export default defineComponent({
|
||||||
|
name: "SysSelect",
|
||||||
|
props: {
|
||||||
|
modelValue: [Number, String],
|
||||||
|
dictType: String,
|
||||||
|
placeholder: String
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const store = useAppStore();
|
||||||
|
return {
|
||||||
|
value: computed(() => `${props.modelValue}`),
|
||||||
|
dataList: getDictDataList(store.state.dicts, props.dictType)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div style="border: 1px solid #ccc; z-index: 100">
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<Toolbar :editor="editorRef" :mode="mode" style="border-bottom: 1px solid #ccc" />
|
||||||
|
<!-- 编辑器 -->
|
||||||
|
<Editor :model-value="modelValue" :style="style" :disabled="disabled" :default-config="editorConfig" :mode="mode" @onCreated="handleCreated" @onChange="handleChange" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import "@wangeditor/editor/dist/css/style.css";
|
||||||
|
import { onBeforeUnmount, shallowRef } from "vue";
|
||||||
|
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
|
||||||
|
import { IDomEditor, IEditorConfig } from "@wangeditor/editor";
|
||||||
|
import app from "@/constants/app";
|
||||||
|
import { getToken } from "@/utils/cache";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: "default" // 可选值:[default | simple]
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
type: String,
|
||||||
|
default: "height: 300px;"
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 编辑器实例,必须用 shallowRef
|
||||||
|
const editorRef = shallowRef();
|
||||||
|
|
||||||
|
type InsertFnType = (url: string, alt: string, href: string) => void;
|
||||||
|
|
||||||
|
// 编辑器配置
|
||||||
|
const editorConfig: Partial<IEditorConfig> = {
|
||||||
|
placeholder: props.placeholder,
|
||||||
|
readOnly: props.disabled,
|
||||||
|
MENU_CONF: {
|
||||||
|
uploadImage: {
|
||||||
|
server: `${app.api}/sys/oss/upload?token=${getToken()}`, // 上传地址
|
||||||
|
fieldName: "file",
|
||||||
|
// 自定义插入图片
|
||||||
|
customInsert(res: any, insertFn: InsertFnType) {
|
||||||
|
// res 即服务端的返回结果
|
||||||
|
// 从 res 中找到 url alt href ,然后插图图片
|
||||||
|
insertFn(res.data.src, "", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件销毁时,也及时销毁编辑器
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
const editor = editorRef.value;
|
||||||
|
if (editor == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editor.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCreated = (editor: IDomEditor) => {
|
||||||
|
editorRef.value = editor;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑器change事件触发
|
||||||
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
|
const handleChange = (editor: IDomEditor) => {
|
||||||
|
emit("update:modelValue", editor.getHtml());
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
// composables/useAlertMarquee.ts
|
||||||
|
// 紧急通知状态管理 — 模块级单例
|
||||||
|
// 管理:消息队列 / 通知中心抽屉 / 详情弹窗 / 悬浮滚动条位置
|
||||||
|
// 后续接入后端时,只需修改 mockAlerts() 为真实 API 调用
|
||||||
|
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
|
||||||
|
export type AlertLevel = "info" | "warning" | "danger";
|
||||||
|
|
||||||
|
export interface AlertMessage {
|
||||||
|
id: string;
|
||||||
|
level: AlertLevel;
|
||||||
|
content: string;
|
||||||
|
publishTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- sessionStorage 辅助 ----------
|
||||||
|
|
||||||
|
const SEEN_KEY = "alert_last_seen_id";
|
||||||
|
|
||||||
|
function getLastSeenId(): string {
|
||||||
|
try {
|
||||||
|
return sessionStorage.getItem(SEEN_KEY) || "";
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLastSeenId(id: string): void {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(SEEN_KEY, id);
|
||||||
|
} catch {
|
||||||
|
// 静默失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 模块级单例状态 ----------
|
||||||
|
|
||||||
|
/** 通知队列(最新在末尾) */
|
||||||
|
const messages = ref<AlertMessage[]>([]);
|
||||||
|
|
||||||
|
/** 通知中心抽屉是否打开 */
|
||||||
|
const drawerVisible = ref(false);
|
||||||
|
|
||||||
|
/** 当前查看详情的消息(null = 详情弹窗关闭) */
|
||||||
|
const detailTarget = ref<AlertMessage | null>(null);
|
||||||
|
|
||||||
|
/** 悬浮滚动条拖拽位置 */
|
||||||
|
const position = ref<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
/** 队列上限 */
|
||||||
|
const MAX_QUEUE = 50;
|
||||||
|
|
||||||
|
// ---------- 计算属性 ----------
|
||||||
|
|
||||||
|
/** 最新一条通知 */
|
||||||
|
const latestMessage = computed(() => {
|
||||||
|
return messages.value.length > 0
|
||||||
|
? messages.value[messages.value.length - 1]
|
||||||
|
: null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 通知数量 */
|
||||||
|
const messageCount = computed(() => messages.value.length);
|
||||||
|
|
||||||
|
/** 是否有通知 */
|
||||||
|
const hasMessages = computed(() => messages.value.length > 0);
|
||||||
|
|
||||||
|
/** 详情弹窗是否可见 */
|
||||||
|
const detailVisible = computed({
|
||||||
|
get: () => detailTarget.value !== null,
|
||||||
|
set: (v: boolean) => {
|
||||||
|
if (!v) detailTarget.value = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 按时间倒序排列的消息(最新在前,给通知中心和详情列表用) */
|
||||||
|
const messagesReversed = computed(() => [...messages.value].reverse());
|
||||||
|
|
||||||
|
// ---------- Mock 数据 ----------
|
||||||
|
|
||||||
|
let mockIdCounter = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成模拟气象预警通知
|
||||||
|
* 后续接入后端时,替换为 baseService.get('/sys/alert/active')
|
||||||
|
*/
|
||||||
|
async function mockAlerts(): Promise<AlertMessage[]> {
|
||||||
|
mockIdCounter++;
|
||||||
|
const now = new Date();
|
||||||
|
const ts = now.toLocaleTimeString("zh-CN", { hour12: false });
|
||||||
|
|
||||||
|
const templates: Omit<AlertMessage, "id" | "publishTime">[] = [
|
||||||
|
{
|
||||||
|
level: "danger",
|
||||||
|
content: `【暴雨红色预警】省气象台${now.getHours()}时发布:预计未来6小时内将有特大暴雨,降雨量可达200毫米以上,请做好防汛准备。`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: "warning",
|
||||||
|
content: `【台风蓝色预警】第${mockIdCounter}号台风"海燕"正在向东南沿海靠近,中心风力12级,请沿海地区密切关注。`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: "info",
|
||||||
|
content: `【高温橙色预警】预计未来三天最高气温将达38℃以上,请做好防暑降温工作,避免高温时段户外作业。`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: "danger",
|
||||||
|
content: `【山洪灾害红色预警】受持续降雨影响,东部山区发生山洪灾害风险极高,请立即转移安置危险区域群众。`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: "warning",
|
||||||
|
content: `【大风黄色预警】受冷空气影响,预计今夜至明天将有8-10级大风,伴有沙尘天气,请注意防风防沙。`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: "info",
|
||||||
|
content: `【系统维护通知】计划于${now.getMonth() + 1}月${now.getDate() + 2}日凌晨2:00-4:00进行数据系统例行维护,届时查询功能可能短暂中断。`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: "warning",
|
||||||
|
content: `【寒潮蓝色预警】北方强冷空气南下,预计48小时内气温将下降12-14℃,最低气温可达-10℃,请注意防寒保暖。`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: "info",
|
||||||
|
content: `【雷电黄色预警】预计今天下午将出现雷电活动,并伴有短时强降水和短时大风,请注意防范。`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 随机选取 1-3 条
|
||||||
|
const count = 1 + Math.floor(Math.random() * 3);
|
||||||
|
const shuffled = [...templates].sort(() => Math.random() - 0.5).slice(0, count);
|
||||||
|
|
||||||
|
return shuffled.map((t, i) => ({
|
||||||
|
...t,
|
||||||
|
id: `alert_${now.getTime()}_${mockIdCounter}_${i}`,
|
||||||
|
publishTime: ts
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 轮询控制 ----------
|
||||||
|
|
||||||
|
let pollingTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let started = false;
|
||||||
|
|
||||||
|
// ---------- API ----------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉取通知。当前使用 mock 数据,后续改为:
|
||||||
|
* const res = await baseService.get('/sys/alert/active')
|
||||||
|
* return res.data ?? []
|
||||||
|
*/
|
||||||
|
async function fetchNotifications(): Promise<AlertMessage[]> {
|
||||||
|
// TODO: 替换为真实 API 调用
|
||||||
|
// import baseService from "@/service/baseService";
|
||||||
|
// const res = await baseService.get("/sys/alert/active");
|
||||||
|
// return res.code === 0 ? (res.data as AlertMessage[]) : [];
|
||||||
|
return mockAlerts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 检测并处理新通知 */
|
||||||
|
function checkNewAlerts(newMsgs: AlertMessage[]): void {
|
||||||
|
if (newMsgs.length === 0) return;
|
||||||
|
|
||||||
|
// 合并去重
|
||||||
|
const existingIds = new Set(messages.value.map((m) => m.id));
|
||||||
|
for (const msg of newMsgs) {
|
||||||
|
if (!existingIds.has(msg.id)) {
|
||||||
|
messages.value.push(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIFO 截断
|
||||||
|
if (messages.value.length > MAX_QUEUE) {
|
||||||
|
messages.value = messages.value.slice(messages.value.length - MAX_QUEUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有新通知 → 只更新队列,不主动展开侧栏
|
||||||
|
// 用户通过顶栏铃铛手动打开通知中心
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 通知中心抽屉 ----------
|
||||||
|
|
||||||
|
function toggleDrawer(): void {
|
||||||
|
drawerVisible.value = !drawerVisible.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDrawer(): void {
|
||||||
|
drawerVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDrawer(): void {
|
||||||
|
drawerVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 详情弹窗 ----------
|
||||||
|
|
||||||
|
function showDetail(msg: AlertMessage): void {
|
||||||
|
detailTarget.value = msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDetail(): void {
|
||||||
|
detailTarget.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 消息操作 ----------
|
||||||
|
|
||||||
|
/** 外部推送一条警告 */
|
||||||
|
function pushAlert(msg: AlertMessage): void {
|
||||||
|
const existingIds = new Set(messages.value.map((m) => m.id));
|
||||||
|
if (!existingIds.has(msg.id)) {
|
||||||
|
messages.value.push(msg);
|
||||||
|
}
|
||||||
|
if (messages.value.length > MAX_QUEUE) {
|
||||||
|
messages.value = messages.value.slice(messages.value.length - MAX_QUEUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除单条通知 */
|
||||||
|
function removeAlert(id: string): void {
|
||||||
|
// 如果正在查看的详情就是这条,先关闭弹窗
|
||||||
|
if (detailTarget.value?.id === id) {
|
||||||
|
detailTarget.value = null;
|
||||||
|
}
|
||||||
|
messages.value = messages.value.filter((m) => m.id !== id);
|
||||||
|
if (messages.value.length === 0) {
|
||||||
|
drawerVisible.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清空全部通知 */
|
||||||
|
function clearAll(): void {
|
||||||
|
if (messages.value.length > 0) {
|
||||||
|
const maxId = messages.value.reduce(
|
||||||
|
(max, m) => (m.id > max ? m.id : max),
|
||||||
|
messages.value[0].id
|
||||||
|
);
|
||||||
|
setLastSeenId(maxId);
|
||||||
|
}
|
||||||
|
messages.value = [];
|
||||||
|
drawerVisible.value = false;
|
||||||
|
detailTarget.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设置拖拽位置 */
|
||||||
|
function setPosition(pos: { x: number; y: number }): void {
|
||||||
|
position.value = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 轮询 ----------
|
||||||
|
|
||||||
|
async function startPolling(intervalMs = 30000): Promise<void> {
|
||||||
|
if (started) return;
|
||||||
|
started = true;
|
||||||
|
|
||||||
|
// 立即拉取一次
|
||||||
|
try {
|
||||||
|
const data = await fetchNotifications();
|
||||||
|
checkNewAlerts(data);
|
||||||
|
} catch {
|
||||||
|
// 静默失败
|
||||||
|
}
|
||||||
|
|
||||||
|
pollingTimer = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const data = await fetchNotifications();
|
||||||
|
checkNewAlerts(data);
|
||||||
|
} catch {
|
||||||
|
// 静默失败
|
||||||
|
}
|
||||||
|
}, intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPolling(): void {
|
||||||
|
if (pollingTimer !== null) {
|
||||||
|
clearInterval(pollingTimer);
|
||||||
|
pollingTimer = null;
|
||||||
|
}
|
||||||
|
started = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- composable 导出 ----------
|
||||||
|
|
||||||
|
export function useAlertMarquee() {
|
||||||
|
return {
|
||||||
|
// 消息
|
||||||
|
messages,
|
||||||
|
messagesReversed,
|
||||||
|
latestMessage,
|
||||||
|
messageCount,
|
||||||
|
hasMessages,
|
||||||
|
// 通知中心
|
||||||
|
drawerVisible,
|
||||||
|
toggleDrawer,
|
||||||
|
openDrawer,
|
||||||
|
closeDrawer,
|
||||||
|
// 详情
|
||||||
|
detailTarget,
|
||||||
|
detailVisible,
|
||||||
|
showDetail,
|
||||||
|
closeDetail,
|
||||||
|
// 滚动条位置
|
||||||
|
position,
|
||||||
|
setPosition,
|
||||||
|
// 操作
|
||||||
|
pushAlert,
|
||||||
|
removeAlert,
|
||||||
|
clearAll,
|
||||||
|
// 轮询
|
||||||
|
fetchNotifications,
|
||||||
|
checkNewAlerts,
|
||||||
|
startPolling,
|
||||||
|
stopPolling
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// composables/useFloatingDrag.ts
|
||||||
|
// 浮窗拖动逻辑 — Pointer Events 统一处理鼠标和触屏
|
||||||
|
|
||||||
|
import { ref, type Ref } from "vue";
|
||||||
|
|
||||||
|
export interface DragPosition {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFloatingDrag(
|
||||||
|
position: Ref<DragPosition>,
|
||||||
|
elementRef: Ref<HTMLElement | null>
|
||||||
|
) {
|
||||||
|
const isDragging = ref(false);
|
||||||
|
const dragMoved = ref(false);
|
||||||
|
let dragResetTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
/** pointerdown 时记录的元素尺寸和指针在元素内的偏移 */
|
||||||
|
let dragMeta: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
offsetX: number;
|
||||||
|
offsetY: number;
|
||||||
|
hasMoved: boolean;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
|
function onPointerDown(e: PointerEvent): void {
|
||||||
|
// 只响应主按键(左键 / 单指触屏)
|
||||||
|
if (e.button !== 0 && e.pointerType === "mouse") return;
|
||||||
|
|
||||||
|
const el = elementRef.value;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
|
||||||
|
dragMeta = {
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
offsetX: e.clientX - rect.left,
|
||||||
|
offsetY: e.clientY - rect.top,
|
||||||
|
hasMoved: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// 先 capture 指针以保证可靠追踪,但先不标记为拖拽中
|
||||||
|
// isDragging 推迟到 pointermove 超过阈值才设置,避免点击按钮时闪一下 grab 光标
|
||||||
|
el.setPointerCapture(e.pointerId);
|
||||||
|
|
||||||
|
el.addEventListener("pointermove", onPointerMove);
|
||||||
|
el.addEventListener("pointerup", onPointerUp);
|
||||||
|
el.addEventListener("pointercancel", onPointerUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerMove(e: PointerEvent): void {
|
||||||
|
if (!dragMeta) return;
|
||||||
|
|
||||||
|
const vw = window.innerWidth;
|
||||||
|
const vh = window.innerHeight;
|
||||||
|
|
||||||
|
let newX = e.clientX - dragMeta.offsetX;
|
||||||
|
let newY = e.clientY - dragMeta.offsetY;
|
||||||
|
|
||||||
|
// 边界钳制
|
||||||
|
newX = Math.max(0, Math.min(newX, vw - dragMeta.width));
|
||||||
|
newY = Math.max(0, Math.min(newY, vh - dragMeta.height));
|
||||||
|
|
||||||
|
// 移动超过 3px 才标记为拖拽(避免误触)
|
||||||
|
if (!dragMeta.hasMoved && (Math.abs(newX - position.value.x) > 3 || Math.abs(newY - position.value.y) > 3)) {
|
||||||
|
dragMeta.hasMoved = true;
|
||||||
|
isDragging.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dragMeta.hasMoved) {
|
||||||
|
position.value = { x: newX, y: newY };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerUp(_e: PointerEvent): void {
|
||||||
|
const didMove = dragMeta?.hasMoved ?? false;
|
||||||
|
|
||||||
|
const el = elementRef.value;
|
||||||
|
if (el) {
|
||||||
|
el.removeEventListener("pointermove", onPointerMove);
|
||||||
|
el.removeEventListener("pointerup", onPointerUp);
|
||||||
|
el.removeEventListener("pointercancel", onPointerUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
isDragging.value = false;
|
||||||
|
|
||||||
|
// Pointer up 先于 click 触发,用 dragMoved 标记本次是否为拖拽,
|
||||||
|
// click 处理器可通过 dragMoved 跳过拖拽后的误触展开
|
||||||
|
if (didMove) {
|
||||||
|
dragMoved.value = true;
|
||||||
|
if (dragResetTimer) clearTimeout(dragResetTimer);
|
||||||
|
dragResetTimer = setTimeout(() => {
|
||||||
|
dragMoved.value = false;
|
||||||
|
dragResetTimer = null;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
dragMeta = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDragging,
|
||||||
|
dragMoved,
|
||||||
|
onPointerDown
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
// composables/useWeatherChart.ts
|
||||||
|
import { ref, computed, watch, onBeforeUnmount, type Ref, type ShallowRef, shallowRef } from "vue";
|
||||||
|
import type { WeatherDataRow } from "./useWeatherStats";
|
||||||
|
import { rainLevelLabel } from "./useWeatherConstants";
|
||||||
|
|
||||||
|
// ---------- 类型 ----------
|
||||||
|
|
||||||
|
export interface FilterExtremes {
|
||||||
|
maxRainYear: number | null;
|
||||||
|
maxRain: number | null;
|
||||||
|
maxTmaxYear: number | null;
|
||||||
|
maxTmax: number | null;
|
||||||
|
minTminYear: number | null;
|
||||||
|
minTmin: number | null;
|
||||||
|
maxTavgYear?: number;
|
||||||
|
maxTavg?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 图表触发键(精确 diff,替代 deep watch) ----------
|
||||||
|
|
||||||
|
export interface ChartTriggerKey {
|
||||||
|
dataHash: number; // weatherData.length 即可作为简易 hash
|
||||||
|
filteredHash: number; // filteredRows.length
|
||||||
|
showOnlyFiltered: boolean;
|
||||||
|
hasActiveFilter: boolean;
|
||||||
|
extremesVersion: number; // filterExtremes 变更时 +1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Composable ----------
|
||||||
|
|
||||||
|
export function useWeatherChart(
|
||||||
|
weatherData: Ref<WeatherDataRow[]>,
|
||||||
|
filteredRows: Ref<WeatherDataRow[]>,
|
||||||
|
showOnlyFiltered: Ref<boolean>,
|
||||||
|
hasActiveFilter: Ref<boolean>,
|
||||||
|
filterExtremes: Ref<FilterExtremes | null>,
|
||||||
|
anchorMonth: Ref<number>,
|
||||||
|
anchorDay: Ref<number>
|
||||||
|
) {
|
||||||
|
const chartRef = ref<HTMLElement | null>(null);
|
||||||
|
const chartInst: ShallowRef<any> = shallowRef(null);
|
||||||
|
let chartResizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
|
// ---------- 精确触发键(替代 deep watch) ----------
|
||||||
|
|
||||||
|
const chartTrigger = computed<ChartTriggerKey>(() => ({
|
||||||
|
dataHash: weatherData.value.length,
|
||||||
|
filteredHash: filteredRows.value.length,
|
||||||
|
showOnlyFiltered: showOnlyFiltered.value,
|
||||||
|
hasActiveFilter: hasActiveFilter.value,
|
||||||
|
extremesVersion: filterExtremes.value
|
||||||
|
? filterExtremes.value.maxRainYear +
|
||||||
|
filterExtremes.value.maxTmaxYear +
|
||||||
|
filterExtremes.value.minTminYear
|
||||||
|
: 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ---------- 构建 ECharts 配置 ----------
|
||||||
|
|
||||||
|
const chartOptions = computed(() => {
|
||||||
|
const month = anchorMonth.value;
|
||||||
|
const day = anchorDay.value;
|
||||||
|
const hasFilter = hasActiveFilter.value;
|
||||||
|
const extremes = filterExtremes.value;
|
||||||
|
|
||||||
|
// 源数据(倒序,使图表从左到右为时间递增)
|
||||||
|
let src = [...weatherData.value].reverse();
|
||||||
|
const matchSet = new Set(filteredRows.value.map((r) => r.year));
|
||||||
|
|
||||||
|
if (showOnlyFiltered.value && hasFilter) {
|
||||||
|
src = src.filter((d) => matchSet.has(d.year));
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveMatchSet =
|
||||||
|
showOnlyFiltered.value && hasFilter ? new Set(src.map((d) => d.year)) : matchSet;
|
||||||
|
|
||||||
|
return buildChartOption(src, effectiveMatchSet, extremes, hasFilter, month, day);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------- 渲染 ----------
|
||||||
|
|
||||||
|
let renderTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
function scheduleRender() {
|
||||||
|
if (renderTimer) clearTimeout(renderTimer);
|
||||||
|
renderTimer = setTimeout(() => {
|
||||||
|
renderChart();
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderChart() {
|
||||||
|
if (!chartRef.value) return;
|
||||||
|
|
||||||
|
// 动态按需加载 ECharts
|
||||||
|
const echarts = await import("echarts");
|
||||||
|
const mod = (echarts as any).default || echarts;
|
||||||
|
|
||||||
|
if (chartInst.value) {
|
||||||
|
chartInst.value.setOption(chartOptions.value, true);
|
||||||
|
} else {
|
||||||
|
chartInst.value = mod.init(chartRef.value);
|
||||||
|
chartInst.value.setOption(chartOptions.value, true);
|
||||||
|
|
||||||
|
chartResizeObserver = new ResizeObserver(() => {
|
||||||
|
chartInst.value?.resize();
|
||||||
|
});
|
||||||
|
chartResizeObserver.observe(chartRef.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyChart() {
|
||||||
|
if (renderTimer) clearTimeout(renderTimer);
|
||||||
|
chartResizeObserver?.disconnect();
|
||||||
|
chartResizeObserver = null;
|
||||||
|
chartInst.value?.dispose();
|
||||||
|
chartInst.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 监听触发键变更 ----------
|
||||||
|
|
||||||
|
watch(chartTrigger, () => {
|
||||||
|
scheduleRender();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
destroyChart();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
chartRef,
|
||||||
|
chartOptions,
|
||||||
|
renderChart,
|
||||||
|
destroyChart
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// buildChartOption(纯函数,零依赖)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function buildChartOption(
|
||||||
|
src: WeatherDataRow[],
|
||||||
|
matchedYears: Set<number>,
|
||||||
|
extremes: FilterExtremes | null | undefined,
|
||||||
|
hasFilter: boolean,
|
||||||
|
month: number,
|
||||||
|
day: number
|
||||||
|
) {
|
||||||
|
const xData = src.map((d) => d.year);
|
||||||
|
|
||||||
|
const isHit = (d: WeatherDataRow) => !hasFilter || matchedYears.has(d.year);
|
||||||
|
|
||||||
|
const gradHit = {
|
||||||
|
type: "linear" as const,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{ offset: 0, color: "#60a5fa" },
|
||||||
|
{ offset: 1, color: "#2563eb" }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const markPoints: any[] = [];
|
||||||
|
const markPointsTmax: any[] = [];
|
||||||
|
const markPointsTmin: any[] = [];
|
||||||
|
const markPointsTavg: any[] = [];
|
||||||
|
|
||||||
|
if (hasFilter && extremes) {
|
||||||
|
addExtremeMarkers(src, extremes, markPoints, markPointsTmax, markPointsTmin, markPointsTavg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const markLines: any[] = [];
|
||||||
|
if (hasFilter) {
|
||||||
|
for (let i = 0; i < src.length; i++) {
|
||||||
|
if (matchedYears.has(src[i].year)) {
|
||||||
|
markLines.push([
|
||||||
|
{ xAxis: i, yAxis: 0, symbol: "none" },
|
||||||
|
{
|
||||||
|
xAxis: i,
|
||||||
|
yAxis: "max",
|
||||||
|
symbol: "none",
|
||||||
|
lineStyle: { color: "rgba(16,185,129,0.22)", width: 2, type: "dashed" as const },
|
||||||
|
label: { show: false }
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rainData = src.map((d) => ({
|
||||||
|
value: d.rainfall,
|
||||||
|
itemStyle: {
|
||||||
|
color: isHit(d) ? gradHit : "rgba(148,163,184,0.18)",
|
||||||
|
borderRadius: [3, 3, 0, 0]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mkSeries = (vals: number[], hitColor: string, dimColor: string, hitSize = 6, dimSize = 2) =>
|
||||||
|
src.map((d, i) => ({
|
||||||
|
value: vals[i],
|
||||||
|
symbolSize: isHit(d) ? hitSize : dimSize,
|
||||||
|
itemStyle: { color: isHit(d) ? hitColor : dimColor }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const dateLabel = `${month}月${String(day).padStart(2, "0")}日`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
tooltip: {
|
||||||
|
trigger: "axis" as const,
|
||||||
|
backgroundColor: "rgba(255,255,255,.96)",
|
||||||
|
borderColor: "#e2e8f0",
|
||||||
|
textStyle: { fontSize: 13 },
|
||||||
|
formatter(ps: any[]) {
|
||||||
|
const yr = ps[0]?.axisValue;
|
||||||
|
const hit = !hasFilter || matchedYears.has(yr);
|
||||||
|
const isExtreme =
|
||||||
|
extremes &&
|
||||||
|
(yr === extremes.maxRainYear ||
|
||||||
|
yr === extremes.maxTmaxYear ||
|
||||||
|
yr === extremes.minTminYear ||
|
||||||
|
yr === extremes.maxTavgYear);
|
||||||
|
let h = `<div style="font-weight:700;margin-bottom:4px">${yr}年 ${dateLabel}`;
|
||||||
|
if (hit && hasFilter) h += " ✅";
|
||||||
|
if (isExtreme) h += ' <span style="color:#7c3aed;font-size:11px">⭐极值年份</span>';
|
||||||
|
h += "</div>";
|
||||||
|
ps.forEach((p: any) => {
|
||||||
|
h += `${p.marker} ${p.seriesName}: <b>${p.value}</b><br/>`;
|
||||||
|
});
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: { bottom: 0, textStyle: { fontSize: 12 } },
|
||||||
|
grid: { top: "12%", bottom: "14%", left: "6%", right: "6%" },
|
||||||
|
xAxis: {
|
||||||
|
type: "category" as const,
|
||||||
|
data: xData,
|
||||||
|
axisLabel: { rotate: xData.length > 25 ? 45 : 0, fontSize: 11 }
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: "value" as const,
|
||||||
|
name: "降雨(mm)",
|
||||||
|
nameTextStyle: { color: "#2563eb", fontWeight: 600 as const },
|
||||||
|
splitLine: { lineStyle: { type: "dashed" as const, color: "#f1f5f9" } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "value" as const,
|
||||||
|
name: "气温(℃)",
|
||||||
|
position: "right" as const,
|
||||||
|
scale: true,
|
||||||
|
nameTextStyle: { color: "#ef4444", fontWeight: 600 as const },
|
||||||
|
splitLine: { show: false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "降雨量",
|
||||||
|
type: "bar" as const,
|
||||||
|
barMaxWidth: 20,
|
||||||
|
data: rainData,
|
||||||
|
markLine: { silent: true, symbol: ["none" as const, "none" as const], data: markLines },
|
||||||
|
markPoint: { data: markPoints }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "最高气温",
|
||||||
|
type: "line" as const,
|
||||||
|
yAxisIndex: 1,
|
||||||
|
smooth: true,
|
||||||
|
symbolSize: 0,
|
||||||
|
lineStyle: { width: 2.5, color: "#ef4444" },
|
||||||
|
data: mkSeries(
|
||||||
|
src.map((d) => d.tmax),
|
||||||
|
"#ef4444",
|
||||||
|
"rgba(239,68,68,0.12)"
|
||||||
|
),
|
||||||
|
markPoint: { data: markPointsTmax }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "最低气温",
|
||||||
|
type: "line" as const,
|
||||||
|
yAxisIndex: 1,
|
||||||
|
smooth: true,
|
||||||
|
symbolSize: 0,
|
||||||
|
lineStyle: { width: 2, color: "#3b82f6", type: "dashed" as const },
|
||||||
|
data: mkSeries(
|
||||||
|
src.map((d) => d.tmin),
|
||||||
|
"#3b82f6",
|
||||||
|
"rgba(59,130,246,0.12)",
|
||||||
|
5,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
markPoint: { data: markPointsTmin }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "平均气温",
|
||||||
|
type: "line" as const,
|
||||||
|
yAxisIndex: 1,
|
||||||
|
smooth: true,
|
||||||
|
symbolSize: 0,
|
||||||
|
lineStyle: { width: 2, color: "#f59e0b" },
|
||||||
|
data: mkSeries(
|
||||||
|
src.map((d) => d.tavg),
|
||||||
|
"#f59e0b",
|
||||||
|
"rgba(245,158,11,0.15)",
|
||||||
|
5,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
markPoint: { data: markPointsTavg }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 极值标记 ----------
|
||||||
|
|
||||||
|
function addExtremeMarkers(
|
||||||
|
src: WeatherDataRow[],
|
||||||
|
extremes: FilterExtremes,
|
||||||
|
rain: any[],
|
||||||
|
tmax: any[],
|
||||||
|
tmin: any[],
|
||||||
|
tavg: any[]
|
||||||
|
) {
|
||||||
|
if (extremes.maxRainYear != null && extremes.maxRain != null) {
|
||||||
|
const idxRain = src.findIndex((d) => d.year === extremes.maxRainYear);
|
||||||
|
if (idxRain >= 0) {
|
||||||
|
rain.push({
|
||||||
|
name: "最大降雨",
|
||||||
|
coord: [idxRain, extremes.maxRain],
|
||||||
|
value: `${extremes.maxRain}mm`,
|
||||||
|
symbol: "circle",
|
||||||
|
symbolSize: 8,
|
||||||
|
itemStyle: { color: "#7c3aed" },
|
||||||
|
label: {
|
||||||
|
position: "top",
|
||||||
|
offset: [0, -10],
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
backgroundColor: "#7c3aed",
|
||||||
|
padding: [4, 10],
|
||||||
|
borderRadius: 4
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extremes.maxTmaxYear != null && extremes.maxTmax != null) {
|
||||||
|
const idxTmax = src.findIndex((d) => d.year === extremes.maxTmaxYear);
|
||||||
|
if (idxTmax >= 0) {
|
||||||
|
tmax.push({
|
||||||
|
name: "最高温",
|
||||||
|
coord: [idxTmax, extremes.maxTmax],
|
||||||
|
value: `${extremes.maxTmax}℃`,
|
||||||
|
symbol: "circle",
|
||||||
|
symbolSize: 8,
|
||||||
|
itemStyle: { color: "#dc2626" },
|
||||||
|
label: {
|
||||||
|
position: "top",
|
||||||
|
offset: [0, -10],
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
backgroundColor: "#dc2626",
|
||||||
|
padding: [4, 10],
|
||||||
|
borderRadius: 4
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extremes.minTminYear != null && extremes.minTmin != null) {
|
||||||
|
const idxTmin = src.findIndex((d) => d.year === extremes.minTminYear);
|
||||||
|
if (idxTmin >= 0) {
|
||||||
|
tmin.push({
|
||||||
|
name: "最低温",
|
||||||
|
coord: [idxTmin, extremes.minTmin],
|
||||||
|
value: `${extremes.minTmin}℃`,
|
||||||
|
symbol: "circle",
|
||||||
|
symbolSize: 8,
|
||||||
|
itemStyle: { color: "#1d4ed8" },
|
||||||
|
label: {
|
||||||
|
position: "bottom",
|
||||||
|
offset: [0, 10],
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
backgroundColor: "#1d4ed8",
|
||||||
|
padding: [4, 10],
|
||||||
|
borderRadius: 4
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extremes.maxTavgYear != null && extremes.maxTavg != null) {
|
||||||
|
const idxTavg = src.findIndex((d) => d.year === extremes.maxTavgYear);
|
||||||
|
if (idxTavg >= 0) {
|
||||||
|
tavg.push({
|
||||||
|
name: "最高平均温",
|
||||||
|
coord: [idxTavg, extremes.maxTavg],
|
||||||
|
value: `${extremes.maxTavg}℃`,
|
||||||
|
symbol: "circle",
|
||||||
|
symbolSize: 8,
|
||||||
|
itemStyle: { color: "#f59e0b" },
|
||||||
|
label: {
|
||||||
|
position: "top",
|
||||||
|
offset: [0, -10],
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
backgroundColor: "#f59e0b",
|
||||||
|
padding: [4, 10],
|
||||||
|
borderRadius: 4
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// composables/useWeatherConstants.ts
|
||||||
|
// 集中管理气象页面的所有阈值、等级定义和筛选字段配置
|
||||||
|
|
||||||
|
/** 降雨等级定义(统一用于筛选、图例、分布计算) */
|
||||||
|
export const RAIN_LEVELS = [
|
||||||
|
{ label: "无雨", min: -Infinity, max: 0.1 },
|
||||||
|
{ label: "小雨", min: 0.1, max: 10 },
|
||||||
|
{ label: "中雨", min: 10, max: 25 },
|
||||||
|
{ label: "大雨", min: 25, max: 50 },
|
||||||
|
{ label: "暴雨", min: 50, max: 100 },
|
||||||
|
{ label: "大暴雨", min: 100, max: 250 },
|
||||||
|
{ label: "特大暴雨", min: 250, max: 9999 }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** 可筛选的降雨等级(排除"无雨",因为无雨通常单独判断) */
|
||||||
|
export const RAIN_FILTER_LEVELS = RAIN_LEVELS.filter((l) => l.label !== "无雨");
|
||||||
|
|
||||||
|
/** 格式化数值:null/undefined → "—",否则原样输出 */
|
||||||
|
export function fmtVal(v: number | null | undefined, fixed?: number): string {
|
||||||
|
if (v == null || isNaN(v)) return "—";
|
||||||
|
return fixed != null ? v.toFixed(fixed) : String(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据降雨量返回等级标签 */
|
||||||
|
export function rainLevelLabel(v: number | null | undefined | string): string {
|
||||||
|
if (v == null || v === "") return "—";
|
||||||
|
const n = Number(v);
|
||||||
|
if (isNaN(n)) return "—";
|
||||||
|
for (const level of RAIN_LEVELS) {
|
||||||
|
if (n >= level.min && n < level.max) return level.label;
|
||||||
|
}
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 温度阈值 ----------
|
||||||
|
|
||||||
|
export const TEMP_THRESHOLDS = {
|
||||||
|
tmax: { extreme: 38, danger: 35, warn: 30 },
|
||||||
|
tmin: { extreme: 0, cold: 5 }
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 最高温 CSS 类名(null 返回空字符串,不标色) */
|
||||||
|
export function tmaxValClass(v: number | null | undefined): string {
|
||||||
|
if (v == null || isNaN(v)) return "";
|
||||||
|
if (v >= TEMP_THRESHOLDS.tmax.extreme) return "val-extreme";
|
||||||
|
if (v >= TEMP_THRESHOLDS.tmax.danger) return "val-danger";
|
||||||
|
if (v >= TEMP_THRESHOLDS.tmax.warn) return "val-warn";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 最低温 CSS 类名 */
|
||||||
|
export function tminValClass(v: number | null | undefined): string {
|
||||||
|
if (v == null || isNaN(v)) return "";
|
||||||
|
if (v <= TEMP_THRESHOLDS.tmin.extreme) return "val-cold-x";
|
||||||
|
if (v <= TEMP_THRESHOLDS.tmin.cold) return "val-cold";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 降雨量 CSS 类名 */
|
||||||
|
export function rainValClass(v: number | null | undefined): string {
|
||||||
|
if (v == null || isNaN(v)) return "";
|
||||||
|
if (v >= 100) return "val-extreme";
|
||||||
|
if (v >= 50) return "val-danger";
|
||||||
|
if (v >= 25) return "val-warn";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 筛选字段定义 ----------
|
||||||
|
|
||||||
|
export interface TempFilterField {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
range: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TEMP_FILTER_FIELDS: TempFilterField[] = [
|
||||||
|
{ key: "tmaxF", label: "最高气温", range: true },
|
||||||
|
{ key: "tminF", label: "最低气温", range: true },
|
||||||
|
{ key: "tavgF", label: "平均气温", range: false }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FILTER_KEYS = ["rainfall", "tmaxF", "tminF", "tavgF"] as const;
|
||||||
|
|
||||||
|
// ---------- 其他常量 ----------
|
||||||
|
|
||||||
|
export const YEAR_PRESETS = [10, 20, 30, 50, 60];
|
||||||
|
export const PREVIEW_COUNT = 8;
|
||||||
|
export const MIN_YEAR = 1960;
|
||||||
|
export const MAX_YEAR_SPAN = 100;
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
// composables/useWeatherExport.ts
|
||||||
|
import { ref, nextTick, type Ref } from "vue";
|
||||||
|
import { ElMessage, ElLoading } from "element-plus";
|
||||||
|
|
||||||
|
export interface ExportOptions {
|
||||||
|
/** 导出前需要展开表格? */
|
||||||
|
expandTable: Ref<boolean>;
|
||||||
|
/** 导出前需要收起筛选面板? */
|
||||||
|
collapseFilter: Ref<boolean>;
|
||||||
|
/** 月/日锚点 */
|
||||||
|
anchorMonth: Ref<number> | number;
|
||||||
|
/** pad 后的日期字符串 */
|
||||||
|
padDay: Ref<string> | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出为 PNG 或 PDF。
|
||||||
|
* html2canvas + jspdf 均为动态 import,不生成为首屏包体积。
|
||||||
|
*/
|
||||||
|
export function useWeatherExport(rootRef: Ref<HTMLElement | null>, opts: ExportOptions) {
|
||||||
|
const exporting = ref(false);
|
||||||
|
|
||||||
|
const resolveDay = () => {
|
||||||
|
const m = typeof opts.anchorMonth === "number" ? opts.anchorMonth : opts.anchorMonth.value;
|
||||||
|
const d = typeof opts.padDay === "string" ? opts.padDay : opts.padDay.value;
|
||||||
|
return `${m}月${d}日`;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function doExport(type: "png" | "pdf") {
|
||||||
|
const el = rootRef.value;
|
||||||
|
if (!el || exporting.value) return;
|
||||||
|
|
||||||
|
exporting.value = true;
|
||||||
|
|
||||||
|
// 全屏 loading(大页面渲染可能需要数秒)
|
||||||
|
const loadingInstance = ElLoading.service({
|
||||||
|
fullscreen: true,
|
||||||
|
text: "正在渲染导出内容…",
|
||||||
|
background: "rgba(255,255,255,0.7)"
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 保存并临时修改 UI 状态
|
||||||
|
const prevTableExp = opts.expandTable.value;
|
||||||
|
const prevFilterOpen = opts.collapseFilter.value;
|
||||||
|
opts.expandTable.value = true;
|
||||||
|
opts.collapseFilter.value = false;
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
// 等待两帧确保 DOM 完全渲染
|
||||||
|
await new Promise<void>((r) => requestAnimationFrame(() => requestAnimationFrame(() => r())));
|
||||||
|
|
||||||
|
// 动态 import 重型库
|
||||||
|
const [{ default: html2canvas }, { default: jsPDF }] = await Promise.all([
|
||||||
|
import("html2canvas"),
|
||||||
|
import("jspdf")
|
||||||
|
]);
|
||||||
|
|
||||||
|
const canvas = await html2canvas(el, {
|
||||||
|
useCORS: true,
|
||||||
|
scale: 2,
|
||||||
|
backgroundColor: "#f1f5f9",
|
||||||
|
logging: false,
|
||||||
|
allowTaint: true,
|
||||||
|
ignoreElements: (e: Element) => {
|
||||||
|
if (e.classList?.contains("el-popper") || e.classList?.contains("el-popover"))
|
||||||
|
return true;
|
||||||
|
if (e.tagName === "STYLE" && e.textContent?.includes("v-popper")) return true;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
onclone: (clonedDoc: Document) => {
|
||||||
|
// 修复 Element Plus 图标在 canvas 中的显示
|
||||||
|
clonedDoc.querySelectorAll(".el-icon").forEach((icon) => {
|
||||||
|
if (!(icon as HTMLElement).querySelector("svg")) return;
|
||||||
|
(icon as HTMLElement).style.display = "inline-block";
|
||||||
|
(icon as HTMLElement).style.width = "14px";
|
||||||
|
(icon as HTMLElement).style.height = "14px";
|
||||||
|
});
|
||||||
|
// 确保 canvas 图表可见
|
||||||
|
clonedDoc.querySelectorAll("canvas").forEach((c) => {
|
||||||
|
(c as HTMLElement).style.display = "block";
|
||||||
|
});
|
||||||
|
// 表格内容不裁剪
|
||||||
|
clonedDoc.querySelectorAll(".el-table__body-wrapper").forEach((w) => {
|
||||||
|
(w as HTMLElement).style.overflow = "visible";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 恢复 UI 状态
|
||||||
|
opts.expandTable.value = prevTableExp;
|
||||||
|
opts.collapseFilter.value = prevFilterOpen;
|
||||||
|
|
||||||
|
const dateStr = resolveDay();
|
||||||
|
const ext = type === "png" ? "png" : "pdf";
|
||||||
|
const label = type === "png" ? "PNG 图片" : "PDF 文档";
|
||||||
|
|
||||||
|
if (type === "png") {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = canvas.toDataURL("image/png");
|
||||||
|
a.download = `气象回溯_${dateStr}.png`;
|
||||||
|
a.click();
|
||||||
|
} else {
|
||||||
|
const imgData = canvas.toDataURL("image/png");
|
||||||
|
const pdf = new jsPDF("p", "mm", "a4");
|
||||||
|
const w = pdf.internal.pageSize.getWidth();
|
||||||
|
const h = (canvas.height * w) / canvas.width;
|
||||||
|
const pageH = pdf.internal.pageSize.getHeight();
|
||||||
|
|
||||||
|
if (h <= pageH) {
|
||||||
|
pdf.addImage(imgData, "PNG", 0, 0, w, h);
|
||||||
|
} else {
|
||||||
|
let remaining = h;
|
||||||
|
let yOffset = 0;
|
||||||
|
while (remaining > 0) {
|
||||||
|
if (yOffset > 0) pdf.addPage();
|
||||||
|
pdf.addImage(imgData, "PNG", 0, -yOffset, w, h);
|
||||||
|
yOffset += pageH;
|
||||||
|
remaining -= pageH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pdf.save(`气象回溯_${dateStr}.pdf`);
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success(`${label}已导出 — 气象回溯_${dateStr}.${ext}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("导出失败:", err);
|
||||||
|
ElMessage.error("导出失败,请重试");
|
||||||
|
} finally {
|
||||||
|
loadingInstance.close();
|
||||||
|
exporting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { doExport, exporting };
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
// composables/useWeatherFilter.ts
|
||||||
|
import { ref, computed, type Ref } from "vue";
|
||||||
|
import { RAIN_FILTER_LEVELS, FILTER_KEYS, TEMP_FILTER_FIELDS, type TempFilterField } from "./useWeatherConstants";
|
||||||
|
import type { WeatherDataRow } from "./useWeatherStats";
|
||||||
|
|
||||||
|
// ---------- 类型 ----------
|
||||||
|
|
||||||
|
export interface NumericFilter {
|
||||||
|
op: "" | "gte" | "lte" | "range";
|
||||||
|
val: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeatherFilters {
|
||||||
|
rainfall: NumericFilter;
|
||||||
|
rainLevel: string[];
|
||||||
|
tmaxF: NumericFilter;
|
||||||
|
tminF: NumericFilter;
|
||||||
|
tavgF: NumericFilter & { range?: undefined }; // tavgF 不支持 range
|
||||||
|
yearRange: [number, number];
|
||||||
|
decades: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 工厂 ----------
|
||||||
|
|
||||||
|
export function defaultNumericFilter(overrides?: Partial<NumericFilter>): NumericFilter {
|
||||||
|
return { op: "", val: 0, min: 0, max: 999, ...overrides };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDefaultFilters(
|
||||||
|
startYear: number,
|
||||||
|
endYear: number
|
||||||
|
): WeatherFilters {
|
||||||
|
return {
|
||||||
|
rainfall: { op: "", val: 25, min: 0, max: 999 },
|
||||||
|
rainLevel: [],
|
||||||
|
tmaxF: { op: "", val: 35, min: 25, max: 40 },
|
||||||
|
tminF: { op: "", val: 5, min: 0, max: 15 },
|
||||||
|
tavgF: { op: "", val: 20, min: 0, max: 999 },
|
||||||
|
yearRange: [startYear, endYear],
|
||||||
|
decades: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 匹配逻辑 ----------
|
||||||
|
|
||||||
|
export function matchOp(val: number | null, cfg: NumericFilter | undefined): boolean {
|
||||||
|
if (!cfg || !cfg.op) return true;
|
||||||
|
// null 表示无数据,不参与任何数值筛选(既不匹配 ≥ 也不匹配 ≤)
|
||||||
|
if (val == null) return false;
|
||||||
|
const n = Number(val);
|
||||||
|
if (isNaN(n)) return false;
|
||||||
|
if (cfg.op === "gte") return n >= cfg.val;
|
||||||
|
if (cfg.op === "lte") return n <= cfg.val;
|
||||||
|
if (cfg.op === "range") return n >= (cfg.min ?? -Infinity) && n <= (cfg.max ?? Infinity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Composable ----------
|
||||||
|
|
||||||
|
export interface UseWeatherFilterOptions {
|
||||||
|
/** 当前数据起始年份(computed) */
|
||||||
|
startYear: Ref<number> | (() => number);
|
||||||
|
/** 当前数据结束年份(computed) */
|
||||||
|
endYear: Ref<number> | (() => number);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWeatherFilter(opts: UseWeatherFilterOptions) {
|
||||||
|
const filterOpen = ref(false);
|
||||||
|
const showOnlyFiltered = ref(false);
|
||||||
|
|
||||||
|
const resolveStart = () =>
|
||||||
|
typeof opts.startYear === "function" ? opts.startYear() : opts.startYear.value;
|
||||||
|
const resolveEnd = () =>
|
||||||
|
typeof opts.endYear === "function" ? opts.endYear() : opts.endYear.value;
|
||||||
|
|
||||||
|
// 初始化 filters
|
||||||
|
const filters = ref<WeatherFilters>(createDefaultFilters(resolveStart(), resolveEnd()));
|
||||||
|
|
||||||
|
/** 同步年份 range 到 filters(数据变更时调用) */
|
||||||
|
function syncYearRange(from: number, to: number) {
|
||||||
|
filters.value.yearRange = [from, to];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置筛选条件 */
|
||||||
|
function resetFilters() {
|
||||||
|
filters.value = createDefaultFilters(resolveStart(), resolveEnd());
|
||||||
|
showOnlyFiltered.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 年代 ----------
|
||||||
|
|
||||||
|
const availableDecades = computed(() => {
|
||||||
|
const s = new Set<number>();
|
||||||
|
for (let y = resolveStart(); y <= resolveEnd(); y++) {
|
||||||
|
s.add(Math.floor(y / 10) * 10);
|
||||||
|
}
|
||||||
|
return Array.from(s).sort();
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleDecade(d: number) {
|
||||||
|
const i = filters.value.decades.indexOf(d);
|
||||||
|
if (i >= 0) {
|
||||||
|
filters.value.decades.splice(i, 1);
|
||||||
|
} else {
|
||||||
|
filters.value.decades.push(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 雨量级别 ----------
|
||||||
|
|
||||||
|
function toggleRainLevel(lv: { label: string; min: number; max: number }) {
|
||||||
|
const idx = filters.value.rainLevel.indexOf(lv.label);
|
||||||
|
if (idx >= 0) {
|
||||||
|
filters.value.rainLevel.splice(idx, 1);
|
||||||
|
} else {
|
||||||
|
filters.value.rainLevel.push(lv.label);
|
||||||
|
}
|
||||||
|
syncRainfallFromLevels();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当选中的雨量级别变更后,自动同步 rain.range */
|
||||||
|
function syncRainfallFromLevels() {
|
||||||
|
const labels = filters.value.rainLevel;
|
||||||
|
if (labels.length === 0) {
|
||||||
|
filters.value.rainfall.op = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let gmin = Infinity;
|
||||||
|
let gmax = -Infinity;
|
||||||
|
for (const def of RAIN_FILTER_LEVELS) {
|
||||||
|
if (labels.includes(def.label)) {
|
||||||
|
if (def.min < gmin) gmin = def.min;
|
||||||
|
if (def.max > gmax) gmax = def.max;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filters.value.rainfall = { op: "range", min: gmin, max: gmax, val: gmax };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 筛选状态 ----------
|
||||||
|
|
||||||
|
const hasActiveFilter = computed(() => {
|
||||||
|
const f = filters.value;
|
||||||
|
return (
|
||||||
|
FILTER_KEYS.some((k) => f[k]?.op) ||
|
||||||
|
f.rainLevel.length > 0 ||
|
||||||
|
f.decades.length > 0 ||
|
||||||
|
f.yearRange[0] !== resolveStart() ||
|
||||||
|
f.yearRange[1] !== resolveEnd()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeFilterCount = computed(() => {
|
||||||
|
const f = filters.value;
|
||||||
|
let c = FILTER_KEYS.filter((k) => f[k]?.op).length;
|
||||||
|
if (f.rainLevel.length) c++;
|
||||||
|
if (f.decades.length) c++;
|
||||||
|
if (f.yearRange[0] !== resolveStart() || f.yearRange[1] !== resolveEnd()) c++;
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------- 行匹配 ----------
|
||||||
|
|
||||||
|
/** 判断单行是否命中筛选 */
|
||||||
|
function rowMatchesFilter(row: WeatherDataRow, f: WeatherFilters): boolean {
|
||||||
|
if (!matchOp(row.rainfall, f.rainfall)) return false;
|
||||||
|
if (!matchOp(row.tmax, f.tmaxF)) return false;
|
||||||
|
if (!matchOp(row.tmin, f.tminF)) return false;
|
||||||
|
if (!matchOp(row.tavg, f.tavgF)) return false;
|
||||||
|
if (row.year < f.yearRange[0] || row.year > f.yearRange[1]) return false;
|
||||||
|
if (f.decades.length > 0 && !f.decades.includes(Math.floor(row.year / 10) * 10)) return false;
|
||||||
|
if (f.rainLevel.length > 0 && !f.rainLevel.includes(row._rainLevel ?? "")) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 暴露导出 ----------
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
filterOpen,
|
||||||
|
showOnlyFiltered,
|
||||||
|
filters,
|
||||||
|
// 计算
|
||||||
|
availableDecades,
|
||||||
|
hasActiveFilter,
|
||||||
|
activeFilterCount,
|
||||||
|
// 方法
|
||||||
|
syncYearRange,
|
||||||
|
resetFilters,
|
||||||
|
toggleDecade,
|
||||||
|
toggleRainLevel,
|
||||||
|
rowMatchesFilter,
|
||||||
|
// 常量
|
||||||
|
TEMP_FILTER_FIELDS,
|
||||||
|
RAIN_FILTER_LEVELS
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
// composables/useWeatherStats.ts
|
||||||
|
import { computed, type Ref } from "vue";
|
||||||
|
import { RAIN_LEVELS, rainLevelLabel } from "./useWeatherConstants";
|
||||||
|
|
||||||
|
// ---------- 类型 ----------
|
||||||
|
|
||||||
|
export interface WeatherDataRow {
|
||||||
|
year: number;
|
||||||
|
rainfall: number | null;
|
||||||
|
tmax: number | null;
|
||||||
|
tavg: number | null;
|
||||||
|
tmin: number | null;
|
||||||
|
windSpeed: number | null;
|
||||||
|
windDirection: number | null;
|
||||||
|
humidity?: number;
|
||||||
|
pressure?: number;
|
||||||
|
// 运行时标记
|
||||||
|
_matched?: boolean;
|
||||||
|
_isMaxRain?: boolean;
|
||||||
|
_isMaxTmax?: boolean;
|
||||||
|
_isMinTmin?: boolean;
|
||||||
|
_rainLevel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeatherStats {
|
||||||
|
avgTmax: string;
|
||||||
|
avgTmaxYear: number | string;
|
||||||
|
avgTmin: string;
|
||||||
|
avgTminYear: number | string;
|
||||||
|
avgRain: string;
|
||||||
|
maxRain: number | string;
|
||||||
|
maxRainYear: number | string;
|
||||||
|
rainyCount: number;
|
||||||
|
rainyPct: string;
|
||||||
|
maxTmax: number | string;
|
||||||
|
maxTmaxYear: number | string;
|
||||||
|
minTmin: number | string;
|
||||||
|
minTminYear: number | string;
|
||||||
|
maxTavg?: number | string;
|
||||||
|
maxTavgYear?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatCard {
|
||||||
|
items: StatCardItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatCardItem {
|
||||||
|
label: string;
|
||||||
|
value: number | string;
|
||||||
|
unit: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SummaryMeta {
|
||||||
|
stationName: string;
|
||||||
|
fromYear: number;
|
||||||
|
toYear: number;
|
||||||
|
month: number;
|
||||||
|
day: number;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 纯函数 ----------
|
||||||
|
|
||||||
|
export function emptyStats(): WeatherStats {
|
||||||
|
return {
|
||||||
|
avgTmax: "-",
|
||||||
|
avgTmaxYear: "-",
|
||||||
|
avgTmin: "-",
|
||||||
|
avgTminYear: "-",
|
||||||
|
avgRain: "-",
|
||||||
|
maxRain: "-",
|
||||||
|
maxRainYear: "-",
|
||||||
|
rainyCount: 0,
|
||||||
|
rainyPct: "0",
|
||||||
|
maxTmax: "-",
|
||||||
|
maxTmaxYear: "-",
|
||||||
|
minTmin: "-",
|
||||||
|
minTminYear: "-"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** null-safe 数值比较:a > b,但 null 视为无数据(永不大于有效值) */
|
||||||
|
function gtNullSafe(a: number | null, b: number | null): boolean {
|
||||||
|
if (a == null) return false;
|
||||||
|
if (b == null) return true;
|
||||||
|
return a > b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** null-safe 数值比较:a < b,但 null 视为无数据(永不小于有效值) */
|
||||||
|
function ltNullSafe(a: number | null, b: number | null): boolean {
|
||||||
|
if (a == null) return false;
|
||||||
|
if (b == null) return true;
|
||||||
|
return a < b;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeStats(data: WeatherDataRow[]): WeatherStats {
|
||||||
|
if (!data.length) return emptyStats();
|
||||||
|
|
||||||
|
const result = data.reduce(
|
||||||
|
(acc, row) => ({
|
||||||
|
// 只有非 null 值才计入求和
|
||||||
|
sumTmax: row.tmax != null ? acc.sumTmax + row.tmax : acc.sumTmax,
|
||||||
|
sumTmin: row.tmin != null ? acc.sumTmin + row.tmin : acc.sumTmin,
|
||||||
|
sumRain: row.rainfall != null ? acc.sumRain + row.rainfall : acc.sumRain,
|
||||||
|
// count 仍然计所有行,但用于均值的分母需改为有效值计数
|
||||||
|
count: acc.count + 1,
|
||||||
|
validTmaxCount: acc.validTmaxCount + (row.tmax != null ? 1 : 0),
|
||||||
|
validTminCount: acc.validTminCount + (row.tmin != null ? 1 : 0),
|
||||||
|
validRainCount: acc.validRainCount + (row.rainfall != null ? 1 : 0),
|
||||||
|
rainyCount: acc.rainyCount + (row.rainfall != null && row.rainfall > 0.1 ? 1 : 0),
|
||||||
|
maxRainRow:
|
||||||
|
gtNullSafe(row.rainfall, acc.maxRainRow?.rainfall ?? null) ? row : acc.maxRainRow,
|
||||||
|
maxTmaxRow:
|
||||||
|
gtNullSafe(row.tmax, acc.maxTmaxRow?.tmax ?? null) ? row : acc.maxTmaxRow,
|
||||||
|
minTminRow:
|
||||||
|
ltNullSafe(row.tmin, acc.minTminRow?.tmin ?? null) ? row : acc.minTminRow,
|
||||||
|
maxTavgRow:
|
||||||
|
gtNullSafe(row.tavg, acc.maxTavgRow?.tavg ?? null) ? row : acc.maxTavgRow,
|
||||||
|
maxAvgTmaxRow:
|
||||||
|
gtNullSafe(row.tmax, acc.maxAvgTmaxRow?.tmax ?? null) ? row : acc.maxAvgTmaxRow,
|
||||||
|
minAvgTminRow:
|
||||||
|
ltNullSafe(row.tmin, acc.minAvgTminRow?.tmin ?? null) ? row : acc.minAvgTminRow
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
sumTmax: 0,
|
||||||
|
sumTmin: 0,
|
||||||
|
sumRain: 0,
|
||||||
|
count: 0,
|
||||||
|
validTmaxCount: 0,
|
||||||
|
validTminCount: 0,
|
||||||
|
validRainCount: 0,
|
||||||
|
rainyCount: 0,
|
||||||
|
maxRainRow: null as WeatherDataRow | null,
|
||||||
|
maxTmaxRow: null as WeatherDataRow | null,
|
||||||
|
minTminRow: null as WeatherDataRow | null,
|
||||||
|
maxTavgRow: null as WeatherDataRow | null,
|
||||||
|
maxAvgTmaxRow: null as WeatherDataRow | null,
|
||||||
|
minAvgTminRow: null as WeatherDataRow | null
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const avg = (sum: number, validCount: number) =>
|
||||||
|
validCount > 0 ? (sum / validCount).toFixed(1) : "-";
|
||||||
|
|
||||||
|
return {
|
||||||
|
avgTmax: avg(result.sumTmax, result.validTmaxCount),
|
||||||
|
avgTmaxYear: result.maxAvgTmaxRow?.year ?? "-",
|
||||||
|
avgTmin: avg(result.sumTmin, result.validTminCount),
|
||||||
|
avgTminYear: result.minAvgTminRow?.year ?? "-",
|
||||||
|
avgRain: avg(result.sumRain, result.validRainCount),
|
||||||
|
maxRain: result.maxRainRow?.rainfall ?? "-",
|
||||||
|
maxRainYear: result.maxRainRow?.year ?? "-",
|
||||||
|
rainyCount: result.rainyCount,
|
||||||
|
rainyPct: ((result.rainyCount / result.count) * 100).toFixed(0),
|
||||||
|
maxTmax: result.maxTmaxRow?.tmax ?? "-",
|
||||||
|
maxTmaxYear: result.maxTmaxRow?.year ?? "-",
|
||||||
|
minTmin: result.minTminRow?.tmin ?? "-",
|
||||||
|
minTminYear: result.minTminRow?.year ?? "-",
|
||||||
|
maxTavg: result.maxTavgRow?.tavg ?? "-",
|
||||||
|
maxTavgYear: result.maxTavgRow?.year ?? "-"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建统计卡片(去除了原版中与"多年平均最高/低温"重复的第4/5组)
|
||||||
|
*/
|
||||||
|
export function buildStatCards(stats: WeatherStats): StatCard[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ label: "多年平均最高温", value: stats.avgTmax, unit: "℃", color: "#ef4444" },
|
||||||
|
{ label: "多年平均最低温", value: stats.avgTmin, unit: "℃", color: "#2563eb" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ label: "历史最高温度", value: stats.maxTmax, unit: "℃", color: "#ef4444" },
|
||||||
|
{ label: "出现年份", value: stats.maxTmaxYear, unit: "年", color: "#4a4645" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ label: "历史最低温度", value: stats.minTmin, unit: "℃", color: "#2563eb" },
|
||||||
|
{ label: "出现年份", value: stats.minTminYear, unit: "年", color: "#4a4645" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ label: "最大单日降水", value: stats.maxRain, unit: "mm", color: "#7c3aed" },
|
||||||
|
{ label: "出现年份", value: stats.maxRainYear, unit: "年", color: "#4a4645" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ label: "平均降水量", value: stats.avgRain, unit: "mm", color: "#0891b2" },
|
||||||
|
{ label: "降水概率", value: stats.rainyPct, unit: "%", color: "#059669" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSummary(stats: WeatherStats, meta: SummaryMeta): string {
|
||||||
|
const { stationName, fromYear, toYear, month, day, count } = meta;
|
||||||
|
const d = String(day).padStart(2, "0");
|
||||||
|
return [
|
||||||
|
`${stationName}在 ${fromYear}~${toYear} 年间,每年${month}月${d}日这一天共回溯 ${count} 条单日记录。`,
|
||||||
|
`其中 ${stats.rainyCount} 年当天出现降水,历史平均单日降雨量 ${stats.avgRain}mm,`,
|
||||||
|
`单日最大降雨 ${stats.maxRain}mm(${stats.maxRainYear}年)。`,
|
||||||
|
`气候整体${+stats.avgRain > 15 ? "偏湿润" : "较干燥"},`,
|
||||||
|
`需关注${stats.rainyCount / (count || 1) > 0.5 ? "短时强降水" : "高温干旱"}风险。`
|
||||||
|
].join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Composable ----------
|
||||||
|
|
||||||
|
export function useWeatherStats(displayRows: Ref<WeatherDataRow[]>) {
|
||||||
|
/** 雨量等级分布(null 值不计入任何等级) */
|
||||||
|
const rainLevelDistribution = computed(() => {
|
||||||
|
const data = displayRows.value;
|
||||||
|
const total = data.length;
|
||||||
|
if (!total) return [] as { label: string; count: number; pct: string }[];
|
||||||
|
|
||||||
|
const counts = new Array(RAIN_LEVELS.length).fill(0);
|
||||||
|
let nullCount = 0;
|
||||||
|
data.forEach((r) => {
|
||||||
|
if (r.rainfall == null) {
|
||||||
|
nullCount++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const v = Number(r.rainfall);
|
||||||
|
for (let i = 0; i < RAIN_LEVELS.length; i++) {
|
||||||
|
if (v >= RAIN_LEVELS[i].min && v < RAIN_LEVELS[i].max) {
|
||||||
|
counts[i]++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = RAIN_LEVELS.map((l, i) => ({
|
||||||
|
label: l.label,
|
||||||
|
count: counts[i],
|
||||||
|
pct: total > 0 ? ((counts[i] / total) * 100).toFixed(1) : "0.0"
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 如果存在 null 值,追加"无数据"条目
|
||||||
|
if (nullCount > 0) {
|
||||||
|
items.push({
|
||||||
|
label: "无数据",
|
||||||
|
count: nullCount,
|
||||||
|
pct: ((nullCount / total) * 100).toFixed(1)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { rainLevelDistribution };
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { getValueByKeys } from "@/utils/utils";
|
||||||
|
import appPack from "../../package.json";
|
||||||
|
/**
|
||||||
|
* app系统配置
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* 系统版本号,自动读取package.json中的version字段
|
||||||
|
*/
|
||||||
|
version: appPack.version,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统默认语言
|
||||||
|
*/
|
||||||
|
defaultLang: "zh-CN",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* api请求地址,这里读取env环境变量中的VITE_APP_API,优先使用全局变量window.SITE_CONFIG.apiURL钩子,支持在index.html中配置
|
||||||
|
*/
|
||||||
|
api: getValueByKeys(window, "SITE_CONFIG.apiURL") || import.meta.env.VITE_APP_API,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用logo图标,logo尺寸32*32,存放路径@/assets/images/logo.png
|
||||||
|
*/
|
||||||
|
enabledLogo: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开启页面缓存
|
||||||
|
*/
|
||||||
|
enabledKeepAlive: true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网络请求超时时间,单位毫秒
|
||||||
|
*/
|
||||||
|
requestTimeout: 30000,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全屏渲染的页面
|
||||||
|
*/
|
||||||
|
fullscreenPages: ["/login"]
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* token值
|
||||||
|
*/
|
||||||
|
export const CacheToken = "CacheToken";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语言
|
||||||
|
*/
|
||||||
|
export const CacheLang = "CacheLang";
|
||||||
|
/**
|
||||||
|
* 主题
|
||||||
|
*/
|
||||||
|
export const CacheTheme = "CacheTheme";
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* 主题设置默认值
|
||||||
|
*/
|
||||||
|
export const themeSetting = {
|
||||||
|
sidebar: "dark",
|
||||||
|
topHeader: "primary",
|
||||||
|
themeColor: "#2d88ac",
|
||||||
|
navLayout: "left",
|
||||||
|
contentFull: true,
|
||||||
|
logoAuto: false,
|
||||||
|
colorIcon: false,
|
||||||
|
sidebarUniOpened: true,
|
||||||
|
openTabsPage: true,
|
||||||
|
tabStyle: "default",
|
||||||
|
sidebarCollapse: false
|
||||||
|
};
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/**
|
||||||
|
* 页面渲染布局
|
||||||
|
*/
|
||||||
|
export enum EPageLayoutEnum {
|
||||||
|
"page",
|
||||||
|
"fullscreen"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导航模式
|
||||||
|
*/
|
||||||
|
export enum ESidebarLayoutEnum {
|
||||||
|
/**
|
||||||
|
* 左侧导航
|
||||||
|
*/
|
||||||
|
Left = "left",
|
||||||
|
/**
|
||||||
|
* 顶部导航
|
||||||
|
*/
|
||||||
|
Top = "top",
|
||||||
|
/**
|
||||||
|
* 混合导航
|
||||||
|
*/
|
||||||
|
Mix = "mix"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主题设置
|
||||||
|
*/
|
||||||
|
export enum EThemeSetting {
|
||||||
|
/**
|
||||||
|
* 侧边栏风格
|
||||||
|
*/
|
||||||
|
Sidebar = "sidebar",
|
||||||
|
/**
|
||||||
|
* 顶部风格
|
||||||
|
*/
|
||||||
|
TopHeader = "topHeader",
|
||||||
|
/**
|
||||||
|
* 主题色
|
||||||
|
*/
|
||||||
|
ThemeColor = "themeColor",
|
||||||
|
//---
|
||||||
|
/**
|
||||||
|
* 布局模式
|
||||||
|
*/
|
||||||
|
NavLayout = "navLayout",
|
||||||
|
/**
|
||||||
|
* 内容是否铺满
|
||||||
|
*/
|
||||||
|
ContentFull = "contentFull",
|
||||||
|
//---
|
||||||
|
/**
|
||||||
|
* logo宽度自动
|
||||||
|
*/
|
||||||
|
LogoAuto = "logoAuto",
|
||||||
|
/**
|
||||||
|
* 多彩图标
|
||||||
|
*/
|
||||||
|
ColorIcon = "colorIcon",
|
||||||
|
/**
|
||||||
|
* 侧边栏排他展开
|
||||||
|
*/
|
||||||
|
SidebarUniOpened = "sidebarUniOpened",
|
||||||
|
/**
|
||||||
|
* 开启tab标签页
|
||||||
|
*/
|
||||||
|
OpenTabsPage = "openTabsPage",
|
||||||
|
/**
|
||||||
|
* tab标签风格
|
||||||
|
*/
|
||||||
|
TabStyle = "tabStyle",
|
||||||
|
//---
|
||||||
|
/**
|
||||||
|
* 侧边栏展开收起
|
||||||
|
*/
|
||||||
|
SidebarCollapse = "sidebarCollapse"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统框架事件枚举
|
||||||
|
*/
|
||||||
|
export enum EMitt {
|
||||||
|
/**
|
||||||
|
* 全局加载
|
||||||
|
*/
|
||||||
|
OnLoading = "onLoading",
|
||||||
|
/**
|
||||||
|
* 切换左侧侧边栏
|
||||||
|
*/
|
||||||
|
OnSwitchLeftSidebar = "onSwitchLeftSidebar",
|
||||||
|
/**
|
||||||
|
* 推送菜单到tab标签页
|
||||||
|
*/
|
||||||
|
OnPushMenuToTabs = "onPushMenuToTabs",
|
||||||
|
/**
|
||||||
|
* 设置主题
|
||||||
|
*/
|
||||||
|
OnSetTheme = "onSetTheme",
|
||||||
|
/**
|
||||||
|
* 设置侧边栏排他展开
|
||||||
|
*/
|
||||||
|
OnSetThemeNotUniqueOpened = "onSetTheme_not_uniqueOpened",
|
||||||
|
/**
|
||||||
|
* 设置开启标签页
|
||||||
|
*/
|
||||||
|
OnSetThemeTabsPage = "onSetTheme_tabsPage",
|
||||||
|
/**
|
||||||
|
* 设置导航模式
|
||||||
|
*/
|
||||||
|
OnSetNavLayout = "onSetNavLayout",
|
||||||
|
/**
|
||||||
|
* 刷新tab标签页
|
||||||
|
*/
|
||||||
|
OnReloadTabPage = "onReloadTabPage",
|
||||||
|
|
||||||
|
//
|
||||||
|
/**
|
||||||
|
* 移动端打开侧边栏
|
||||||
|
*/
|
||||||
|
OnMobileOpenSidebar = "onMobileOpenSidebar",
|
||||||
|
|
||||||
|
//
|
||||||
|
/**
|
||||||
|
* 混合导航选中顶部主菜单
|
||||||
|
*/
|
||||||
|
OnSelectHeaderNavMenusByMixNav = "onSelectHeaderNavMenusByMixNav",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭当前tab页
|
||||||
|
*/
|
||||||
|
OnCloseCurrTab = "onCloseCurrTab"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主题是key
|
||||||
|
*/
|
||||||
|
export enum EThemeColor {
|
||||||
|
/**
|
||||||
|
* 主题色
|
||||||
|
*/
|
||||||
|
ThemeColor = "--color-primary"
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
import app from "@/constants/app";
|
||||||
|
import { EMitt, EThemeSetting } from "@/constants/enum";
|
||||||
|
import { IObject, IViewHooks, IViewHooksOptions } from "@/types/interface";
|
||||||
|
import { registerDynamicToRouterAndNext } from "@/router";
|
||||||
|
import baseService from "@/service/baseService";
|
||||||
|
import { getToken } from "@/utils/cache";
|
||||||
|
import emits from "@/utils/emits";
|
||||||
|
import { getThemeConfigCacheByKey } from "@/utils/theme";
|
||||||
|
import { checkPermission, getDictLabel } from "@/utils/utils";
|
||||||
|
import qs from "qs";
|
||||||
|
import { onActivated, onMounted } from "vue";
|
||||||
|
import { useRouter, useRoute } from "vue-router";
|
||||||
|
import { useAppStore } from "@/store";
|
||||||
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用视图业务逻辑(列表/增删改查基本业务)
|
||||||
|
* @param props 自定义通用业务state
|
||||||
|
* @returns 返回响应式自定义state和通用方法
|
||||||
|
*/
|
||||||
|
const useView = (props: IViewHooksOptions | IObject): IViewHooks => {
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const store = useAppStore();
|
||||||
|
const defaultOptions: IViewHooksOptions = {
|
||||||
|
createdIsNeed: true,
|
||||||
|
activatedIsNeed: false,
|
||||||
|
getDataListURL: "",
|
||||||
|
getDataListIsPage: false,
|
||||||
|
deleteURL: "",
|
||||||
|
deleteIsBatch: false,
|
||||||
|
deleteIsBatchKey: "id",
|
||||||
|
exportURL: "",
|
||||||
|
dataForm: {},
|
||||||
|
dataList: [],
|
||||||
|
order: "",
|
||||||
|
orderField: "",
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
total: 0,
|
||||||
|
dataListLoading: false,
|
||||||
|
dataListSelections: [],
|
||||||
|
elTable: {}
|
||||||
|
};
|
||||||
|
const mergeDefaultStateToPageState = (options: IObject, props: IObject): IViewHooksOptions => {
|
||||||
|
for (const key in options) {
|
||||||
|
if (!Object.getOwnPropertyDescriptor(props, key)) {
|
||||||
|
props[key] = options[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
};
|
||||||
|
const state = mergeDefaultStateToPageState(defaultOptions, props);
|
||||||
|
onMounted(() => {
|
||||||
|
if (state.createdIsNeed && !state.activatedIsNeed) {
|
||||||
|
viewFns.query();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onActivated(() => {
|
||||||
|
if (store.state.closedTabs.includes(store.state.activeTabName)) {
|
||||||
|
//如果当前打开的tab页面是之前已经关闭过的会存在keep-alive缓存
|
||||||
|
//这里采用临时刷新页面解决方案
|
||||||
|
//待vue官方开放缓存策略后再行实现 https://github.com/vuejs/vue-next/pull/4339 https://github.com/vuejs/rfcs/pull/284
|
||||||
|
|
||||||
|
const closedTabs = store.state.closedTabs;
|
||||||
|
store.updateState({
|
||||||
|
closedTabs: closedTabs.filter((x: string) => x !== store.state.activeTabName)
|
||||||
|
});
|
||||||
|
emits.emit(EMitt.OnReloadTabPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.activatedIsNeed) {
|
||||||
|
viewFns.query();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
const rejectFns = {
|
||||||
|
hasPermission(key: string) {
|
||||||
|
return checkPermission(store.state.permissions as string[], key);
|
||||||
|
},
|
||||||
|
getDictLabel(dictType: string, dictValue: number) {
|
||||||
|
return getDictLabel(store.state.dicts, dictType, dictValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
const viewFns = {
|
||||||
|
// 获取数据列表
|
||||||
|
query() {
|
||||||
|
if (!state.getDataListURL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.dataListLoading = true;
|
||||||
|
baseService
|
||||||
|
.get(state.getDataListURL, {
|
||||||
|
order: state.order,
|
||||||
|
orderField: state.orderField,
|
||||||
|
page: state.getDataListIsPage ? state.page : null,
|
||||||
|
limit: state.getDataListIsPage ? state.limit : null,
|
||||||
|
...state.dataForm
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
state.dataListLoading = false;
|
||||||
|
state.dataList = state.getDataListIsPage ? res.data.list : res.data;
|
||||||
|
state.total = state.getDataListIsPage ? res.data.total : 0;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
state.dataListLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 多选
|
||||||
|
dataListSelectionChangeHandle(val: IObject[]) {
|
||||||
|
state.dataListSelections = val;
|
||||||
|
},
|
||||||
|
// 排序
|
||||||
|
dataListSortChangeHandle(data: IObject) {
|
||||||
|
if (!data.order || !data.prop) {
|
||||||
|
state.order = "";
|
||||||
|
state.orderField = "";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
state.order = data.order.replace(/ending$/, "");
|
||||||
|
state.orderField = data.prop.replace(/([A-Z])/g, "_$1").toLowerCase();
|
||||||
|
viewFns.query();
|
||||||
|
},
|
||||||
|
// 分页, 每页条数
|
||||||
|
pageSizeChangeHandle(val: number) {
|
||||||
|
state.page = 1;
|
||||||
|
state.limit = val;
|
||||||
|
viewFns.query();
|
||||||
|
},
|
||||||
|
// 分页, 当前页
|
||||||
|
pageCurrentChangeHandle(val: number) {
|
||||||
|
state.page = val;
|
||||||
|
viewFns.query();
|
||||||
|
},
|
||||||
|
//搜索
|
||||||
|
getDataList() {
|
||||||
|
state.page = 1;
|
||||||
|
viewFns.query();
|
||||||
|
},
|
||||||
|
// 删除
|
||||||
|
deleteHandle(id?: string): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (
|
||||||
|
state.deleteIsBatch &&
|
||||||
|
!id &&
|
||||||
|
state.dataListSelections &&
|
||||||
|
state.dataListSelections.length <= 0
|
||||||
|
) {
|
||||||
|
ElMessage.warning({
|
||||||
|
message: "请选择操作项",
|
||||||
|
duration: 500
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElMessageBox.confirm("确定进行[删除]操作?", "提示", {
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning"
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
baseService
|
||||||
|
.delete(
|
||||||
|
`${state.deleteURL}${state.deleteIsBatch ? "" : "/" + id}`,
|
||||||
|
state.deleteIsBatch
|
||||||
|
? id
|
||||||
|
? [id]
|
||||||
|
: state.dataListSelections
|
||||||
|
? state.dataListSelections.map(
|
||||||
|
(item: IObject) => state.deleteIsBatchKey && item[state.deleteIsBatchKey]
|
||||||
|
)
|
||||||
|
: {}
|
||||||
|
: {}
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
ElMessage.success({
|
||||||
|
message: "成功",
|
||||||
|
duration: 500,
|
||||||
|
onClose: () => {
|
||||||
|
viewFns.query();
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
//
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 导出
|
||||||
|
exportHandle() {
|
||||||
|
window.location.href = `${app.api}${state.exportURL}?${qs.stringify({
|
||||||
|
...state.dataForm,
|
||||||
|
token: getToken()
|
||||||
|
})}`;
|
||||||
|
// baseService.download(state.exportURL, { ...state.dataForm, token: getToken() });
|
||||||
|
},
|
||||||
|
//关闭当前窗口
|
||||||
|
closeCurrentTab() {
|
||||||
|
if (getThemeConfigCacheByKey(EThemeSetting.OpenTabsPage)) {
|
||||||
|
emits.emit(EMitt.OnCloseCurrTab);
|
||||||
|
} else {
|
||||||
|
router.replace("/home");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 处理流程路由
|
||||||
|
handleFlowRoute(data: IObject) {
|
||||||
|
const routeParams = {
|
||||||
|
path: `/flow/task-form`,
|
||||||
|
query: {
|
||||||
|
taskId: data.taskId,
|
||||||
|
processInstanceId: data.processInstanceId,
|
||||||
|
processDefinitionId: data.processDefinitionId,
|
||||||
|
showType: "taskHandle",
|
||||||
|
_mt: `${route.meta.title} - ${data.processDefinitionName}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
registerDynamicToRouterAndNext(routeParams);
|
||||||
|
},
|
||||||
|
// 查看流程详情
|
||||||
|
flowDetailRoute(data: IObject) {
|
||||||
|
const routeParams = {
|
||||||
|
path: `/flow/task-form`,
|
||||||
|
query: {
|
||||||
|
taskId: data.taskId,
|
||||||
|
processInstanceId: data.processInstanceId,
|
||||||
|
processDefinitionId: data.processDefinitionId,
|
||||||
|
showType: "detail",
|
||||||
|
_mt: `${route.meta.title} - ${data.processDefinitionName}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
registerDynamicToRouterAndNext(routeParams);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
return {
|
||||||
|
...viewFns,
|
||||||
|
...rejectFns
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useView;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全屏布局
|
||||||
|
*/
|
||||||
|
export default defineComponent({
|
||||||
|
name: "FullScreenLayout",
|
||||||
|
setup() {
|
||||||
|
const route = useRoute();
|
||||||
|
return { route };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div :class="`rr-fullscreen ${route.query.pop ? 'new-pop-window' : ''}`">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import logo from "@/assets/images/logo.png";
|
||||||
|
import { EMitt, ESidebarLayoutEnum, EThemeSetting } from "@/constants/enum";
|
||||||
|
import emits from "@/utils/emits";
|
||||||
|
import { getThemeConfigCacheByKey } from "@/utils/theme";
|
||||||
|
import { computed, defineComponent, reactive } from "vue";
|
||||||
|
import { useAppStore } from "@/store";
|
||||||
|
import { useAlertMarquee } from "@/composables/useAlertMarquee";
|
||||||
|
import { useImportTaskStore } from "@/store/importTasks";
|
||||||
|
import { Bell } from "@element-plus/icons-vue";
|
||||||
|
import BaseSidebar from "../sidebar/base-sidebar.vue";
|
||||||
|
import Breadcrumb from "./breadcrumb.vue";
|
||||||
|
import CollapseSidebarBtn from "./collapse-sidebar-btn.vue";
|
||||||
|
import Expand from "./expand.vue";
|
||||||
|
import HeaderMixNavMenus from "./header-mix-nav-menus.vue";
|
||||||
|
import Logo from "./logo.vue";
|
||||||
|
import "@/assets/css/header.less";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顶部主区域
|
||||||
|
*/
|
||||||
|
export default defineComponent({
|
||||||
|
name: "Header",
|
||||||
|
components: { BaseSidebar, Breadcrumb, CollapseSidebarBtn, Expand, HeaderMixNavMenus, Logo },
|
||||||
|
setup() {
|
||||||
|
const store = useAppStore();
|
||||||
|
const { messageCount: alertCount, toggleDrawer: toggleNotificationCenter } = useAlertMarquee();
|
||||||
|
const importTaskStore = useImportTaskStore();
|
||||||
|
const combinedCount = computed(() => alertCount.value + importTaskStore.activeTasks.length);
|
||||||
|
const state = reactive({
|
||||||
|
sidebarLayout: getThemeConfigCacheByKey(EThemeSetting.NavLayout)
|
||||||
|
});
|
||||||
|
emits.on(EMitt.OnSetNavLayout, (vl) => {
|
||||||
|
state.sidebarLayout = vl;
|
||||||
|
});
|
||||||
|
const onRefresh = () => {
|
||||||
|
emits.emit(EMitt.OnReloadTabPage);
|
||||||
|
};
|
||||||
|
return { store, state, onRefresh, logo, ESidebarLayoutEnum, combinedCount, toggleNotificationCenter, Bell };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="rr-header-ctx">
|
||||||
|
<div class="rr-header-ctx-logo hidden-xs-only">
|
||||||
|
<logo :logoUrl="logo" logoName="气象数据管理系统"></logo>
|
||||||
|
</div>
|
||||||
|
<div class="rr-header-right">
|
||||||
|
<div class="rr-header-right-left">
|
||||||
|
<div class="rr-header-right-items rr-header-action" :style="`display:${state.sidebarLayout === ESidebarLayoutEnum.Top ? 'none' : ''}`">
|
||||||
|
<collapse-sidebar-btn></collapse-sidebar-btn>
|
||||||
|
<div @click="onRefresh" style="cursor: pointer">
|
||||||
|
<div class="el-badge">
|
||||||
|
<el-icon><refresh-right /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rr-header-right-left-br ele-scrollbar-hide hidden-xs-only">
|
||||||
|
<base-sidebar v-if="state.sidebarLayout === ESidebarLayoutEnum.Top" mode="horizontal" :router="true"></base-sidebar>
|
||||||
|
<header-mix-nav-menus v-else-if="state.sidebarLayout === ESidebarLayoutEnum.Mix"></header-mix-nav-menus>
|
||||||
|
<breadcrumb v-else></breadcrumb>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; align-items: center; flex-shrink: 0">
|
||||||
|
<div class="rr-header-notify" @click="toggleNotificationCenter">
|
||||||
|
<el-badge :value="combinedCount" :hidden="combinedCount === 0">
|
||||||
|
<el-icon :size="18"><Bell /></el-icon>
|
||||||
|
</el-badge>
|
||||||
|
</div>
|
||||||
|
<expand :userName="store.state.user.username"></expand>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rr-header-notify {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 50px;
|
||||||
|
padding: 0 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #909399;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rr-header-notify:hover {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { IObject } from "@/types/interface";
|
||||||
|
import { getValueByKeys } from "@/utils/utils";
|
||||||
|
import { defineComponent, ref, watch } from "vue";
|
||||||
|
import { RouteLocationMatched, useRouter } from "vue-router";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顶部面包屑
|
||||||
|
*/
|
||||||
|
export default defineComponent({
|
||||||
|
name: "Breadcrumb",
|
||||||
|
setup() {
|
||||||
|
const router = useRouter();
|
||||||
|
const breadcrumbs = ref<IObject[]>([]);
|
||||||
|
const { currentRoute } = router;
|
||||||
|
const firstRoute = (router.options.routes[0] || {}) as RouteLocationMatched;
|
||||||
|
const home: RouteLocationMatched = firstRoute.children && firstRoute.children.length > 0 ? (firstRoute.children[0] as RouteLocationMatched) : firstRoute;
|
||||||
|
watch(
|
||||||
|
() => currentRoute.value,
|
||||||
|
() => {
|
||||||
|
breadcrumbs.value = currentRoute.value.path !== home.path ? getValueByKeys(currentRoute.value, "meta.matched", []) : [];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return { breadcrumbs, currentRoute, home };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<el-breadcrumb separator="/" style="padding-top: 4px">
|
||||||
|
<el-breadcrumb-item :to="{ path: home.path }"> 主页 </el-breadcrumb-item>
|
||||||
|
<el-breadcrumb-item v-for="x in breadcrumbs" :key="x.path">{{ currentRoute.query._mt || x.title || "" }} </el-breadcrumb-item>
|
||||||
|
</el-breadcrumb>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SvgIcon from "@/components/base/svg-icon";
|
||||||
|
import { EMitt, EThemeSetting } from "@/constants/enum";
|
||||||
|
import emits from "@/utils/emits";
|
||||||
|
import { getThemeConfigCacheByKey, setThemeConfigToCache } from "@/utils/theme";
|
||||||
|
import { defineComponent, reactive } from "vue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PC和移动端下的侧边栏展开收起按钮
|
||||||
|
*/
|
||||||
|
export default defineComponent({
|
||||||
|
name: "CollapseSidebarBtn",
|
||||||
|
components: { SvgIcon },
|
||||||
|
setup() {
|
||||||
|
const state = reactive({
|
||||||
|
collapseSidebar: getThemeConfigCacheByKey(EThemeSetting.SidebarCollapse)
|
||||||
|
});
|
||||||
|
const onClickSidebarSwitcher = () => {
|
||||||
|
const key = EThemeSetting.SidebarCollapse;
|
||||||
|
state.collapseSidebar = !state.collapseSidebar;
|
||||||
|
emits.emit(EMitt.OnSwitchLeftSidebar);
|
||||||
|
emits.emit(EMitt.OnSetTheme, [key, key + "-" + state.collapseSidebar]);
|
||||||
|
setThemeConfigToCache(key, state.collapseSidebar);
|
||||||
|
};
|
||||||
|
const onClickSidebarSwitcherByMobile = () => {
|
||||||
|
emits.emit(EMitt.OnMobileOpenSidebar);
|
||||||
|
};
|
||||||
|
return { state, onClickSidebarSwitcher, onClickSidebarSwitcherByMobile };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="hidden-xs-only" @click="onClickSidebarSwitcher">
|
||||||
|
<svg-icon :name="state.collapseSidebar ? 'indent' : 'outdent'"></svg-icon>
|
||||||
|
</div>
|
||||||
|
<div class="hidden-sm-and-up show-xs-only" @click="onClickSidebarSwitcherByMobile">
|
||||||
|
<svg-icon name="icon-indent"></svg-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SvgIcon from "@/components/base/svg-icon";
|
||||||
|
import baseService from "@/service/baseService";
|
||||||
|
import { useFullscreen } from "@vueuse/core";
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { useAppStore } from "@/store";
|
||||||
|
import userLogo from "@/assets/images/user.png";
|
||||||
|
import "@/assets/css/header.less";
|
||||||
|
import { ElMessageBox } from "element-plus";
|
||||||
|
|
||||||
|
interface IExpand {
|
||||||
|
userName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顶部右侧扩展区域
|
||||||
|
*/
|
||||||
|
export default defineComponent({
|
||||||
|
name: "Expand",
|
||||||
|
components: { SvgIcon },
|
||||||
|
props: {
|
||||||
|
userName: String
|
||||||
|
},
|
||||||
|
setup(props: IExpand) {
|
||||||
|
const router = useRouter();
|
||||||
|
const store = useAppStore();
|
||||||
|
const { isFullscreen, toggle } = useFullscreen();
|
||||||
|
|
||||||
|
const onClickUserMenus = (path: string) => {
|
||||||
|
if (path === "/login") {
|
||||||
|
ElMessageBox.confirm("确定进行[退出]操作?", "提示", {
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning"
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
baseService.post("/logout").finally(() => {
|
||||||
|
router.push(path);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
//
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
router.push(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
props,
|
||||||
|
store,
|
||||||
|
isFullscreen,
|
||||||
|
userLogo,
|
||||||
|
onClickUserMenus,
|
||||||
|
toggle
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="rr-header-right-items">
|
||||||
|
<div @click="toggle" class="hidden-xs-only">
|
||||||
|
<span>
|
||||||
|
<svg-icon :name="isFullscreen ? 'tuichuquanping' : 'fullscreen2'"></svg-icon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: center; align-items: center">
|
||||||
|
<img :src="userLogo" :alt="props.userName" style="width: 30px; height: 30px; border-radius: 50%; margin-top: 3px; margin-right: 5px" />
|
||||||
|
<el-dropdown @command="onClickUserMenus">
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item icon="lock" command="/user/password"> 修改密码 </el-dropdown-item>
|
||||||
|
<el-dropdown-item icon="switch-button" divided command="/login"> 退出登录 </el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
<span class="el-dropdown-link" style="display: flex">
|
||||||
|
{{ props.userName }}
|
||||||
|
<el-icon class="el-icon--right" style="font-size: 14px"><arrow-down /></el-icon>
|
||||||
|
</span>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { EMitt, ESidebarLayoutEnum, EThemeSetting } from "@/constants/enum";
|
||||||
|
import emits from "@/utils/emits";
|
||||||
|
import { getThemeConfigCacheByKey } from "@/utils/theme";
|
||||||
|
import { getValueByKeys } from "@/utils/utils";
|
||||||
|
import { computed, defineComponent, reactive, watch } from "vue";
|
||||||
|
import { RouteRecordRaw, useRoute, useRouter } from "vue-router";
|
||||||
|
import { useAppStore } from "@/store";
|
||||||
|
import BaseSidebar from "../sidebar/base-sidebar.vue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顶部导航菜单,混合布局模式下用到
|
||||||
|
*/
|
||||||
|
export default defineComponent({
|
||||||
|
name: "HeaderMixNavMenus",
|
||||||
|
components: { BaseSidebar },
|
||||||
|
setup() {
|
||||||
|
const store = useAppStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const routers = router.options.routes;
|
||||||
|
const state = reactive({
|
||||||
|
currRoute: getValueByKeys(getValueByKeys(router.currentRoute.value.meta, "matched", [])[0], "path", "")
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
() => {
|
||||||
|
if (getThemeConfigCacheByKey(EThemeSetting.NavLayout) === ESidebarLayoutEnum.Mix) {
|
||||||
|
const matchedRoute = getValueByKeys(getValueByKeys(router.currentRoute.value.meta, "matched", [])[0], "path", "");
|
||||||
|
if (matchedRoute) {
|
||||||
|
state.currRoute = matchedRoute;
|
||||||
|
emits.emit(EMitt.OnSelectHeaderNavMenusByMixNav, matchedRoute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const topHeaderMenus = computed(() => {
|
||||||
|
const rs: any[] = [];
|
||||||
|
store.state.routes.forEach((x: RouteRecordRaw) => {
|
||||||
|
rs.push({
|
||||||
|
path: x.path,
|
||||||
|
children: [],
|
||||||
|
meta: x.meta ? x.meta : {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return rs;
|
||||||
|
});
|
||||||
|
const onSelect = (path: string) => {
|
||||||
|
const curr = routers.find((x: RouteRecordRaw) => x.path === path);
|
||||||
|
|
||||||
|
if (!curr?.children?.length) {
|
||||||
|
router.push(path);
|
||||||
|
} else {
|
||||||
|
state.currRoute = path;
|
||||||
|
emits.emit(EMitt.OnSelectHeaderNavMenusByMixNav, path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return { state, topHeaderMenus, onSelect };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<base-sidebar mode="horizontal" :menus="topHeaderMenus" :router="false" :currRoute="state.currRoute" :is-mobile="false" :onSelect="onSelect"></base-sidebar>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { useImportTaskStore } from "@/store/importTasks";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "ImportTaskIndicator",
|
||||||
|
setup() {
|
||||||
|
const taskStore = useImportTaskStore();
|
||||||
|
|
||||||
|
const statusIcon = (status: string) => {
|
||||||
|
if (status === "pending" || status === "uploading" || status === "processing") return "loading";
|
||||||
|
if (status === "success") return "circle-check";
|
||||||
|
return "circle-close";
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusClass = (status: string) => {
|
||||||
|
if (status === "success") return "task-success";
|
||||||
|
if (status === "error") return "task-error";
|
||||||
|
return "task-running";
|
||||||
|
};
|
||||||
|
|
||||||
|
const progressLabel = (task: any) => {
|
||||||
|
if (task.totalRows > 0) {
|
||||||
|
const pct = Math.round((task.processedRows / task.totalRows) * 100);
|
||||||
|
return ` ${pct}%`;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (ts: number) => {
|
||||||
|
const d = new Date(ts);
|
||||||
|
return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}:${d.getSeconds().toString().padStart(2, "0")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { taskStore, statusIcon, statusClass, formatTime, progressLabel };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="rr-header-right-items rr-header-action">
|
||||||
|
<el-popover placement="bottom" :width="360" trigger="click" popper-class="import-task-popover">
|
||||||
|
<template #reference>
|
||||||
|
<div class="import-task-trigger">
|
||||||
|
<template v-if="taskStore.hasActiveTasks">
|
||||||
|
<el-badge :value="taskStore.activeTasks.length" :max="99" class="task-badge">
|
||||||
|
<el-icon class="is-loading"><refresh-right /></el-icon>
|
||||||
|
</el-badge>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-icon class="task-idle-icon"><bell /></el-icon>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="import-task-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>导入任务</span>
|
||||||
|
<el-button size="small" text @click="taskStore.clearCompleted()">清除已完成</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div v-for="task in taskStore.recentTasks" :key="task.id" class="task-item">
|
||||||
|
<el-icon :class="[statusClass(task.status), { 'is-loading': task.status === 'uploading' || task.status === 'processing' }]">
|
||||||
|
<component :is="statusIcon(task.status)" />
|
||||||
|
</el-icon>
|
||||||
|
<div class="task-info">
|
||||||
|
<span class="task-name">{{ task.fileName }}</span>
|
||||||
|
<span class="task-message">{{ task.message }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="task.status === 'processing' && task.totalRows > 0" class="task-progress">{{ Math.round((task.processedRows / task.totalRows) * 100) }}%</span>
|
||||||
|
<span class="task-time">{{ formatTime(task.createdAt) }}</span>
|
||||||
|
<el-button v-if="task.status === 'success' || task.status === 'error'" size="small" text @click="taskStore.removeTask(task.id)">
|
||||||
|
<el-icon><close /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="taskStore.recentTasks.length === 0" class="panel-empty">暂无任务</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-popover>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.import-task-trigger {
|
||||||
|
padding: 0 12px;
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.task-badge {
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
.task-idle-icon {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.import-task-panel .panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.import-task-panel .panel-body {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.import-task-panel .task-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 4px;
|
||||||
|
border-bottom: 1px solid #f2f2f2;
|
||||||
|
}
|
||||||
|
.import-task-panel .task-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.import-task-panel .task-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.import-task-panel .task-name {
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.import-task-panel .task-message {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.import-task-panel .task-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.task-success { color: #67c23a; }
|
||||||
|
.task-error { color: #f56c6c; }
|
||||||
|
.task-running { color: #409eff; }
|
||||||
|
.panel-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: #c0c4cc;
|
||||||
|
padding: 24px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import app from "@/constants/app";
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
interface ILogo {
|
||||||
|
logoUrl?: string;
|
||||||
|
logoName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顶部logo
|
||||||
|
*/
|
||||||
|
export default defineComponent({
|
||||||
|
name: "Logo",
|
||||||
|
props: {
|
||||||
|
logoUrl: String,
|
||||||
|
logoName: {
|
||||||
|
type: String,
|
||||||
|
default: "logo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props: ILogo) {
|
||||||
|
return { props, app };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span :class="`rr-header-ctx-logo-img-wrap ${'enabled-logo-' + app.enabledLogo}`">
|
||||||
|
<!-- 支持显示图片logo或者产品名称缩写,二选一模式,通过注释开启功能,app.enabledLogo控制正常模式下图片logo是否显示,如果有图片logo,收起状态会强制显示图片logo -->
|
||||||
|
<!-- <img :src="props.logoUrl" class="rr-header-ctx-logo-img" :alt="props.logoName" /> -->
|
||||||
|
<span>XX</span>
|
||||||
|
<span class="rr-header-ctx-logo-line"></span>
|
||||||
|
</span>
|
||||||
|
<span class="rr-header-ctx-logo-text">{{ props.logoName }}</span>
|
||||||
|
</template>
|
||||||