Compare commits
132 Commits
df90aefad6
...
2d2f46b6f1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d2f46b6f1 | ||
|
|
e4d014cf8c | ||
|
|
80a360fa0e | ||
|
|
7d2c70a19f | ||
|
|
a248e3b234 | ||
|
|
70ab453d94 | ||
|
|
36f0cedc62 | ||
|
|
df52a3e343 | ||
|
|
8d27df05d6 | ||
|
|
e47b8d354e | ||
|
|
ced12d5805 | ||
|
|
dd54ce489e | ||
|
|
00efe08873 | ||
|
|
c3420640a7 | ||
|
|
4c882ac309 | ||
|
|
334787f2cf | ||
|
|
de48277991 | ||
|
|
753f84ec26 | ||
|
|
3d58c73a88 | ||
|
|
457d0c3888 | ||
|
|
a8ea3746f4 | ||
|
|
55f0cd2192 | ||
|
|
2492b8ff5a | ||
|
|
13b00ac8de | ||
|
|
89d0dd2a1d | ||
|
|
47b272441c | ||
|
|
159d53080a | ||
|
|
7f9d9ed8bd | ||
|
|
ac997998e1 | ||
|
|
2c45ef4e05 | ||
|
|
5fe9342bbf | ||
|
|
93b97a1571 | ||
|
|
7b8509cc31 | ||
|
|
a5cdcedb38 | ||
|
|
17f7c0ad78 | ||
|
|
19252dd7cc | ||
|
|
3aad0a27cf | ||
|
|
1659e8f9bc | ||
|
|
b10f4c4172 | ||
|
|
b3d8da2365 | ||
|
|
3fadbf881b | ||
|
|
0eaa8a44f4 | ||
|
|
b404d4ff29 | ||
|
|
dc4415a1ff | ||
|
|
a4c8af1082 | ||
|
|
d5ed2b80b2 | ||
|
|
6f29b40e99 | ||
|
|
b68741b4a9 | ||
|
|
29cc052611 | ||
|
|
98c53dce04 | ||
|
|
624cfb4d0a | ||
|
|
f10c5bd4ea | ||
|
|
e05b524c39 | ||
|
|
f603317761 | ||
|
|
4b7ed6f727 | ||
|
|
d1ed96b8fc | ||
|
|
f149fca426 | ||
|
|
2dae286fa9 | ||
|
|
0e76ac4fcc | ||
|
|
7ef49859c1 | ||
|
|
c876b1e527 | ||
|
|
0e8dc5fd93 | ||
|
|
844515acc4 | ||
|
|
a5ccc6d57f | ||
|
|
ccf152cb6a | ||
|
|
8296fec882 | ||
|
|
9cec62260a | ||
|
|
8c15bca5c7 | ||
|
|
d94f28f1d3 | ||
|
|
dd5510e5c0 | ||
|
|
f30ed0b68c | ||
|
|
05ee38b119 | ||
|
|
2903640d8d | ||
|
|
d6a59b5ebb | ||
|
|
d840737c5e | ||
|
|
c761be6e31 | ||
|
|
ad6c6aa9c1 | ||
|
|
9fd64e62da | ||
|
|
a0d1c3739d | ||
|
|
72f30359c6 | ||
|
|
5fde4ea0f7 | ||
|
|
0422e43070 | ||
|
|
3f5b81424a | ||
|
|
949dd1f7f0 | ||
|
|
64d60b27dc | ||
|
|
d6e8afab6c | ||
|
|
42ee161da3 | ||
|
|
732eb453b4 | ||
|
|
0b2606bdf4 | ||
|
|
988cf99a03 | ||
|
|
3b001f79e0 | ||
|
|
bacbb84d54 | ||
|
|
da54e9db8d | ||
|
|
9894eef530 | ||
|
|
fc3d81f482 | ||
|
|
74e3b26616 | ||
|
|
3259d35641 | ||
|
|
ee4ca3a5ff | ||
|
|
0fa4a2cdac | ||
|
|
6901976657 | ||
|
|
2b809f227b | ||
|
|
2c5b9d4fcc | ||
|
|
cef368b9dc | ||
|
|
1644a9c450 | ||
|
|
fc81616118 | ||
|
|
466230c2c0 | ||
|
|
0341cb07dc | ||
|
|
7f30d2f629 | ||
|
|
8a8e2b0c65 | ||
|
|
12a8ee4991 | ||
|
|
3f8a04f06b | ||
|
|
e429228221 | ||
|
|
1091581251 | ||
|
|
ed0a1655ca | ||
|
|
8b85e1c441 | ||
|
|
415540ffc4 | ||
|
|
36d7cdd155 | ||
|
|
d4129d66b1 | ||
|
|
4c9cd63ac0 | ||
|
|
462ba124a2 | ||
|
|
542f816e8d | ||
|
|
5ec61b3eea | ||
|
|
490d4349fb | ||
|
|
f7906bb579 | ||
|
|
8cac136e25 | ||
|
|
6dc113e438 | ||
|
|
ce94dd756b | ||
|
|
6e62bcd085 | ||
|
|
f88a367372 | ||
|
|
3d1bd1612c | ||
|
|
adfe36cffb | ||
|
|
4c66f630a9 |
@@ -1,5 +1,5 @@
|
|||||||
# 页面标题
|
# 页面标题
|
||||||
VITE_APP_TITLE = RuoYi-Vue-Plus多租户管理系统
|
VITE_APP_TITLE = 可视化电子围栏管理系统
|
||||||
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
|
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
|
||||||
|
|
||||||
# 开发环境配置
|
# 开发环境配置
|
||||||
@@ -34,3 +34,5 @@ VITE_APP_WEBSOCKET = false
|
|||||||
|
|
||||||
# sse 开关
|
# sse 开关
|
||||||
VITE_APP_SSE = true
|
VITE_APP_SSE = true
|
||||||
|
|
||||||
|
VITE_BASE_API = "http://47.104.208.94:8089"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# 页面标题
|
# 页面标题
|
||||||
VITE_APP_TITLE = RuoYi-Vue-Plus多租户管理系统
|
VITE_APP_TITLE = 可视化电子围栏管理系统
|
||||||
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
|
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
|
||||||
|
|
||||||
# 生产环境配置
|
# 生产环境配置
|
||||||
@@ -37,3 +37,5 @@ VITE_APP_WEBSOCKET = false
|
|||||||
|
|
||||||
# sse 开关
|
# sse 开关
|
||||||
VITE_APP_SSE = true
|
VITE_APP_SSE = true
|
||||||
|
|
||||||
|
VITE_BASE_API = "http://47.104.208.94:8089"
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
"ComputedRef": true,
|
"ComputedRef": true,
|
||||||
"DirectiveBinding": true,
|
"DirectiveBinding": true,
|
||||||
"EffectScope": true,
|
"EffectScope": true,
|
||||||
|
"ElLoading": true,
|
||||||
|
"ElMessage": true,
|
||||||
|
"ElMessageBox": true,
|
||||||
|
"ElNotification": true,
|
||||||
"ExtractDefaultPropTypes": true,
|
"ExtractDefaultPropTypes": true,
|
||||||
"ExtractPropTypes": true,
|
"ExtractPropTypes": true,
|
||||||
"ExtractPublicPropTypes": true,
|
"ExtractPublicPropTypes": true,
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,3 @@
|
|||||||
CLAUDE.md
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.history
|
.history
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
247
CLAUDE.md
Normal file
247
CLAUDE.md
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
RuoYi-Vue-Plus frontend — a multi-tenant admin management system. Built with **Vue 3 + TypeScript + Vite + Element Plus + Pinia + UnoCSS**. The backend is a Java/Spring Boot service (separate repo).
|
||||||
|
|
||||||
|
## Project Directory Structure
|
||||||
|
|
||||||
|
High-level map of the repository (excluding `node_modules/`, `dist/`, and `.git/`). The app boots from **`index.html`** → **`src/main.ts`** → **`src/App.vue`**.
|
||||||
|
|
||||||
|
### Repository root
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `src/` | All Vue application source |
|
||||||
|
| `public/` | Static assets served as-is by Vite (favicon, etc.) |
|
||||||
|
| `vite/` | Vite config fragments; **`vite/plugins/`** holds custom plugins (auto-import, icons, compression, etc.) |
|
||||||
|
| `bin/` | Optional shell/helper scripts |
|
||||||
|
| `html/` | Extra static HTML assets used by the project |
|
||||||
|
| `.claude/` | Local Claude Code agents, rules, skills (optional; may be gitignored in some clones) |
|
||||||
|
| `CLAUDE.md` | AI / contributor guidance for this repo |
|
||||||
|
| `vite.config.ts` | Vite entry config |
|
||||||
|
| `tsconfig.json` | TypeScript compiler options |
|
||||||
|
| `uno.config.ts` | UnoCSS utility config |
|
||||||
|
| `eslint.config.ts` | ESLint flat config |
|
||||||
|
| `.env.development` / `.env.production` | `VITE_*` environment variables |
|
||||||
|
| `package.json` / `pnpm-lock.yaml` | Dependencies and scripts (pnpm) |
|
||||||
|
| `README.md` | Human-oriented project readme |
|
||||||
|
| `LICENSE` | License file |
|
||||||
|
| `.editorconfig` / `.prettierrc` / `.prettierignore` | Editor & Prettier defaults |
|
||||||
|
| `.eslintrc-auto-import.json` | ESLint metadata for auto-imported globals |
|
||||||
|
|
||||||
|
### `.claude/` (local only, gitignored)
|
||||||
|
|
||||||
|
Not committed to the repo; present on some dev machines for Claude Code:
|
||||||
|
|
||||||
|
- `agents/` — agent prompt files (e.g. `jarvis.md`)
|
||||||
|
- `commands/` — custom slash commands
|
||||||
|
- `rules/` — extra coding rules
|
||||||
|
- `skills/` — reusable skill docs (e.g. `mock-data`, `vue3-admin`)
|
||||||
|
|
||||||
|
### `src/` — top-level files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `main.ts` | App bootstrap, global styles, plugin registration |
|
||||||
|
| `App.vue` | Root component |
|
||||||
|
| `permission.ts` | Router navigation guard (token, dynamic routes, progress bar) |
|
||||||
|
| `settings.ts` | Default layout / UI settings |
|
||||||
|
| `animate.ts` | Transition / animation helpers (if used by layout) |
|
||||||
|
|
||||||
|
### `src/api/` — HTTP clients (mirrors backend modules)
|
||||||
|
|
||||||
|
Organized by business area; each folder usually has `index.ts` and sometimes `types.ts`.
|
||||||
|
|
||||||
|
| Area | Folders |
|
||||||
|
|------|---------|
|
||||||
|
| **System** | `system/` — user, role, menu, dept, post, dict, config, notice, oss, ossConfig, tenant, tenantPackage, client, social, … |
|
||||||
|
| **Monitor** | `monitor/` — online, operlog, loginInfo, cache |
|
||||||
|
| **Tool** | `tool/gen/` — code generation |
|
||||||
|
| **Workflow** | `workflow/` — category, definition, instance, task, leave, spel, workflowCommon |
|
||||||
|
| **Demo** | `demo/demo`, `demo/tree` |
|
||||||
|
| **Project extensions** | `WarningList/`, `equipmentList/`, `fileList/`, `systemList/` — domain-specific APIs alongside RuoYi modules |
|
||||||
|
| **Other** | `menu.ts` — menu-related calls at API root |
|
||||||
|
|
||||||
|
### `src/views/` — routed pages
|
||||||
|
|
||||||
|
Lazy-loaded via `import.meta.glob` in the permission store. Mirrors `src/api/` layout:
|
||||||
|
|
||||||
|
- **`system/`** — CRUD pages for users, roles, menus, tenants, dict, OSS, etc.
|
||||||
|
- **`monitor/`** — online users, operlog, logininfor, cache, admin, snailjob
|
||||||
|
- **`tool/gen/`** — generator UI
|
||||||
|
- **`workflow/`** — process definition/instance, tasks, leave, spel, category
|
||||||
|
- **`demo/`** — sample pages
|
||||||
|
- **`error/`** — 404 and error views
|
||||||
|
- **`redirect/`** — redirect helper route
|
||||||
|
- **Extensions** — `WarningList/`, `equipmentList/`, `fileList/`, `systemList/` (e.g. `systemList/user`, `base`, `password`, `role`)
|
||||||
|
- **Root views** — `login.vue`, `register.vue`, `index.vue` (home/dashboard)
|
||||||
|
|
||||||
|
### `src/components/` — shared Vue components
|
||||||
|
|
||||||
|
Reusable UI (each subfolder is typically a small component package): `Breadcrumb`, `DictTag`, `Editor`, `FileUpload`, `ImageUpload`, `ImagePreview`, `Pagination`, `SvgIcon`, `IconSelect`, `Process` (workflow), `UserSelect`, `RoleSelect`, `LangSelect`, `SizeSelect`, `Screenfull`, `Hamburger`, `iFrame`, `TopNav`, `RightToolbar`, `ParentView`, `RuoYiDoc`, `RuoYiGit`, etc.
|
||||||
|
|
||||||
|
### `src/layout/` — application shell
|
||||||
|
|
||||||
|
- **`index.vue`** — main layout wrapper
|
||||||
|
- **`components/`** — `AppMain`, `Navbar`, `Sidebar/` (menu, logo, items), `TagsView`, `Settings`, `TopBar`, `notice`, `IframeToggle`, `InnerLink`, `SocialCallback`
|
||||||
|
|
||||||
|
### `src/store/modules/` — Pinia stores
|
||||||
|
|
||||||
|
`user`, `permission`, `settings`, `tagsView`, `dict`, `notice`, `app`, etc. (see Architecture table below).
|
||||||
|
|
||||||
|
### Other `src/` directories
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `router/` | Static route table; dynamic routes appended at runtime |
|
||||||
|
| `plugins/` | `index.ts` registers global properties (`$tab`, `$modal`, `$download`, …) |
|
||||||
|
| `directive/` | `permission/` (v-hasPermi, v-hasRoles), `common/` |
|
||||||
|
| `utils/` | `request.ts`, crypto, theme, ruoyi helpers, websocket, SSE, … |
|
||||||
|
| `lang/` | i18n messages (`zh_CN`, `en_US`) and `index.ts` |
|
||||||
|
| `hooks/` | Composables (e.g. `useDialog`) |
|
||||||
|
| `enums/` | Shared enums (HTTP codes, etc.) |
|
||||||
|
| `types/` | Global `.d.ts` and TS augmentations |
|
||||||
|
| `assets/` | `logo/`, `images/`, `icons/svg/`, `styles/` (global SCSS) |
|
||||||
|
|
||||||
|
### Directory tree (compact)
|
||||||
|
|
||||||
|
```
|
||||||
|
Visualize/
|
||||||
|
├── bin/
|
||||||
|
├── html/
|
||||||
|
├── public/
|
||||||
|
├── vite/
|
||||||
|
│ └── plugins/ # auto-import, icons, compression, …
|
||||||
|
├── src/
|
||||||
|
│ ├── api/ # REST modules by domain (+ WarningList, equipmentList, …)
|
||||||
|
│ ├── assets/ # images, svg icons, logo, scss
|
||||||
|
│ ├── components/ # shared UI (see list above)
|
||||||
|
│ ├── directive/
|
||||||
|
│ ├── enums/
|
||||||
|
│ ├── hooks/
|
||||||
|
│ ├── lang/
|
||||||
|
│ ├── layout/ # shell + layout/components/*
|
||||||
|
│ ├── plugins/
|
||||||
|
│ ├── router/
|
||||||
|
│ ├── store/modules/
|
||||||
|
│ ├── types/
|
||||||
|
│ ├── utils/
|
||||||
|
│ ├── views/ # pages: system, monitor, workflow, tool, demo, …
|
||||||
|
│ ├── App.vue
|
||||||
|
│ ├── main.ts
|
||||||
|
│ ├── permission.ts
|
||||||
|
│ └── settings.ts
|
||||||
|
├── CLAUDE.md
|
||||||
|
├── index.html
|
||||||
|
├── package.json
|
||||||
|
├── uno.config.ts
|
||||||
|
├── vite.config.ts
|
||||||
|
└── tsconfig.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies (pnpm is used, despite docs mentioning npm)
|
||||||
|
pnpm install --registry=https://registry.npmmirror.com
|
||||||
|
|
||||||
|
# Dev server on port 80, proxies /dev-api to localhost:8080
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# Production build
|
||||||
|
pnpm build:prod
|
||||||
|
|
||||||
|
# Linting
|
||||||
|
pnpm lint:eslint # check only
|
||||||
|
pnpm lint:eslint:fix # auto-fix
|
||||||
|
pnpm prettier # format all files
|
||||||
|
|
||||||
|
# Preview production build
|
||||||
|
pnpm preview
|
||||||
|
```
|
||||||
|
|
||||||
|
There are no tests configured in this project.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Permission & Routing
|
||||||
|
|
||||||
|
The routing system is permission-driven:
|
||||||
|
|
||||||
|
1. [src/permission.ts](src/permission.ts) is the navigation guard. On first visit with a valid token, it calls `useUserStore().getInfo()` to fetch roles/permissions, then `usePermissionStore().generateRoutes()` to build dynamic routes from the backend menu API.
|
||||||
|
2. [src/router/index.ts](src/router/index.ts) defines `constantRoutes` (login, 404, redirect, home) and an empty `dynamicRoutes` array. Dynamic routes returned from the backend are added via `router.addRoute()`.
|
||||||
|
3. The backend returns route metadata as JSON; [src/store/modules/permission.ts](src/store/modules/permission.ts) maps component strings like `'Layout'`, `'ParentView'` to actual Vue components, and lazy-loads view components via `import.meta.glob('./../../views/**/*.vue')`.
|
||||||
|
4. Route guards check `roles` and `permissions` arrays on each user. Button-level permission uses the `v-hasPermi` / `v-hasRoles` directives defined in [src/directive/permission/](src/directive/permission/).
|
||||||
|
|
||||||
|
### API Layer & Encryption
|
||||||
|
|
||||||
|
- [src/utils/request.ts](src/utils/request.ts): Axios instance with interceptors for token injection, repeat-submit prevention (session-based, 500ms window), and optional RSA+AES body encryption (`VITE_APP_ENCRYPT=true`). Response interceptor handles 401 → re-login prompt, error code mapping from [src/enums/RespEnum.ts](src/enums/RespEnum.ts).
|
||||||
|
- [src/utils/crypto.ts](src/utils/crypto.ts): AES key generation, encrypt/decrypt utilities.
|
||||||
|
- [src/utils/jsencrypt.ts](src/utils/jsencrypt.ts): RSA encrypt/decrypt for the AES key exchange header.
|
||||||
|
- API modules are organized under [src/api/](src/api/) mirroring backend controllers: `system/`, `monitor/`, `tool/`, `workflow/`, `demo/`, plus project-specific folders (e.g. `WarningList/`, `equipmentList/`) — see **Project Directory Structure**.
|
||||||
|
|
||||||
|
### State Management (Pinia)
|
||||||
|
|
||||||
|
| Store module | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `app` | Sidebar toggle, device type, language locale, element size |
|
||||||
|
| `user` | Token, roles, permissions, login/logout/getInfo |
|
||||||
|
| `permission` | Dynamic route generation from backend menu data |
|
||||||
|
| `settings` | Layout config (theme, nav layout, tagsView, etc.), persisted to localStorage |
|
||||||
|
| `tagsView` | Open page tabs state |
|
||||||
|
| `dict` | Data dictionary cache |
|
||||||
|
| `notice` | Notification/websocket state |
|
||||||
|
|
||||||
|
### Global Plugins & Directives
|
||||||
|
|
||||||
|
[src/plugins/index.ts](src/plugins/index.ts) installs these on `app.config.globalProperties`:
|
||||||
|
- `$tab` — page tab operations
|
||||||
|
- `$modal` — modal dialogs
|
||||||
|
- `$cache` — session/local storage helpers
|
||||||
|
- `$download` — file download with loading indicator
|
||||||
|
- `$auth` — permission check methods (`hasPermi`, `hasRole`, etc.)
|
||||||
|
- Utility functions: `useDict`, `parseTime`, `addDateRange`, `handleTree`, `selectDictLabel`
|
||||||
|
|
||||||
|
Custom directives (v-hasPermi, v-hasRoles, v-copyText) registered in [src/directive/index.ts](src/directive/index.ts).
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
[src/layout/index.vue](src/layout/index.vue) is the main layout shell. Key sub-components:
|
||||||
|
- `Sidebar/` — left sidebar menu (renders from `permissionStore.sidebarRouters`)
|
||||||
|
- `Navbar.vue` — top bar with breadcrumb, user menu, fullscreen toggle
|
||||||
|
- `TagsView/` — tabbed page navigation
|
||||||
|
- `Settings/` — layout configuration drawer
|
||||||
|
- `TopBar/` — alternative top-navigation bar layout
|
||||||
|
|
||||||
|
### Views & API Organization
|
||||||
|
|
||||||
|
`src/views/` and `src/api/` are organized by business domain:
|
||||||
|
|
||||||
|
- `system/` — Users, roles, menus, departments, tenants, dictionary, config, notices, OSS
|
||||||
|
- `monitor/` — Online users, operation logs, login logs, cache monitoring, admin/snail-job dashboards
|
||||||
|
- `workflow/` — Process definitions, instances, tasks, categories
|
||||||
|
- `tool/` — Code generation
|
||||||
|
- `demo/` — Demo/example pages
|
||||||
|
- **This repo also adds** `WarningList/`, `equipmentList/`, `fileList/`, `systemList/` under both `views/` and `api/` (see **Project Directory Structure** above).
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
|
||||||
|
- [UnoCSS](https://unocss.dev) for utility-first CSS (see [uno.config.ts](uno.config.ts))
|
||||||
|
- Element Plus with SCSS preprocessing
|
||||||
|
- Dark mode via Element Plus CSS vars (`element-plus/theme-chalk/dark/css-vars.css`)
|
||||||
|
- Theme color configurable in settings, applied in [src/utils/theme.ts](src/utils/theme.ts)
|
||||||
|
|
||||||
|
### i18n
|
||||||
|
|
||||||
|
Vue I18n with `zh_CN` (default) and `en_US` in [src/lang/](src/lang/). Language preference persisted to localStorage.
|
||||||
|
|
||||||
|
## Key Conventions
|
||||||
|
|
||||||
|
- Path alias `@/` maps to `src/`
|
||||||
|
- `@/` is used for all internal imports
|
||||||
|
- Environment variables prefixed with `VITE_APP_` are available via `import.meta.env`
|
||||||
|
- Vite dev server proxies `VITE_APP_BASE_API` (default `/dev-api`) to `http://localhost:8080`
|
||||||
|
- `.claude/` is listed in [`.gitignore`](.gitignore) and is not committed; **`CLAUDE.md` at repo root is tracked** and documents this project for AI / contributors.
|
||||||
@@ -2,10 +2,11 @@
|
|||||||
"$schema": "https://json.schemastore.org/package",
|
"$schema": "https://json.schemastore.org/package",
|
||||||
"name": "ruoyi-vue-plus",
|
"name": "ruoyi-vue-plus",
|
||||||
"version": "5.6.1-2.6.1",
|
"version": "5.6.1-2.6.1",
|
||||||
"description": "RuoYi-Vue-Plus多租户管理系统",
|
"description": "可视化电子围栏管理系统",
|
||||||
"author": "LionLi",
|
"author": "LionLi",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"packageManager": "pnpm@10.34.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite serve --mode development",
|
"dev": "vite serve --mode development",
|
||||||
"build:prod": "vite build --mode production",
|
"build:prod": "vite build --mode production",
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
"element-plus": "2.13.5",
|
"element-plus": "2.13.5",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"highlight.js": "11.11.1",
|
"highlight.js": "11.11.1",
|
||||||
|
"hls.js": "^1.6.16",
|
||||||
"image-conversion": "2.1.1",
|
"image-conversion": "2.1.1",
|
||||||
"js-cookie": "3.0.5",
|
"js-cookie": "3.0.5",
|
||||||
"jsencrypt": "3.5.4",
|
"jsencrypt": "3.5.4",
|
||||||
|
|||||||
26
src/api/WarningList/index.ts
Normal file
26
src/api/WarningList/index.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import { DemoQuery } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备告警分页列表
|
||||||
|
* @param query id、deviceId、分页、时间范围等
|
||||||
|
*/
|
||||||
|
export const listDemo = (query?: any) => {
|
||||||
|
return request({
|
||||||
|
url: '/device/deviceAlarm/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理告警(与后端约定:POST /device/deviceAlarm/edit?ids=1,2)
|
||||||
|
*/
|
||||||
|
export const updateDemo = (ids: string | number | Array<string | number>) => {
|
||||||
|
const idsParam = Array.isArray(ids) ? ids.join(',') : String(ids);
|
||||||
|
return request({
|
||||||
|
url: '/device/deviceAlarm/edit',
|
||||||
|
method: 'post',
|
||||||
|
params: { ids: idsParam }
|
||||||
|
});
|
||||||
|
};
|
||||||
32
src/api/WarningList/types.ts
Normal file
32
src/api/WarningList/types.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/** 设备告警列表行(与 /device/deviceAlarm/list 返回一致) */
|
||||||
|
export interface DemoVO {
|
||||||
|
id?: string | number;
|
||||||
|
deviceId?: string | number | null;
|
||||||
|
deviceName?: string | null;
|
||||||
|
userName?: string | null;
|
||||||
|
monitoringArea?: string | null;
|
||||||
|
alarmTime?: string | null;
|
||||||
|
status?: number | null;
|
||||||
|
userId?: string | number | null;
|
||||||
|
processingTime?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DemoForm extends BaseEntity, DemoVO {}
|
||||||
|
|
||||||
|
export interface DemoQuery extends PageQuery {
|
||||||
|
id?: string | number;
|
||||||
|
deviceId?: string | number;
|
||||||
|
deviceName?: string;
|
||||||
|
monitoringArea?: string;
|
||||||
|
status?: number;
|
||||||
|
userName?: string;
|
||||||
|
userId?: string | number;
|
||||||
|
beginTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 告警列表响应结构 */
|
||||||
|
export interface WarningListResult {
|
||||||
|
rows: DemoVO[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
@@ -88,3 +88,13 @@ export interface DemoQuery extends PageQuery {
|
|||||||
*/
|
*/
|
||||||
value?: string;
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DrawFrom {
|
||||||
|
x?: string | number;
|
||||||
|
|
||||||
|
y?: string | number;
|
||||||
|
|
||||||
|
w?: string | number;
|
||||||
|
|
||||||
|
h?: string | number;
|
||||||
|
}
|
||||||
|
|||||||
92
src/api/equipmentList/index.ts
Normal file
92
src/api/equipmentList/index.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { EquipmentList, DeviceInfoForm } from '@/api/equipmentList/types';
|
||||||
|
import { AxiosPromise } from 'axios';
|
||||||
|
import { DemoVO, DemoForm, DemoQuery, DrawFrom } from '@/api/demo/demo/types';
|
||||||
|
import { AlarmAreaData } from '@/api/equipmentList/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增设备
|
||||||
|
* @param data 设备数据
|
||||||
|
* @returns AxiosPromise<any>
|
||||||
|
*/
|
||||||
|
export const addEquipment = (data: DemoForm) => {
|
||||||
|
return request({
|
||||||
|
url: '/device/info/add',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询设备列表
|
||||||
|
* @param query 查询参数
|
||||||
|
* @returns AxiosPromise<DemoVO[]>
|
||||||
|
*/
|
||||||
|
export const getDevicetList = (query: EquipmentList) => {
|
||||||
|
return request({
|
||||||
|
url: '/device/info/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备详情(路径参数 id)
|
||||||
|
* @param id 设备主键
|
||||||
|
*/
|
||||||
|
export const getDeviceInfo = (id: string | number) => {
|
||||||
|
return request({
|
||||||
|
url: `/device/info/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除设备
|
||||||
|
* @param ids 设备主键列表(Query:ids=1,2,3)
|
||||||
|
*/
|
||||||
|
export const delDevice = (id: string | number | Array<string | number>) => {
|
||||||
|
const idsParam = Array.isArray(id) ? id.join(',') : String(id);
|
||||||
|
return request({
|
||||||
|
url: '/device/info/delete',
|
||||||
|
method: 'post',
|
||||||
|
params: { ids: idsParam }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 激活设备
|
||||||
|
* @param id 单个主键或主键数组(与删除相同:POST + Query,ids=1,2,3)
|
||||||
|
*/
|
||||||
|
export const actDevice = (id: string | number | Array<string | number>) => {
|
||||||
|
const idsParam = Array.isArray(id) ? id.join(',') : String(id);
|
||||||
|
return request({
|
||||||
|
url: '/device/info/activate',
|
||||||
|
method: 'post',
|
||||||
|
params: { ids: idsParam }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑设备
|
||||||
|
* @param data 设备信息(含 id)
|
||||||
|
*/
|
||||||
|
export const updateDeviceInfo = (data: DeviceInfoForm) => {
|
||||||
|
return request({
|
||||||
|
url: '/device/info/edit',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* todo绘制区域
|
||||||
|
* @param data 绘制信息数组
|
||||||
|
* /haikang/isup/device/motion-detection
|
||||||
|
*/
|
||||||
|
export const drawInfo = (data: AlarmAreaData[]) => {
|
||||||
|
return request({
|
||||||
|
url: '/haikang/isup/device/motion-detection',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
};
|
||||||
44
src/api/equipmentList/types.ts
Normal file
44
src/api/equipmentList/types.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
//查询设备列表参数
|
||||||
|
export interface EquipmentList {
|
||||||
|
code: string | number;
|
||||||
|
|
||||||
|
name: string | number;
|
||||||
|
|
||||||
|
monitoringArea: string | number;
|
||||||
|
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
/** 设备编辑 / 新增提交体(与 /device/info/edit 入参一致) */
|
||||||
|
export interface DeviceInfoForm extends BaseEntity {
|
||||||
|
id?: string | number;
|
||||||
|
code?: string;
|
||||||
|
name?: string;
|
||||||
|
monitoringArea?: string;
|
||||||
|
drawArea?: string;
|
||||||
|
status?: number;
|
||||||
|
model?: string;
|
||||||
|
isDrawArea?: number;
|
||||||
|
isDelete?: number;
|
||||||
|
/** 若依扩展参数 */
|
||||||
|
params?: Record<string, Record<string, unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceInfo {
|
||||||
|
id?: number;
|
||||||
|
code?: string;
|
||||||
|
name?: string;
|
||||||
|
monitoringArea?: string;
|
||||||
|
status?: number | string;
|
||||||
|
model?: string;
|
||||||
|
isDrawArea?: number | string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 报警区域数据类型
|
||||||
|
export interface AlarmAreaData {
|
||||||
|
deviceId: number;
|
||||||
|
x1: number;
|
||||||
|
y1: number;
|
||||||
|
x2: number;
|
||||||
|
y2: number;
|
||||||
|
}
|
||||||
61
src/api/fileList/index.ts
Normal file
61
src/api/fileList/index.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import { AxiosPromise } from 'axios';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询测试单列表
|
||||||
|
* @param query
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
export const listDemo = (query?: any): AxiosPromise<any[]> => {
|
||||||
|
return request({
|
||||||
|
url: '/device/deviceVideo/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询测试单详细
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
export const getDemo = (id: string | number): AxiosPromise<DemoVO> => {
|
||||||
|
return request({
|
||||||
|
url: '/demo/demo/' + id,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增测试单
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export const addDemo = (data: DemoForm) => {
|
||||||
|
return request({
|
||||||
|
url: '/demo/demo',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改测试单
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export const updateDemo = (data: DemoForm) => {
|
||||||
|
return request({
|
||||||
|
url: '/demo/demo',
|
||||||
|
method: 'put',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除测试单
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
export const delDemo = (id: string | number | Array<string | number>) => {
|
||||||
|
return request({
|
||||||
|
url: '/demo/demo/' + id,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
};
|
||||||
0
src/api/fileList/types.ts
Normal file
0
src/api/fileList/types.ts
Normal file
46
src/api/systemList/index.ts
Normal file
46
src/api/systemList/index.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import { AxiosPromise } from 'axios';
|
||||||
|
import { DemoVO, DemoForm, DemoQuery } from '@/api/demo/demo/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询首页logo
|
||||||
|
* /system/logo/get
|
||||||
|
*/
|
||||||
|
export const getDemo = () => {
|
||||||
|
return request({
|
||||||
|
url: '/system/logo/get',
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传图片
|
||||||
|
* /system/logo/upload
|
||||||
|
* @param file 文件对象
|
||||||
|
*/
|
||||||
|
export const uploadDemo = (file: File) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
return request({
|
||||||
|
url: '/system/logo/upload',
|
||||||
|
method: 'post',
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改logo
|
||||||
|
* /system/logo/edit
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export const updateDemo = (data: any) => {
|
||||||
|
return request({
|
||||||
|
url: '/system/logo/edit',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
};
|
||||||
0
src/api/systemList/types.ts
Normal file
0
src/api/systemList/types.ts
Normal file
29
src/api/video/index.ts
Normal file
29
src/api/video/index.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import { AxiosPromise } from 'axios';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始一直播预览
|
||||||
|
* @param query
|
||||||
|
* post
|
||||||
|
* /haikang/isup/stream/live/start?deviceId=2057338485989367810
|
||||||
|
*/
|
||||||
|
export const listDemo = (deviceId?: string | number) => {
|
||||||
|
return request({
|
||||||
|
url: `/haikang/isup/stream/live/start?deviceId=${deviceId}`,
|
||||||
|
method: 'post'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止-直播预览
|
||||||
|
* @param query
|
||||||
|
* post
|
||||||
|
* /haikang/isup/stream/live/stop?id=2057338485989367810&channel=1
|
||||||
|
*/
|
||||||
|
export const addDemo = (data: any) => {
|
||||||
|
return request({
|
||||||
|
url: '/haikang/isup/stream/live/stop',
|
||||||
|
method: 'get',
|
||||||
|
params: data
|
||||||
|
});
|
||||||
|
};
|
||||||
0
src/api/video/types.ts
Normal file
0
src/api/video/types.ts
Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -4,14 +4,16 @@
|
|||||||
|
|
||||||
<breadcrumb v-if="navType == NavTypeEnum.LEFT" id="breadcrumb-container" class="breadcrumb-container" />
|
<breadcrumb v-if="navType == NavTypeEnum.LEFT" id="breadcrumb-container" class="breadcrumb-container" />
|
||||||
<top-nav v-if="navType == NavTypeEnum.MIX" id="topmenu-container" class="topmenu-container" />
|
<top-nav v-if="navType == NavTypeEnum.MIX" id="topmenu-container" class="topmenu-container" />
|
||||||
|
<div class="ml-[10px]">
|
||||||
|
<el-button link icon="Refresh" @click="refresh()" />
|
||||||
|
</div>
|
||||||
<template v-if="navType == NavTypeEnum.TOP">
|
<template v-if="navType == NavTypeEnum.TOP">
|
||||||
<logo v-show="showLogo" :collapse="false"></logo>
|
<logo v-show="showLogo" :collapse="false"></logo>
|
||||||
<top-bar id="topbar-container" class="topbar-container" />
|
<top-bar id="topbar-container" class="topbar-container" />
|
||||||
</template>
|
</template>
|
||||||
<div class="right-menu flex align-center">
|
<div class="right-menu flex align-center">
|
||||||
<template v-if="appStore.device !== 'mobile'">
|
<div v-if="appStore.device !== 'mobile'">
|
||||||
<el-select
|
<!-- <el-select
|
||||||
v-if="userId === 1 && tenantEnabled"
|
v-if="userId === 1 && tenantEnabled"
|
||||||
v-model="companyName"
|
v-model="companyName"
|
||||||
class="min-w-244px mr-2"
|
class="min-w-244px mr-2"
|
||||||
@@ -24,30 +26,39 @@
|
|||||||
>
|
>
|
||||||
<el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"> </el-option>
|
<el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"> </el-option>
|
||||||
<template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
|
<template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
|
||||||
</el-select>
|
</el-select> -->
|
||||||
|
|
||||||
<search-menu ref="searchMenuRef" />
|
<search-menu ref="searchMenuRef" />
|
||||||
<el-tooltip content="搜索" effect="dark" placement="bottom">
|
<!-- <el-tooltip content="搜索" effect="dark" placement="bottom">
|
||||||
<div class="right-menu-item hover-effect" @click="openSearchMenu">
|
<div class="right-menu-item hover-effect" @click="openSearchMenu">
|
||||||
<svg-icon class-name="search-icon" icon-class="search" />
|
<svg-icon class-name="search-icon" icon-class="search" />
|
||||||
</div>
|
</div>
|
||||||
</el-tooltip>
|
</el-tooltip> -->
|
||||||
<!-- 消息 -->
|
<!-- 消息 -->
|
||||||
<el-tooltip :content="proxy.$t('navbar.message')" effect="dark" placement="bottom">
|
<el-tooltip content="警报" effect="dark" placement="bottom">
|
||||||
<div style="display:flex;align-items:center">
|
<div style="display: flex; align-items: center" class="mr-[30px]">
|
||||||
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
|
<el-popover
|
||||||
|
placement="bottom"
|
||||||
|
transition="el-zoom-in-top"
|
||||||
|
:width="400"
|
||||||
|
:persistent="false"
|
||||||
|
trigger="click"
|
||||||
|
v-model:visible="popoverVisible"
|
||||||
|
>
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-badge :value="newNotice > 0 ? newNotice : ''" :max="99">
|
<el-badge :value="infoData" :max="999">
|
||||||
<div class="right-menu-item hover-effect"><svg-icon icon-class="message" /></div>
|
<div class="right-menu-item hover-effect" @click="goToWarningList">
|
||||||
|
<el-icon><BellFilled /></el-icon>
|
||||||
|
</div>
|
||||||
</el-badge>
|
</el-badge>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<notice></notice>
|
<notice :warningList="warningList"></notice>
|
||||||
</template>
|
</template>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
</div>
|
</div>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip content="Github" effect="dark" placement="bottom">
|
<!-- <el-tooltip content="Github" effect="dark" placement="bottom">
|
||||||
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
|
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
|
|
||||||
@@ -65,11 +76,12 @@
|
|||||||
|
|
||||||
<el-tooltip :content="proxy.$t('navbar.layoutSize')" effect="dark" placement="bottom">
|
<el-tooltip :content="proxy.$t('navbar.layoutSize')" effect="dark" placement="bottom">
|
||||||
<size-select id="size-select" class="right-menu-item hover-effect" />
|
<size-select id="size-select" class="right-menu-item hover-effect" />
|
||||||
</el-tooltip>
|
</el-tooltip> -->
|
||||||
</template>
|
</div>
|
||||||
<div class="avatar-container">
|
<div class="avatar-container">
|
||||||
<el-dropdown class="right-menu-item hover-effect" trigger="click" @command="handleCommand">
|
<el-dropdown class="right-menu-item hover-effect" trigger="click" @command="handleCommand">
|
||||||
<div class="avatar-wrapper">
|
<div class="avatar-wrapper">
|
||||||
|
<!-- <img :src="logoUrl" class="user-avatar" /> -->
|
||||||
<img :src="userStore.avatar" class="user-avatar" />
|
<img :src="userStore.avatar" class="user-avatar" />
|
||||||
<el-icon><caret-bottom /></el-icon>
|
<el-icon><caret-bottom /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,9 +90,9 @@
|
|||||||
<router-link v-if="!dynamic" to="/user/profile">
|
<router-link v-if="!dynamic" to="/user/profile">
|
||||||
<el-dropdown-item>{{ proxy.$t('navbar.personalCenter') }}</el-dropdown-item>
|
<el-dropdown-item>{{ proxy.$t('navbar.personalCenter') }}</el-dropdown-item>
|
||||||
</router-link>
|
</router-link>
|
||||||
<el-dropdown-item v-if="settingsStore.showSettings" command="setLayout">
|
<!-- <el-dropdown-item v-if="settingsStore.showSettings" command="setLayout">
|
||||||
<span>{{ proxy.$t('navbar.layoutSetting') }}</span>
|
<span>{{ proxy.$t('navbar.layoutSetting') }}</span>
|
||||||
</el-dropdown-item>
|
</el-dropdown-item> -->
|
||||||
<el-dropdown-item divided command="logout">
|
<el-dropdown-item divided command="logout">
|
||||||
<span>{{ proxy.$t('navbar.logout') }}</span>
|
<span>{{ proxy.$t('navbar.logout') }}</span>
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
@@ -105,14 +117,21 @@ import notice from './notice/index.vue';
|
|||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
import { ElMessageBoxOptions } from 'element-plus/es/components/message-box/src/message-box.type';
|
import { ElMessageBoxOptions } from 'element-plus/es/components/message-box/src/message-box.type';
|
||||||
import { NavTypeEnum } from '@/enums/NavTypeEnum';
|
import { NavTypeEnum } from '@/enums/NavTypeEnum';
|
||||||
import Logo from "@/layout/components/Sidebar/Logo.vue";
|
import Logo from '@/layout/components/Sidebar/Logo.vue';
|
||||||
import TopBar from './TopBar'
|
import TopBar from './TopBar';
|
||||||
|
import { getDemo } from '@/api/systemList/index';
|
||||||
|
import { useWarningListStore } from '@/store/modules/warnningList';
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
const warningListStore = useWarningListStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const noticeStore = storeToRefs(useNoticeStore());
|
const noticeStore = storeToRefs(useNoticeStore());
|
||||||
const newNotice = ref(<number>0);
|
const newNotice = ref(<number>0);
|
||||||
|
const { warningList, warningListTotal } = storeToRefs(warningListStore);
|
||||||
|
|
||||||
|
const infoData = ref(warningListStore.warningListTotal ?? 0);
|
||||||
|
const popoverVisible = ref(false);
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
|
|
||||||
@@ -204,8 +223,67 @@ const handleCommand = (command: string) => {
|
|||||||
//用深度监听 消息
|
//用深度监听 消息
|
||||||
watch(
|
watch(
|
||||||
() => noticeStore.state.value.notices,
|
() => noticeStore.state.value.notices,
|
||||||
(newVal) => {
|
async (newVal) => {
|
||||||
newNotice.value = newVal.filter((item: any) => !item.read).length;
|
newNotice.value = newVal.filter((item: any) => !item.read).length;
|
||||||
|
//监听到消息变化,刷新告警列表
|
||||||
|
await warningListStore.getWarningList({
|
||||||
|
status: 0,
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 300
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const refresh = () => {
|
||||||
|
proxy?.$tab.refreshPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToWarningList = () => {
|
||||||
|
router.push('/WarningList/WarningList/list');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取Logo图片
|
||||||
|
const logoUrl = ref();
|
||||||
|
const logoName = ref();
|
||||||
|
onMounted(() => {
|
||||||
|
getDemo().then((res) => {
|
||||||
|
console.log('系统11111111111', res);
|
||||||
|
|
||||||
|
logoName.value = res.data.name;
|
||||||
|
logoUrl.value = res.data.url;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
warningListStore.getWarningList({
|
||||||
|
status: 0,
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 300
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//监听数据变化
|
||||||
|
watch(
|
||||||
|
() => warningListStore.warningListTotal,
|
||||||
|
(newVal) => {
|
||||||
|
infoData.value = newVal;
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
// 告警数据变化时自动弹出弹窗
|
||||||
|
// 当 isNotice 变为 true 时弹出弹窗,并重置为 false
|
||||||
|
// let noticeFirstLoad = true; //解决初始化时弹出弹窗问题
|
||||||
|
watch(
|
||||||
|
() => noticeStore.state.value.isNotice,
|
||||||
|
(newVal) => {
|
||||||
|
// if (noticeFirstLoad) {
|
||||||
|
// noticeFirstLoad = false;
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
if (newVal) {
|
||||||
|
noticeStore.state.value.isNotice = false;
|
||||||
|
popoverVisible.value = true;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
);
|
);
|
||||||
@@ -282,7 +360,6 @@ watch(
|
|||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.errLog-container {
|
.errLog-container {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
@@ -310,7 +387,9 @@ watch(
|
|||||||
|
|
||||||
&.hover-effect {
|
&.hover-effect {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s ease, color 0.2s ease;
|
transition:
|
||||||
|
background 0.2s ease,
|
||||||
|
color 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--el-fill-color-light);
|
background: var(--el-fill-color-light);
|
||||||
@@ -339,7 +418,7 @@ watch(
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -20px;
|
right: -20px;
|
||||||
top: 25px;
|
top: 16px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="sidebar-logo-container" :class="{ collapse: collapse }">
|
||||||
class="sidebar-logo-container"
|
|
||||||
:class="{ collapse: collapse }"
|
|
||||||
>
|
|
||||||
<transition :enter-active-class="proxy?.animate.logoAnimate.enter" mode="out-in">
|
<transition :enter-active-class="proxy?.animate.logoAnimate.enter" mode="out-in">
|
||||||
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
|
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
|
||||||
<img v-if="logo" :src="logo" class="sidebar-logo" />
|
<img v-if="logo" :src="logoUrl" class="sidebar-logo" />
|
||||||
<h1 v-else class="sidebar-title">
|
<h1 v-else class="sidebar-title">{{ logoName }}</h1>
|
||||||
{{ title }}
|
|
||||||
</h1>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
||||||
<img v-if="logo" :src="logo" class="sidebar-logo" />
|
<img v-if="logo" :src="logoUrl" class="sidebar-logo" />
|
||||||
<h1 class="sidebar-title">
|
<h1 class="sidebar-title">{{ logoName }}</h1>
|
||||||
{{ title }}
|
|
||||||
</h1>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,6 +19,7 @@ import logo from '@/assets/logo/logo.png';
|
|||||||
import { useSettingsStore } from '@/store/modules/settings';
|
import { useSettingsStore } from '@/store/modules/settings';
|
||||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
import { NavTypeEnum } from '@/enums/NavTypeEnum';
|
import { NavTypeEnum } from '@/enums/NavTypeEnum';
|
||||||
|
import { getDemo } from '@/api/systemList/index';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
collapse: {
|
collapse: {
|
||||||
@@ -41,24 +35,36 @@ const sideTheme = computed(() => settingsStore.sideTheme);
|
|||||||
// 获取Logo背景色
|
// 获取Logo背景色
|
||||||
const getLogoBackground = computed(() => {
|
const getLogoBackground = computed(() => {
|
||||||
if (settingsStore.isDark) {
|
if (settingsStore.isDark) {
|
||||||
return 'var(--sidebar-bg)'
|
return 'var(--sidebar-bg)';
|
||||||
}
|
}
|
||||||
if (settingsStore.navType == NavTypeEnum.TOP) {
|
if (settingsStore.navType == NavTypeEnum.TOP) {
|
||||||
return variables.menuLightBackground
|
return variables.menuLightBackground;
|
||||||
}
|
}
|
||||||
return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBackground
|
return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBackground;
|
||||||
})
|
});
|
||||||
|
|
||||||
// 获取Logo文字颜色
|
// 获取Logo文字颜色
|
||||||
const getLogoTextColor = computed(() => {
|
const getLogoTextColor = computed(() => {
|
||||||
if (settingsStore.isDark) {
|
if (settingsStore.isDark) {
|
||||||
return 'var(--sidebar-text)'
|
return 'var(--sidebar-text)';
|
||||||
}
|
}
|
||||||
if (settingsStore.navType == NavTypeEnum.TOP) {
|
if (settingsStore.navType == NavTypeEnum.TOP) {
|
||||||
return variables.logoLightTitleColor
|
return variables.logoLightTitleColor;
|
||||||
}
|
}
|
||||||
return sideTheme.value === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor
|
return sideTheme.value === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor;
|
||||||
})
|
});
|
||||||
|
|
||||||
|
// 获取Logo图片
|
||||||
|
const logoUrl = ref();
|
||||||
|
const logoName = ref();
|
||||||
|
onMounted(() => {
|
||||||
|
getDemo().then((res) => {
|
||||||
|
console.log('系统11111111111', res);
|
||||||
|
|
||||||
|
logoName.value = res.data.name;
|
||||||
|
logoUrl.value = res.data.url;
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -97,8 +103,13 @@ const getLogoTextColor = computed(() => {
|
|||||||
color: v-bind(getLogoTextColor);
|
color: v-bind(getLogoTextColor);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
|
font-family:
|
||||||
|
Avenir,
|
||||||
|
Helvetica Neue,
|
||||||
|
Arial,
|
||||||
|
Helvetica,
|
||||||
|
sans-serif;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,10 +95,13 @@ const isLastView = () => {
|
|||||||
};
|
};
|
||||||
const filterAffixTags = (routes: RouteRecordRaw[], basePath = '') => {
|
const filterAffixTags = (routes: RouteRecordRaw[], basePath = '') => {
|
||||||
let tags: RouteLocationNormalized[] = [];
|
let tags: RouteLocationNormalized[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
routes.forEach((route) => {
|
routes.forEach((route) => {
|
||||||
if (route.meta && route.meta.affix) {
|
if (route.meta && route.meta.affix) {
|
||||||
const tagPath = getNormalPath(basePath + '/' + route.path);
|
const tagPath = getNormalPath(basePath + '/' + route.path);
|
||||||
|
if (!seen.has(tagPath)) {
|
||||||
|
seen.add(tagPath);
|
||||||
tags.push({
|
tags.push({
|
||||||
hash: '',
|
hash: '',
|
||||||
matched: [],
|
matched: [],
|
||||||
@@ -111,6 +114,7 @@ const filterAffixTags = (routes: RouteRecordRaw[], basePath = '') => {
|
|||||||
meta: { ...route.meta }
|
meta: { ...route.meta }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (route.children) {
|
if (route.children) {
|
||||||
const tempTags = filterAffixTags(route.children, route.path);
|
const tempTags = filterAffixTags(route.children, route.path);
|
||||||
if (tempTags.length >= 1) {
|
if (tempTags.length >= 1) {
|
||||||
@@ -124,7 +128,6 @@ const initTags = () => {
|
|||||||
const res = filterAffixTags(routes.value);
|
const res = filterAffixTags(routes.value);
|
||||||
affixTags.value = res;
|
affixTags.value = res;
|
||||||
for (const tag of res) {
|
for (const tag of res) {
|
||||||
// Must have tag name
|
|
||||||
if (tag.name) {
|
if (tag.name) {
|
||||||
useTagsViewStore().addVisitedView(tag);
|
useTagsViewStore().addVisitedView(tag);
|
||||||
}
|
}
|
||||||
@@ -136,6 +139,10 @@ const addTags = () => {
|
|||||||
route.meta.title = route.query.title as string;
|
route.meta.title = route.query.title as string;
|
||||||
}
|
}
|
||||||
if (name) {
|
if (name) {
|
||||||
|
// 如果 visitedViews 中已有同 title 的固定标签(affix),不再添加普通标签
|
||||||
|
if (visitedViews.value.some((v) => v.meta?.affix && v.title === route.meta?.title)) return;
|
||||||
|
// 常规去重:已有同 path 或 fullPath 的标签则跳过
|
||||||
|
if (visitedViews.value.some((v) => v.path === route.path || v.fullPath === route.fullPath)) return;
|
||||||
useTagsViewStore().addView(route as any);
|
useTagsViewStore().addView(route as any);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,33 +1,89 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-loading="state.loading" class="layout-navbars-breadcrumb-user-news">
|
<div v-loading="state.loading" class="layout-navbars-breadcrumb-user-news">
|
||||||
<div class="head-box">
|
<div class="head-box">
|
||||||
<div class="head-box-title">通知公告</div>
|
<el-icon><Sunrise /></el-icon>
|
||||||
<div class="head-box-btn" @click="readAll">全部已读</div>
|
<div class="head-box-title"><p style="color: red; font-size: 16px; font-weight: 800">入侵警告</p></div>
|
||||||
|
<!-- <div class="head-box-btn" @click="readAll">全部已读</div> -->
|
||||||
</div>
|
</div>
|
||||||
<div v-loading="state.loading" class="content-box">
|
<div v-loading="state.loading" class="content-box">
|
||||||
<template v-if="newsList.length > 0">
|
<template v-if="warningListRows.length > 0">
|
||||||
<div v-for="(v, k) in newsList" :key="k" class="content-box-item" @click="onNewsClick(k)">
|
<!-- <div v-for="(v, k) in newsList" :key="k" class="content-box-item" @click="onNewsClick(k)"> -->
|
||||||
|
<div v-for="(v, k) in warningListRows" :key="k" class="content-box-item">
|
||||||
<div class="item-conten">
|
<div class="item-conten">
|
||||||
<div>{{ v.message }}</div>
|
<div>{{ `设备[${v.deviceName}] 检测到异常移动, ${v.alarmTime}` }}</div>
|
||||||
<div class="content-box-msg"></div>
|
<div class="content-box-msg"></div>
|
||||||
<div class="content-box-time">{{ v.time }}</div>
|
<!-- <div class="content-box-time">{{ v.time }}</div> -->
|
||||||
</div>
|
</div>
|
||||||
<!-- 已读/未读 -->
|
<!-- 已读/未读 -->
|
||||||
<span v-if="v.read" class="el-tag el-tag--success el-tag--mini read">已读</span>
|
<!-- <span v-if="v.read" class="el-tag el-tag--success el-tag--mini read">已读</span> -->
|
||||||
<span v-else class="el-tag el-tag--danger el-tag--mini read">未读</span>
|
<el-button type="danger" @click="handleSee(v)">查看画面</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-empty v-else :description="'消息为空'"></el-empty>
|
<el-empty v-else :description="'消息为空'"></el-empty>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="newsList.length > 0" class="foot-box" @click="onGoToGiteeClick">前往gitee</div>
|
<!-- <div v-if="newsList.length > 0" class="foot-box" @click="onGoToGiteeClick">前往gitee</div> -->
|
||||||
|
<!-- 添加或修改测试单对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialog.visible"
|
||||||
|
:title="dialog.title"
|
||||||
|
width="500px"
|
||||||
|
class="monitor-dialog"
|
||||||
|
:before-close="handleMonitorDialogClose"
|
||||||
|
fullscreen
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex w-full items-center justify-between gap-2 min-w-0 pr-8">
|
||||||
|
<span class="truncate">实时监控-{{ monitoringArea }}监控</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="monitor-video-wrap">
|
||||||
|
<div class="monitor-video-stack">
|
||||||
|
<video ref="monitorVideoRef" class="monitor-video" controls preload="metadata" :src="monitorVideoUrl">您的浏览器不支持视频播放</video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts" name="layoutBreadcrumbUserNews">
|
<script setup lang="ts" name="layoutBreadcrumbUserNews">
|
||||||
import { useNoticeStore } from '@/store/modules/notice';
|
import { useNoticeStore } from '@/store/modules/notice';
|
||||||
|
import { listNotice } from '@/api/system/notice';
|
||||||
|
import { initHls, destroyHls } from '@/utils/video';
|
||||||
|
import { onBeforeRouteLeave } from 'vue-router';
|
||||||
|
import { useWarningListStore } from '@/store/modules/warnningList';
|
||||||
|
import type { WarningListResult } from '@/api/WarningList/types';
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
|
|
||||||
|
import { nextTick } from 'vue';
|
||||||
|
import { useVideosStore } from '@/store/modules/video';
|
||||||
|
const videosStore = useVideosStore();
|
||||||
|
const monitoringArea = ref<string>('');
|
||||||
|
|
||||||
|
const warningListStore = useWarningListStore();
|
||||||
const noticeStore = useNoticeStore();
|
const noticeStore = useNoticeStore();
|
||||||
|
const { getWarningList, handleAction } = warningListStore;
|
||||||
|
const { warningListTotal, warningList } = storeToRefs(warningListStore);
|
||||||
|
|
||||||
|
// const props = defineProps<{
|
||||||
|
// warningList?: WarningListResult;
|
||||||
|
// }>();
|
||||||
|
const warningListRows = computed(() => warningList.value?.rows || []);
|
||||||
const { readAll } = useNoticeStore();
|
const { readAll } = useNoticeStore();
|
||||||
|
const monitorVideoRef = ref<HTMLVideoElement>();
|
||||||
|
|
||||||
|
const getList = async () => {
|
||||||
|
try {
|
||||||
|
await getWarningList({
|
||||||
|
status: 0,
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 300
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取消息列表失败', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 定义变量内容
|
// 定义变量内容
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
@@ -35,21 +91,108 @@ const state = reactive({
|
|||||||
});
|
});
|
||||||
const newsList = ref([]) as any;
|
const newsList = ref([]) as any;
|
||||||
|
|
||||||
|
const monitorVideoUrl = ref();
|
||||||
|
const dialog = reactive<DialogOption>({
|
||||||
|
visible: false,
|
||||||
|
title: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const parseNoticeItem = (notice: any) => {
|
||||||
|
const rawMessage = String(notice.message ?? '');
|
||||||
|
const [messagePart, videoIdPart] = rawMessage.split('|', 2);
|
||||||
|
return {
|
||||||
|
...notice,
|
||||||
|
rawMessage,
|
||||||
|
message: messagePart?.trim() ?? rawMessage,
|
||||||
|
videoId: videoIdPart?.trim() ?? ''
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
//查看画面
|
||||||
|
const handleSee = async (row: any) => {
|
||||||
|
const videoId = row?.deviceId;
|
||||||
|
console.log('查看画面', row, 'deviceId', videoId);
|
||||||
|
if (!videoId) {
|
||||||
|
proxy?.$modal.msgError('视频ID不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await videosStore.getVideos(videoId);
|
||||||
|
monitorVideoUrl.value = videosStore.hisUrl;
|
||||||
|
monitoringArea.value = row.monitoringArea;
|
||||||
|
dialog.visible = true;
|
||||||
|
await nextTick();
|
||||||
|
initHls(monitorVideoUrl, monitorVideoRef);
|
||||||
|
await handleAction(row.id);
|
||||||
|
await getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMonitorDialogClose = async (done: () => void) => {
|
||||||
|
// 卸载 Hls 与清理 video
|
||||||
|
destroyHls(monitorVideoRef);
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 过滤消息(只保留最新的5条)
|
||||||
|
* @param messages 消息列表
|
||||||
|
* @returns 过滤后的消息列表
|
||||||
|
*/
|
||||||
|
const filterMessages = (messages: any[]) => {
|
||||||
|
// 如果消息超过5条,则只保留最新的5条
|
||||||
|
if (messages.length > 5) {
|
||||||
|
return messages.slice(-5); // 只取最后5条最新消息
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化数据
|
* 初始化数据
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const getTableData = async () => {
|
const getTableData = async () => {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
newsList.value = noticeStore.state.notices;
|
const allNotices = [...noticeStore.state.notices]; // 复制数组避免直接修改原数据
|
||||||
|
|
||||||
|
// 过滤消息并解析 message/videoId
|
||||||
|
const filteredNotices = allNotices.map(parseNoticeItem);
|
||||||
|
newsList.value = filteredNotices;
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 监听通知变化,自动过滤
|
||||||
|
watch(
|
||||||
|
() => noticeStore.state.notices,
|
||||||
|
(newNotices) => {
|
||||||
|
const filteredNotices = [...newNotices].map(parseNoticeItem);
|
||||||
|
newsList.value = filteredNotices;
|
||||||
|
console.log('监听消息队列', newsList.value);
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
//点击消息,写入已读
|
//点击消息,写入已读
|
||||||
const onNewsClick = (item: any) => {
|
const onNewsClick = (item: any) => {
|
||||||
|
if (newsList.value[item]) {
|
||||||
newsList.value[item].read = true;
|
newsList.value[item].read = true;
|
||||||
//并且写入pinia
|
//并且写入pinia
|
||||||
noticeStore.state.notices = newsList.value;
|
noticeStore.state.notices = noticeStore.state.notices.map((notice: any, index: number) => {
|
||||||
|
if (index === findOriginalIndex(item)) {
|
||||||
|
return { ...notice, read: true };
|
||||||
|
}
|
||||||
|
return notice;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 辅助函数:找到原始索引
|
||||||
|
const findOriginalIndex = (filteredIndex: number) => {
|
||||||
|
const filteredNotices = [...noticeStore.state.notices].map(parseNoticeItem);
|
||||||
|
const currentMessage = filteredNotices[filteredIndex];
|
||||||
|
|
||||||
|
// 在原始数组中查找对应消息的索引
|
||||||
|
return noticeStore.state.notices.findIndex(
|
||||||
|
(notice: any) => String(notice.message) === currentMessage.rawMessage && notice.time === currentMessage.time
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 前往通知中心点击
|
// 前往通知中心点击
|
||||||
@@ -60,20 +203,96 @@ const onGoToGiteeClick = () => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
getTableData();
|
getTableData();
|
||||||
|
console.log('消息列表:', newsList.value);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeRouteLeave(() => {
|
||||||
|
dialog.visible = false;
|
||||||
|
monitorVideoUrl.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
console.log('111111111111111111111_warningList.value', warningList.value);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
/* 全屏监控弹窗:占满视口、禁止外层滚动,视频在可视区内尽可能放大 */
|
||||||
|
:deep(.monitor-dialog.el-dialog) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
max-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.monitor-dialog .el-dialog__header) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.monitor-dialog .el-dialog__body) {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-video-wrap {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-video-stack {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-video {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain; //cover裁剪
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-draw-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2;
|
||||||
|
cursor: crosshair;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.layout-navbars-breadcrumb-user-news {
|
.layout-navbars-breadcrumb-user-news {
|
||||||
.head-box {
|
.head-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
height: 35px;
|
height: 35px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
.el-icon {
|
||||||
|
font-size: 25px;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
.head-box-btn {
|
.head-box-btn {
|
||||||
color: var(--el-color-primary);
|
color: var(--el-color-primary);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -89,15 +308,19 @@ onMounted(() => {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
.content-box-item {
|
.content-box-item {
|
||||||
padding-top: 12px;
|
background-color: rgba(255, 182, 193, 0.5);
|
||||||
|
padding: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
// gap: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
&:last-of-type {
|
&:last-of-type {
|
||||||
padding-bottom: 12px;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
.content-box-msg {
|
.content-box-msg {
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
margin-top: 5px;
|
// margin-top: 5px;
|
||||||
margin-bottom: 5px;
|
// margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
.content-box-time {
|
.content-box-time {
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ const setLayout = () => {
|
|||||||
width: calc(100% - #{$base-sidebar-width});
|
width: calc(100% - #{$base-sidebar-width});
|
||||||
transition: width 0.28s;
|
transition: width 0.28s;
|
||||||
background: $fixed-header-bg;
|
background: $fixed-header-bg;
|
||||||
box-shadow: 0 2px 8px rgba(0, 21, 41, 0.10);
|
box-shadow: 0 2px 8px rgba(0, 21, 41, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hideSidebar .fixed-header {
|
.hideSidebar .fixed-header {
|
||||||
|
|||||||
@@ -26,6 +26,95 @@ import Layout from '@/layout/index.vue';
|
|||||||
|
|
||||||
// 公共路由
|
// 公共路由
|
||||||
export const constantRoutes: RouteRecordRaw[] = [
|
export const constantRoutes: RouteRecordRaw[] = [
|
||||||
|
/* ----------------------------------------自定义路由(查看)-------------------------------------------------------- */
|
||||||
|
// 以下路由已迁移至后端动态路由管理
|
||||||
|
// 设备管理
|
||||||
|
// {
|
||||||
|
// path: '/equipmentList',
|
||||||
|
// component: Layout,
|
||||||
|
// name: 'Equipment',
|
||||||
|
// redirect: '/equipmentList/list',
|
||||||
|
// alwaysShow: true,
|
||||||
|
// meta: { title: '设备管理', icon: 'equipment' },
|
||||||
|
// children: [
|
||||||
|
// {
|
||||||
|
// path: 'list',
|
||||||
|
// name: 'EquipmentList',
|
||||||
|
// component: () => import('@/views/equipmentList/list.vue'),
|
||||||
|
// meta: { title: '设备列表', icon: 'equipment' }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// // 报警管理
|
||||||
|
// {
|
||||||
|
// path: '/warningList',
|
||||||
|
// component: Layout,
|
||||||
|
// name: 'Warning',
|
||||||
|
// redirect: '/warningList/list',
|
||||||
|
// alwaysShow: true,
|
||||||
|
// meta: { title: '报警管理', icon: 'warningList' },
|
||||||
|
// children: [
|
||||||
|
// {
|
||||||
|
// path: 'list',
|
||||||
|
// name: 'WarningList',
|
||||||
|
// component: () => import('@/views/WarningList/list.vue'),
|
||||||
|
// meta: { title: '报警列表', icon: 'warningList' }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// // 录像文件管理
|
||||||
|
// {
|
||||||
|
// path: '/fileList',
|
||||||
|
// component: Layout,
|
||||||
|
// name: 'File',
|
||||||
|
// redirect: '/fileList/list',
|
||||||
|
// meta: { title: '录像文件管理', icon: 'fileList' },
|
||||||
|
// alwaysShow: true,
|
||||||
|
// children: [
|
||||||
|
// {
|
||||||
|
// path: 'list',
|
||||||
|
// name: 'FileList',
|
||||||
|
// component: () => import('@/views/fileList/list.vue'),
|
||||||
|
// meta: { title: '录像列表', icon: 'fileList' }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// // 系统设置
|
||||||
|
// {
|
||||||
|
// path: '/systemList',
|
||||||
|
// component: Layout,
|
||||||
|
// name: 'system',
|
||||||
|
// redirect: '/systemList/base',
|
||||||
|
// alwaysShow: true,
|
||||||
|
// meta: { title: '系统设置', icon: 'systemList' },
|
||||||
|
// children: [
|
||||||
|
// {
|
||||||
|
// path: 'base',
|
||||||
|
// name: 'Base',
|
||||||
|
// component: () => import('@/views/systemList/base/base.vue'),
|
||||||
|
// meta: { title: '基础设置', icon: 'base' }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// path: 'password',
|
||||||
|
// name: 'Password',
|
||||||
|
// component: () => import('@/views/systemList/password/password.vue'),
|
||||||
|
// meta: { title: '修改密码', icon: 'password' }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// path: 'role',
|
||||||
|
// name: 'Role',
|
||||||
|
// component: () => import('@/views/systemList/role/role.vue'),
|
||||||
|
// meta: { title: '角色配置', icon: 'role' }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// path: 'user',
|
||||||
|
// name: 'User',
|
||||||
|
// component: () => import('@/views/systemList/user/user.vue'),
|
||||||
|
// meta: { title: '用户配置', icon: 'user' }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
/* ----------------------------------------自带路由配置-------------------------------------------------------- */
|
||||||
{
|
{
|
||||||
path: '/redirect',
|
path: '/redirect',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
@@ -66,12 +155,13 @@ export const constantRoutes: RouteRecordRaw[] = [
|
|||||||
path: '',
|
path: '',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
redirect: '/index',
|
redirect: '/index',
|
||||||
|
hidden: true,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '/index',
|
path: 'index',
|
||||||
component: () => import('@/views/index.vue'),
|
component: () => import('@/views/equipmentList/list.vue'),
|
||||||
name: 'Index',
|
name: 'EquipmentList',
|
||||||
meta: { title: '首页', icon: 'dashboard', affix: true }
|
meta: { title: '设备列表', icon: 'equipment', affix: true }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -92,9 +182,7 @@ export const constantRoutes: RouteRecordRaw[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
// 动态路由,基于用户权限动态去加载
|
// 动态路由,基于用户权限动态去加载
|
||||||
export const dynamicRoutes: RouteRecordRaw[] = [
|
export const dynamicRoutes: RouteRecordRaw[] = [];
|
||||||
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建路由
|
* 创建路由
|
||||||
|
|||||||
30
src/store/modules/equipmentList.ts
Normal file
30
src/store/modules/equipmentList.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { getDevicetList } from '@/api/equipmentList/index';
|
||||||
|
|
||||||
|
export const useDeviceStore = defineStore('devices', () => {
|
||||||
|
// --- State (状态) ---
|
||||||
|
// 相当于 Vuex 中的 state,用 ref 或 reactive 定义
|
||||||
|
const deviceList = ref();
|
||||||
|
|
||||||
|
// --- Getters (计算属性) ---
|
||||||
|
// 相当于 Vuex 中的 getters,直接用 computed 定义
|
||||||
|
|
||||||
|
// --- Actions (动作) ---
|
||||||
|
// 相当于 Vuex 中的 actions/mutations,直接写同步或异步函数
|
||||||
|
async function getdeviceList(query?: any) {
|
||||||
|
try {
|
||||||
|
const response = await getDevicetList(query);
|
||||||
|
console.log('获取设备列表:', response);
|
||||||
|
deviceList.value = response.rows;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取设备列表失败', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⚠️ 核心点:必须把需要暴露出去的变量和方法 return 出去!
|
||||||
|
return {
|
||||||
|
deviceList,
|
||||||
|
getdeviceList
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -10,7 +10,8 @@ interface NoticeItem {
|
|||||||
|
|
||||||
export const useNoticeStore = defineStore('notice', () => {
|
export const useNoticeStore = defineStore('notice', () => {
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
notices: [] as NoticeItem[]
|
notices: [] as NoticeItem[],
|
||||||
|
isNotice: false as boolean
|
||||||
});
|
});
|
||||||
|
|
||||||
const addNotice = (notice: NoticeItem) => {
|
const addNotice = (notice: NoticeItem) => {
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ export const usePermissionStore = defineStore('permission', () => {
|
|||||||
const setSidebarRouters = (routes: RouteRecordRaw[]): void => {
|
const setSidebarRouters = (routes: RouteRecordRaw[]): void => {
|
||||||
sidebarRouters.value = routes;
|
sidebarRouters.value = routes;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* 生成路由
|
||||||
|
* @returns 路由数组
|
||||||
|
*/
|
||||||
const generateRoutes = async (): Promise<RouteRecordRaw[]> => {
|
const generateRoutes = async (): Promise<RouteRecordRaw[]> => {
|
||||||
const res = await getRouters();
|
const res = await getRouters();
|
||||||
const { data } = res;
|
const { data } = res;
|
||||||
|
|||||||
@@ -37,7 +37,12 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
const addVisitedView = (view: RouteLocationNormalized): void => {
|
const addVisitedView = (view: RouteLocationNormalized): void => {
|
||||||
if (visitedViews.value.some((v: RouteLocationNormalized) => v.path === view.path)) return;
|
// 同时通过 path、fullPath、name 去重
|
||||||
|
if (visitedViews.value.some((v: RouteLocationNormalized) =>
|
||||||
|
v.path === view.path ||
|
||||||
|
v.fullPath === view.fullPath ||
|
||||||
|
(v.name && view.name && v.name === view.name)
|
||||||
|
)) return;
|
||||||
visitedViews.value.push(
|
visitedViews.value.push(
|
||||||
Object.assign({}, view, {
|
Object.assign({}, view, {
|
||||||
title: view.meta?.title || 'no-name'
|
title: view.meta?.title || 'no-name'
|
||||||
|
|||||||
43
src/store/modules/video.ts
Normal file
43
src/store/modules/video.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { addDemo, listDemo } from '@/api/video';
|
||||||
|
import Hls from 'hls.js';
|
||||||
|
|
||||||
|
export const useVideosStore = defineStore('videos', () => {
|
||||||
|
// --- State (状态) ---
|
||||||
|
// 相当于 Vuex 中的 state,用 ref 或 reactive 定义
|
||||||
|
const hisUrl = ref('');
|
||||||
|
|
||||||
|
// --- Getters (计算属性) ---
|
||||||
|
// 相当于 Vuex 中的 getters,直接用 computed 定义
|
||||||
|
|
||||||
|
// --- Actions (动作) ---
|
||||||
|
// 相当于 Vuex 中的 actions/mutations,直接写同步或异步函数
|
||||||
|
async function getVideos(deviceId?: string | number) {
|
||||||
|
hisUrl.value = '';
|
||||||
|
try {
|
||||||
|
const response = await listDemo(deviceId);
|
||||||
|
console.log('获取视频:', response);
|
||||||
|
hisUrl.value = response?.data || '';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取视频失败', error);
|
||||||
|
hisUrl.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopVideos(query: any) {
|
||||||
|
try {
|
||||||
|
const response = await addDemo(query);
|
||||||
|
console.log('停止视频:', response);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('停止视频失败', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⚠️ 核心点:必须把需要暴露出去的变量和方法 return 出去!
|
||||||
|
return {
|
||||||
|
hisUrl,
|
||||||
|
getVideos,
|
||||||
|
stopVideos
|
||||||
|
};
|
||||||
|
});
|
||||||
45
src/store/modules/warnningList.ts
Normal file
45
src/store/modules/warnningList.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/** 此数据源跟表列不一致,(后端接口重复使用因逻辑复杂, 经商定不动后端接口, 设置此数据源) 适用于消息 */
|
||||||
|
/** 抽取公用代码更加复杂模糊-暂存数据源 */
|
||||||
|
import { listDemo, updateDemo } from '@/api/WarningList/index';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { any } from 'vue-types';
|
||||||
|
|
||||||
|
export const useWarningListStore = defineStore('warningListStore', () => {
|
||||||
|
// --- State (状态) ---
|
||||||
|
const warningList = ref();
|
||||||
|
const warningListTotal = ref(0);
|
||||||
|
// --- Getters (计算属性) ---
|
||||||
|
|
||||||
|
// --- Actions (动作) ---
|
||||||
|
async function getWarningList(query?: any) {
|
||||||
|
try {
|
||||||
|
const response = await listDemo({
|
||||||
|
...query
|
||||||
|
// deviceId,
|
||||||
|
// status: 0,
|
||||||
|
// pageNum: 1,
|
||||||
|
// pageSize: 10
|
||||||
|
});
|
||||||
|
warningList.value = response.data;
|
||||||
|
warningListTotal.value = response.data.total || 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取消息列表失败', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function handleAction(ids: string | number | Array<string | number>) {
|
||||||
|
try {
|
||||||
|
await updateDemo(ids);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理告警失败', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 核心点:必须把需要暴露出去的变量和方法 return 出去!
|
||||||
|
return {
|
||||||
|
warningList,
|
||||||
|
warningListTotal,
|
||||||
|
getWarningList,
|
||||||
|
handleAction
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { getToken } from '@/utils/auth';
|
import { getToken } from '@/utils/auth';
|
||||||
import { ElNotification } from 'element-plus';
|
import { ElNotification } from 'element-plus';
|
||||||
import { useNoticeStore } from '@/store/modules/notice';
|
import { useNoticeStore } from '@/store/modules/notice';
|
||||||
|
import { toRefs } from 'vue';
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
export const initSSE = (url: any) => {
|
export const initSSE = (url: any) => {
|
||||||
|
const noticeStore = useNoticeStore();
|
||||||
|
const { isNotice, notices } = toRefs(noticeStore.state);
|
||||||
if (import.meta.env.VITE_APP_SSE === 'false') {
|
if (import.meta.env.VITE_APP_SSE === 'false') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -26,17 +29,27 @@ export const initSSE = (url: any) => {
|
|||||||
|
|
||||||
watch(data, () => {
|
watch(data, () => {
|
||||||
if (!data.value) return;
|
if (!data.value) return;
|
||||||
|
|
||||||
|
// 打印接收到的 SSE 消息
|
||||||
|
console.log('=== SSE 接收到消息 ===');
|
||||||
|
console.log('消息内容:', data.value);
|
||||||
|
console.log('消息类型:', typeof data.value);
|
||||||
|
console.log('接收时间:', new Date().toLocaleString());
|
||||||
|
console.log('=====================');
|
||||||
|
|
||||||
useNoticeStore().addNotice({
|
useNoticeStore().addNotice({
|
||||||
message: data.value,
|
message: data.value,
|
||||||
read: false,
|
read: false,
|
||||||
time: new Date().toLocaleString()
|
time: new Date().toLocaleString()
|
||||||
});
|
});
|
||||||
ElNotification({
|
const displayMessage = data.value.split('|')[0].trim();
|
||||||
title: '消息',
|
// ElNotification({
|
||||||
message: data.value,
|
// title: '消息',
|
||||||
type: 'success',
|
// message: displayMessage,
|
||||||
duration: 3000
|
// type: 'success',
|
||||||
});
|
// duration: 3000
|
||||||
|
// });
|
||||||
|
noticeStore.state.isNotice = true;
|
||||||
data.value = null;
|
data.value = null;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
63
src/utils/video.ts
Normal file
63
src/utils/video.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import Hls from 'hls.js';
|
||||||
|
let hlsInstance: Hls | null = null;
|
||||||
|
|
||||||
|
export const initHls = (monitorVideoUrl, monitorVideoRef) => {
|
||||||
|
const url = monitorVideoUrl.value;
|
||||||
|
const video = monitorVideoRef.value;
|
||||||
|
if (!url || !video) return;
|
||||||
|
|
||||||
|
// 销毁已有实例
|
||||||
|
if (hlsInstance) {
|
||||||
|
try {
|
||||||
|
hlsInstance.destroy();
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
hlsInstance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Hls.isSupported()) {
|
||||||
|
const hls = new Hls();
|
||||||
|
hls.loadSource(url);
|
||||||
|
hls.attachMedia(video);
|
||||||
|
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||||
|
// 自动播放(若浏览器允许)
|
||||||
|
try {
|
||||||
|
video.play().catch(() => {});
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
hlsInstance = hls;
|
||||||
|
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
|
// Safari/原生支持 HLS
|
||||||
|
video.src = url;
|
||||||
|
video.addEventListener('loadedmetadata', () => {
|
||||||
|
try {
|
||||||
|
video.play().catch(() => {});
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
//proxy?.$modal.msgWarning('当前浏览器不支持 HLS 播放');
|
||||||
|
ElMessage.error('当前浏览器不支持 HLS 播放');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const destroyHls = (monitorVideoRef) => {
|
||||||
|
try {
|
||||||
|
if (hlsInstance) {
|
||||||
|
hlsInstance.destroy();
|
||||||
|
hlsInstance = null;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
const video = monitorVideoRef.value;
|
||||||
|
if (video) {
|
||||||
|
// 清理原生 src,避免残留
|
||||||
|
try {
|
||||||
|
video.pause();
|
||||||
|
video.removeAttribute('src');
|
||||||
|
// 对某些浏览器,需要调用 load() 来重置 media element
|
||||||
|
if (typeof video.load === 'function') video.load();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default { initHls, destroyHls };
|
||||||
321
src/views/WarningList/list.vue
Normal file
321
src/views/WarningList/list.vue
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-2">
|
||||||
|
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||||
|
<div v-show="showSearch" class="mb-[10px]">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||||
|
<el-form-item label="报警编号" prop="alarmCode">
|
||||||
|
<el-input v-model="queryParams.alarmCode" placeholder="请输入报警编号" clearable @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备选择" prop="deviceId">
|
||||||
|
<el-select v-model="queryParams.deviceId" placeholder="请选择" clearable style="width: 160px">
|
||||||
|
<el-option v-for="item in deviceInfo" :key="item.id || item.code" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="报警时间" prop="queryTime">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="queryTime"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="报警区域" prop="monitoringArea">
|
||||||
|
<el-input v-model="queryParams.monitoringArea" placeholder="请输入监控区域" clearable @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<el-row :gutter="10" class="flex justify-end mb8">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="success" plain icon="Edit" :disabled="multiple" @click="handleBatchProcess">批量处理</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" border :data="demoList" @selection-change="handleSelectionChange">
|
||||||
|
<el-table-column type="selection" width="55" align="center" :selectable="checkSelectable" />
|
||||||
|
<el-table-column label="报警编号" align="center" prop="alarmCode" width="180" />
|
||||||
|
<el-table-column label="报警时间" align="center" prop="alarmTime" min-width="170" />
|
||||||
|
<el-table-column label="所属设备" align="center" prop="deviceName" min-width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="报警区域" align="center" prop="monitoringArea" min-width="120" show-overflow-tooltip />
|
||||||
|
<!-- pay content -->
|
||||||
|
<el-table-column label="报警图片" align="center" prop="alarmImage" min-width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-image
|
||||||
|
v-if="scope.row.alarmImage"
|
||||||
|
:src="scope.row.alarmImage"
|
||||||
|
:preview-src-list="[scope.row.alarmImage]"
|
||||||
|
preview-teleported
|
||||||
|
fit="cover"
|
||||||
|
style="width: 60px; height: 60px; border-radius: 4px"
|
||||||
|
/>
|
||||||
|
<span v-else>暂无图片</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="处理状态" align="center" prop="status" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag v-if="scope.row.status === 0" type="warning">未处理</el-tag>
|
||||||
|
<el-tag v-else-if="scope.row.status === 1" type="success">已处理</el-tag>
|
||||||
|
<span v-else>{{ scope.row.status ?? '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="处理人" align="center" prop="userId" min-width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ scope.row.userName ?? '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="处理时间" align="center" prop="processingTime" min-width="170">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ scope.row.processingTime ?? '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button :disabled="scope.row.status === 1" type="primary" @click="handleUpdate(scope.row)">处理</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- <el-dialog v-model="dialog.visible" :title="dialog.title" width="520px" append-to-body>
|
||||||
|
<el-form ref="demoFormRef" :model="form" label-width="100px">
|
||||||
|
<el-form-item label="设备编号">
|
||||||
|
<el-input v-model="form.deviceId" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备名称">
|
||||||
|
<el-input v-model="form.deviceName" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="监控区域">
|
||||||
|
<el-input v-model="form.monitoringArea" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="报警时间">
|
||||||
|
<el-input v-model="form.alarmTime" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="处理状态">
|
||||||
|
<el-tag v-if="form.status === 0" type="warning">未处理</el-tag>
|
||||||
|
<el-tag v-else-if="form.status === 1" type="success">已处理</el-tag>
|
||||||
|
<span v-else>{{ form.status ?? '-' }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="处理人">
|
||||||
|
<el-input :model-value="form.userId ?? '-'" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="处理时间">
|
||||||
|
<el-input :model-value="form.processingTime ?? '-'" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
<el-button @click="cancel">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="WarningList" lang="ts">
|
||||||
|
import { listDemo, updateDemo } from '@/api/WarningList/index';
|
||||||
|
//import { DemoVO, DemoQuery, DemoForm } from '@/api/WarningList/types';
|
||||||
|
import { useDeviceStore } from '@/store/modules/equipmentList';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useWarningListStore } from '@/store/modules/warnningList';
|
||||||
|
|
||||||
|
const warningListStore = useWarningListStore();
|
||||||
|
const { getWarningList, handleAction } = warningListStore;
|
||||||
|
const { warningListTotal, warningList } = storeToRefs(warningListStore);
|
||||||
|
|
||||||
|
const deviceStore = useDeviceStore();
|
||||||
|
const { deviceList } = storeToRefs(deviceStore);
|
||||||
|
const { getdeviceList } = deviceStore;
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
|
|
||||||
|
const demoList = ref([]);
|
||||||
|
const buttonLoading = ref(false);
|
||||||
|
const loading = ref(true);
|
||||||
|
const showSearch = ref(true);
|
||||||
|
const ids = ref<Array<string | number>>([]);
|
||||||
|
const multiple = ref(true);
|
||||||
|
const total = ref(0);
|
||||||
|
|
||||||
|
const queryFormRef = ref<ElFormInstance>();
|
||||||
|
const demoFormRef = ref<ElFormInstance>();
|
||||||
|
|
||||||
|
const dialog = reactive<DialogOption>({
|
||||||
|
visible: false,
|
||||||
|
title: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const initFormData: any = {
|
||||||
|
id: undefined,
|
||||||
|
deviceId: undefined,
|
||||||
|
deviceName: undefined,
|
||||||
|
userName: undefined,
|
||||||
|
monitoringArea: undefined,
|
||||||
|
alarmTime: undefined,
|
||||||
|
status: undefined,
|
||||||
|
userId: undefined,
|
||||||
|
processingTime: undefined
|
||||||
|
};
|
||||||
|
/* ?id=&deviceId=&pageSize=1&pageNum=1&beginTime=&endTime= */
|
||||||
|
/** 查询参数 */
|
||||||
|
const data = reactive({
|
||||||
|
form: { ...initFormData },
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
id: undefined,
|
||||||
|
deviceId: undefined,
|
||||||
|
//status: undefined,
|
||||||
|
beginTime: undefined,
|
||||||
|
endTime: undefined,
|
||||||
|
monitoringArea: undefined,
|
||||||
|
userName: undefined,
|
||||||
|
alarmCode: undefined
|
||||||
|
},
|
||||||
|
rules: {},
|
||||||
|
queryTime: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const { queryParams, form, rules, queryTime } = toRefs(data);
|
||||||
|
|
||||||
|
/** 获取列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await listDemo(queryParams.value);
|
||||||
|
demoList.value = res.data.rows;
|
||||||
|
total.value = res.data.total;
|
||||||
|
await getWarningList({
|
||||||
|
status: 0,
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 300
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 取消按钮 */
|
||||||
|
const cancel = () => {
|
||||||
|
reset();
|
||||||
|
dialog.visible = false;
|
||||||
|
};
|
||||||
|
/** 重置按钮 */
|
||||||
|
const reset = () => {
|
||||||
|
form.value = { ...initFormData };
|
||||||
|
demoFormRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查询按钮 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.value.pageNum = 1;
|
||||||
|
queryParams.value.beginTime = queryTime.value[0];
|
||||||
|
queryParams.value.endTime = queryTime.value[1];
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 重置按钮 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value?.resetFields();
|
||||||
|
queryTime.value = [];
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
/** 多选框选中数据 */
|
||||||
|
const handleSelectionChange = (selection: any) => {
|
||||||
|
ids.value = selection.map((item) => item.id as string | number);
|
||||||
|
multiple.value = !selection.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 单条:打开确认弹窗 */
|
||||||
|
const handleUpdate = async (row: any) => {
|
||||||
|
console.log(row.id);
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await handleAction(row.id);
|
||||||
|
proxy?.$modal.msgSuccess('处理成功');
|
||||||
|
await getList();
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 检查行是否可选(已处理不可选) */
|
||||||
|
const checkSelectable = (row: any) => {
|
||||||
|
return row.status !== 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 批量:确认后直接提交 */
|
||||||
|
const handleBatchProcess = async () => {
|
||||||
|
if (!ids.value.length) {
|
||||||
|
proxy?.$modal.msgWarning('请先选择要处理的报警');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await proxy?.$modal.confirm('是否确认批量处理已选中的报警?');
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await handleAction(ids.value);
|
||||||
|
proxy?.$modal.msgSuccess('处理成功');
|
||||||
|
await getList();
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 提交按钮 */
|
||||||
|
const submitForm = async () => {
|
||||||
|
if (form.value.id == null) {
|
||||||
|
proxy?.$modal.msgWarning('数据异常');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
buttonLoading.value = true;
|
||||||
|
try {
|
||||||
|
await updateDemo(form.value.id);
|
||||||
|
proxy?.$modal.msgSuccess('处理成功');
|
||||||
|
dialog.visible = false;
|
||||||
|
await getList();
|
||||||
|
} finally {
|
||||||
|
buttonLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//获取设备名称-搜索_设备选择
|
||||||
|
const deviceInfo = ref([]);
|
||||||
|
const getDeviceName = async () => {
|
||||||
|
try {
|
||||||
|
await getdeviceList();
|
||||||
|
deviceInfo.value = deviceList.value;
|
||||||
|
console.log('设备列表:', deviceInfo.value);
|
||||||
|
} catch (error) {
|
||||||
|
proxy?.$modal.msgError('获取设备列表失败');
|
||||||
|
}
|
||||||
|
// const res = await getdeviceList();
|
||||||
|
// deviceInfo.value = res?.rows;
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
getDeviceName();
|
||||||
|
});
|
||||||
|
|
||||||
|
//监听数据源变化,自动刷新列表
|
||||||
|
watch(warningListTotal, async (newVal) => {
|
||||||
|
//console.log('监听到报警列表变化,自动刷新列表', newVal);
|
||||||
|
//数据源不同, 接口复用,请求多了一遍(不属于bug)
|
||||||
|
await getList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -212,7 +212,7 @@ const submitForm = () => {
|
|||||||
} else {
|
} else {
|
||||||
await addDemo(form.value).finally(() => (buttonLoading.value = false));
|
await addDemo(form.value).finally(() => (buttonLoading.value = false));
|
||||||
}
|
}
|
||||||
proxy?.$modal.msgSuccess('修改成功');
|
proxy?.$modal.msgSuccess('保存成功');
|
||||||
dialog.visible = false;
|
dialog.visible = false;
|
||||||
await getList();
|
await getList();
|
||||||
}
|
}
|
||||||
|
|||||||
849
src/views/equipmentList/list.vue
Normal file
849
src/views/equipmentList/list.vue
Normal file
@@ -0,0 +1,849 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-2">
|
||||||
|
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||||
|
<div v-show="showSearch" class="mb-[10px]">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||||
|
<el-form-item label="设备编号" prop="code">
|
||||||
|
<el-input v-model="queryParams.code" placeholder="请输入设备编号" clearable @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备名称" prop="name">
|
||||||
|
<el-input v-model="queryParams.name" placeholder="请输入设备名称" clearable @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="监控区域" prop="monitoringArea">
|
||||||
|
<el-input v-model="queryParams.monitoringArea" placeholder="请输入监控区域" clearable @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备状态" prop="status">
|
||||||
|
<el-select v-model="queryParams.status" placeholder="请选择设备状态" clearable style="width: 160px">
|
||||||
|
<el-option label="在线" :value="1" />
|
||||||
|
<el-option label="离线" :value="2" />
|
||||||
|
<el-option label="低帧" :value="3" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<el-row :gutter="10" class="flex justify-end mb8">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="primary" plain icon="Plus" @click="handleAdd()">添加设备</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button v-hasPermi="['demo:demo:edit']" type="success" plain icon="Edit" :disabled="multiple" @click="handleActivate()"
|
||||||
|
>批量唤醒</el-button
|
||||||
|
>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button v-hasPermi="['demo:demo:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()"
|
||||||
|
>批量删除</el-button
|
||||||
|
>
|
||||||
|
</el-col>
|
||||||
|
<!-- <el-col :span="1.5">
|
||||||
|
<el-button v-hasPermi="['demo:demo:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
|
||||||
|
</el-col> -->
|
||||||
|
<!-- <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar> -->
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" border :data="demoList" @selection-change="handleSelectionChange">
|
||||||
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
|
<el-table-column v-if="true" label="设备编号" align="center" prop="code" />
|
||||||
|
<el-table-column label="设备名称" align="center" prop="name" />
|
||||||
|
<el-table-column label="监控区域" align="center" prop="monitoringArea" />
|
||||||
|
<el-table-column label="设备状态" align="center" prop="statusName" width="100">
|
||||||
|
<!-- <template #default="scope">
|
||||||
|
<el-tag v-if="scope.row.status === 1 || scope.row.status === '1'">在线</el-tag>
|
||||||
|
<el-tag v-else-if="scope.row.status === 2 || scope.row.status === '2'" type="warning">离线</el-tag>
|
||||||
|
<el-tag v-else-if="scope.row.status === 3 || scope.row.status === '3'" type="info">低帧</el-tag>
|
||||||
|
<el-tag v-else>{{ scope.row.status ?? '-' }}</el-tag>
|
||||||
|
</template> -->
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="电池电量" align="center" prop="batteryLevel1" width="150">
|
||||||
|
<template #default="scope">
|
||||||
|
<!-- <el-button type="primary" v-if="scope.row.batteryLevel1" plain size="small">{{ scope.row.batteryLevel1 + '%' }}</el-button>
|
||||||
|
<el-button type="primary" v-if="scope.row.batteryLevel2" plain size="small">
|
||||||
|
{{ scope.row.batteryLevel2 + '%' }}
|
||||||
|
</el-button> -->
|
||||||
|
<!-- <el-button type="primary" v-else plain size="small"> 未获取 </el-button> -->
|
||||||
|
<el-tag v-if="scope.row.batteryLevel1">{{ scope.row.batteryLevel1 + '%' }}</el-tag>
|
||||||
|
<el-tag v-if="scope.row.batteryLevel2" :style="scope.row.batteryLevel1 && scope.row.batteryLevel2 ? 'margin-left: 10px' : ''">
|
||||||
|
{{ scope.row.batteryLevel2 + '%' }}
|
||||||
|
</el-tag>
|
||||||
|
<!-- <el-tag v-else type="info"> 未获取 </el-tag> -->
|
||||||
|
<el-tag v-if="!scope.row.batteryLevel1 && !scope.row.batteryLevel2"> 未获取 </el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="设备型号" align="center" prop="model" />
|
||||||
|
<el-table-column label="固件版本" align="center" prop="id" />
|
||||||
|
<el-table-column label="是否绘制区域" align="center" prop="isDrawArea">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag v-if="scope.row.isDrawArea === 1 || scope.row.isDrawArea === '1'">是</el-tag>
|
||||||
|
<el-tag v-else-if="scope.row.isDrawArea === 0 || scope.row.isDrawArea === '0'" type="info">否</el-tag>
|
||||||
|
<el-tag v-else>{{ scope.row.isDrawArea ?? '-' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="绑定时间" align="center" prop="activateTime" />
|
||||||
|
<el-table-column label="设备异常说明" align="center" prop="remark" />
|
||||||
|
<el-table-column width="400" label="操作" align="center" class-name="small-padding fixed-width">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button type="primary" @click="handleUpdate(scope.row)">编辑</el-button>
|
||||||
|
<el-button :disabled="scope.row.status == 2 && scope.row.status == '2'" type="primary" @click="handleSee(scope.row)">查看监控</el-button>
|
||||||
|
<el-button
|
||||||
|
:disabled="scope.row.status === 1 || scope.row.status === 2 || scope.row.status === '1' || scope.row.status === '2'"
|
||||||
|
type="primary"
|
||||||
|
@click="handleActivate(scope.row)"
|
||||||
|
>唤醒设备</el-button
|
||||||
|
>
|
||||||
|
<el-button type="primary" @click="handleDelete(scope.row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||||
|
</el-card>
|
||||||
|
<!-- 添加或修改测试单对话框 -->
|
||||||
|
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
|
||||||
|
<el-form ref="demoFormRef" :model="form" :rules="rules">
|
||||||
|
<el-form-item label="设备编号:" prop="code">
|
||||||
|
<el-input v-model="form.code" placeholder="请输入设备编号" :disabled="haveCode" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备名称:" prop="name">
|
||||||
|
<el-input v-model="form.name" placeholder="请输入设备名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="监控区域:" prop="monitoringArea">
|
||||||
|
<el-input v-model="form.monitoringArea" placeholder="请输入监控区域" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
<el-button @click="cancel">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
<!-- 查看监控对话框 -->
|
||||||
|
<el-dialog v-model="dialog1.visible" class="monitor-dialog" :before-close="handleMonitorDialogClose" fullscreen destroy-on-close>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex w-full items-center justify-between gap-2 min-w-0 pr-8">
|
||||||
|
<span class="truncate">{{ videoName }}-{{ monitoringArea }}监控</span>
|
||||||
|
<div>
|
||||||
|
<el-button v-if="!seeSee" type="primary" class="shrink-0" @click="drawImage()">绘制区域</el-button>
|
||||||
|
<el-button v-if="seeSee" type="primary" class="shrink-0" @click="clearImage()">清空区域</el-button>
|
||||||
|
<el-button v-if="seeSee" type="primary" class="shrink-0" @click="saveImage()">保存区域</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="monitor-video-wrap">
|
||||||
|
<div class="monitor-video-stack">
|
||||||
|
<video ref="monitorVideoRef" class="monitor-video" controls preload="metadata" :src="monitorVideoUrl">您的浏览器不支持视频播放</video>
|
||||||
|
<div v-show="monitorDrawCollecting" class="monitor-draw-overlay" @click.stop="onMonitorDrawClick" />
|
||||||
|
<div v-if="monitorRectPx" class="monitor-draw-rect" :style="drawRectStyle" />
|
||||||
|
<div v-if="monitorRectPx" class="monitor-draw-label" :style="drawLabelStyle">当前报警区域</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="Demo" lang="ts">
|
||||||
|
import { addEquipment, getDevicetList, getDeviceInfo, delDevice, actDevice, updateDeviceInfo, drawInfo } from '@/api/equipmentList/index';
|
||||||
|
import { EquipmentList, DeviceInfoForm, AlarmAreaData } from '@/api/equipmentList/types';
|
||||||
|
import { listDemo, getDemo, delDemo, addDemo, updateDemo } from '@/api/demo/demo';
|
||||||
|
import { DemoVO, DemoQuery, DemoForm } from '@/api/demo/demo/types';
|
||||||
|
import { log } from 'node:console';
|
||||||
|
import Hls from 'hls.js';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
|
import { useVideosStore } from '@/store/modules/video';
|
||||||
|
|
||||||
|
const videosStore = useVideosStore();
|
||||||
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
|
|
||||||
|
const demoList = ref([]);
|
||||||
|
const buttonLoading = ref(false);
|
||||||
|
const loading = ref(true);
|
||||||
|
const showSearch = ref(true);
|
||||||
|
const ids = ref([]);
|
||||||
|
const multiple = ref(true);
|
||||||
|
const total = ref(0);
|
||||||
|
const seeSee = ref(0);
|
||||||
|
const haveCode = ref(false);
|
||||||
|
const videoName = ref<string>('实时监控');
|
||||||
|
|
||||||
|
const queryFormRef = ref<ElFormInstance>();
|
||||||
|
const demoFormRef = ref<ElFormInstance>();
|
||||||
|
|
||||||
|
const dialog = reactive<DialogOption>({
|
||||||
|
visible: false,
|
||||||
|
title: ''
|
||||||
|
});
|
||||||
|
const dialog1 = reactive<DialogOption>({
|
||||||
|
visible: false,
|
||||||
|
title: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const monitorVideoUrl = ref('');
|
||||||
|
const monitorVideoRef = ref<HTMLVideoElement>();
|
||||||
|
let hlsInstance: Hls | null = null;
|
||||||
|
let visibilityHandler: (() => void) | null = null;
|
||||||
|
const monitoringArea = ref<string>('');
|
||||||
|
|
||||||
|
const initHls = () => {
|
||||||
|
const url = monitorVideoUrl.value;
|
||||||
|
const video = monitorVideoRef.value;
|
||||||
|
if (!url || !video) return;
|
||||||
|
|
||||||
|
// 销毁已有实例
|
||||||
|
if (hlsInstance) {
|
||||||
|
try {
|
||||||
|
hlsInstance.destroy();
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
hlsInstance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Hls.isSupported()) {
|
||||||
|
const hls = new Hls();
|
||||||
|
hls.loadSource(url);
|
||||||
|
hls.attachMedia(video);
|
||||||
|
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||||
|
// 自动播放(若浏览器允许)
|
||||||
|
try {
|
||||||
|
video.play().catch(() => {});
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
hlsInstance = hls;
|
||||||
|
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
|
// Safari/原生支持 HLS
|
||||||
|
video.src = url;
|
||||||
|
video.addEventListener('loadedmetadata', () => {
|
||||||
|
try {
|
||||||
|
video.play().catch(() => {});
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
proxy?.$modal.msgWarning('当前浏览器不支持 HLS 播放');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const destroyHls = () => {
|
||||||
|
try {
|
||||||
|
if (hlsInstance) {
|
||||||
|
hlsInstance.destroy();
|
||||||
|
hlsInstance = null;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
const video = monitorVideoRef.value;
|
||||||
|
if (video) {
|
||||||
|
// 清理原生 src,避免残留
|
||||||
|
try {
|
||||||
|
video.pause();
|
||||||
|
video.removeAttribute('src');
|
||||||
|
// 对某些浏览器,需要调用 load() 来重置 media element
|
||||||
|
if (typeof video.load === 'function') video.load();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type MonitorPoint = { x: number; y: number };
|
||||||
|
const monitorDrawCollecting = ref(false);
|
||||||
|
const monitorDrawPoints = ref<MonitorPoint[]>([]);
|
||||||
|
const monitorRectPx = ref<{ left: number; top: number; width: number; height: number } | null>(null);
|
||||||
|
const currentDrawArea = ref<AlarmAreaData | null>(null);
|
||||||
|
const videoResizeObserver = ref<ResizeObserver | null>(null);
|
||||||
|
|
||||||
|
const drawRectStyle = computed<Record<string, string>>(() => {
|
||||||
|
const r = monitorRectPx.value;
|
||||||
|
if (!r) return {};
|
||||||
|
return {
|
||||||
|
left: `${r.left}px`,
|
||||||
|
top: `${r.top}px`,
|
||||||
|
width: `${Math.max(r.width, 2)}px`,
|
||||||
|
height: `${Math.max(r.height, 2)}px`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const drawLabelStyle = computed<Record<string, string>>(() => {
|
||||||
|
const r = monitorRectPx.value;
|
||||||
|
if (!r) return {};
|
||||||
|
return {
|
||||||
|
left: `${r.left + r.width / 2}px`,
|
||||||
|
top: `${r.top}px`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const initFormData = {
|
||||||
|
id: undefined,
|
||||||
|
code: undefined,
|
||||||
|
name: undefined,
|
||||||
|
monitoringArea: undefined
|
||||||
|
};
|
||||||
|
const data = reactive({
|
||||||
|
form: { ...initFormData },
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
code: undefined,
|
||||||
|
name: undefined,
|
||||||
|
monitoringArea: undefined,
|
||||||
|
status: undefined
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
code: [{ required: true, message: '请输入设备编号', trigger: 'blur' }],
|
||||||
|
name: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
|
||||||
|
monitoringArea: [{ required: true, message: '请输入监控区域', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { queryParams, form, rules } = toRefs(data);
|
||||||
|
|
||||||
|
/** 查询测试单列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
const res = await getDevicetList(queryParams.value);
|
||||||
|
demoList.value = res.rows;
|
||||||
|
total.value = res.total;
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 取消按钮 */
|
||||||
|
const cancel = () => {
|
||||||
|
reset();
|
||||||
|
dialog.visible = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 表单重置 */
|
||||||
|
const reset = () => {
|
||||||
|
form.value = { ...initFormData };
|
||||||
|
demoFormRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.value.pageNum = 1;
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value?.resetFields();
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 多选框选中数据 */
|
||||||
|
const handleSelectionChange = (selection: any) => {
|
||||||
|
ids.value = selection.map((item) => item.id);
|
||||||
|
multiple.value = !selection.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 新增按钮操作 */
|
||||||
|
const handleAdd = () => {
|
||||||
|
haveCode.value = false;
|
||||||
|
reset();
|
||||||
|
dialog.visible = true;
|
||||||
|
dialog.title = '添加设备';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 修改按钮操作 */
|
||||||
|
const handleUpdate = async (row?: any) => {
|
||||||
|
haveCode.value = true;
|
||||||
|
reset();
|
||||||
|
const _id = row?.id || ids.value[0];
|
||||||
|
const res = await getDeviceInfo(_id);
|
||||||
|
Object.assign(form.value, res.data);
|
||||||
|
dialog.visible = true;
|
||||||
|
dialog.title = '修改设备';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 提交按钮 */
|
||||||
|
const submitForm = () => {
|
||||||
|
demoFormRef.value?.validate(async (valid: boolean) => {
|
||||||
|
if (valid) {
|
||||||
|
buttonLoading.value = true;
|
||||||
|
if (form.value.id) {
|
||||||
|
await updateDeviceInfo(form.value).finally(() => (buttonLoading.value = false));
|
||||||
|
} else {
|
||||||
|
await addEquipment(form.value).finally(() => (buttonLoading.value = false));
|
||||||
|
}
|
||||||
|
proxy?.$modal.msgSuccess('修改成功');
|
||||||
|
dialog.visible = false;
|
||||||
|
await getList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 唤醒设备(单行传 row,批量不传则使用表格勾选 ids,调用 actDevice) */
|
||||||
|
const handleActivate = async (row?: any) => {
|
||||||
|
const targetIds = row?.id != null ? row.id : ids.value;
|
||||||
|
const isBatch = row == null;
|
||||||
|
if (isBatch && (!Array.isArray(targetIds) || !targetIds.length)) {
|
||||||
|
proxy?.$modal.msgWarning('请先勾选要唤醒的设备');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const msg = isBatch ? `是否确认批量唤醒已选中的 ${targetIds.length} 台设备?` : `是否确认唤醒设备「${row!.name ?? row!.code ?? row!.id}」?`;
|
||||||
|
await proxy?.$modal.confirm(msg);
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await actDevice(targetIds);
|
||||||
|
proxy?.$modal.msgSuccess('唤醒成功');
|
||||||
|
await getList();
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 查看监控 */
|
||||||
|
const resetMonitorDraw = () => {
|
||||||
|
monitorDrawCollecting.value = false;
|
||||||
|
monitorDrawPoints.value = [];
|
||||||
|
monitorRectPx.value = null;
|
||||||
|
currentDrawArea.value = null;
|
||||||
|
};
|
||||||
|
const handleSee = async (row?: any) => {
|
||||||
|
console.log('1111111111111111', row, row.drawArea);
|
||||||
|
|
||||||
|
reset();
|
||||||
|
resetMonitorDraw();
|
||||||
|
seeSee.value = row?.isDrawArea === 1 ? 1 : 0;
|
||||||
|
monitoringArea.value = row?.monitoringArea;
|
||||||
|
const _id = row?.id || ids.value[0];
|
||||||
|
const res = await getDeviceInfo(_id);
|
||||||
|
Object.assign(form.value, res.data);
|
||||||
|
dialog1.visible = true;
|
||||||
|
dialog1.title = '修改设备';
|
||||||
|
|
||||||
|
destroyHls();
|
||||||
|
monitorVideoUrl.value = '';
|
||||||
|
await videosStore.getVideos(row.id);
|
||||||
|
monitorVideoUrl.value = videosStore.hisUrl || '';
|
||||||
|
|
||||||
|
if (!monitorVideoUrl.value) {
|
||||||
|
//proxy?.$modal.msgWarning('该设备未登录或无法获取直播地址,已停止播放旧画面');
|
||||||
|
initVideoResizeObserver();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待 DOM 更新后初始化 Hls
|
||||||
|
await nextTick();
|
||||||
|
initHls();
|
||||||
|
// 等待视频加载完成后初始化 ResizeObserver,以便能正确获取视频尺寸
|
||||||
|
initVideoResizeObserver();
|
||||||
|
|
||||||
|
// 如果有 drawArea,直接使用它显示矩形区域,不允许点击绘制
|
||||||
|
if (row?.drawArea) {
|
||||||
|
try {
|
||||||
|
// 将 drawArea 从字符串转为对象
|
||||||
|
const drawArea = typeof row.drawArea === 'string' ? JSON.parse(row.drawArea) : row.drawArea;
|
||||||
|
console.log('[查看监控] 解析后的 drawArea:', drawArea);
|
||||||
|
|
||||||
|
if (drawArea && drawArea.x1 !== undefined && drawArea.y1 !== undefined && drawArea.x2 !== undefined && drawArea.y2 !== undefined) {
|
||||||
|
currentDrawArea.value = drawArea;
|
||||||
|
// 等待视频加载完成
|
||||||
|
await nextTick();
|
||||||
|
const video = monitorVideoRef.value;
|
||||||
|
|
||||||
|
// 如果视频已加载,立即显示矩形
|
||||||
|
if (video && video.videoWidth > 0 && video.videoHeight > 0) {
|
||||||
|
displayDrawArea(drawArea, video);
|
||||||
|
} else if (video) {
|
||||||
|
// 如果视频还未加载,等待 loadedmetadata 事件
|
||||||
|
video.addEventListener(
|
||||||
|
'loadedmetadata',
|
||||||
|
() => {
|
||||||
|
displayDrawArea(drawArea, video);
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[查看监控] 解析 drawArea 失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 后台运行的时候恢复播放
|
||||||
|
cleanupVisibilityChange();
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
if (!document.hidden) {
|
||||||
|
const video = monitorVideoRef.value;
|
||||||
|
if (video && video.paused) {
|
||||||
|
video.play().catch((e) => console.warn('恢复播放失败:', e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
visibilityHandler = handleVisibilityChange;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提取显示报警区域的逻辑为独立函数
|
||||||
|
const displayDrawArea = (drawArea: any, video: HTMLVideoElement) => {
|
||||||
|
const VW = video.videoWidth;
|
||||||
|
const VH = video.videoHeight;
|
||||||
|
if (!VW || !VH) return;
|
||||||
|
|
||||||
|
// 计算视频原始像素坐标(x 是距离右侧的千分比,y 是距离顶部的千分比)
|
||||||
|
const x1Video = VW - (drawArea.x1 / 1000) * VW;
|
||||||
|
const y1Video = (drawArea.y1 / 1000) * VH;
|
||||||
|
const x2Video = VW - (drawArea.x2 / 1000) * VW;
|
||||||
|
const y2Video = (drawArea.y2 / 1000) * VH;
|
||||||
|
|
||||||
|
const rect = video.getBoundingClientRect();
|
||||||
|
const dw = rect.width;
|
||||||
|
const dh = rect.height;
|
||||||
|
if (dw <= 0 || dh <= 0) return;
|
||||||
|
|
||||||
|
const scale = Math.min(dw / VW, dh / VH);
|
||||||
|
const contentW = VW * scale;
|
||||||
|
const contentH = VH * scale;
|
||||||
|
const offsetX = (dw - contentW) / 2;
|
||||||
|
const offsetY = (dh - contentH) / 2;
|
||||||
|
|
||||||
|
const x1Px = offsetX + x1Video * scale;
|
||||||
|
const y1Px = offsetY + y1Video * scale;
|
||||||
|
const x2Px = offsetX + x2Video * scale;
|
||||||
|
const y2Px = offsetY + y2Video * scale;
|
||||||
|
|
||||||
|
const left = Math.min(x1Px, x2Px);
|
||||||
|
const top = Math.min(y1Px, y2Px);
|
||||||
|
const width = Math.max(Math.abs(x2Px - x1Px), 2);
|
||||||
|
const height = Math.max(Math.abs(y2Px - y1Px), 2);
|
||||||
|
|
||||||
|
monitorRectPx.value = { left, top, width, height };
|
||||||
|
currentDrawArea.value = drawArea;
|
||||||
|
monitorDrawCollecting.value = false; // 禁止点击绘制
|
||||||
|
|
||||||
|
// 同时添加到 payload 用于保存
|
||||||
|
payload.value = [
|
||||||
|
{
|
||||||
|
deviceId: form.value.id,
|
||||||
|
x1: drawArea.x1,
|
||||||
|
y1: drawArea.y1,
|
||||||
|
x2: drawArea.x2,
|
||||||
|
y2: drawArea.y2
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log('[查看监控] 已加载已有报警区域', payload.value);
|
||||||
|
};
|
||||||
|
// 在窗口调整大小时更新绘制区域位置和大小
|
||||||
|
const updateDrawAreaOnResize = () => {
|
||||||
|
const drawArea = currentDrawArea.value;
|
||||||
|
const video = monitorVideoRef.value;
|
||||||
|
if (!drawArea || !video || video.videoWidth <= 0 || video.videoHeight <= 0) return;
|
||||||
|
displayDrawArea(drawArea, video);
|
||||||
|
};
|
||||||
|
// 初始化 ResizeObserver 监听视频元素尺寸变化,以便在窗口调整大小时更新绘制区域位置和大小
|
||||||
|
const initVideoResizeObserver = () => {
|
||||||
|
if (!window.ResizeObserver || videoResizeObserver.value) return;
|
||||||
|
const video = monitorVideoRef.value;
|
||||||
|
if (!video) return;
|
||||||
|
videoResizeObserver.value = new ResizeObserver(() => {
|
||||||
|
updateDrawAreaOnResize();
|
||||||
|
});
|
||||||
|
videoResizeObserver.value.observe(video);
|
||||||
|
};
|
||||||
|
// 销毁 ResizeObserver
|
||||||
|
const destroyVideoResizeObserver = () => {
|
||||||
|
if (videoResizeObserver.value) {
|
||||||
|
videoResizeObserver.value.disconnect();
|
||||||
|
videoResizeObserver.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 清理 visibilitychange 事件监听
|
||||||
|
const cleanupVisibilityChange = () => {
|
||||||
|
if (visibilityHandler) {
|
||||||
|
document.removeEventListener('visibilitychange', visibilityHandler);
|
||||||
|
visibilityHandler = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMonitorDialogClose = (done: () => void) => {
|
||||||
|
// 卸载 Hls 与清理 video
|
||||||
|
destroyHls();
|
||||||
|
monitorVideoUrl.value = '';
|
||||||
|
destroyVideoResizeObserver();
|
||||||
|
resetMonitorDraw();
|
||||||
|
cleanupVisibilityChange();
|
||||||
|
done();
|
||||||
|
//刷新列表
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMonitorDialogOk = async () => {
|
||||||
|
if (monitorDrawCollecting.value) {
|
||||||
|
proxy?.$modal.msgWarning('请先完成报警区域绘制');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!currentDrawArea.value) {
|
||||||
|
proxy?.$modal.msgWarning('请先绘制报警区域');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 显示坐标(相对 video 元素) → 视频原始像素坐标,含 object-fit: contain 留白换算 */
|
||||||
|
const displayToVideoPixel = (px: number, py: number, video: HTMLVideoElement): { vx: number; vy: number } | null => {
|
||||||
|
const dw = video.clientWidth;
|
||||||
|
const dh = video.clientHeight;
|
||||||
|
const VW = video.videoWidth;
|
||||||
|
const VH = video.videoHeight;
|
||||||
|
if (dw <= 0 || dh <= 0 || VW <= 0 || VH <= 0) return null;
|
||||||
|
|
||||||
|
const scale = Math.min(dw / VW, dh / VH);
|
||||||
|
const contentW = VW * scale;
|
||||||
|
const contentH = VH * scale;
|
||||||
|
const offsetX = (dw - contentW) / 2;
|
||||||
|
const offsetY = (dh - contentH) / 2;
|
||||||
|
|
||||||
|
const vx = Math.min(VW, Math.max(0, (px - offsetX) / scale));
|
||||||
|
const vy = Math.min(VH, Math.max(0, (py - offsetY) / scale));
|
||||||
|
return { vx, vy };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 开始采集:视频上点击两个对角点 */
|
||||||
|
const drawStart = ref(false);
|
||||||
|
|
||||||
|
const payload = ref<AlarmAreaData[]>([]);
|
||||||
|
|
||||||
|
const drawImage = () => {
|
||||||
|
resetMonitorDraw();
|
||||||
|
monitorDrawCollecting.value = true;
|
||||||
|
videoName.value = '绘制区域';
|
||||||
|
proxy?.$modal.msgSuccess('请在视频画面上连续点击两个点,生成报警区域');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMonitorDrawClick = (e: MouseEvent) => {
|
||||||
|
if (!monitorDrawCollecting.value) return;
|
||||||
|
const video = monitorVideoRef.value;
|
||||||
|
if (!video) {
|
||||||
|
proxy?.$modal.msgWarning('视频未就绪');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rect = video.getBoundingClientRect();
|
||||||
|
const dw = rect.width;
|
||||||
|
const dh = rect.height;
|
||||||
|
if (dw <= 0 || dh <= 0) {
|
||||||
|
proxy?.$modal.msgWarning('无法获取视频区域尺寸');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
const p: MonitorPoint = { x, y };
|
||||||
|
monitorDrawPoints.value = [...monitorDrawPoints.value, p];
|
||||||
|
if (monitorDrawPoints.value.length < 2) return;
|
||||||
|
|
||||||
|
const [p1, p2] = monitorDrawPoints.value;
|
||||||
|
|
||||||
|
const VW = video.videoWidth;
|
||||||
|
const VH = video.videoHeight;
|
||||||
|
const v1 = displayToVideoPixel(p1.x, p1.y, video);
|
||||||
|
const v2 = displayToVideoPixel(p2.x, p2.y, video);
|
||||||
|
if (!v1 || !v2 || !VW || !VH) {
|
||||||
|
monitorDrawPoints.value = [];
|
||||||
|
proxy?.$modal.msgWarning('请等待视频加载完成后再绘制(需能读取原始宽高)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const left = Math.min(p1.x, p2.x);
|
||||||
|
const top = Math.min(p1.y, p2.y);
|
||||||
|
const width = Math.max(Math.abs(p2.x - p1.x), 1);
|
||||||
|
const height = Math.max(Math.abs(p2.y - p1.y), 1);
|
||||||
|
monitorRectPx.value = { left, top, width, height };
|
||||||
|
monitorDrawCollecting.value = false;
|
||||||
|
monitorDrawPoints.value = [];
|
||||||
|
|
||||||
|
// 计算两个点相对于video的千分比坐标
|
||||||
|
const point1RightPctOfWidth = ((VW - v1.vx) / VW) * 1000;
|
||||||
|
const point1TopPctOfHeight = (v1.vy / VH) * 1000;
|
||||||
|
const point2RightPctOfWidth = ((VW - v2.vx) / VW) * 1000;
|
||||||
|
const point2TopPctOfHeight = (v2.vy / VH) * 1000;
|
||||||
|
|
||||||
|
const clamp = (value: number) => Math.max(0, Math.min(1000, Math.round(value)));
|
||||||
|
|
||||||
|
const newArea: AlarmAreaData = {
|
||||||
|
deviceId: form.value.id,
|
||||||
|
x1: clamp(point1RightPctOfWidth),
|
||||||
|
y1: clamp(point1TopPctOfHeight),
|
||||||
|
x2: clamp(point2RightPctOfWidth),
|
||||||
|
y2: clamp(point2TopPctOfHeight)
|
||||||
|
};
|
||||||
|
payload.value.push(newArea);
|
||||||
|
currentDrawArea.value = newArea;
|
||||||
|
console.log('[监控区域] 两点千分比坐标', newArea);
|
||||||
|
console.log('[监控区域] 所有报警区域', payload.value);
|
||||||
|
seeSee.value = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearImage = () => {
|
||||||
|
payload.value = [];
|
||||||
|
console.log('[清空区域] 当前报警区域列表', payload.value);
|
||||||
|
drawStart.value = false;
|
||||||
|
seeSee.value = 0;
|
||||||
|
// 清除绘制的矩形图案
|
||||||
|
monitorRectPx.value = null;
|
||||||
|
monitorDrawPoints.value = [];
|
||||||
|
monitorDrawCollecting.value = false; // 重置为不允许点击绘制状态
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveImage = async () => {
|
||||||
|
console.log('[保存区域] 准备保存的报警区域列表', payload.value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await drawInfo(payload.value);
|
||||||
|
proxy?.$modal.msgSuccess('保存成功');
|
||||||
|
console.log('[保存区域] 保存结果', response);
|
||||||
|
seeSee.value = 0;
|
||||||
|
dialog1.visible = false;
|
||||||
|
//重新获取数据
|
||||||
|
await getList();
|
||||||
|
} catch (error) {
|
||||||
|
proxy?.$modal.msgSuccess('保存成功, 请联系管理员');
|
||||||
|
console.error('[保存区域] 保存失败', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMonitorDialogOpen = () => {
|
||||||
|
// 初始化 Hls 与 video
|
||||||
|
initHls();
|
||||||
|
initVideoResizeObserver();
|
||||||
|
monitorDrawCollecting.value = false;
|
||||||
|
monitorDrawPoints.value = [];
|
||||||
|
monitorRectPx.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 删除按钮操作 */
|
||||||
|
const handleDelete = async (row?: DemoVO) => {
|
||||||
|
const _ids = row?.id || ids.value;
|
||||||
|
await proxy?.$modal.confirm('是否确认删除测试单编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
|
||||||
|
await delDevice(_ids);
|
||||||
|
proxy?.$modal.msgSuccess('删除成功');
|
||||||
|
await getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 导出按钮操作 */
|
||||||
|
const handleExport = () => {
|
||||||
|
proxy?.download(
|
||||||
|
'demo/demo/export',
|
||||||
|
{
|
||||||
|
...queryParams.value
|
||||||
|
},
|
||||||
|
`demo_${new Date().getTime()}.xlsx`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => seeSee.value,
|
||||||
|
(newValue) => {
|
||||||
|
videoName.value = newValue === 1 ? '绘制区域' : '实时监控';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
/* 全屏监控弹窗:占满视口、禁止外层滚动,视频在可视区内尽可能放大 */
|
||||||
|
:deep(.monitor-dialog.el-dialog) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
max-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.monitor-dialog .el-dialog__header) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.monitor-dialog .el-dialog__body) {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-video-wrap {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-video-stack {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-video {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain; //cover裁剪
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-draw-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2;
|
||||||
|
cursor: crosshair;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-draw-rect {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
pointer-events: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 2px dashed #2563eb;
|
||||||
|
background: rgba(37, 99, 235, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-draw-label {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 4;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(37, 99, 235, 0.85);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏视频进度条,保留其他控制按钮 */
|
||||||
|
.monitor-video::-webkit-media-controls-timeline {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.monitor-video::-webkit-media-controls-current-time-display,
|
||||||
|
.monitor-video::-webkit-media-controls-time-remaining-display {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.monitor-video::-moz-range-track {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.monitor-video::-moz-progress-bar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
372
src/views/fileList/list.vue
Normal file
372
src/views/fileList/list.vue
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-2">
|
||||||
|
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||||
|
<div v-show="showSearch" class="mb-[10px]">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||||
|
<el-form-item label="设备选择" prop="deviceId">
|
||||||
|
<el-select v-model="queryParams.deviceId" placeholder="请选择" clearable style="width: 160px">
|
||||||
|
<el-option v-for="item in deviceInfo" :key="item.id || item.code" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="报警编号" prop="alarmCode">
|
||||||
|
<el-input v-model="queryParams.alarmCode" placeholder="请输入报警编号" clearable @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="报警时间" prop="queryTime">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="queryParams.queryTime"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<!-- <template #header>
|
||||||
|
<el-row :gutter="10" class="flex justify-end mb8">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button v-hasPermi="['demo:demo:add']" type="primary" plain icon="Plus" @click="handleAdd">添加设备</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button v-hasPermi="['demo:demo:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()"
|
||||||
|
>批量唤醒</el-button
|
||||||
|
>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button v-hasPermi="['demo:demo:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()"
|
||||||
|
>批量删除</el-button
|
||||||
|
>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template> -->
|
||||||
|
|
||||||
|
<el-table v-loading="loading" border :data="demoList" @selection-change="handleSelectionChange">
|
||||||
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
|
<el-table-column label="录像编号" align="center" prop="code" width="180" />
|
||||||
|
<el-table-column label="文件名称" align="center" prop="videoName" min-width="170" />
|
||||||
|
<el-table-column label="所属设备" align="center" prop="deviceName" min-width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="报警编号" align="center" prop="alarmCode" min-width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="录像开始时间" align="center" prop="startTime" min-width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="录像结束时间" align="center" prop="endTime" min-width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="录像时长" align="center" prop="durationFormat" min-width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="300">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button type="primary" @click="handlePlay(scope.row)">播放</el-button>
|
||||||
|
<el-button type="primary" @click="handleDown(scope.row)">下载至本地</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||||
|
</el-card>
|
||||||
|
<!-- 添加或修改测试单对话框 -->
|
||||||
|
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" class="monitor-dialog" fullscreen destroy-on-close>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex w-full items-center justify-between gap-2 min-w-0 pr-8">
|
||||||
|
<span class="truncate">播放-{{ videoName }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="monitor-video-wrap">
|
||||||
|
<div class="monitor-video-stack">
|
||||||
|
<video ref="monitorVideoRef" class="monitor-video" controls preload="metadata" :src="monitorVideoUrl">您的浏览器不支持视频播放</video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="Demo" lang="ts">
|
||||||
|
import { listDemo, getDemo, delDemo, addDemo, updateDemo } from '@/api/fileList/index';
|
||||||
|
import { download } from '@/utils/request';
|
||||||
|
import { onBeforeRouteLeave } from 'vue-router';
|
||||||
|
//import { DemoVO, DemoQuery, DemoForm } from '@/api/demo/demo/types';
|
||||||
|
import { useDeviceStore } from '@/store/modules/equipmentList';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
const deviceStore = useDeviceStore();
|
||||||
|
const { deviceList } = storeToRefs(deviceStore);
|
||||||
|
const { getdeviceList } = deviceStore;
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
|
|
||||||
|
const demoList = ref<any[]>([]);
|
||||||
|
const buttonLoading = ref(false);
|
||||||
|
const loading = ref(true);
|
||||||
|
const showSearch = ref(true);
|
||||||
|
const ids = ref<Array<string | number>>([]);
|
||||||
|
const single = ref(true);
|
||||||
|
const multiple = ref(true);
|
||||||
|
const total = ref(0);
|
||||||
|
const monitorVideoUrl = ref();
|
||||||
|
const downloadUrl = ref();
|
||||||
|
const videoName = ref<string>('录像文件');
|
||||||
|
|
||||||
|
const queryFormRef = ref<ElFormInstance>();
|
||||||
|
const demoFormRef = ref<ElFormInstance>();
|
||||||
|
|
||||||
|
const dialog = reactive<DialogOption>({
|
||||||
|
visible: false,
|
||||||
|
title: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const initFormData = {
|
||||||
|
id: undefined,
|
||||||
|
deptId: undefined,
|
||||||
|
userId: undefined,
|
||||||
|
orderNum: undefined,
|
||||||
|
testKey: undefined,
|
||||||
|
value: undefined
|
||||||
|
};
|
||||||
|
const data = reactive<any>({
|
||||||
|
form: { ...initFormData },
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
userName: undefined,
|
||||||
|
deviceId: undefined,
|
||||||
|
startTime: undefined,
|
||||||
|
endTime: undefined,
|
||||||
|
queryTime: []
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
id: [{ required: true, message: '主键不能为空', trigger: 'blur' }],
|
||||||
|
deptId: [{ required: true, message: '部门id不能为空', trigger: 'blur' }],
|
||||||
|
userId: [{ required: true, message: '用户id不能为空', trigger: 'blur' }],
|
||||||
|
orderNum: [{ required: true, message: '排序号不能为空', trigger: 'blur' }],
|
||||||
|
testKey: [{ required: true, message: 'key键不能为空', trigger: 'blur' }],
|
||||||
|
value: [{ required: true, message: '值不能为空', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { queryParams, form, rules } = toRefs(data);
|
||||||
|
|
||||||
|
/** 查询测试单列表 */
|
||||||
|
const getList = async (params?: any) => {
|
||||||
|
loading.value = true;
|
||||||
|
const res = await listDemo(queryParams.value);
|
||||||
|
demoList.value = res.rows;
|
||||||
|
total.value = res.total;
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 取消按钮 */
|
||||||
|
const cancel = () => {
|
||||||
|
reset();
|
||||||
|
dialog.visible = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 表单重置 */
|
||||||
|
const reset = () => {
|
||||||
|
form.value = { ...initFormData };
|
||||||
|
demoFormRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.value.pageNum = 1;
|
||||||
|
queryParams.value.startTime = queryParams.value.queryTime?.[0];
|
||||||
|
queryParams.value.endTime = queryParams.value.queryTime?.[1];
|
||||||
|
// queryParams.value.queryTime = undefined;
|
||||||
|
// getList();
|
||||||
|
const { queryTime, ...reqParams } = queryParams.value;
|
||||||
|
getList(reqParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value?.resetFields();
|
||||||
|
queryParams.value.startTime = undefined;
|
||||||
|
queryParams.value.endTime = undefined;
|
||||||
|
queryParams.value.queryTime = [];
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 多选框选中数据 */
|
||||||
|
const handleSelectionChange = (selection: any[]) => {
|
||||||
|
ids.value = selection.map((item) => item.id);
|
||||||
|
single.value = selection.length != 1;
|
||||||
|
multiple.value = !selection.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 新增按钮操作 */
|
||||||
|
const handleAdd = () => {
|
||||||
|
reset();
|
||||||
|
dialog.visible = true;
|
||||||
|
dialog.title = '添加测试单';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 修改按钮操作 */
|
||||||
|
const handlePlay = async (row?: any) => {
|
||||||
|
// reset();
|
||||||
|
// const _id = row?.id || ids.value[0];
|
||||||
|
// const res = await getDemo(_id);
|
||||||
|
// Object.assign(form.value, res.data);
|
||||||
|
// dialog.visible = true;
|
||||||
|
// dialog.title = '修改测试单';
|
||||||
|
|
||||||
|
if (!row || !row.videoUrl) {
|
||||||
|
proxy?.$modal.msgError('视频地址不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
monitorVideoUrl.value = row.videoUrl;
|
||||||
|
videoName.value = row.videoName;
|
||||||
|
dialog.visible = true;
|
||||||
|
};
|
||||||
|
const handleDown = async (row?: any) => {
|
||||||
|
console.log('111111111', row);
|
||||||
|
if (!row || !row.zipUrl) {
|
||||||
|
proxy?.$modal.msgError('下载地址不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fileName = row.videoName || `file_${new Date().getTime()}.zip`;
|
||||||
|
//console.log(row, fileName);
|
||||||
|
|
||||||
|
// 1. 创建隐藏的 a 元素
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = row.zipUrl; // 文件路径
|
||||||
|
link.download = fileName; // 指定下载的文件名
|
||||||
|
link.style.display = 'none';
|
||||||
|
|
||||||
|
// 2. 挂载到 DOM 并模拟点击
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
// 3. 移除元素,保持页面整洁
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 提交按钮 */
|
||||||
|
const submitForm = () => {
|
||||||
|
demoFormRef.value?.validate(async (valid: boolean) => {
|
||||||
|
if (valid) {
|
||||||
|
buttonLoading.value = true;
|
||||||
|
if (form.value.id) {
|
||||||
|
await updateDemo(form.value).finally(() => (buttonLoading.value = false));
|
||||||
|
} else {
|
||||||
|
await addDemo(form.value).finally(() => (buttonLoading.value = false));
|
||||||
|
}
|
||||||
|
proxy?.$modal.msgSuccess('修改成功');
|
||||||
|
dialog.visible = false;
|
||||||
|
await getList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 删除按钮操作 */
|
||||||
|
const handleDelete = async (row?: any) => {
|
||||||
|
const _ids = row?.id || ids.value;
|
||||||
|
await proxy?.$modal.confirm('是否确认删除测试单编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
|
||||||
|
await delDemo(_ids);
|
||||||
|
proxy?.$modal.msgSuccess('删除成功');
|
||||||
|
await getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 导出按钮操作 */
|
||||||
|
const handleExport = () => {
|
||||||
|
proxy?.download(
|
||||||
|
'demo/demo/export',
|
||||||
|
{
|
||||||
|
...queryParams.value
|
||||||
|
},
|
||||||
|
`demo_${new Date().getTime()}.xlsx`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeRouteLeave(() => {
|
||||||
|
dialog.visible = false;
|
||||||
|
monitorVideoUrl.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
//获取设备名称-搜索_设备选择
|
||||||
|
const deviceInfo = ref([]);
|
||||||
|
const getDeviceName = async () => {
|
||||||
|
try {
|
||||||
|
await getdeviceList();
|
||||||
|
deviceInfo.value = deviceList.value;
|
||||||
|
console.log('设备列表:', deviceInfo.value);
|
||||||
|
} catch (error) {
|
||||||
|
proxy?.$modal.msgError('获取设备列表失败');
|
||||||
|
}
|
||||||
|
// const res = await getdeviceList();
|
||||||
|
// deviceInfo.value = res?.rows;
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
getDeviceName();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* 全屏监控弹窗:占满视口、禁止外层滚动,视频在可视区内尽可能放大 */
|
||||||
|
:deep(.monitor-dialog.el-dialog) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
max-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.monitor-dialog .el-dialog__header) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.monitor-dialog .el-dialog__body) {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-video-wrap {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-video-stack {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-video {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain; //cover裁剪
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-draw-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2;
|
||||||
|
cursor: crosshair;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="app-container home">
|
<div class="app-container home">
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :sm="24" :lg="12" style="padding-left: 20px">
|
<el-col :sm="24" :lg="12" style="padding-left: 20px">
|
||||||
<h2>RuoYi-Vue-Plus多租户管理系统</h2>
|
<h2>可视化电子围栏管理系统</h2>
|
||||||
<p>
|
<p>
|
||||||
RuoYi-Vue-Plus 是基于 RuoYi-Vue 针对 分布式集群 场景升级(不兼容原框架)
|
RuoYi-Vue-Plus 是基于 RuoYi-Vue 针对 分布式集群 场景升级(不兼容原框架)
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login">
|
<div class="login">
|
||||||
<div>1111</div>
|
<div class="login-cards">
|
||||||
|
<div class="login-brand">
|
||||||
|
<div class="login-brand__inner">
|
||||||
|
<div class="login-brand__logo-wrap">
|
||||||
|
<img class="login-brand__logo" :src="logoUrl" alt="" />
|
||||||
|
</div>
|
||||||
|
<h1 class="login-brand__title">{{ logoName }}</h1>
|
||||||
|
<p class="login-brand__subtitle">智能安防 · 实时监控 · 高效管理</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
|
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
|
||||||
<div class="title-box">
|
<div class="title-box">
|
||||||
<h3 class="title">欢迎登录</h3>
|
<h3 class="title">欢迎登录</h3>
|
||||||
@@ -30,7 +39,7 @@
|
|||||||
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
|
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-if="captchaEnabled" prop="code">
|
<el-form-item label="验证码" v-if="captchaEnabled" prop="code">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="loginForm.code"
|
v-model="loginForm.code"
|
||||||
size="large"
|
size="large"
|
||||||
@@ -45,10 +54,10 @@
|
|||||||
<img :src="codeUrl" class="login-code-img" @click="getCode" />
|
<img :src="codeUrl" class="login-code-img" @click="getCode" />
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-checkbox v-model="loginForm.rememberMe" style="margin: 0 0 25px 0">记住密码</el-checkbox>
|
<div class="login-form__extras">
|
||||||
<el-form-item style="float: right">
|
<el-checkbox v-model="loginForm.rememberMe">记住密码</el-checkbox>
|
||||||
<el-button link type="primary" @click="doSocialLogin('github')"> 忘记密码 </el-button>
|
<el-button link type="primary" @click="doSocialLogin('github')">忘记密码</el-button>
|
||||||
</el-form-item>
|
</div>
|
||||||
<el-form-item style="width: 100%">
|
<el-form-item style="width: 100%">
|
||||||
<el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleLogin">
|
<el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleLogin">
|
||||||
<span v-if="!loading">{{ proxy.$t('login.login') }}</span>
|
<span v-if="!loading">{{ proxy.$t('login.login') }}</span>
|
||||||
@@ -60,6 +69,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -70,10 +80,11 @@ import { LoginData, TenantVO } from '@/api/types';
|
|||||||
import { to } from 'await-to-js';
|
import { to } from 'await-to-js';
|
||||||
import { HttpStatus } from '@/enums/RespEnum';
|
import { HttpStatus } from '@/enums/RespEnum';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import logo from '@/assets/logo/logo.png';
|
||||||
|
import { getDemo } from '@/api/systemList/index';
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
|
|
||||||
const title = import.meta.env.VITE_APP_TITLE;
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -214,6 +225,18 @@ onMounted(() => {
|
|||||||
initTenantList();
|
initTenantList();
|
||||||
getLoginData();
|
getLoginData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取Logo图片
|
||||||
|
const logoUrl = ref();
|
||||||
|
const logoName = ref();
|
||||||
|
onMounted(() => {
|
||||||
|
getDemo().then((res) => {
|
||||||
|
console.log('系统11111111111', res);
|
||||||
|
|
||||||
|
logoName.value = res.data.name;
|
||||||
|
logoUrl.value = res.data.url;
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -221,23 +244,126 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
box-sizing: border-box;
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
padding: 24px;
|
||||||
background-image: url('../assets/images/logbg.jpg');
|
background-image: url('../assets/images/logbg.jpg');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-cards {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: min(420px, 90vw);
|
||||||
|
max-width: 420px;
|
||||||
|
height: 450px;
|
||||||
|
max-height: 450px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--app-radius-lg);
|
||||||
|
background: linear-gradient(165deg, #2d60e3 0%, #1a4bb8 55%, #153d9a 100%);
|
||||||
|
box-shadow: 0 24px 48px rgba(30, 64, 175, 0.35);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand__inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 36px 28px;
|
||||||
|
gap: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand__logo-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 88px;
|
||||||
|
height: 88px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand__logo {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand__subtitle {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.login-cards {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand {
|
||||||
|
width: min(420px, 100%);
|
||||||
|
max-width: 420px;
|
||||||
|
height: 450px;
|
||||||
|
max-height: 450px;
|
||||||
|
border-radius: var(--app-radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.title-box {
|
.title-box {
|
||||||
//display: flex;
|
flex-shrink: 0;
|
||||||
//align-items: center;
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin: 0px auto 26px auto;
|
margin: 0 0 10px 0;
|
||||||
//text-align: center;
|
font-size: 22px;
|
||||||
color: var(--el-text-color-primary);
|
font-weight: 700;
|
||||||
font-weight: 600;
|
line-height: 1.3;
|
||||||
letter-spacing: 0.5px;
|
color: #111827;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin: 0 0 28px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.lang-select--style) {
|
:deep(.lang-select--style) {
|
||||||
@@ -247,15 +373,89 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-form {
|
.login-form {
|
||||||
border-radius: var(--app-radius-lg);
|
flex: 0 0 auto;
|
||||||
background: rgba(255, 255, 255, 0.94);
|
display: flex;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 0;
|
||||||
width: min(420px, 90vw);
|
width: min(420px, 90vw);
|
||||||
padding: 32px 30px 12px 30px;
|
height: 450px;
|
||||||
|
max-height: 450px;
|
||||||
|
padding: 44px 48px 36px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #ebebeb;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
box-shadow: var(--app-shadow-lg);
|
box-shadow:
|
||||||
backdrop-filter: blur(6px);
|
0 12px 40px rgba(15, 23, 42, 0.08),
|
||||||
-webkit-backdrop-filter: blur(6px);
|
0 2px 10px rgba(15, 23, 42, 0.04);
|
||||||
|
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
color: #1f2937;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item:last-child) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 0 1px #e5e7eb inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__wrapper:hover) {
|
||||||
|
box-shadow: 0 0 0 1px #d1d5db inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__wrapper.is-focus) {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px #2d60e3 inset,
|
||||||
|
0 0 0 3px rgba(45, 96, 227, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-button--primary) {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form__extras {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.login-form__extras .el-checkbox) {
|
||||||
|
margin-right: 0;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.login-form__extras .el-checkbox__label) {
|
||||||
|
color: #374151;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.login-form__extras .el-button.is-link) {
|
||||||
|
padding: 0;
|
||||||
|
height: auto;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
.el-input {
|
.el-input {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
input {
|
input {
|
||||||
@@ -276,19 +476,6 @@ onMounted(() => {
|
|||||||
color: #bfbfbf;
|
color: #bfbfbf;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form :deep(.el-input__wrapper) {
|
|
||||||
background-color: rgba(255, 255, 255, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-form :deep(.el-input__wrapper.is-focus) {
|
|
||||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
//.login-form :deep(.el-button--primary) {
|
|
||||||
// border-radius: var(--app-radius-md);
|
|
||||||
// box-shadow: 0 8px 20px rgba(59, 130, 246, 0.25);
|
|
||||||
//}
|
|
||||||
|
|
||||||
.login-form :deep(.el-button.is-circle) {
|
.login-form :deep(.el-button.is-circle) {
|
||||||
background: rgba(15, 23, 42, 0.04);
|
background: rgba(15, 23, 42, 0.04);
|
||||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||||
@@ -340,6 +527,11 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:global(html.dark) {
|
:global(html.dark) {
|
||||||
|
.login-brand {
|
||||||
|
background: linear-gradient(165deg, #2563eb 0%, #1d4ed8 55%, #1e3a8a 100%);
|
||||||
|
box-shadow: 0 24px 48px rgba(15, 23, 42, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
.login-form {
|
.login-form {
|
||||||
background: rgba(17, 24, 39, 0.9);
|
background: rgba(17, 24, 39, 0.9);
|
||||||
border-color: rgba(148, 163, 184, 0.2);
|
border-color: rgba(148, 163, 184, 0.2);
|
||||||
|
|||||||
@@ -25,10 +25,10 @@
|
|||||||
<svg-icon icon-class="email" />用户邮箱
|
<svg-icon icon-class="email" />用户邮箱
|
||||||
<div class="pull-right">{{ state.user.email }}</div>
|
<div class="pull-right">{{ state.user.email }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item">
|
<!-- <li class="list-group-item">
|
||||||
<svg-icon icon-class="tree" />所属部门
|
<svg-icon icon-class="tree" />所属部门
|
||||||
<div v-if="state.user.deptName" class="pull-right">{{ state.user.deptName }} / {{ state.postGroup }}</div>
|
<div v-if="state.user.deptName" class="pull-right">{{ state.user.deptName }} / {{ state.postGroup }}</div>
|
||||||
</li>
|
</li> -->
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<svg-icon icon-class="peoples" />所属角色
|
<svg-icon icon-class="peoples" />所属角色
|
||||||
<div class="pull-right">{{ state.roleGroup }}</div>
|
<div class="pull-right">{{ state.roleGroup }}</div>
|
||||||
@@ -55,9 +55,9 @@
|
|||||||
<el-tab-pane label="修改密码" name="resetPwd">
|
<el-tab-pane label="修改密码" name="resetPwd">
|
||||||
<resetPwd />
|
<resetPwd />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="第三方应用" name="thirdParty">
|
<!-- <el-tab-pane label="第三方应用" name="thirdParty">
|
||||||
<thirdParty :auths="state.auths" />
|
<thirdParty :auths="state.auths" />
|
||||||
</el-tab-pane>
|
</el-tab-pane> -->
|
||||||
<el-tab-pane label="在线设备" name="onlineDevice">
|
<el-tab-pane label="在线设备" name="onlineDevice">
|
||||||
<onlineDevice :devices="state.devices" />
|
<onlineDevice :devices="state.devices" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|||||||
189
src/views/systemList/base/base.vue
Normal file
189
src/views/systemList/base/base.vue
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<template>
|
||||||
|
<div class="base">
|
||||||
|
<div class="base-content">
|
||||||
|
<p class="title">系统名称与LOGO配置</p>
|
||||||
|
<el-form ref="ruleFormRef" :model="form">
|
||||||
|
<el-form-item label="系统名称" prop="name">
|
||||||
|
<el-input v-model="form.name" maxlength="11" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="系统图标" prop="logo">
|
||||||
|
<div class="text-center">
|
||||||
|
<el-upload
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
list-type="picture-card"
|
||||||
|
:limit="1"
|
||||||
|
:on-remove="handleRemove"
|
||||||
|
:action="uploadUrl"
|
||||||
|
:on-change="handleChange"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:on-exceed="handleExceed"
|
||||||
|
:auto-upload="false"
|
||||||
|
accept="image/jpeg,image/png"
|
||||||
|
:on-preview="handlePictureCardPreview"
|
||||||
|
>
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
</el-upload>
|
||||||
|
|
||||||
|
<el-dialog v-model="dialogVisible">
|
||||||
|
<img w-full :src="dialogImageUrl" alt="Preview Image" />
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item class="ml-17">
|
||||||
|
<p>只支持.jpg、.png格式,最多上传1张,单张大小不超过500kb</p>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item class="button-item">
|
||||||
|
<div class="flex justify-center gap-4">
|
||||||
|
<el-button @click="cancel" class="w-120px mr-100px -ml-100px">取 消</el-button>
|
||||||
|
<el-button type="primary" @click="save" class="w-120px">保存配置</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import type { UploadProps, UploadUserFile } from 'element-plus';
|
||||||
|
import { uploadDemo, getDemo, updateDemo } from '@/api/systemList/index';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { Plus } from '@element-plus/icons-vue';
|
||||||
|
import router from '@/router';
|
||||||
|
|
||||||
|
const uploadUrl = import.meta.env.VITE_BASE_API + '/system/logo/upload';
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
name: '可视化电子围栏管理系统',
|
||||||
|
url: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const getList = async () => {
|
||||||
|
const res = await getDemo();
|
||||||
|
|
||||||
|
console.log('getList基本数据', res);
|
||||||
|
|
||||||
|
if (res.data.url) {
|
||||||
|
fileList.value = [
|
||||||
|
{
|
||||||
|
url: res.data.url,
|
||||||
|
name: '系统图标'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
form.value.url = res.data.url;
|
||||||
|
form.value.name = res.data.name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
fileList.value = [];
|
||||||
|
form.value.url = '';
|
||||||
|
form.value.name = '';
|
||||||
|
};
|
||||||
|
const save = async () => {
|
||||||
|
console.log('保存数据', form.value);
|
||||||
|
updateDemo(form.value)
|
||||||
|
.then((res) => {
|
||||||
|
console.log('保存成功', res);
|
||||||
|
ElMessage.success('保存成功');
|
||||||
|
// 刷新当前路由
|
||||||
|
router.go(0);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('保存失败', err);
|
||||||
|
ElMessage.error('保存失败');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ------------------------------------------ 图片上传逻辑----------------------------------------- */
|
||||||
|
|
||||||
|
const fileList = ref<UploadUserFile[]>([]);
|
||||||
|
|
||||||
|
const dialogImageUrl = ref('');
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
|
||||||
|
const handleRemove: UploadProps['onRemove'] = (uploadFile, uploadFiles) => {
|
||||||
|
// 清空 logo 字段
|
||||||
|
form.value.url = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理超出限制的情况
|
||||||
|
const handleExceed: UploadProps['onExceed'] = () => {
|
||||||
|
ElMessage.warning('最多只能上传1张图片,请先删除现有图片后再上传');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自定义 change 处理函数
|
||||||
|
const handleChange = (file: any) => {
|
||||||
|
// ⚠️ 核心点:必须使用 file.raw 才能获取到真实的原生 File 对象
|
||||||
|
const realFile = file.raw;
|
||||||
|
|
||||||
|
console.log('原生file对象', realFile);
|
||||||
|
|
||||||
|
// 直接调用你封装好的 uploadDemo 方法
|
||||||
|
uploadDemo(realFile)
|
||||||
|
.then((res) => {
|
||||||
|
console.log('上传成功', res);
|
||||||
|
form.value.url = res.data.imgUrl;
|
||||||
|
console.log('上传设备', form.value);
|
||||||
|
fileList.value = [form.value];
|
||||||
|
//getList(); // 刷新列表获取最新的 logo URL
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('上传失败', err);
|
||||||
|
fileList.value = []; // 清空上传列表
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//验证逻辑
|
||||||
|
const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
||||||
|
console.log('开始上传');
|
||||||
|
|
||||||
|
// 验证文件大小(500KB)
|
||||||
|
const isLt500K = rawFile.size / 1024 / 1024 < 0.5;
|
||||||
|
if (!isLt500K) {
|
||||||
|
ElMessage.error('图片大小不能超过 500KB!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证文件类型
|
||||||
|
const isJPGorPNG = rawFile.type === 'image/jpeg' || rawFile.type === 'image/png';
|
||||||
|
if (!isJPGorPNG) {
|
||||||
|
ElMessage.error('只能上传 JPG/PNG 格式的图片!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
|
||||||
|
dialogImageUrl.value = uploadFile.url!;
|
||||||
|
dialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.base {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
background-color: #fff;
|
||||||
|
.base-content {
|
||||||
|
width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 覆盖 el-form-item 默认样式,确保按钮居中
|
||||||
|
:deep(.button-item) {
|
||||||
|
.el-form-item__content {
|
||||||
|
justify-content: center !important;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
103
src/views/systemList/password/password.vue
Normal file
103
src/views/systemList/password/password.vue
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<div class="base">
|
||||||
|
<div class="base-content">
|
||||||
|
<el-form ref="pwdRef" :model="user" :rules="rules" label-width="80px">
|
||||||
|
<el-form-item label="账号" prop="username" label-position="left">
|
||||||
|
<el-input v-model="user.username" placeholder="请输入账号" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="旧密码" prop="oldPassword" label-position="left">
|
||||||
|
<el-input v-model="user.oldPassword" placeholder="请输入旧密码" type="password" show-password label-position="left" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新密码" prop="newPassword" label-position="left">
|
||||||
|
<el-input v-model="user.newPassword" placeholder="请输入新密码" type="password" show-password label-position="left" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认密码" prop="confirmPassword" label-position="left">
|
||||||
|
<el-input v-model="user.confirmPassword" placeholder="请确认新密码" type="password" show-password label-position="left" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item style="margin: 50px 0 0 0">
|
||||||
|
<el-button style="width: 100px; margin-left: 200px" type="primary" @click="submit">提交</el-button>
|
||||||
|
<!-- <el-button type="danger" @click="close">关闭</el-button> -->
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { updateUserPwd, getUserProfile } from '@/api/system/user';
|
||||||
|
import type { ResetPwdForm } from '@/api/system/user/types';
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
|
const pwdRef = ref<ElFormInstance>();
|
||||||
|
const user = ref({
|
||||||
|
username: '',
|
||||||
|
oldPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const equalToPassword = (rule: any, value: string, callback: any) => {
|
||||||
|
if (user.value.newPassword !== value) {
|
||||||
|
callback(new Error('两次输入的密码不一致'));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const rules = ref({
|
||||||
|
username: [{ required: true, message: '账号不能为空', trigger: 'blur' }],
|
||||||
|
oldPassword: [{ required: true, message: '旧密码不能为空', trigger: 'blur' }],
|
||||||
|
newPassword: [
|
||||||
|
{ required: true, message: '新密码不能为空', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
min: 6,
|
||||||
|
max: 20,
|
||||||
|
message: '长度在 6 到 20 个字符',
|
||||||
|
trigger: 'blur'
|
||||||
|
},
|
||||||
|
{ pattern: /^[^<>"'|\\]+$/, message: '不能包含非法字符:< > " \' \\ |', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
confirmPassword: [
|
||||||
|
{ required: true, message: '确认密码不能为空', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
validator: equalToPassword,
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 提交按钮 */
|
||||||
|
const submit = async () => {
|
||||||
|
const res = await getUserProfile();
|
||||||
|
console.log('当前用户信息', res);
|
||||||
|
if (user.value.username != res.data.user?.userName) {
|
||||||
|
proxy?.$modal.msgError('账号填写错误');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
pwdRef.value?.validate(async (valid: boolean) => {
|
||||||
|
if (valid) {
|
||||||
|
await updateUserPwd(user.value.oldPassword, user.value.newPassword);
|
||||||
|
proxy?.$modal.msgSuccess('修改成功');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/** 关闭按钮 */
|
||||||
|
const close = () => {
|
||||||
|
proxy?.$tab.closePage();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.base {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
background-color: #fff;
|
||||||
|
.base-content {
|
||||||
|
width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
489
src/views/systemList/role/role.vue
Normal file
489
src/views/systemList/role/role.vue
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-2">
|
||||||
|
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||||
|
<div v-show="showSearch" class="mb-[10px]">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||||
|
<el-form-item label="角色名称" prop="roleName">
|
||||||
|
<el-input v-model="queryParams.roleName" placeholder="请输入角色名称" clearable @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<el-row :gutter="10" class="flex justify-end mb8">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button v-hasPermi="['system:role:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
|
||||||
|
</el-col>
|
||||||
|
<!-- <el-col :span="1.5">
|
||||||
|
<el-button v-hasPermi="['system:role:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button v-hasPermi="['system:role:remove']" type="danger" plain :disabled="ids.length === 0" @click="handleDelete()">删除</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button v-hasPermi="['system:role:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
|
||||||
|
</el-col> -->
|
||||||
|
<!-- <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar> -->
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table ref="roleTableRef" border v-loading="loading" :data="roleList" @selection-change="handleSelectionChange">
|
||||||
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
|
<el-table-column v-if="false" label="角色编号" prop="roleId" />
|
||||||
|
<el-table-column label="角色名称" prop="roleName" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column label="角色描述" prop="remark" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column label="操作人" prop="createUserName" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column label="排序" prop="roleSort" />
|
||||||
|
<el-table-column label="状态" align="center" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<!-- <el-table-column label="创建时间" align="center" prop="createTime">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ proxy.parseTime(scope.row.createTime) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column> -->
|
||||||
|
|
||||||
|
<el-table-column fixed="right" label="操作" width="380" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button type="primary" @click="handleUpdate(scope.row)">权限配置</el-button>
|
||||||
|
|
||||||
|
<!-- <el-button type="primary" @click="handleUpdate(scope.row)">编辑</el-button> -->
|
||||||
|
|
||||||
|
<el-button @click="handleDelete(scope.row)">删除</el-button>
|
||||||
|
|
||||||
|
<!-- <el-tooltip v-if="scope.row.roleId !== 1" content="数据权限" placement="top">
|
||||||
|
<el-button v-hasPermi="['system:role:edit']" link type="primary" icon="CircleCheck" @click="handleDataScope(scope.row)"></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip v-if="scope.row.roleId !== 1" content="分配用户" placement="top">
|
||||||
|
<el-button v-hasPermi="['system:role:edit']" link type="primary" icon="User" @click="handleAuthUser(scope.row)"></el-button>
|
||||||
|
</el-tooltip> -->
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-if="total > 0"
|
||||||
|
v-model:total="total"
|
||||||
|
v-model:page="queryParams.pageNum"
|
||||||
|
v-model:limit="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
|
||||||
|
<el-form ref="roleFormRef" :model="form" :rules="rules" label-width="100px">
|
||||||
|
<el-form-item label="角色名称" prop="roleName">
|
||||||
|
<el-input v-model="form.roleName" placeholder="请输入角色名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<!-- <el-form-item prop="roleKey">
|
||||||
|
<template #label>
|
||||||
|
<span>
|
||||||
|
<el-tooltip content="控制器中定义的权限字符,如:@SaCheckRole('admin')" placement="top">
|
||||||
|
<el-icon><question-filled /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
权限字符
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-input v-model="form.roleKey" placeholder="请输入权限字符" />
|
||||||
|
</el-form-item> -->
|
||||||
|
<el-form-item label="角色顺序" prop="roleSort">
|
||||||
|
<el-input-number v-model="form.roleSort" controls-position="right" :min="0" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-radio-group v-model="form.status">
|
||||||
|
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="菜单权限">
|
||||||
|
<el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand($event, 'menu')">展开/折叠</el-checkbox>
|
||||||
|
<el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event, 'menu')">全选/全不选</el-checkbox>
|
||||||
|
<el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event, 'menu')">父子联动</el-checkbox>
|
||||||
|
<el-tree
|
||||||
|
ref="menuRef"
|
||||||
|
class="tree-border"
|
||||||
|
:data="menuOptions"
|
||||||
|
show-checkbox
|
||||||
|
node-key="id"
|
||||||
|
:check-strictly="!form.menuCheckStrictly"
|
||||||
|
empty-text="加载中,请稍候"
|
||||||
|
:props="{ label: 'label', children: 'children' } as any"
|
||||||
|
></el-tree>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
<el-button @click="cancel">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 分配角色数据权限对话框 -->
|
||||||
|
<el-dialog v-model="openDataScope" :title="dialog.title" width="500px" append-to-body>
|
||||||
|
<el-form ref="dataScopeRef" :model="form" label-width="80px">
|
||||||
|
<el-form-item label="角色名称">
|
||||||
|
<el-input v-model="form.roleName" :disabled="true" />
|
||||||
|
</el-form-item>
|
||||||
|
<!-- <el-form-item label="权限字符">
|
||||||
|
<el-input v-model="form.roleKey" :disabled="true" />
|
||||||
|
</el-form-item> -->
|
||||||
|
<el-form-item label="权限范围">
|
||||||
|
<el-select v-model="form.dataScope" @change="dataScopeSelectChange">
|
||||||
|
<el-option v-for="item in dataScopeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-show="form.dataScope === '2'" label="数据权限">
|
||||||
|
<el-checkbox v-model="deptExpand" @change="handleCheckedTreeExpand($event, 'dept')">展开/折叠</el-checkbox>
|
||||||
|
<el-checkbox v-model="deptNodeAll" @change="handleCheckedTreeNodeAll($event, 'dept')">全选/全不选</el-checkbox>
|
||||||
|
<el-checkbox v-model="form.deptCheckStrictly" @change="handleCheckedTreeConnect($event, 'dept')">父子联动</el-checkbox>
|
||||||
|
<el-tree
|
||||||
|
ref="deptRef"
|
||||||
|
class="tree-border"
|
||||||
|
:data="deptOptions"
|
||||||
|
show-checkbox
|
||||||
|
default-expand-all
|
||||||
|
node-key="id"
|
||||||
|
:check-strictly="!form.deptCheckStrictly"
|
||||||
|
empty-text="加载中,请稍候"
|
||||||
|
:props="{ label: 'label', children: 'children' } as any"
|
||||||
|
></el-tree>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitDataScope">确 定</el-button>
|
||||||
|
<el-button @click="cancelDataScope">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="Role" lang="ts">
|
||||||
|
import { addRole, changeRoleStatus, dataScope, delRole, getRole, listRole, updateRole, deptTreeSelect } from '@/api/system/role';
|
||||||
|
import { roleMenuTreeselect, treeselect as menuTreeselect } from '@/api/system/menu/index';
|
||||||
|
import { RoleVO, RoleForm, RoleQuery, DeptTreeOption } from '@/api/system/role/types';
|
||||||
|
import { MenuTreeOption, RoleMenuTree } from '@/api/system/menu/types';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
|
const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
|
||||||
|
|
||||||
|
const roleList = ref<RoleVO[]>();
|
||||||
|
const loading = ref(true);
|
||||||
|
const showSearch = ref(true);
|
||||||
|
const ids = ref<Array<string | number>>([]);
|
||||||
|
const single = ref(true);
|
||||||
|
const multiple = ref(true);
|
||||||
|
const total = ref(0);
|
||||||
|
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
|
||||||
|
const menuOptions = ref<MenuTreeOption[]>([]);
|
||||||
|
const menuExpand = ref(false);
|
||||||
|
const menuNodeAll = ref(false);
|
||||||
|
const deptExpand = ref(true);
|
||||||
|
const deptNodeAll = ref(false);
|
||||||
|
const deptOptions = ref<DeptTreeOption[]>([]);
|
||||||
|
const openDataScope = ref(false);
|
||||||
|
|
||||||
|
/** 数据范围选项*/
|
||||||
|
const dataScopeOptions = ref([
|
||||||
|
{ value: '1', label: '全部数据权限' },
|
||||||
|
{ value: '2', label: '自定数据权限' },
|
||||||
|
{ value: '3', label: '本部门数据权限' },
|
||||||
|
{ value: '4', label: '本部门及以下数据权限' },
|
||||||
|
{ value: '5', label: '仅本人数据权限' },
|
||||||
|
{ value: '6', label: '部门及以下或本人数据权限' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const queryFormRef = ref<ElFormInstance>();
|
||||||
|
const roleFormRef = ref<ElFormInstance>();
|
||||||
|
const dataScopeRef = ref<ElFormInstance>();
|
||||||
|
const menuRef = ref<ElTreeInstance>();
|
||||||
|
const deptRef = ref<ElTreeInstance>();
|
||||||
|
|
||||||
|
const initForm: RoleForm = {
|
||||||
|
roleId: undefined,
|
||||||
|
roleSort: 1,
|
||||||
|
status: '0',
|
||||||
|
roleName: '',
|
||||||
|
roleKey: '',
|
||||||
|
menuCheckStrictly: true,
|
||||||
|
deptCheckStrictly: true,
|
||||||
|
remark: '',
|
||||||
|
dataScope: '1',
|
||||||
|
menuIds: [],
|
||||||
|
deptIds: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = reactive<PageData<RoleForm, RoleQuery>>({
|
||||||
|
form: { ...initForm },
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
roleName: '',
|
||||||
|
roleKey: '',
|
||||||
|
status: ''
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
roleName: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }],
|
||||||
|
roleKey: [{ required: true, message: '权限字符不能为空', trigger: 'blur' }],
|
||||||
|
roleSort: [{ required: true, message: '角色顺序不能为空', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const { form, queryParams, rules } = toRefs(data);
|
||||||
|
|
||||||
|
const dialog = reactive<DialogOption>({
|
||||||
|
visible: false,
|
||||||
|
title: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询角色列表
|
||||||
|
*/
|
||||||
|
const getList = () => {
|
||||||
|
loading.value = true;
|
||||||
|
listRole(proxy?.addDateRange(queryParams.value, dateRange.value)).then((res) => {
|
||||||
|
roleList.value = res.rows;
|
||||||
|
total.value = res.total;
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索按钮操作
|
||||||
|
*/
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.value.pageNum = 1;
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 重置 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
dateRange.value = ['', ''];
|
||||||
|
queryFormRef.value?.resetFields();
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
/**删除按钮操作 */
|
||||||
|
const handleDelete = async (row?: RoleVO) => {
|
||||||
|
const roleids = row?.roleId || ids.value;
|
||||||
|
await proxy?.$modal.confirm('是否确认删除角色编号为' + roleids + '数据项目');
|
||||||
|
await delRole(roleids);
|
||||||
|
getList();
|
||||||
|
proxy?.$modal.msgSuccess('删除成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 导出按钮操作 */
|
||||||
|
const handleExport = () => {
|
||||||
|
proxy?.download(
|
||||||
|
'system/role/export',
|
||||||
|
{
|
||||||
|
...queryParams.value
|
||||||
|
},
|
||||||
|
`role_${new Date().getTime()}.xlsx`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/** 多选框选中数据 */
|
||||||
|
const handleSelectionChange = (selection: RoleVO[]) => {
|
||||||
|
ids.value = selection.map((item: RoleVO) => item.roleId);
|
||||||
|
single.value = selection.length != 1;
|
||||||
|
multiple.value = !selection.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 角色状态修改 */
|
||||||
|
const handleStatusChange = async (row: RoleVO) => {
|
||||||
|
const text = row.status === '0' ? '启用' : '停用';
|
||||||
|
try {
|
||||||
|
await proxy?.$modal.confirm('确认要"' + text + '""' + row.roleName + '"角色吗?');
|
||||||
|
await changeRoleStatus(row.roleId, row.status);
|
||||||
|
proxy?.$modal.msgSuccess(text + '成功');
|
||||||
|
} catch {
|
||||||
|
row.status = row.status === '0' ? '1' : '0';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 分配用户 */
|
||||||
|
const handleAuthUser = (row: RoleVO) => {
|
||||||
|
router.push('/system/role-auth/user/' + row.roleId);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查询菜单树结构 */
|
||||||
|
const getMenuTreeselect = async () => {
|
||||||
|
const res = await menuTreeselect();
|
||||||
|
menuOptions.value = res.data;
|
||||||
|
};
|
||||||
|
/** 所有部门节点数据 */
|
||||||
|
const getDeptAllCheckedKeys = (): any => {
|
||||||
|
// 目前被选中的部门节点
|
||||||
|
const checkedKeys = deptRef.value?.getCheckedKeys();
|
||||||
|
// 半选中的部门节点
|
||||||
|
const halfCheckedKeys = deptRef.value?.getHalfCheckedKeys();
|
||||||
|
if (halfCheckedKeys) {
|
||||||
|
checkedKeys?.unshift(...halfCheckedKeys);
|
||||||
|
}
|
||||||
|
return checkedKeys;
|
||||||
|
};
|
||||||
|
/** 重置新增的表单以及其他数据 */
|
||||||
|
const reset = () => {
|
||||||
|
menuRef.value?.setCheckedKeys([]);
|
||||||
|
menuExpand.value = false;
|
||||||
|
menuNodeAll.value = false;
|
||||||
|
deptExpand.value = true;
|
||||||
|
deptNodeAll.value = false;
|
||||||
|
form.value = { ...initForm };
|
||||||
|
roleFormRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 添加角色 */
|
||||||
|
const handleAdd = () => {
|
||||||
|
reset();
|
||||||
|
getMenuTreeselect();
|
||||||
|
dialog.visible = true;
|
||||||
|
dialog.title = '添加角色';
|
||||||
|
};
|
||||||
|
/** 修改角色 */
|
||||||
|
const handleUpdate = async (row?: RoleVO) => {
|
||||||
|
reset();
|
||||||
|
const roleId = row?.roleId || ids.value[0];
|
||||||
|
const { data } = await getRole(roleId);
|
||||||
|
Object.assign(form.value, data);
|
||||||
|
form.value.roleSort = Number(form.value.roleSort);
|
||||||
|
const res = await getRoleMenuTreeselect(roleId);
|
||||||
|
dialog.title = '修改角色';
|
||||||
|
dialog.visible = true;
|
||||||
|
res.checkedKeys.forEach((v) => {
|
||||||
|
nextTick(() => {
|
||||||
|
menuRef.value?.setChecked(v, true, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/** 根据角色ID查询菜单树结构 */
|
||||||
|
const getRoleMenuTreeselect = (roleId: string | number) => {
|
||||||
|
return roleMenuTreeselect(roleId).then((res): RoleMenuTree => {
|
||||||
|
menuOptions.value = res.data.menus;
|
||||||
|
return res.data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/** 根据角色ID查询部门树结构 */
|
||||||
|
const getRoleDeptTreeSelect = async (roleId: string | number) => {
|
||||||
|
const res = await deptTreeSelect(roleId);
|
||||||
|
deptOptions.value = res.data.depts;
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
/** 树权限(展开/折叠)*/
|
||||||
|
const handleCheckedTreeExpand = (value: boolean, type: string) => {
|
||||||
|
if (type == 'menu') {
|
||||||
|
const treeList = menuOptions.value;
|
||||||
|
for (let i = 0; i < treeList.length; i++) {
|
||||||
|
if (menuRef.value) {
|
||||||
|
menuRef.value.store.nodesMap[treeList[i].id].expanded = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (type == 'dept') {
|
||||||
|
const treeList = deptOptions.value;
|
||||||
|
for (let i = 0; i < treeList.length; i++) {
|
||||||
|
if (deptRef.value) {
|
||||||
|
deptRef.value.store.nodesMap[treeList[i].id].expanded = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/** 树权限(全选/全不选) */
|
||||||
|
const handleCheckedTreeNodeAll = (value: any, type: string) => {
|
||||||
|
if (type == 'menu') {
|
||||||
|
menuRef.value?.setCheckedNodes(value ? (menuOptions.value as any) : []);
|
||||||
|
} else if (type == 'dept') {
|
||||||
|
deptRef.value?.setCheckedNodes(value ? (deptOptions.value as any) : []);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/** 树权限(父子联动) */
|
||||||
|
const handleCheckedTreeConnect = (value: any, type: string) => {
|
||||||
|
if (type == 'menu') {
|
||||||
|
form.value.menuCheckStrictly = value;
|
||||||
|
} else if (type == 'dept') {
|
||||||
|
form.value.deptCheckStrictly = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/** 所有菜单节点数据 */
|
||||||
|
const getMenuAllCheckedKeys = (): any => {
|
||||||
|
// 目前被选中的菜单节点
|
||||||
|
const checkedKeys = menuRef.value?.getCheckedKeys();
|
||||||
|
// 半选中的菜单节点
|
||||||
|
const halfCheckedKeys = menuRef.value?.getHalfCheckedKeys();
|
||||||
|
if (halfCheckedKeys) {
|
||||||
|
checkedKeys?.unshift(...halfCheckedKeys);
|
||||||
|
}
|
||||||
|
return checkedKeys;
|
||||||
|
};
|
||||||
|
/** 提交按钮 */
|
||||||
|
const submitForm = () => {
|
||||||
|
roleFormRef.value?.validate(async (valid: boolean) => {
|
||||||
|
if (valid) {
|
||||||
|
// 如果 rolekey 为空,自动生成唯一标识
|
||||||
|
if (!form.value.roleKey) {
|
||||||
|
form.value.roleKey = `role_${Date.now()}_${Math.random().toString(36)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.value.menuIds = getMenuAllCheckedKeys();
|
||||||
|
form.value.roleId ? await updateRole(form.value) : await addRole(form.value);
|
||||||
|
proxy?.$modal.msgSuccess('操作成功');
|
||||||
|
dialog.visible = false;
|
||||||
|
getList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/** 取消按钮 */
|
||||||
|
const cancel = () => {
|
||||||
|
reset();
|
||||||
|
dialog.visible = false;
|
||||||
|
};
|
||||||
|
/** 选择角色权限范围触发 */
|
||||||
|
const dataScopeSelectChange = (value: string) => {
|
||||||
|
if (value !== '2') {
|
||||||
|
deptRef.value?.setCheckedKeys([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/** 分配数据权限操作 */
|
||||||
|
const handleDataScope = async (row: RoleVO) => {
|
||||||
|
const response = await getRole(row.roleId);
|
||||||
|
Object.assign(form.value, response.data);
|
||||||
|
const res = await getRoleDeptTreeSelect(row.roleId);
|
||||||
|
openDataScope.value = true;
|
||||||
|
dialog.title = '分配数据权限';
|
||||||
|
await nextTick(() => {
|
||||||
|
deptRef.value?.setCheckedKeys(res.checkedKeys);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/** 提交按钮(数据权限) */
|
||||||
|
const submitDataScope = async () => {
|
||||||
|
if (form.value.roleId) {
|
||||||
|
form.value.deptIds = getDeptAllCheckedKeys();
|
||||||
|
await dataScope(form.value);
|
||||||
|
proxy?.$modal.msgSuccess('修改成功');
|
||||||
|
openDataScope.value = false;
|
||||||
|
getList();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/** 取消按钮(数据权限)*/
|
||||||
|
const cancelDataScope = () => {
|
||||||
|
dataScopeRef.value?.resetFields();
|
||||||
|
form.value = { ...initForm };
|
||||||
|
openDataScope.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
683
src/views/systemList/user/user.vue
Normal file
683
src/views/systemList/user/user.vue
Normal file
@@ -0,0 +1,683 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-2">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<!-- <el-col :lg="4" :xs="24" style="">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable />
|
||||||
|
<el-tree
|
||||||
|
ref="deptTreeRef"
|
||||||
|
class="mt-2"
|
||||||
|
node-key="id"
|
||||||
|
:data="deptOptions"
|
||||||
|
:props="{ label: 'label', children: 'children' } as any"
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
:filter-node-method="filterNode"
|
||||||
|
highlight-current
|
||||||
|
default-expand-all
|
||||||
|
@node-click="handleNodeClick"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</el-col> -->
|
||||||
|
<el-col>
|
||||||
|
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||||
|
<div v-show="showSearch" class="mb-[10px]">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||||
|
<el-form-item label="用户名称" prop="userName">
|
||||||
|
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<!-- <el-form-item label="用户账号" prop="nickName">
|
||||||
|
<el-input v-model="queryParams.nickName" placeholder="请输入用户账号" clearable @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号码" prop="phonenumber">
|
||||||
|
<el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="queryParams.status" placeholder="用户状态" clearable>
|
||||||
|
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间" style="width: 308px">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||||
|
></el-date-picker>
|
||||||
|
</el-form-item> -->
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<el-row :gutter="10" class="flex justify-end mb8">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button v-has-permi="['system:user:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
|
||||||
|
</el-col>
|
||||||
|
<!-- <el-col :span="1.5">
|
||||||
|
<el-button v-has-permi="['system:user:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">
|
||||||
|
修改
|
||||||
|
</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button v-has-permi="['system:user:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-dropdown class="mt-[1px]">
|
||||||
|
<el-button plain type="info">
|
||||||
|
更多
|
||||||
|
<el-icon class="el-icon--right"><arrow-down /></el-icon
|
||||||
|
></el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item icon="Download" @click="importTemplate">下载模板</el-dropdown-item> -->
|
||||||
|
<!-- 注意 由于el-dropdown-item标签是延迟加载的 所以v-has-permi自定义标签不生效 需要使用v-if调用方法执行 -->
|
||||||
|
<!-- <el-dropdown-item v-if="checkPermi(['system:user:import'])" icon="Top" @click="handleImport">导入数据</el-dropdown-item>
|
||||||
|
<el-dropdown-item v-if="checkPermi(['system:user:export'])" icon="Download" @click="handleExport">导出数据</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</el-col> -->
|
||||||
|
<!-- <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar> -->
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" border :data="userList" @selection-change="handleSelectionChange">
|
||||||
|
<el-table-column type="selection" width="50" align="center" />
|
||||||
|
<el-table-column key="userName" label="用户账号" align="center" prop="userName" />
|
||||||
|
<el-table-column v-if="columns[1].visible" key="roleName" label="角色名称" align="center" prop="roleName" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column v-if="columns[2].visible" key="nickName" label="操作人" align="center" prop="nickName" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column
|
||||||
|
v-if="columns[3].visible"
|
||||||
|
key="createTime"
|
||||||
|
label="添加时间"
|
||||||
|
align="center"
|
||||||
|
prop="createTime"
|
||||||
|
:show-overflow-tooltip="true"
|
||||||
|
/>
|
||||||
|
<el-table-column v-if="columns[4].visible" key="sort" label="排序" align="center" prop="sort" width="120" />
|
||||||
|
<el-table-column v-if="columns[5].visible" key="status" label="状态" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- <el-table-column v-if="columns[6].visible" label="创建时间" align="center" prop="createTime" width="160">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ scope.row.createTime }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column> -->
|
||||||
|
|
||||||
|
<el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
|
||||||
|
<template #default="scope">
|
||||||
|
<!-- <el-tooltip v-if="scope.row.userId !== 1" content="修改" placement="top"> -->
|
||||||
|
<el-button v-hasPermi="['system:user:edit']" type="primary" @click="handleUpdate(scope.row)">编辑</el-button>
|
||||||
|
<!-- </el-tooltip>
|
||||||
|
<el-tooltip v-if="scope.row.userId !== 1" content="删除" placement="top"> -->
|
||||||
|
<el-button v-hasPermi="['system:user:remove']" type="primary" @click="handleDelete(scope.row)">删除</el-button>
|
||||||
|
<!-- </el-tooltip> -->
|
||||||
|
|
||||||
|
<!-- <el-tooltip v-if="scope.row.userId !== 1" content="重置密码" placement="top">
|
||||||
|
<el-button v-hasPermi="['system:user:resetPwd']" link type="primary" icon="Key" @click="handleResetPwd(scope.row)"></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<el-tooltip v-if="scope.row.userId !== 1" content="分配角色" placement="top">
|
||||||
|
<el-button v-hasPermi="['system:user:edit']" link type="primary" icon="CircleCheck" @click="handleAuthRole(scope.row)"></el-button>
|
||||||
|
</el-tooltip> -->
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="total > 0"
|
||||||
|
v-model:page="queryParams.pageNum"
|
||||||
|
v-model:limit="queryParams.pageSize"
|
||||||
|
:total="total"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 添加或修改用户配置对话框 -->
|
||||||
|
<el-dialog ref="formDialogRef" v-model="dialog.visible" :title="dialog.title" width="600px" append-to-body @close="closeDialog">
|
||||||
|
<el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="用户账号" prop="nickName">
|
||||||
|
<el-input v-model="form.nickName" placeholder="请输入用户账号" maxlength="30" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<!-- <el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId">
|
||||||
|
<el-form-item label="归属部门" prop="deptId">
|
||||||
|
<el-tree-select
|
||||||
|
v-model="form.deptId"
|
||||||
|
:data="enabledDeptOptions"
|
||||||
|
:props="{ value: 'id', label: 'label', children: 'children' } as any"
|
||||||
|
value-key="id"
|
||||||
|
placeholder="请选择归属部门"
|
||||||
|
check-strictly
|
||||||
|
@change="handleDeptChange"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col> -->
|
||||||
|
<!-- </el-row>
|
||||||
|
<el-row> -->
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="手机号码" prop="phonenumber">
|
||||||
|
<el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<!-- </el-row>
|
||||||
|
<el-row> -->
|
||||||
|
<el-col :span="12" v-if="form.userId == undefined">
|
||||||
|
<el-form-item label="用户名称" prop="userName">
|
||||||
|
<el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12" v-if="form.userId == undefined">
|
||||||
|
<el-form-item label="用户密码" prop="password">
|
||||||
|
<el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<!-- </el-row>
|
||||||
|
<el-row> -->
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="用户性别">
|
||||||
|
<el-select v-model="form.sex" placeholder="请选择">
|
||||||
|
<el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-radio-group v-model="form.status">
|
||||||
|
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<!-- </el-row>
|
||||||
|
<el-row> -->
|
||||||
|
<!-- <el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId">
|
||||||
|
<el-form-item label="岗位">
|
||||||
|
<el-select v-model="form.postIds" multiple placeholder="请选择">
|
||||||
|
<el-option
|
||||||
|
v-for="item in postOptions"
|
||||||
|
:key="item.postId"
|
||||||
|
:label="item.postName"
|
||||||
|
:value="item.postId"
|
||||||
|
:disabled="item.status == '1'"
|
||||||
|
></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col> -->
|
||||||
|
<el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId">
|
||||||
|
<el-form-item label="角色" prop="roleIds">
|
||||||
|
<el-select v-model="form.roleIds" filterable multiple placeholder="请选择">
|
||||||
|
<el-option
|
||||||
|
v-for="item in roleOptions"
|
||||||
|
:key="item.roleId"
|
||||||
|
:label="item.roleName"
|
||||||
|
:value="item.roleId"
|
||||||
|
:disabled="item.status == '1'"
|
||||||
|
></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<!-- </el-row>
|
||||||
|
<el-row> -->
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
<el-button @click="cancel()">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 用户导入对话框 -->
|
||||||
|
<el-dialog v-model="upload.open" :title="upload.title" width="400px" append-to-body>
|
||||||
|
<el-upload
|
||||||
|
ref="uploadRef"
|
||||||
|
:limit="1"
|
||||||
|
accept=".xlsx, .xls"
|
||||||
|
:headers="upload.headers"
|
||||||
|
:action="upload.url + '?updateSupport=' + upload.updateSupport"
|
||||||
|
:disabled="upload.isUploading"
|
||||||
|
:on-progress="handleFileUploadProgress"
|
||||||
|
:on-success="handleFileSuccess"
|
||||||
|
:auto-upload="false"
|
||||||
|
drag
|
||||||
|
>
|
||||||
|
<el-icon class="el-icon--upload">
|
||||||
|
<i-ep-upload-filled />
|
||||||
|
</el-icon>
|
||||||
|
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||||
|
<template #tip>
|
||||||
|
<div class="text-center el-upload__tip">
|
||||||
|
<div class="el-upload__tip"><el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据</div>
|
||||||
|
<span>仅允许导入xls、xlsx格式文件。</span>
|
||||||
|
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板</el-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitFileForm">确 定</el-button>
|
||||||
|
<el-button @click="upload.open = false">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="User" lang="ts">
|
||||||
|
import api from '@/api/system/user';
|
||||||
|
import { UserForm, UserQuery, UserVO } from '@/api/system/user/types';
|
||||||
|
import { DeptTreeVO, DeptVO } from '@/api/system/dept/types';
|
||||||
|
import { RoleVO } from '@/api/system/role/types';
|
||||||
|
import { PostVO } from '@/api/system/post/types';
|
||||||
|
import { globalHeaders } from '@/utils/request';
|
||||||
|
import { to } from 'await-to-js';
|
||||||
|
import { optionselect } from '@/api/system/post';
|
||||||
|
import { checkPermi } from '@/utils/permission';
|
||||||
|
import { useUserStore } from '@/store/modules/user';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
|
const { sys_normal_disable, sys_user_sex } = toRefs<any>(proxy?.useDict('sys_normal_disable', 'sys_user_sex'));
|
||||||
|
const userList = ref<UserVO[]>();
|
||||||
|
const loading = ref(true);
|
||||||
|
const showSearch = ref(true);
|
||||||
|
const ids = ref<Array<number | string>>([]);
|
||||||
|
const single = ref(true);
|
||||||
|
const multiple = ref(true);
|
||||||
|
const total = ref(0);
|
||||||
|
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
|
||||||
|
const deptName = ref('');
|
||||||
|
const deptOptions = ref<DeptTreeVO[]>([]);
|
||||||
|
const enabledDeptOptions = ref<DeptTreeVO[]>([]);
|
||||||
|
const initPassword = ref<string>('');
|
||||||
|
const postOptions = ref<PostVO[]>([]);
|
||||||
|
const roleOptions = ref<RoleVO[]>([]);
|
||||||
|
/*** 用户导入参数 */
|
||||||
|
const upload = reactive<ImportOption>({
|
||||||
|
// 是否显示弹出层(用户导入)
|
||||||
|
open: false,
|
||||||
|
// 弹出层标题(用户导入)
|
||||||
|
title: '',
|
||||||
|
// 是否禁用上传
|
||||||
|
isUploading: false,
|
||||||
|
// 是否更新已经存在的用户数据
|
||||||
|
updateSupport: 0,
|
||||||
|
// 设置上传的请求头部
|
||||||
|
headers: globalHeaders(),
|
||||||
|
// 上传的地址
|
||||||
|
url: import.meta.env.VITE_APP_BASE_API + '/system/user/importData'
|
||||||
|
});
|
||||||
|
// 列显隐信息
|
||||||
|
const columns = ref<FieldOption[]>([
|
||||||
|
{ key: 0, label: `用户编号`, visible: false, children: [] },
|
||||||
|
{ key: 1, label: `用户名称`, visible: true, children: [] },
|
||||||
|
{ key: 2, label: `用户账号`, visible: true, children: [] },
|
||||||
|
{ key: 3, label: `部门`, visible: true, children: [] },
|
||||||
|
{ key: 4, label: `手机号码`, visible: true, children: [] },
|
||||||
|
{ key: 5, label: `状态`, visible: true, children: [] },
|
||||||
|
{ key: 6, label: `创建时间`, visible: true, children: [] }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const deptTreeRef = ref<ElTreeInstance>();
|
||||||
|
const queryFormRef = ref<ElFormInstance>();
|
||||||
|
const userFormRef = ref<ElFormInstance>();
|
||||||
|
const uploadRef = ref<ElUploadInstance>();
|
||||||
|
const formDialogRef = ref<ElDialogInstance>();
|
||||||
|
|
||||||
|
const dialog = reactive<DialogOption>({
|
||||||
|
visible: false,
|
||||||
|
title: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const initFormData: UserForm = {
|
||||||
|
userId: undefined,
|
||||||
|
deptId: undefined,
|
||||||
|
userName: '',
|
||||||
|
nickName: undefined,
|
||||||
|
password: '',
|
||||||
|
phonenumber: undefined,
|
||||||
|
email: undefined,
|
||||||
|
sex: undefined,
|
||||||
|
status: '0',
|
||||||
|
remark: '',
|
||||||
|
postIds: [],
|
||||||
|
roleIds: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const initData: PageData<UserForm, UserQuery> = {
|
||||||
|
form: { ...initFormData },
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
userName: '',
|
||||||
|
phonenumber: '',
|
||||||
|
status: '',
|
||||||
|
deptId: '',
|
||||||
|
roleId: ''
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
userName: [
|
||||||
|
{ required: true, message: '用户名称不能为空', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
min: 2,
|
||||||
|
max: 20,
|
||||||
|
message: '用户名称长度必须介于 2 和 20 之间',
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
nickName: [{ required: true, message: '用户账号不能为空', trigger: 'blur' }],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '用户密码不能为空', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
min: 5,
|
||||||
|
max: 20,
|
||||||
|
message: '用户密码长度必须介于 5 和 20 之间',
|
||||||
|
trigger: 'blur'
|
||||||
|
},
|
||||||
|
{ pattern: /^[^<>"'|\\]+$/, message: '不能包含非法字符:< > " \' \\ |', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{
|
||||||
|
type: 'email',
|
||||||
|
message: '请输入正确的邮箱地址',
|
||||||
|
trigger: ['blur', 'change']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
phonenumber: [
|
||||||
|
{
|
||||||
|
pattern: /^1[3456789][0-9]\d{8}$/,
|
||||||
|
message: '请输入正确的手机号码',
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
roleIds: [{ required: true, message: '用户角色不能为空', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const data = reactive<PageData<UserForm, UserQuery>>(initData);
|
||||||
|
|
||||||
|
const { queryParams, form, rules } = toRefs<PageData<UserForm, UserQuery>>(data);
|
||||||
|
|
||||||
|
/** 通过条件过滤节点 */
|
||||||
|
const filterNode = (value: string, data: any) => {
|
||||||
|
if (!value) return true;
|
||||||
|
return data.label.indexOf(value) !== -1;
|
||||||
|
};
|
||||||
|
/** 根据名称筛选部门树 */
|
||||||
|
watchEffect(
|
||||||
|
() => {
|
||||||
|
deptTreeRef.value?.filter(deptName.value);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发,此属性控制在DOM元素更新后运行
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 查询用户列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
const res = await api.listUser(proxy?.addDateRange(queryParams.value, dateRange.value));
|
||||||
|
loading.value = false;
|
||||||
|
userList.value = res.rows;
|
||||||
|
total.value = res.total;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查询部门下拉树结构 */
|
||||||
|
const getDeptTree = async () => {
|
||||||
|
const res = await api.deptTreeSelect();
|
||||||
|
deptOptions.value = res.data;
|
||||||
|
enabledDeptOptions.value = filterDisabledDept(res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 过滤禁用的部门 */
|
||||||
|
const filterDisabledDept = (deptList: DeptTreeVO[]) => {
|
||||||
|
return deptList.filter((dept) => {
|
||||||
|
if (dept.disabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (dept.children && dept.children.length) {
|
||||||
|
dept.children = filterDisabledDept(dept.children);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 节点单击事件 */
|
||||||
|
const handleNodeClick = (data: DeptVO) => {
|
||||||
|
queryParams.value.deptId = data.id;
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.value.pageNum = 1;
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
dateRange.value = ['', ''];
|
||||||
|
queryFormRef.value?.resetFields();
|
||||||
|
queryParams.value.pageNum = 1;
|
||||||
|
queryParams.value.deptId = undefined;
|
||||||
|
deptTreeRef.value?.setCurrentKey(undefined);
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 删除按钮操作 */
|
||||||
|
const handleDelete = async (row?: UserVO) => {
|
||||||
|
const userIds = row?.userId || ids.value;
|
||||||
|
const [err] = await to(proxy?.$modal.confirm('是否确认删除用户编号为"' + userIds + '"的数据项?') as any);
|
||||||
|
if (!err) {
|
||||||
|
await api.delUser(userIds);
|
||||||
|
await getList();
|
||||||
|
proxy?.$modal.msgSuccess('删除成功');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 用户状态修改 */
|
||||||
|
const handleStatusChange = async (row: UserVO) => {
|
||||||
|
const text = row.status === '0' ? '启用' : '停用';
|
||||||
|
try {
|
||||||
|
await proxy?.$modal.confirm('确认要"' + text + '""' + row.userName + '"用户吗?');
|
||||||
|
await api.changeUserStatus(row.userId, row.status);
|
||||||
|
proxy?.$modal.msgSuccess(text + '成功');
|
||||||
|
} catch (err) {
|
||||||
|
row.status = row.status === '0' ? '1' : '0';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/** 跳转角色分配 */
|
||||||
|
const handleAuthRole = (row: UserVO) => {
|
||||||
|
const userId = row.userId;
|
||||||
|
router.push('/system/user-auth/role/' + userId);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 重置密码按钮操作 */
|
||||||
|
const handleResetPwd = async (row: UserVO) => {
|
||||||
|
const [err, res] = await to(
|
||||||
|
ElMessageBox.prompt('请输入"' + row.userName + '"的新密码', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
closeOnClickModal: false,
|
||||||
|
inputPattern: /^.{5,20}$/,
|
||||||
|
inputErrorMessage: '用户密码长度必须介于 5 和 20 之间',
|
||||||
|
inputValidator: (value) => {
|
||||||
|
if (/<|>|"|'|\||\\/.test(value)) {
|
||||||
|
return '不能包含非法字符:< > " \' \\ |';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if (!err && res) {
|
||||||
|
await api.resetUserPwd(row.userId, res.value);
|
||||||
|
proxy?.$modal.msgSuccess('修改成功,新密码是:' + res.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 选择条数 */
|
||||||
|
const handleSelectionChange = (selection: UserVO[]) => {
|
||||||
|
ids.value = selection.map((item) => item.userId);
|
||||||
|
single.value = selection.length != 1;
|
||||||
|
multiple.value = !selection.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 导入按钮操作 */
|
||||||
|
const handleImport = () => {
|
||||||
|
upload.title = '用户导入';
|
||||||
|
upload.open = true;
|
||||||
|
};
|
||||||
|
/** 导出按钮操作 */
|
||||||
|
const handleExport = () => {
|
||||||
|
proxy?.download(
|
||||||
|
'system/user/export',
|
||||||
|
{
|
||||||
|
...queryParams.value
|
||||||
|
},
|
||||||
|
`user_${new Date().getTime()}.xlsx`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/** 下载模板操作 */
|
||||||
|
const importTemplate = () => {
|
||||||
|
proxy?.download('system/user/importTemplate', {}, `user_template_${new Date().getTime()}.xlsx`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**文件上传中处理 */
|
||||||
|
const handleFileUploadProgress = () => {
|
||||||
|
upload.isUploading = true;
|
||||||
|
};
|
||||||
|
/** 文件上传成功处理 */
|
||||||
|
const handleFileSuccess = (response: any, file: UploadFile) => {
|
||||||
|
upload.open = false;
|
||||||
|
upload.isUploading = false;
|
||||||
|
uploadRef.value?.handleRemove(file);
|
||||||
|
ElMessageBox.alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + '</div>', '导入结果', {
|
||||||
|
dangerouslyUseHTMLString: true
|
||||||
|
});
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 提交上传文件 */
|
||||||
|
function submitFileForm() {
|
||||||
|
uploadRef.value?.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置操作表单 */
|
||||||
|
const reset = () => {
|
||||||
|
form.value = { ...initFormData };
|
||||||
|
userFormRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
/** 取消按钮 */
|
||||||
|
const cancel = () => {
|
||||||
|
dialog.visible = false;
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 新增按钮操作 */
|
||||||
|
const handleAdd = async () => {
|
||||||
|
reset();
|
||||||
|
const { data } = await api.getUser();
|
||||||
|
dialog.visible = true;
|
||||||
|
dialog.title = '新增用户';
|
||||||
|
postOptions.value = data.posts;
|
||||||
|
roleOptions.value = data.roles;
|
||||||
|
form.value.password = initPassword.value.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 修改按钮操作 */
|
||||||
|
const handleUpdate = async (row?: UserForm) => {
|
||||||
|
reset();
|
||||||
|
const userId = row?.userId || ids.value[0];
|
||||||
|
const { data } = await api.getUser(userId);
|
||||||
|
dialog.visible = true;
|
||||||
|
dialog.title = '修改用户';
|
||||||
|
Object.assign(form.value, data.user);
|
||||||
|
postOptions.value = data.posts;
|
||||||
|
roleOptions.value = Array.from(new Map([...data.roles, ...data.user.roles].map((role) => [role.roleId, role])).values());
|
||||||
|
form.value.postIds = data.postIds;
|
||||||
|
form.value.roleIds = data.roleIds;
|
||||||
|
form.value.password = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 提交按钮 */
|
||||||
|
const submitForm = () => {
|
||||||
|
userFormRef.value?.validate(async (valid: boolean) => {
|
||||||
|
if (valid) {
|
||||||
|
if (form.value.userId) {
|
||||||
|
// 自己编辑自己的情况下 不允许编辑角色部门岗位
|
||||||
|
if (form.value.userId == useUserStore().userId) {
|
||||||
|
form.value.roleIds = null;
|
||||||
|
form.value.deptId = null;
|
||||||
|
form.value.postIds = null;
|
||||||
|
}
|
||||||
|
await api.updateUser(form.value);
|
||||||
|
} else {
|
||||||
|
await api.addUser(form.value);
|
||||||
|
}
|
||||||
|
proxy?.$modal.msgSuccess('操作成功');
|
||||||
|
dialog.visible = false;
|
||||||
|
await getList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭用户弹窗
|
||||||
|
*/
|
||||||
|
const closeDialog = () => {
|
||||||
|
dialog.visible = false;
|
||||||
|
resetForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置表单
|
||||||
|
*/
|
||||||
|
const resetForm = () => {
|
||||||
|
userFormRef.value?.resetFields();
|
||||||
|
userFormRef.value?.clearValidate();
|
||||||
|
|
||||||
|
form.value.id = undefined;
|
||||||
|
form.value.status = '1';
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
getDeptTree(); // 初始化部门数据
|
||||||
|
getList(); // 初始化列表数据
|
||||||
|
proxy?.getConfigKey('sys.user.initPassword').then((response) => {
|
||||||
|
initPassword.value = response.data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleDeptChange(value: number | string) {
|
||||||
|
const response = await optionselect(value);
|
||||||
|
postOptions.value = response.data;
|
||||||
|
form.value.postIds = [];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -24,6 +24,7 @@ export default defineConfig(({ mode, command }) => {
|
|||||||
open: true,
|
open: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
[env.VITE_APP_BASE_API]: {
|
[env.VITE_APP_BASE_API]: {
|
||||||
|
// target: 'http://47.104.208.94:8089',
|
||||||
target: 'http://192.168.0.214:8089',
|
target: 'http://192.168.0.214:8089',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
ws: true,
|
ws: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user