1
This commit is contained in:
33
ems-frontend/ems-monitoring-system/README.md
Normal file
33
ems-frontend/ems-monitoring-system/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# ems-monitoring-system
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
3752
ems-frontend/ems-monitoring-system/package-lock.json
generated
Normal file
3752
ems-frontend/ems-monitoring-system/package-lock.json
generated
Normal file
@@ -0,0 +1,3752 @@
|
||||
{
|
||||
"name": "ems-monitoring-system",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ems-monitoring-system",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.10.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"element-plus": "^2.10.2",
|
||||
"pinia": "^3.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "^22.0.1",
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"prettier": "3.5.3",
|
||||
"typescript": "~5.8.0",
|
||||
"vite": "^6.2.4",
|
||||
"vite-plugin-vue-devtools": "^7.7.2",
|
||||
"vue-tsc": "^2.2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@antfu/utils": {
|
||||
"version": "0.7.10",
|
||||
"resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz",
|
||||
"integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.27.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz",
|
||||
"integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
|
||||
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.27.3",
|
||||
"@babel/helper-compilation-targets": "^7.27.2",
|
||||
"@babel/helper-module-transforms": "^7.27.3",
|
||||
"@babel/helpers": "^7.27.4",
|
||||
"@babel/parser": "^7.27.4",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/traverse": "^7.27.4",
|
||||
"@babel/types": "^7.27.3",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
"json5": "^2.2.3",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/babel"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.27.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz",
|
||||
"integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.27.5",
|
||||
"@babel/types": "^7.27.3",
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jsesc": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-annotate-as-pure": {
|
||||
"version": "7.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
|
||||
"integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.27.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
|
||||
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.27.2",
|
||||
"@babel/helper-validator-option": "^7.27.1",
|
||||
"browserslist": "^4.24.0",
|
||||
"lru-cache": "^5.1.1",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-create-class-features-plugin": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz",
|
||||
"integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.27.1",
|
||||
"@babel/helper-member-expression-to-functions": "^7.27.1",
|
||||
"@babel/helper-optimise-call-expression": "^7.27.1",
|
||||
"@babel/helper-replace-supers": "^7.27.1",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-member-expression-to-functions": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz",
|
||||
"integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-imports": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
|
||||
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
|
||||
"integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-optimise-call-expression": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz",
|
||||
"integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-plugin-utils": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
|
||||
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-replace-supers": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz",
|
||||
"integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.27.1",
|
||||
"@babel/helper-optimise-call-expression": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-skip-transparent-expression-wrappers": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz",
|
||||
"integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-option": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
||||
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
|
||||
"integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.27.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.27.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
|
||||
"integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.27.3"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-proposal-decorators": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.27.1.tgz",
|
||||
"integrity": "sha512-DTxe4LBPrtFdsWzgpmbBKevg3e9PBy+dXRt19kSbucbZvL2uqtdqwwpluL1jfxYE0wIDTFp1nTy/q6gNLsxXrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/plugin-syntax-decorators": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-decorators": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz",
|
||||
"integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-import-attributes": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz",
|
||||
"integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-import-meta": {
|
||||
"version": "7.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
|
||||
"integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.10.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-jsx": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
|
||||
"integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-typescript": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
|
||||
"integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-typescript": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.1.tgz",
|
||||
"integrity": "sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.27.1",
|
||||
"@babel/helper-create-class-features-plugin": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
|
||||
"@babel/plugin-syntax-typescript": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/parser": "^7.27.2",
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz",
|
||||
"integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.27.3",
|
||||
"@babel/parser": "^7.27.4",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.27.3",
|
||||
"debug": "^4.3.1",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz",
|
||||
"integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ctrl/tinycolor": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
|
||||
"integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@element-plus/icons-vue": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
|
||||
"integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
|
||||
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
|
||||
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
|
||||
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
|
||||
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
|
||||
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
|
||||
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
|
||||
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
|
||||
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
|
||||
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
|
||||
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.1.tgz",
|
||||
"integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.1.tgz",
|
||||
"integrity": "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.1",
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/set-array": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@polka/url": {
|
||||
"version": "1.0.0-next.29",
|
||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"name": "@sxzz/popperjs-es",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
|
||||
"integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/pluginutils": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz",
|
||||
"integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.0.tgz",
|
||||
"integrity": "sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.0.tgz",
|
||||
"integrity": "sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz",
|
||||
"integrity": "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz",
|
||||
"integrity": "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.0.tgz",
|
||||
"integrity": "sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.0.tgz",
|
||||
"integrity": "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.0.tgz",
|
||||
"integrity": "sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.0.tgz",
|
||||
"integrity": "sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz",
|
||||
"integrity": "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz",
|
||||
"integrity": "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.0.tgz",
|
||||
"integrity": "sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.0.tgz",
|
||||
"integrity": "sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.0.tgz",
|
||||
"integrity": "sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.0.tgz",
|
||||
"integrity": "sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.0.tgz",
|
||||
"integrity": "sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz",
|
||||
"integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz",
|
||||
"integrity": "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz",
|
||||
"integrity": "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.0.tgz",
|
||||
"integrity": "sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz",
|
||||
"integrity": "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@sec-ant/readable-stream": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
|
||||
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sindresorhus/merge-streams": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
|
||||
"integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node22": {
|
||||
"version": "22.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.2.tgz",
|
||||
"integrity": "sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash-es": {
|
||||
"version": "4.17.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.15.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.32.tgz",
|
||||
"integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
|
||||
"integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
|
||||
"integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.0.0 || ^6.0.0",
|
||||
"vue": "^3.2.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/language-core": {
|
||||
"version": "2.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.14.tgz",
|
||||
"integrity": "sha512-X6beusV0DvuVseaOEy7GoagS4rYHgDHnTrdOj5jeUb49fW5ceQyP9Ej5rBhqgz2wJggl+2fDbbojq1XKaxDi6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/source-map": "2.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/source-map": {
|
||||
"version": "2.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.14.tgz",
|
||||
"integrity": "sha512-5TeKKMh7Sfxo8021cJfmBzcjfY1SsXsPMMjMvjY7ivesdnybqqS+GxGAoXHAOUawQTwtdUxgP65Im+dEmvWtYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@volar/typescript": {
|
||||
"version": "2.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.14.tgz",
|
||||
"integrity": "sha512-p8Z6f/bZM3/HyCdRNFZOEEzts51uV8WHeN8Tnfnm2EBv6FDB2TQLzfVx7aJvnl8ofKAOnS64B2O8bImBFaauRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/language-core": "2.4.14",
|
||||
"path-browserify": "^1.0.1",
|
||||
"vscode-uri": "^3.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/babel-helper-vue-transform-on": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.4.0.tgz",
|
||||
"integrity": "sha512-mCokbouEQ/ocRce/FpKCRItGo+013tHg7tixg3DUNS+6bmIchPt66012kBMm476vyEIJPafrvOf4E5OYj3shSw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/babel-plugin-jsx": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.4.0.tgz",
|
||||
"integrity": "sha512-9zAHmwgMWlaN6qRKdrg1uKsBKHvnUU+Py+MOCTuYZBoZsopa90Di10QRjB+YPnVss0BZbG/H5XFwJY1fTxJWhA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.25.9",
|
||||
"@babel/helper-plugin-utils": "^7.26.5",
|
||||
"@babel/plugin-syntax-jsx": "^7.25.9",
|
||||
"@babel/template": "^7.26.9",
|
||||
"@babel/traverse": "^7.26.9",
|
||||
"@babel/types": "^7.26.9",
|
||||
"@vue/babel-helper-vue-transform-on": "1.4.0",
|
||||
"@vue/babel-plugin-resolve-type": "1.4.0",
|
||||
"@vue/shared": "^3.5.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@babel/core": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/babel-plugin-resolve-type": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.4.0.tgz",
|
||||
"integrity": "sha512-4xqDRRbQQEWHQyjlYSgZsWj44KfiF6D+ktCuXyZ8EnVDYV3pztmXJDf1HveAjUAXxAnR8daCQT51RneWWxtTyQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/helper-module-imports": "^7.25.9",
|
||||
"@babel/helper-plugin-utils": "^7.26.5",
|
||||
"@babel/parser": "^7.26.9",
|
||||
"@vue/compiler-sfc": "^3.5.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sxzz"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.17.tgz",
|
||||
"integrity": "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.27.5",
|
||||
"@vue/shared": "3.5.17",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.17.tgz",
|
||||
"integrity": "sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.17",
|
||||
"@vue/shared": "3.5.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.17.tgz",
|
||||
"integrity": "sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.27.5",
|
||||
"@vue/compiler-core": "3.5.17",
|
||||
"@vue/compiler-dom": "3.5.17",
|
||||
"@vue/compiler-ssr": "3.5.17",
|
||||
"@vue/shared": "3.5.17",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.17",
|
||||
"postcss": "^8.5.6",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.17.tgz",
|
||||
"integrity": "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.17",
|
||||
"@vue/shared": "3.5.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-vue2": {
|
||||
"version": "2.7.16",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
|
||||
"integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"de-indent": "^1.0.2",
|
||||
"he": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
"version": "7.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.7.tgz",
|
||||
"integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-kit": "^7.7.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-core": {
|
||||
"version": "7.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.7.7.tgz",
|
||||
"integrity": "sha512-9z9TLbfC+AjAi1PQyWX+OErjIaJmdFlbDHcD+cAMYKY6Bh5VlsAtCeGyRMrXwIlMEQPukvnWt3gZBLwTAIMKzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-kit": "^7.7.7",
|
||||
"@vue/devtools-shared": "^7.7.7",
|
||||
"mitt": "^3.0.1",
|
||||
"nanoid": "^5.1.0",
|
||||
"pathe": "^2.0.3",
|
||||
"vite-hot-client": "^2.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-core/node_modules/nanoid": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz",
|
||||
"integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-kit": {
|
||||
"version": "7.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz",
|
||||
"integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-shared": "^7.7.7",
|
||||
"birpc": "^2.3.0",
|
||||
"hookable": "^5.5.3",
|
||||
"mitt": "^3.0.1",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"speakingurl": "^14.0.1",
|
||||
"superjson": "^2.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-shared": {
|
||||
"version": "7.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz",
|
||||
"integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"rfdc": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/language-core": {
|
||||
"version": "2.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.10.tgz",
|
||||
"integrity": "sha512-+yNoYx6XIKuAO8Mqh1vGytu8jkFEOH5C8iOv3i8Z/65A7x9iAOXA97Q+PqZ3nlm2lxf5rOJuIGI/wDtx/riNYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/language-core": "~2.4.11",
|
||||
"@vue/compiler-dom": "^3.5.0",
|
||||
"@vue/compiler-vue2": "^2.7.16",
|
||||
"@vue/shared": "^3.5.0",
|
||||
"alien-signals": "^1.0.3",
|
||||
"minimatch": "^9.0.3",
|
||||
"muggle-string": "^0.4.1",
|
||||
"path-browserify": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.17.tgz",
|
||||
"integrity": "sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.17.tgz",
|
||||
"integrity": "sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.17",
|
||||
"@vue/shared": "3.5.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.17.tgz",
|
||||
"integrity": "sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.17",
|
||||
"@vue/runtime-core": "3.5.17",
|
||||
"@vue/shared": "3.5.17",
|
||||
"csstype": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.17.tgz",
|
||||
"integrity": "sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.17",
|
||||
"@vue/shared": "3.5.17"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.17.tgz",
|
||||
"integrity": "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/tsconfig": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.7.0.tgz",
|
||||
"integrity": "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"typescript": "5.x",
|
||||
"vue": "^3.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
},
|
||||
"vue": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "9.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz",
|
||||
"integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.16",
|
||||
"@vueuse/metadata": "9.13.0",
|
||||
"@vueuse/shared": "9.13.0",
|
||||
"vue-demi": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "9.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz",
|
||||
"integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "9.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
|
||||
"integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vue-demi": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/alien-signals": {
|
||||
"version": "1.0.13",
|
||||
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
|
||||
"integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/animate.css": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
|
||||
"integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/async-validator": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
||||
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/birpc": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.4.0.tgz",
|
||||
"integrity": "sha512-5IdNxTyhXHv2UlgnPHQ0h+5ypVmkrYHzL8QT+DwFZ//2N/oNV8Ch+BCRmTJ3x6/z9Axo/cXYBc9eprsUVK/Jsg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.25.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
|
||||
"integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001718",
|
||||
"electron-to-chromium": "^1.5.160",
|
||||
"node-releases": "^2.0.19",
|
||||
"update-browserslist-db": "^1.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/bundle-name": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
|
||||
"integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"run-applescript": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001723",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz",
|
||||
"integrity": "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/copy-anything": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
|
||||
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-what": "^4.1.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn/node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/cross-spawn/node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.18",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
|
||||
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/de-indent": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/default-browser": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
|
||||
"integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bundle-name": "^4.1.0",
|
||||
"default-browser-id": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/default-browser-id": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz",
|
||||
"integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/define-lazy-prop": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
|
||||
"integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.170",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.170.tgz",
|
||||
"integrity": "sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/element-plus": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.11.5.tgz",
|
||||
"integrity": "sha512-O+bIVHQCjUDm4GiIznDXRoS8ar2TpWLwfOGnN/Aam0VXf5kbuc4SxdKKJdovWNxmxeqbcwjsSZPKgtXNcqys4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^3.4.1",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@floating-ui/dom": "^1.0.1",
|
||||
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@vueuse/core": "^9.1.0",
|
||||
"async-validator": "^4.2.5",
|
||||
"dayjs": "^1.11.18",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash-unified": "^1.0.3",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-wheel-es": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/error-stack-parser-es": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-0.1.5.tgz",
|
||||
"integrity": "sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
|
||||
"integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.5",
|
||||
"@esbuild/android-arm": "0.25.5",
|
||||
"@esbuild/android-arm64": "0.25.5",
|
||||
"@esbuild/android-x64": "0.25.5",
|
||||
"@esbuild/darwin-arm64": "0.25.5",
|
||||
"@esbuild/darwin-x64": "0.25.5",
|
||||
"@esbuild/freebsd-arm64": "0.25.5",
|
||||
"@esbuild/freebsd-x64": "0.25.5",
|
||||
"@esbuild/linux-arm": "0.25.5",
|
||||
"@esbuild/linux-arm64": "0.25.5",
|
||||
"@esbuild/linux-ia32": "0.25.5",
|
||||
"@esbuild/linux-loong64": "0.25.5",
|
||||
"@esbuild/linux-mips64el": "0.25.5",
|
||||
"@esbuild/linux-ppc64": "0.25.5",
|
||||
"@esbuild/linux-riscv64": "0.25.5",
|
||||
"@esbuild/linux-s390x": "0.25.5",
|
||||
"@esbuild/linux-x64": "0.25.5",
|
||||
"@esbuild/netbsd-arm64": "0.25.5",
|
||||
"@esbuild/netbsd-x64": "0.25.5",
|
||||
"@esbuild/openbsd-arm64": "0.25.5",
|
||||
"@esbuild/openbsd-x64": "0.25.5",
|
||||
"@esbuild/sunos-x64": "0.25.5",
|
||||
"@esbuild/win32-arm64": "0.25.5",
|
||||
"@esbuild/win32-ia32": "0.25.5",
|
||||
"@esbuild/win32-x64": "0.25.5"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "9.6.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz",
|
||||
"integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sindresorhus/merge-streams": "^4.0.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"figures": "^6.1.0",
|
||||
"get-stream": "^9.0.0",
|
||||
"human-signals": "^8.0.1",
|
||||
"is-plain-obj": "^4.1.0",
|
||||
"is-stream": "^4.0.1",
|
||||
"npm-run-path": "^6.0.0",
|
||||
"pretty-ms": "^9.2.0",
|
||||
"signal-exit": "^4.1.0",
|
||||
"strip-final-newline": "^4.0.0",
|
||||
"yoctocolors": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/figures": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
|
||||
"integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-unicode-supported": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
|
||||
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
|
||||
"integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sec-ant/readable-stream": "^0.4.1",
|
||||
"is-stream": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/hookable": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
||||
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz",
|
||||
"integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
|
||||
"integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"is-docker": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-inside-container": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
|
||||
"integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-docker": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"is-inside-container": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-obj": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
||||
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
|
||||
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-unicode-supported": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
||||
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-what": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
|
||||
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-wsl": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
|
||||
"integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-inside-container": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
|
||||
"integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/json-parse-even-better-errors": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz",
|
||||
"integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/kolorist": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
|
||||
"integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-unified": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
|
||||
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/lodash-es": "*",
|
||||
"lodash": "*",
|
||||
"lodash-es": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.17",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/memorystream": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
|
||||
"integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mrmime": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/muggle-string": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
|
||||
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-wheel-es": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
|
||||
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/npm-normalize-package-bin": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz",
|
||||
"integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-all2": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-7.0.2.tgz",
|
||||
"integrity": "sha512-7tXR+r9hzRNOPNTvXegM+QzCuMjzUIIq66VDunL6j60O4RrExx32XUhlrS7UK4VcdGw5/Wxzb3kfNcFix9JKDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.2.1",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"memorystream": "^0.3.1",
|
||||
"minimatch": "^9.0.0",
|
||||
"pidtree": "^0.6.0",
|
||||
"read-package-json-fast": "^4.0.0",
|
||||
"shell-quote": "^1.7.3",
|
||||
"which": "^5.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"npm-run-all": "bin/npm-run-all/index.js",
|
||||
"npm-run-all2": "bin/npm-run-all/index.js",
|
||||
"run-p": "bin/run-p/index.js",
|
||||
"run-s": "bin/run-s/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0",
|
||||
"npm": ">= 9"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
|
||||
"integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^4.0.0",
|
||||
"unicorn-magic": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-path/node_modules/path-key": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
|
||||
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/open": {
|
||||
"version": "10.1.2",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz",
|
||||
"integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"default-browser": "^5.2.1",
|
||||
"define-lazy-prop": "^3.0.0",
|
||||
"is-inside-container": "^1.0.0",
|
||||
"is-wsl": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-ms": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
|
||||
"integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/path-browserify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/perfect-debounce": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pidtree": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
|
||||
"integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"pidtree": "bin/pidtree.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/pinia": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.3.tgz",
|
||||
"integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^7.7.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.4.4",
|
||||
"vue": "^2.7.0 || ^3.5.11"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-ms": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz",
|
||||
"integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parse-ms": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/read-package-json-fast": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz",
|
||||
"integrity": "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"json-parse-even-better-errors": "^4.0.0",
|
||||
"npm-normalize-package-bin": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rfdc": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.0.tgz",
|
||||
"integrity": "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.44.0",
|
||||
"@rollup/rollup-android-arm64": "4.44.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.44.0",
|
||||
"@rollup/rollup-darwin-x64": "4.44.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.44.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.44.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.44.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.44.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.44.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.44.0",
|
||||
"@rollup/rollup-linux-loongarch64-gnu": "4.44.0",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.44.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.44.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.44.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.44.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.44.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.44.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.44.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.44.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.44.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/run-applescript": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
|
||||
"integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/sirv": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz",
|
||||
"integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@polka/url": "^1.0.0-next.24",
|
||||
"mrmime": "^2.0.0",
|
||||
"totalist": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/speakingurl": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
|
||||
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-final-newline": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
|
||||
"integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/superjson": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
|
||||
"integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"copy-anything": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/totalist": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unicorn-magic": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
|
||||
"integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escalade": "^3.2.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"update-browserslist-db": "cli.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2",
|
||||
"postcss": "^8.5.3",
|
||||
"rollup": "^4.34.9",
|
||||
"tinyglobby": "^0.2.13"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-hot-client": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-2.0.4.tgz",
|
||||
"integrity": "sha512-W9LOGAyGMrbGArYJN4LBCdOC5+Zwh7dHvOHC0KmGKkJhsOzaKbpo/jEjpPKVHIW0/jBWj8RZG0NUxfgA8BxgAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-inspect": {
|
||||
"version": "0.8.9",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-0.8.9.tgz",
|
||||
"integrity": "sha512-22/8qn+LYonzibb1VeFZmISdVao5kC22jmEKm24vfFE8siEn47EpVcCLYMv6iKOYMJfjSvSJfueOwcFCkUnV3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antfu/utils": "^0.7.10",
|
||||
"@rollup/pluginutils": "^5.1.3",
|
||||
"debug": "^4.3.7",
|
||||
"error-stack-parser-es": "^0.1.5",
|
||||
"fs-extra": "^11.2.0",
|
||||
"open": "^10.1.0",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"sirv": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nuxt/kit": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-vue-devtools": {
|
||||
"version": "7.7.7",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-7.7.7.tgz",
|
||||
"integrity": "sha512-d0fIh3wRcgSlr4Vz7bAk4va1MkdqhQgj9ANE/rBhsAjOnRfTLs2ocjFMvSUOsv6SRRXU9G+VM7yMgqDb6yI4iQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-core": "^7.7.7",
|
||||
"@vue/devtools-kit": "^7.7.7",
|
||||
"@vue/devtools-shared": "^7.7.7",
|
||||
"execa": "^9.5.2",
|
||||
"sirv": "^3.0.1",
|
||||
"vite-plugin-inspect": "0.8.9",
|
||||
"vite-plugin-vue-inspector": "^5.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v14.21.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^3.1.0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-vue-inspector": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.3.1.tgz",
|
||||
"integrity": "sha512-cBk172kZKTdvGpJuzCCLg8lJ909wopwsu3Ve9FsL1XsnLBiRT9U3MePcqrgGHgCX2ZgkqZmAGR8taxw+TV6s7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.23.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.23.0",
|
||||
"@babel/plugin-syntax-import-attributes": "^7.22.5",
|
||||
"@babel/plugin-syntax-import-meta": "^7.10.4",
|
||||
"@babel/plugin-transform-typescript": "^7.22.15",
|
||||
"@vue/babel-plugin-jsx": "^1.1.5",
|
||||
"@vue/compiler-dom": "^3.3.4",
|
||||
"kolorist": "^1.8.0",
|
||||
"magic-string": "^0.30.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.17",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.17.tgz",
|
||||
"integrity": "sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.17",
|
||||
"@vue/compiler-sfc": "3.5.17",
|
||||
"@vue/runtime-dom": "3.5.17",
|
||||
"@vue/server-renderer": "3.5.17",
|
||||
"@vue/shared": "3.5.17"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
||||
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.6.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router/node_modules/@vue/devtools-api": {
|
||||
"version": "6.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
|
||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue-tsc": {
|
||||
"version": "2.2.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.10.tgz",
|
||||
"integrity": "sha512-jWZ1xSaNbabEV3whpIDMbjVSVawjAyW+x1n3JeGQo7S0uv2n9F/JMgWW90tGWNFRKya4YwKMZgCtr0vRAM7DeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/typescript": "~2.4.11",
|
||||
"@vue/language-core": "2.2.10"
|
||||
},
|
||||
"bin": {
|
||||
"vue-tsc": "bin/vue-tsc.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
|
||||
"integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/which.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yoctocolors": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz",
|
||||
"integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
ems-frontend/ems-monitoring-system/package.json
Normal file
36
ems-frontend/ems-monitoring-system/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "ems-monitoring-system",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.10.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"element-plus": "^2.10.2",
|
||||
"pinia": "^3.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "^22.0.1",
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"prettier": "3.5.3",
|
||||
"typescript": "~5.8.0",
|
||||
"vite": "^6.2.4",
|
||||
"vite-plugin-vue-devtools": "^7.7.2",
|
||||
"vue-tsc": "^2.2.8"
|
||||
}
|
||||
}
|
||||
BIN
ems-frontend/ems-monitoring-system/public/favicon.ico
Normal file
BIN
ems-frontend/ems-monitoring-system/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
29
ems-frontend/ems-monitoring-system/src/App.vue
Normal file
29
ems-frontend/ems-monitoring-system/src/App.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition
|
||||
enter-active-class="animate__animated animate__fadeIn"
|
||||
leave-active-class="animate__animated animate__fadeOut"
|
||||
mode="out-in"
|
||||
>
|
||||
<component :is="Component" :key="route.path" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html, body, #app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden; /* Globally disable scrolling */
|
||||
}
|
||||
|
||||
/* You might want to adjust the animation speed */
|
||||
:root {
|
||||
--animate-duration: 0.35s;
|
||||
}
|
||||
</style>
|
||||
99
ems-frontend/ems-monitoring-system/src/api/auth.ts
Normal file
99
ems-frontend/ems-monitoring-system/src/api/auth.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
// @/api/auth.ts
|
||||
import apiClient from './index';
|
||||
import type {
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
SignUpRequest,
|
||||
ResetPasswordWithCodeRequest
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* 用户登录(使用 Fetch API)
|
||||
* @param credentials - 登录凭据 (邮箱和密码)
|
||||
* @returns Promise<LoginResponse>
|
||||
*/
|
||||
export const loginWithFetch = async (credentials: LoginRequest): Promise<LoginResponse> => {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `HTTP error ${response.status}: ${response.statusText}`;
|
||||
const responseText = await response.text();
|
||||
try {
|
||||
const errorBody = JSON.parse(responseText);
|
||||
if (errorBody && errorBody.message) {
|
||||
errorMessage = errorBody.message;
|
||||
} else if (responseText) {
|
||||
errorMessage = responseText;
|
||||
}
|
||||
} catch (e) {
|
||||
if (responseText) {
|
||||
errorMessage = responseText;
|
||||
}
|
||||
}
|
||||
const error: any = new Error(errorMessage);
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
// If response is OK, we need to parse it as JSON.
|
||||
// The stream hasn't been read if response.ok is true.
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
* @param credentials - 登录凭据 (邮箱和密码)
|
||||
* @returns Promise<AxiosResponse<LoginResponse>>
|
||||
*/
|
||||
export const login = (credentials: LoginRequest) => {
|
||||
return apiClient.post<LoginResponse>('/auth/login', credentials);
|
||||
};
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
* @param data - 注册所需信息
|
||||
*/
|
||||
export const signup = (data: SignUpRequest) => {
|
||||
return apiClient.post('/auth/signup', data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 发送【注册】验证码
|
||||
* @param email - 目标邮箱
|
||||
*/
|
||||
export const sendVerificationCode = (email: string) => {
|
||||
return apiClient.post(`/auth/send-verification-code?email=${email}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 发送【密码重置】验证码
|
||||
* @param email - 目标邮箱
|
||||
*/
|
||||
export const sendPasswordResetCode = (email: string) => {
|
||||
return apiClient.post(`/auth/send-password-reset-code?email=${email}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用验证码重置密码
|
||||
* @param data - 包含邮箱、验证码和新密码的对象
|
||||
*/
|
||||
export const resetPasswordWithCode = (data: ResetPasswordWithCodeRequest) => {
|
||||
return apiClient.post('/auth/reset-password-with-code', data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
* 通知后端记录登出操作
|
||||
*/
|
||||
export const logout = () => {
|
||||
return apiClient.post('/auth/logout');
|
||||
};
|
||||
|
||||
// ... 其他认证相关的API调用
|
||||
147
ems-frontend/ems-monitoring-system/src/api/dashboard.ts
Normal file
147
ems-frontend/ems-monitoring-system/src/api/dashboard.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import apiClient from './index';
|
||||
import type {
|
||||
DashboardStatsDTO,
|
||||
AqiDistributionDTO,
|
||||
TaskStatsDTO,
|
||||
TrendDataPointDTO,
|
||||
GridCoverageDTO,
|
||||
HeatmapPointDTO,
|
||||
PollutionStatsDTO,
|
||||
AqiHeatmapPointDTO,
|
||||
PollutantThresholdDTO,
|
||||
Grid
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Fetches the main dashboard statistics.
|
||||
* @returns A promise that resolves to the dashboard stats.
|
||||
*/
|
||||
export const getDashboardStats = (): Promise<DashboardStatsDTO> => {
|
||||
return apiClient.get('/dashboard/stats');
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the AQI level distribution.
|
||||
* @returns A promise that resolves to an array of AQI distribution data.
|
||||
*/
|
||||
export const getAqiDistribution = (): Promise<AqiDistributionDTO[]> => {
|
||||
return apiClient.get('/dashboard/reports/aqi-distribution');
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the monthly trend of exceedance events.
|
||||
* @returns A promise that resolves to an array of trend data points.
|
||||
*/
|
||||
export const getMonthlyExceedanceTrend = (): Promise<TrendDataPointDTO[]> => {
|
||||
return apiClient.get('/dashboard/reports/monthly-exceedance-trend')
|
||||
.then(data => {
|
||||
console.log('原始趋势数据:', data);
|
||||
// 确保数据格式正确
|
||||
if (Array.isArray(data)) {
|
||||
return data.map(item => ({
|
||||
yearMonth: item.yearMonth || '',
|
||||
count: item.count || 0
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the grid coverage statistics by city.
|
||||
* @returns A promise that resolves to an array of grid coverage data.
|
||||
*/
|
||||
export const getGridCoverageByCity = (): Promise<GridCoverageDTO[]> => {
|
||||
return apiClient.get('/dashboard/reports/grid-coverage');
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the heatmap data for pollution incidents.
|
||||
* @returns A promise that resolves to an array of heatmap points.
|
||||
*/
|
||||
export const getHeatmapData = (): Promise<HeatmapPointDTO[]> => {
|
||||
return apiClient.get('/dashboard/map/heatmap');
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the pollution statistics.
|
||||
* @returns A promise that resolves to an array of pollution stats.
|
||||
*/
|
||||
export const getPollutionStats = (): Promise<PollutionStatsDTO[]> => {
|
||||
return apiClient.get('/dashboard/reports/pollution-stats');
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the task completion statistics.
|
||||
* @returns A promise that resolves to the task stats.
|
||||
*/
|
||||
export const getTaskCompletionStats = (): Promise<TaskStatsDTO> => {
|
||||
return apiClient.get('/dashboard/reports/task-completion-stats');
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the AQI heatmap data.
|
||||
* @returns A promise that resolves to an array of AQI heatmap points.
|
||||
*/
|
||||
export const getAqiHeatmapData = (): Promise<AqiHeatmapPointDTO[]> => {
|
||||
return apiClient.get('/dashboard/map/aqi-heatmap');
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches all pollutant thresholds.
|
||||
* @returns A promise that resolves to an array of pollutant thresholds.
|
||||
*/
|
||||
export const getPollutantThresholds = (): Promise<PollutantThresholdDTO[]> => {
|
||||
return apiClient.get('/dashboard/thresholds');
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches a specific pollutant threshold.
|
||||
* @param pollutantName The name of the pollutant.
|
||||
* @returns A promise that resolves to the pollutant threshold.
|
||||
*/
|
||||
export const getPollutantThreshold = (pollutantName: string): Promise<PollutantThresholdDTO> => {
|
||||
return apiClient.get(`/dashboard/thresholds/${pollutantName}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves a pollutant threshold.
|
||||
* @param threshold The pollutant threshold to save.
|
||||
* @returns A promise that resolves to the saved pollutant threshold.
|
||||
*/
|
||||
export const savePollutantThreshold = (threshold: PollutantThresholdDTO): Promise<PollutantThresholdDTO> => {
|
||||
return apiClient.post('/dashboard/thresholds', threshold);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取每种污染物的月度趋势数据
|
||||
* @returns 每种污染物的月度趋势数据
|
||||
*/
|
||||
export const getPollutantMonthlyTrends = (value: string): Promise<Record<string, TrendDataPointDTO[]>> => {
|
||||
return apiClient.get('/dashboard/reports/pollutant-monthly-trends')
|
||||
.then(data => {
|
||||
console.log('原始污染物趋势数据:', data);
|
||||
// 确保数据格式正确
|
||||
if (data && typeof data === 'object') {
|
||||
const result: Record<string, TrendDataPointDTO[]> = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
result[key] = value.map(item => ({
|
||||
yearMonth: item.yearMonth || '',
|
||||
count: item.count || 0
|
||||
}));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return {};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches all grid data from the server.
|
||||
* @returns A promise that resolves to an array of Grid objects.
|
||||
*/
|
||||
export const getGrids = (): Promise<Grid[]> => {
|
||||
return apiClient.get('/grids');
|
||||
};
|
||||
292
ems-frontend/ems-monitoring-system/src/api/feedback.ts
Normal file
292
ems-frontend/ems-monitoring-system/src/api/feedback.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import apiClient from './index';
|
||||
import type { Page, AssigneeInfoDTO, UserInfoDTO, TaskInfoDTO, AttachmentDTO } from './types';
|
||||
import { PollutionType } from './types';
|
||||
import type { AxiosResponse } from 'axios';
|
||||
|
||||
// 导出必要的类型
|
||||
export { PollutionType };
|
||||
|
||||
// API URL
|
||||
const API_URL = '/feedback';
|
||||
|
||||
// --- Start: Copied from TaskDetailView.vue for now ---
|
||||
// This is technical debt and should be refactored into a central types file.
|
||||
interface FeedbackDTO {
|
||||
feedbackId: number;
|
||||
eventId: string;
|
||||
title: string;
|
||||
}
|
||||
interface AssigneeDTO {
|
||||
id: number;
|
||||
name: string;
|
||||
phone: string;
|
||||
}
|
||||
interface TaskHistoryDTO {
|
||||
id: number;
|
||||
oldStatus: string | null;
|
||||
newStatus: string;
|
||||
comments: string;
|
||||
changedAt: string;
|
||||
changedBy: {
|
||||
id: number;
|
||||
name:string;
|
||||
};
|
||||
}
|
||||
interface SubmissionInfoDTO {
|
||||
comments: string;
|
||||
attachments: AttachmentDTO[];
|
||||
}
|
||||
export interface TaskDetail {
|
||||
id: number;
|
||||
feedback: FeedbackDTO;
|
||||
title: string;
|
||||
description: string;
|
||||
status: string;
|
||||
assignee: AssigneeDTO;
|
||||
assignedAt: string | null;
|
||||
completedAt: string | null;
|
||||
history: TaskHistoryDTO[];
|
||||
submissionInfo: SubmissionInfoDTO | null;
|
||||
}
|
||||
// --- End: Copied from TaskDetailView.vue ---
|
||||
|
||||
/**
|
||||
* 反馈状态枚举
|
||||
*/
|
||||
export enum FeedbackStatus {
|
||||
AI_REVIEWING = 'AI_REVIEWING', // AI审核中
|
||||
PENDING_REVIEW = 'PENDING_REVIEW', // 待人工审核
|
||||
REJECTED = 'REJECTED', // 已拒绝
|
||||
CONFIRMED = 'CONFIRMED', // 已确认
|
||||
ASSIGNED = 'ASSIGNED', // 已分配
|
||||
IN_PROGRESS = 'IN_PROGRESS', // 处理中
|
||||
RESOLVED = 'RESOLVED', // 已解决
|
||||
CLOSED = 'CLOSED', // 已关闭
|
||||
PENDING_ASSIGNMENT = 'PENDING_ASSIGNMENT', // 待分配
|
||||
PROCESSED = 'PROCESSED' // 已处理
|
||||
}
|
||||
|
||||
/**
|
||||
* 严重程度枚举
|
||||
*/
|
||||
export enum SeverityLevel {
|
||||
LOW = 'LOW', // 低
|
||||
MEDIUM = 'MEDIUM', // 中
|
||||
HIGH = 'HIGH' // 高
|
||||
}
|
||||
|
||||
/**
|
||||
* 反馈列表项接口 (现在匹配后端的 FeedbackResponseDTO)
|
||||
*/
|
||||
export interface FeedbackResponse {
|
||||
id: number;
|
||||
eventId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
pollutionType: PollutionType;
|
||||
severityLevel: SeverityLevel;
|
||||
status: FeedbackStatus;
|
||||
textAddress: string;
|
||||
gridX: number;
|
||||
gridY: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
createdAt: string; // ISO date string
|
||||
updatedAt: string; // ISO date string
|
||||
submitterId: number;
|
||||
user: UserInfoDTO;
|
||||
task: TaskDetail | null; // Task can be null if not assigned
|
||||
attachments: AttachmentDTO[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 反馈详情接口 (为了简化,我们可以让它继承自 FeedbackResponse)
|
||||
*/
|
||||
export interface FeedbackDetail extends FeedbackResponse {
|
||||
aiAnalysis?: string;
|
||||
reviewNotes?: string;
|
||||
assignmentId?: number;
|
||||
assignmentMethod?: string;
|
||||
assignmentTime?: string;
|
||||
assignedWorkerId?: number;
|
||||
resolutionNotes?: string;
|
||||
resolutionTime?: string;
|
||||
// `images` is now `attachments`, which is already in FeedbackResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* 反馈筛选参数接口
|
||||
*/
|
||||
export interface FeedbackFilters {
|
||||
status?: FeedbackStatus;
|
||||
pollutionType?: PollutionType;
|
||||
severityLevel?: SeverityLevel;
|
||||
cityName?: string;
|
||||
districtName?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
keyword?: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反馈处理请求接口
|
||||
*/
|
||||
export interface ProcessFeedbackRequest {
|
||||
status: FeedbackStatus;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反馈分配请求接口
|
||||
*/
|
||||
export interface AssignFeedbackRequest {
|
||||
gridWorkerId: number;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反馈拒绝请求接口
|
||||
*/
|
||||
export interface RejectFeedbackRequest {
|
||||
notes: string;
|
||||
}
|
||||
|
||||
export interface LocationInfo {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
textAddress: string;
|
||||
gridX: number;
|
||||
gridY: number;
|
||||
}
|
||||
|
||||
export interface FeedbackSubmissionRequest {
|
||||
title: string;
|
||||
description: string;
|
||||
pollutionType: PollutionType;
|
||||
severityLevel: SeverityLevel;
|
||||
location: LocationInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取反馈列表
|
||||
* @param params 筛选参数
|
||||
* @returns 分页反馈列表
|
||||
*/
|
||||
export function getFeedbackList(params?: FeedbackFilters): Promise<Page<FeedbackResponse>> {
|
||||
return apiClient.get(API_URL, { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取反馈详情
|
||||
* @param id 反馈ID
|
||||
* @returns 反馈详情
|
||||
*/
|
||||
export function getFeedbackDetail(id: number): Promise<AxiosResponse<FeedbackDetail>> {
|
||||
return apiClient.get(`/feedback/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理反馈
|
||||
* @param id 反馈ID
|
||||
* @param data 处理数据
|
||||
* @returns 处理结果
|
||||
*/
|
||||
export function processFeedback(id: number, request: { status: string; notes?: string }): Promise<AxiosResponse<FeedbackDetail>> {
|
||||
return apiClient.post(`/feedback/${id}/process`, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配反馈给网格员
|
||||
* @param id 反馈ID
|
||||
* @param data 分配数据
|
||||
* @returns 分配结果
|
||||
*/
|
||||
export function assignFeedback(id: number, data: AssignFeedbackRequest): Promise<any> {
|
||||
return apiClient.post(`${API_URL}/${id}/assign`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记反馈为已解决
|
||||
* @param id 反馈ID
|
||||
* @param notes 解决说明
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function resolveFeedback(id: number, notes?: string): Promise<any> {
|
||||
return apiClient.post(`${API_URL}/${id}/resolve`, { notes });
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭反馈
|
||||
* @param id 反馈ID
|
||||
* @param notes 关闭说明
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function closeFeedback(id: number, notes?: string): Promise<any> {
|
||||
return apiClient.post(`${API_URL}/${id}/close`, { notes });
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝反馈
|
||||
* @param id 反馈ID
|
||||
* @param data 拒绝数据
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function rejectFeedback(id: number, data: RejectFeedbackRequest): Promise<any> {
|
||||
return apiClient.post(`/supervisor/reviews/${id}/reject`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认反馈
|
||||
* @param id 反馈ID
|
||||
* @returns 响应体
|
||||
*/
|
||||
export function confirmFeedback(id: number): Promise<any> {
|
||||
return apiClient.post(`${API_URL}/${id}/confirm`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交新的反馈
|
||||
* @param formData 包含反馈数据和文件的表单
|
||||
* @returns 提交结果
|
||||
*/
|
||||
export function submitFeedback(formData: FormData): Promise<any> {
|
||||
return apiClient.post(`${API_URL}/submit`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取反馈统计数据
|
||||
* @returns 反馈统计数据
|
||||
*/
|
||||
export const getFeedbackStats = async () => {
|
||||
try {
|
||||
const response = await apiClient.get(`${API_URL}/stats`);
|
||||
return response || {}; // 如果响应为空,返回一个空对象
|
||||
} catch (error) {
|
||||
console.error('获取反馈统计数据API错误:', error);
|
||||
// 即使API失败也返回一个默认的统计对象结构,防止UI崩溃
|
||||
return {
|
||||
total: 0,
|
||||
pending: 0,
|
||||
confirmed: 0,
|
||||
assigned: 0,
|
||||
inProgress: 0,
|
||||
resolved: 0,
|
||||
closed: 0,
|
||||
rejected: 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取待处理的反馈列表
|
||||
* @returns 待处理的反馈列表
|
||||
*/
|
||||
export function getPendingFeedback(): Promise<AxiosResponse<FeedbackResponse[]>> {
|
||||
return apiClient.get('/feedback/pending');
|
||||
}
|
||||
42
ems-frontend/ems-monitoring-system/src/api/grid.ts
Normal file
42
ems-frontend/ems-monitoring-system/src/api/grid.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import apiClient from './index';
|
||||
import type { Grid } from './types';
|
||||
|
||||
/**
|
||||
* DTO for updating a grid's core properties.
|
||||
* This should align with GridUpdateRequest in the backend.
|
||||
*/
|
||||
export interface GridUpdateRequest {
|
||||
isObstacle?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新网格信息
|
||||
* @param gridId - 要更新的网格ID
|
||||
* @param data - 包含更新数据的对象
|
||||
* @returns 更新后的网格对象
|
||||
*/
|
||||
export const updateGrid = (gridId: number, data: GridUpdateRequest): Promise<Grid> => {
|
||||
return apiClient.patch(`/grids/${gridId}`, data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 通过坐标将网格员分配到网格
|
||||
* @param gridX - 网格X坐标
|
||||
* @param gridY - 网格Y坐标
|
||||
* @param userId - 要分配的用户ID
|
||||
* @returns 成功则返回void
|
||||
*/
|
||||
export const assignWorkerByCoordinates = (gridX: number, gridY: number, userId: number): Promise<void> => {
|
||||
return apiClient.post(`/grids/coordinates/${gridX}/${gridY}/assign`, { userId });
|
||||
};
|
||||
|
||||
/**
|
||||
* 通过坐标从网格中移除网格员
|
||||
* @param gridX - 网格X坐标
|
||||
* @param gridY - 网格Y坐标
|
||||
* @returns 成功则返回void
|
||||
*/
|
||||
export const unassignWorkerByCoordinates = (gridX: number, gridY: number): Promise<void> => {
|
||||
return apiClient.post(`/grids/coordinates/${gridX}/${gridY}/unassign`);
|
||||
};
|
||||
50
ems-frontend/ems-monitoring-system/src/api/index.ts
Normal file
50
ems-frontend/ems-monitoring-system/src/api/index.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import axios from 'axios';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
// 创建 Axios 实例
|
||||
const apiClient = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api', // 从环境变量或默认值设置基础URL
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// 添加请求拦截器
|
||||
apiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
// 这个拦截器会在每个请求发送前执行
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
// 为请求头添加Authorization字段
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
// 对请求错误做些什么
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => {
|
||||
// 对响应数据做点什么
|
||||
return response.data;
|
||||
},
|
||||
(error) => {
|
||||
// 对响应错误做点什么
|
||||
if (error.response && error.response.status === 401) {
|
||||
// 如果是401错误,说明token无效或过期
|
||||
// 我们需要调用auth store的logout方法
|
||||
// 为了避免循环依赖,我们在函数内部获取store实例
|
||||
const authStore = useAuthStore();
|
||||
authStore.logout();
|
||||
// 在这里重定向到登录页
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default apiClient;
|
||||
10
ems-frontend/ems-monitoring-system/src/api/log.ts
Normal file
10
ems-frontend/ems-monitoring-system/src/api/log.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import apiClient from '.';
|
||||
import type { OperationLog } from './types';
|
||||
|
||||
export function getOperationLogs(params: any): Promise<OperationLog[]> {
|
||||
return apiClient.get('/logs', { params });
|
||||
}
|
||||
|
||||
export function getMyOperationLogs(): Promise<OperationLog[]> {
|
||||
return apiClient.get('/logs/my-logs');
|
||||
}
|
||||
30
ems-frontend/ems-monitoring-system/src/api/pathfinding.ts
Normal file
30
ems-frontend/ems-monitoring-system/src/api/pathfinding.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import apiClient from './index';
|
||||
|
||||
/**
|
||||
* Represents a point with x and y coordinates.
|
||||
* Matches the backend Point DTO.
|
||||
*/
|
||||
export interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the request payload for finding a path.
|
||||
* Matches the backend PathfindingRequest DTO.
|
||||
*/
|
||||
export interface PathfindingRequest {
|
||||
startX: number;
|
||||
startY: number;
|
||||
endX: number;
|
||||
endY: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the backend API to find a path between two points.
|
||||
* @param request The pathfinding request containing start and end coordinates.
|
||||
* @returns A promise that resolves to a list of points representing the path.
|
||||
*/
|
||||
export const findPath = (request: PathfindingRequest): Promise<Point[]> => {
|
||||
return apiClient.post('/pathfinding/find', request);
|
||||
};
|
||||
40
ems-frontend/ems-monitoring-system/src/api/personnel.ts
Normal file
40
ems-frontend/ems-monitoring-system/src/api/personnel.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import apiClient from './index';
|
||||
import type { Page, Pageable, UserAccount, UserUpdateRequest } from './types';
|
||||
|
||||
/**
|
||||
* 获取用户列表(可分页、可筛选)
|
||||
* @param params - 包含分页和筛选条件的参数
|
||||
* @returns 用户数据分页对象
|
||||
*/
|
||||
export const getUsers = (params: { role?: string; name?: string; } & Pageable): Promise<Page<UserAccount>> => {
|
||||
return apiClient.get('/personnel/users', { params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有网格员信息
|
||||
* @returns 所有网格员的列表
|
||||
*/
|
||||
export const getAllGridWorkers = async (): Promise<UserAccount[]> => {
|
||||
// We fetch with a large size to get all workers, assuming the number is manageable.
|
||||
// A more robust solution might involve paginating through all results if the number of workers is very large.
|
||||
const response = await getUsers({ role: 'GRID_WORKER', page: 0, size: 1000 });
|
||||
return response.content;
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据ID更新用户信息
|
||||
* @param userId - 用户ID
|
||||
* @param data - 需要更新的用户信息
|
||||
* @returns 更新后的用户信息
|
||||
*/
|
||||
export const updateUser = (userId: number, data: UserUpdateRequest): Promise<UserAccount> => {
|
||||
return apiClient.patch(`/personnel/users/${userId}`, data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据ID删除用户
|
||||
* @param userId - 用户ID
|
||||
*/
|
||||
export const deleteUser = (userId: number): Promise<void> => {
|
||||
return apiClient.delete(`/personnel/users/${userId}`);
|
||||
};
|
||||
15
ems-frontend/ems-monitoring-system/src/api/public.ts
Normal file
15
ems-frontend/ems-monitoring-system/src/api/public.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import apiClient from './index';
|
||||
|
||||
/**
|
||||
* Submits public feedback.
|
||||
* This endpoint is for unauthenticated users (guests).
|
||||
* @param formData The form data containing the feedback details and any files.
|
||||
* @returns A promise that resolves on successful submission.
|
||||
*/
|
||||
export const submitPublicFeedback = (formData: FormData): Promise<any> => {
|
||||
return apiClient.post('/public/feedback', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
};
|
||||
37
ems-frontend/ems-monitoring-system/src/api/tasks.ts
Normal file
37
ems-frontend/ems-monitoring-system/src/api/tasks.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { AxiosResponse, AxiosError } from 'axios';
|
||||
import apiClient from './index';
|
||||
import type { UserAccount } from './types';
|
||||
|
||||
export const getAvailableGridWorkers = (): Promise<UserAccount[]> => {
|
||||
// 根据TaskAssignmentController的路径应为/tasks/grid-workers
|
||||
return apiClient.get<UserAccount[]>('/tasks/grid-workers')
|
||||
.then((response: AxiosResponse<UserAccount[]>) => response.data)
|
||||
.catch((error: AxiosError) => {
|
||||
console.error('获取可用网格员API错误:', error);
|
||||
// 如果API调用失败,返回空数组而不是模拟数据
|
||||
return [];
|
||||
});
|
||||
};
|
||||
|
||||
export const assignTask = (feedbackId: number, assigneeId: number): Promise<any> => {
|
||||
return apiClient.post('/tasks/assign', { feedbackId, assigneeId });
|
||||
};
|
||||
|
||||
/**
|
||||
* Approves a submitted task.
|
||||
* @param taskId The ID of the task to approve.
|
||||
* @returns A promise that resolves with the updated task details.
|
||||
*/
|
||||
export const approveTask = (taskId: number): Promise<any> => {
|
||||
return apiClient.post(`/management/tasks/${taskId}/approve`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rejects a submitted task.
|
||||
* @param taskId The ID of the task to reject.
|
||||
* @param reason The reason for rejection.
|
||||
* @returns A promise that resolves with the updated task details.
|
||||
*/
|
||||
export const rejectTask = (taskId: number, reason: string): Promise<any> => {
|
||||
return apiClient.post(`/management/tasks/${taskId}/reject`, { reason });
|
||||
};
|
||||
307
ems-frontend/ems-monitoring-system/src/api/types.ts
Normal file
307
ems-frontend/ems-monitoring-system/src/api/types.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* 通用用户信息类型, 反映了JWT中包含的声明
|
||||
*/
|
||||
export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
phone?: string;
|
||||
status?: string;
|
||||
region?: string;
|
||||
skills?: string[];
|
||||
gridX?: number;
|
||||
gridY?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录请求体
|
||||
*/
|
||||
export interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录成功响应体, 与后端完全匹配
|
||||
*/
|
||||
export interface LoginResponse {
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册请求体
|
||||
*/
|
||||
export interface SignUpRequest {
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
password: string;
|
||||
verificationCode: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用验证码重置密码请求体, 对应后端的 PasswordResetWithCodeDto
|
||||
*/
|
||||
export interface ResetPasswordWithCodeRequest {
|
||||
email: string;
|
||||
code: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页请求参数
|
||||
*/
|
||||
export interface Pageable {
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应体
|
||||
*/
|
||||
export interface Page<T> {
|
||||
content: T[];
|
||||
totalPages: number;
|
||||
totalElements: number;
|
||||
size: number;
|
||||
number: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 后端 UserAccount 实体类的完整映射
|
||||
*/
|
||||
export interface UserAccount {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
role: string;
|
||||
status: string;
|
||||
gender?: string;
|
||||
region?: string;
|
||||
gridX?: number;
|
||||
gridY?: number;
|
||||
level?: string;
|
||||
skills?: string[];
|
||||
currentLatitude?: number;
|
||||
currentLongitude?: number;
|
||||
createdAt: string; // Assuming ISO date string
|
||||
updatedAt: string; // Assuming ISO date string
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户更新请求体, 对应后端的 UserUpdateRequest
|
||||
*/
|
||||
export interface UserUpdateRequest {
|
||||
name?: string;
|
||||
phone?: string;
|
||||
region?: string;
|
||||
role?: string;
|
||||
status?: string;
|
||||
gender?: 'MALE' | 'FEMALE' | 'OTHER';
|
||||
gridX?: number;
|
||||
gridY?: number;
|
||||
level?: string;
|
||||
skills?: string[];
|
||||
currentLatitude?: number;
|
||||
currentLongitude?: number;
|
||||
}
|
||||
|
||||
// --- Dashboard DTOs ---
|
||||
|
||||
/**
|
||||
* Key statistics for the main dashboard.
|
||||
* @param totalFeedbacks - Total number of feedbacks.
|
||||
* @param confirmedFeedbacks - Number of confirmed feedbacks.
|
||||
* @param totalAqiRecords - Total number of AQI records.
|
||||
* @param activeGridWorkers - Number of active grid workers.
|
||||
*/
|
||||
export type DashboardStatsDTO = {
|
||||
totalFeedbacks: number;
|
||||
confirmedFeedbacks: number;
|
||||
totalAqiRecords: number;
|
||||
activeGridWorkers: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data for AQI level distribution chart.
|
||||
* @param level - The AQI descriptive level (e.g., "Good", "Moderate").
|
||||
* @param count - The number of monitoring points at this level.
|
||||
*/
|
||||
export type AqiDistributionDTO = {
|
||||
level: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* A single data point for a time-series trend chart.
|
||||
* @param yearMonth - The date for the data point in "YYYY-MM" format.
|
||||
* @param count - The value for that date (e.g., number of exceedances).
|
||||
*/
|
||||
export type TrendDataPointDTO = {
|
||||
yearMonth: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Grid coverage statistics for a specific city.
|
||||
* @param city - The name of the city.
|
||||
* @param totalGrids - The total number of grids in the city.
|
||||
* @param coveredGrids - The number of grids with assigned personnel.
|
||||
* @param coverageRate - The calculated coverage percentage.
|
||||
*/
|
||||
export type GridCoverageDTO = {
|
||||
city: string;
|
||||
totalGrids: number;
|
||||
coveredGrids: number;
|
||||
coverageRate: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* A single point for a heatmap visualization.
|
||||
* @param lat - Latitude of the data point.
|
||||
* @param lng - Longitude of the data point.
|
||||
* @param value - The intensity value for the heatmap.
|
||||
*/
|
||||
export type HeatmapPointDTO = {
|
||||
lat: number;
|
||||
lng: number;
|
||||
value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detailed statistics for a specific pollutant.
|
||||
* @param pollutantName - The name of the pollutant (e.g., "PM2.5").
|
||||
* @param averageValue - The average value over a period.
|
||||
* @param maxValue - The maximum recorded value.
|
||||
* @param unit - The measurement unit (e.g., "µg/m³").
|
||||
*/
|
||||
export type PollutionStatsDTO = {
|
||||
pollutantName: string;
|
||||
averageValue: number;
|
||||
maxValue: number;
|
||||
unit: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Statistics on task completion status.
|
||||
* @param totalTasks - Total number of tasks.
|
||||
* @param completedTasks - Number of completed tasks.
|
||||
* @param completionRate - Completion rate of tasks.
|
||||
*/
|
||||
export type TaskStatsDTO = {
|
||||
totalTasks: number;
|
||||
completedTasks: number;
|
||||
completionRate: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* A single point for the AQI heatmap, including pollutant info.
|
||||
* Extends HeatmapPointDTO with additional details.
|
||||
*/
|
||||
export type AqiHeatmapPointDTO = HeatmapPointDTO & {
|
||||
primaryPollutant: string;
|
||||
aqi: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 污染物类型枚举
|
||||
*/
|
||||
export enum PollutionType {
|
||||
PM25 = 'PM25',
|
||||
O3 = 'O3',
|
||||
NO2 = 'NO2',
|
||||
SO2 = 'SO2',
|
||||
OTHER = 'OTHER'
|
||||
}
|
||||
|
||||
/**
|
||||
* 污染物阈值设置
|
||||
* @param pollutionType - 污染物类型
|
||||
* @param pollutantName - 污染物名称
|
||||
* @param threshold - 阈值
|
||||
* @param unit - 单位
|
||||
* @param description - 描述
|
||||
*/
|
||||
export type PollutantThresholdDTO = {
|
||||
pollutionType: PollutionType;
|
||||
pollutantName: string;
|
||||
threshold: number;
|
||||
unit: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a geographical grid cell.
|
||||
* Matches the backend Grid entity.
|
||||
*/
|
||||
export interface Grid {
|
||||
id: number;
|
||||
gridX: number;
|
||||
gridY: number;
|
||||
cityName: string;
|
||||
districtName?: string;
|
||||
description?: string;
|
||||
isObstacle: boolean;
|
||||
}
|
||||
|
||||
// ... 您可以根据API文档继续添加其他类型定义
|
||||
|
||||
// --- Task and Assignment DTOs ---
|
||||
|
||||
export interface AssigneeInfoDTO {
|
||||
id: number;
|
||||
name: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
export interface UserInfoDTO {
|
||||
id: number;
|
||||
name: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface TaskInfoDTO {
|
||||
id: number;
|
||||
status: string; // Should match TaskStatus enum from backend
|
||||
assignee: AssigneeInfoDTO;
|
||||
submissionInfo: SubmissionInfoDTO | null;
|
||||
}
|
||||
|
||||
export interface AttachmentDTO {
|
||||
id: number;
|
||||
fileName: string;
|
||||
fileType: string;
|
||||
url: string;
|
||||
uploadedAt: string; // ISO date string
|
||||
}
|
||||
|
||||
export interface SubmissionInfoDTO {
|
||||
comments: string;
|
||||
attachments: AttachmentDTO[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作日志类型
|
||||
*/
|
||||
export interface OperationLog {
|
||||
id: number;
|
||||
userId: number;
|
||||
userName: string;
|
||||
operationType: string;
|
||||
operationTypeDesc: string;
|
||||
description: string;
|
||||
targetId: string;
|
||||
targetType: string;
|
||||
ipAddress: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task Summary DTO for supervisor view.
|
||||
* This should match `TaskSummaryDTO` from the backend.
|
||||
*/
|
||||
74
ems-frontend/ems-monitoring-system/src/api/user.ts
Normal file
74
ems-frontend/ems-monitoring-system/src/api/user.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import apiClient from './index';
|
||||
import type { GridWorker } from './grid';
|
||||
|
||||
/**
|
||||
* 用户信息接口
|
||||
*/
|
||||
export interface UserInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
role: string;
|
||||
avatar?: string;
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户更新请求接口
|
||||
*/
|
||||
export interface UserUpdateRequest {
|
||||
name?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
region?: string;
|
||||
level?: string;
|
||||
gridX?: number | null;
|
||||
gridY?: number | null;
|
||||
currentLatitude?: number | null;
|
||||
currentLongitude?: number | null;
|
||||
skills?: string[];
|
||||
status?: string;
|
||||
gender?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户信息
|
||||
* @returns 用户信息
|
||||
*/
|
||||
export function getCurrentUser(): Promise<UserInfo> {
|
||||
return apiClient.get('/auth/profile');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户资料
|
||||
* @param userId 用户ID
|
||||
* @param data 更新数据
|
||||
* @returns 更新后的用户信息
|
||||
*/
|
||||
export function updateUserProfile(userId: number, data: UserUpdateRequest): Promise<GridWorker> {
|
||||
console.log(`API调用: 更新用户资料, userId=${userId}`, data);
|
||||
return apiClient.patch(`/personnel/users/${userId}`, data)
|
||||
.then(response => {
|
||||
console.log('更新用户资料API调用成功:', response.data);
|
||||
return response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('更新用户资料API调用失败:', error);
|
||||
console.error('错误详情:', error.response?.data || error.message);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户角色
|
||||
* @param userId 用户ID
|
||||
* @param role 角色名称
|
||||
* @param gridX 网格X坐标(可选)
|
||||
* @param gridY 网格Y坐标(可选)
|
||||
* @returns 更新后的用户信息
|
||||
*/
|
||||
export function updateUserRole(userId: number, role: string, gridX?: number, gridY?: number): Promise<GridWorker> {
|
||||
const data = { role, gridX, gridY };
|
||||
return apiClient.put(`/personnel/users/${userId}/role`, data).then(response => response.data);
|
||||
}
|
||||
90
ems-frontend/ems-monitoring-system/src/assets/base.css
Normal file
90
ems-frontend/ems-monitoring-system/src/assets/base.css
Normal file
@@ -0,0 +1,90 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-green-dark: #2E7D32;
|
||||
--vt-c-green: #4CAF50;
|
||||
--vt-c-green-light: #81C784;
|
||||
--vt-c-green-soft: #C8E6C9;
|
||||
--vt-c-green-mute: #E8F5E9;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-green-dark);
|
||||
--vt-c-text-light-2: rgba(46, 125, 50, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-green-mute);
|
||||
--color-background-soft: var(--vt-c-green-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-green-light);
|
||||
--color-text: var(--vt-c-green-soft);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
1
ems-frontend/ems-monitoring-system/src/assets/main.css
Normal file
1
ems-frontend/ems-monitoring-system/src/assets/main.css
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,607 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
title="反馈详情"
|
||||
width="70%"
|
||||
:before-close="handleClose"
|
||||
class="feedback-detail-dialog"
|
||||
top="5vh"
|
||||
>
|
||||
<template #header>
|
||||
<div class="dialog-header">
|
||||
<span class="el-dialog__title">反馈详情</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="Refresh"
|
||||
circle
|
||||
@click="handleRefresh"
|
||||
:loading="loading"
|
||||
title="刷新数据"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="loading" v-loading="loading" class="loading-container"></div>
|
||||
<div v-if="!loading && currentFeedback" class="detail-container">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="ID">{{ currentFeedback.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="事件编号">{{ currentFeedback.eventId || '无' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="标题" :span="2">{{ currentFeedback.title }}</el-descriptions-item>
|
||||
<el-descriptions-item label="污染类型">{{ formatPollutionType(currentFeedback.pollutionType) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="严重程度">
|
||||
<el-tag :type="getSeverityTagType(currentFeedback.severityLevel)">
|
||||
{{ formatSeverityLevel(currentFeedback.severityLevel) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="getStatusTagType(currentFeedback.status)">
|
||||
{{ formatStatus(currentFeedback.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="提交时间">{{ formatDate(currentFeedback.createdAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDate(currentFeedback.updatedAt) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-descriptions :column="1" border class="mt-4">
|
||||
<el-descriptions-item label="详细描述">
|
||||
<div class="description-content">{{ currentFeedback.description }}</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="提交位置" v-if="currentFeedback.latitude && currentFeedback.longitude">
|
||||
{{ currentFeedback.textAddress || `经纬度: (${currentFeedback.longitude}, ${currentFeedback.latitude})` }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="报告人信息" v-if="currentFeedback.user">
|
||||
{{ currentFeedback.user.name }}
|
||||
<el-tag size="small" style="margin-left: 8px;">{{ formatRole(currentFeedback.user.role) }}</el-tag>
|
||||
<div class="mt-1">ID: {{ currentFeedback.user.id }}, 电话: {{ currentFeedback.reporterPhone || '未提供' }}</div>
|
||||
<div class="user-note mt-1" v-if="currentFeedback.user.role === 'GRID_WORKER'">
|
||||
<el-alert
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="注意:通常反馈应由公众监督员提交,此处显示为网格员可能是测试数据"
|
||||
/>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="task-details mt-4" v-if="currentFeedback.task">
|
||||
<h4>任务详情</h4>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="任务ID">{{ currentFeedback.task.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="任务状态">
|
||||
<el-tag :type="getStatusTagType(currentFeedback.task.status)">
|
||||
{{ formatStatus(currentFeedback.task.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="处理人" v-if="currentFeedback.task.assignee">
|
||||
{{ currentFeedback.task.assignee.name }} (ID: {{ currentFeedback.task.assignee.id }})
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="处理人电话" v-if="currentFeedback.task.assignee">
|
||||
{{ currentFeedback.task.assignee.phone || '未提供' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="网格坐标" :span="2" v-if="currentFeedback.task.gridX !== null && currentFeedback.task.gridY !== null">
|
||||
X: {{ currentFeedback.task.gridX }}, Y: {{ currentFeedback.task.gridY }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="分配时间" v-if="currentFeedback.task.assignment?.assignmentTime">
|
||||
{{ formatDate(currentFeedback.task.assignment.assignmentTime) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="分配说明" v-if="currentFeedback.task.assignment?.remarks">
|
||||
{{ currentFeedback.task.assignment.remarks }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 任务提交详情 -->
|
||||
<div class="submission-details mt-4" v-if="currentFeedback.task && currentFeedback.task.submissionInfo">
|
||||
<h4>任务提交详情</h4>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="提交说明">
|
||||
{{ currentFeedback.task.submissionInfo.comments }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="提交附件" v-if="currentFeedback.task.submissionInfo.attachments && currentFeedback.task.submissionInfo.attachments.length > 0">
|
||||
<div v-for="att in currentFeedback.task.submissionInfo.attachments" :key="att.id">
|
||||
<el-link :href="att.url" type="primary" target="_blank" :icon="Document">{{ att.fileName }}</el-link>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="无附件" v-else>
|
||||
未提交任何附件
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="image-gallery mt-4" v-if="currentFeedback.attachments && currentFeedback.attachments.length > 0">
|
||||
<h4>相关图片</h4>
|
||||
<el-image
|
||||
v-for="att in currentFeedback.attachments"
|
||||
:key="att.id"
|
||||
:src="att.url"
|
||||
:preview-src-list="currentFeedback.attachments.map(a => a.url)"
|
||||
:initial-index="currentFeedback.attachments.findIndex(a => a.id === att.id)"
|
||||
fit="cover"
|
||||
class="feedback-image"
|
||||
lazy
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!loading && !currentFeedback" class="empty-state">
|
||||
<el-empty description="无法加载反馈详情" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
<!-- 任务审批按钮 -->
|
||||
<template v-if="currentFeedback?.task?.status === 'SUBMITTED' && canManageSubmittedTask">
|
||||
<el-button type="success" @click="handleApprove" :loading="approving">批准通过</el-button>
|
||||
<el-button type="danger" @click="handleRejectTask" :loading="rejecting">打回任务</el-button>
|
||||
</template>
|
||||
<!-- 根据状态条件性显示操作按钮 -->
|
||||
<el-button type="primary" @click="handleProcess" v-if="canProcess">处理</el-button>
|
||||
<el-button type="success" @click="handleAssign" v-if="canAssign">分配任务</el-button>
|
||||
<el-button type="warning" @click="handleRejectFeedback" v-if="canReject">拒绝反馈</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 分配任务对话框 -->
|
||||
<el-dialog
|
||||
v-model="assignDialogVisible"
|
||||
title="分配任务"
|
||||
width="30%"
|
||||
:before-close="() => assignDialogVisible = false"
|
||||
>
|
||||
<div v-if="loadingGridWorkers">正在加载网格员...</div>
|
||||
<el-select
|
||||
v-else
|
||||
v-model="selectedGridWorkerId"
|
||||
placeholder="请选择网格员"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-option
|
||||
v-for="worker in gridWorkers"
|
||||
:key="worker.id"
|
||||
:label="`${worker.name} (ID: ${worker.id})${worker.phone ? ' - ' + worker.phone : ''}`"
|
||||
:value="worker.id"
|
||||
/>
|
||||
</el-select>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="assignDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmAssign" :disabled="!selectedGridWorkerId">
|
||||
确认分配
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed, onMounted, h } from 'vue';
|
||||
import { useFeedbackStore } from '@/store/feedback';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ElMessage, ElMessageBox, ElSelect, ElOption } from 'element-plus';
|
||||
import { FeedbackStatus, PollutionType, SeverityLevel, processFeedback, assignFeedback, rejectFeedback, resolveFeedback } from '@/api/feedback';
|
||||
import { approveTask, rejectTask } from '@/api/tasks';
|
||||
import type { UserAccount } from '@/api/types';
|
||||
import apiClient from '@/api/index';
|
||||
import { Document, Refresh } from '@element-plus/icons-vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
feedbackId: number | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['close', 'refresh']);
|
||||
|
||||
const feedbackStore = useFeedbackStore();
|
||||
const { currentFeedbackDetail: currentFeedback, detailLoading: loading } = storeToRefs(feedbackStore);
|
||||
const authStore = useAuthStore();
|
||||
const currentUser = computed(() => authStore.user);
|
||||
|
||||
const approving = ref(false);
|
||||
const rejecting = ref(false);
|
||||
|
||||
// 添加网格员列表状态
|
||||
const gridWorkers = ref<UserAccount[]>([]);
|
||||
const loadingGridWorkers = ref(false);
|
||||
const selectedGridWorkerId = ref<number | null>(null);
|
||||
const assignDialogVisible = ref(false);
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (props.feedbackId) {
|
||||
feedbackStore.fetchFeedbackDetail(props.feedbackId);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载网格员列表
|
||||
const loadGridWorkers = async () => {
|
||||
loadingGridWorkers.value = true;
|
||||
try {
|
||||
// 直接调用API
|
||||
const response = await apiClient.get('/tasks/grid-workers');
|
||||
// 根据apiClient的拦截器,response已经是处理过的数据了
|
||||
gridWorkers.value = response as unknown as UserAccount[];
|
||||
console.log('成功获取到网格员列表:', gridWorkers.value);
|
||||
if (gridWorkers.value.length === 0) {
|
||||
ElMessage.info('当前没有可用的网格员。');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载网格员列表失败:', error);
|
||||
ElMessage.error('加载网格员列表失败,请检查后端服务是否正常。');
|
||||
gridWorkers.value = []; // 失败时清空数组
|
||||
} finally {
|
||||
loadingGridWorkers.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取模拟网格员数据
|
||||
const getMockGridWorkers = (): UserAccount[] => {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
name: '张三',
|
||||
email: 'zhangsan@example.com',
|
||||
phone: '13800000001',
|
||||
role: 'GRID_WORKER',
|
||||
status: 'ACTIVE',
|
||||
gender: 'MALE',
|
||||
region: '北京市朝阳区',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '李四',
|
||||
email: 'lisi@example.com',
|
||||
phone: '13800000002',
|
||||
role: 'GRID_WORKER',
|
||||
status: 'ACTIVE',
|
||||
gender: 'FEMALE',
|
||||
region: '北京市海淀区',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '王五',
|
||||
email: 'wangwu@example.com',
|
||||
phone: '13800000003',
|
||||
role: 'GRID_WORKER',
|
||||
status: 'ACTIVE',
|
||||
gender: 'MALE',
|
||||
region: '北京市西城区',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
// 在对话框显示时加载网格员列表
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVisible) => {
|
||||
if (newVisible && canAssign.value) {
|
||||
// 移除这里的加载,改为在点击分配按钮时加载
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// --- Computed Properties for Action Buttons ---
|
||||
|
||||
const canManageSubmittedTask = computed(() => {
|
||||
// Per user requirements, only ADMINs can manage tasks submitted by Grid Workers.
|
||||
return authStore.user?.role === 'ADMIN';
|
||||
});
|
||||
|
||||
const isActionableBySupervisor = computed(() => {
|
||||
// This property now simply checks if the user has the authority to perform
|
||||
// pre-assignment actions like processing or rejecting feedback.
|
||||
if (!authStore.user) return false;
|
||||
const viewerRole = authStore.user.role;
|
||||
return viewerRole === 'ADMIN' || viewerRole === 'SUPERVISOR';
|
||||
});
|
||||
|
||||
const canProcess = computed(() => {
|
||||
if (!currentFeedback.value || !isActionableBySupervisor.value) return false;
|
||||
const processableStatus = [
|
||||
FeedbackStatus.AI_REVIEWING,
|
||||
FeedbackStatus.PENDING_REVIEW,
|
||||
FeedbackStatus.CONFIRMED,
|
||||
];
|
||||
return processableStatus.includes(currentFeedback.value.status);
|
||||
});
|
||||
|
||||
const canAssign = computed(() => {
|
||||
if (!currentFeedback.value || !authStore.user) return false;
|
||||
|
||||
const viewerRole = authStore.user.role;
|
||||
const isEligibleRole = viewerRole === 'ADMIN' || viewerRole === 'SUPERVISOR';
|
||||
if (!isEligibleRole) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// As per user feedback, supervisors and admins should be able to assign tasks.
|
||||
// The button should appear when the feedback is ready for assignment.
|
||||
// Based on the 'handleProcess' function, this status is PENDING_ASSIGNMENT.
|
||||
return currentFeedback.value.status === 'PENDING_ASSIGNMENT';
|
||||
});
|
||||
|
||||
const canReject = computed(() => {
|
||||
if (!currentFeedback.value || !isActionableBySupervisor.value) return false;
|
||||
const rejectableStatus = [
|
||||
FeedbackStatus.AI_REVIEWING,
|
||||
FeedbackStatus.PENDING_REVIEW,
|
||||
];
|
||||
return rejectableStatus.includes(currentFeedback.value.status);
|
||||
});
|
||||
|
||||
// 格式化显示内容的辅助函数
|
||||
const formatPollutionType = (type: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'PM25': 'PM2.5',
|
||||
'O3': '臭氧',
|
||||
'NO2': '二氧化氮',
|
||||
'SO2': '二氧化硫',
|
||||
'OTHER': '其他'
|
||||
};
|
||||
return map[type] || type;
|
||||
};
|
||||
|
||||
const formatSeverityLevel = (level: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'LOW': '低',
|
||||
'MEDIUM': '中',
|
||||
'HIGH': '高'
|
||||
};
|
||||
return map[level] || level;
|
||||
};
|
||||
|
||||
const formatStatus = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'AI_REVIEWING': 'AI审核中',
|
||||
'PENDING_REVIEW': '待人工审核',
|
||||
'REJECTED': '已拒绝',
|
||||
'CONFIRMED': '已确认',
|
||||
'ASSIGNED': '已分配',
|
||||
'IN_PROGRESS': '处理中',
|
||||
'RESOLVED': '已处理',
|
||||
'CLOSED': '已关闭',
|
||||
'PENDING_ASSIGNMENT': '待分配',
|
||||
'PROCESSED': '已处理',
|
||||
'COMPLETED': '已处理',
|
||||
'SUBMITTED': '已提交'
|
||||
};
|
||||
return map[status] || status;
|
||||
};
|
||||
|
||||
const getSeverityTagType = (level: string) => {
|
||||
const map: Record<string, 'info' | 'warning' | 'danger'> = {
|
||||
'LOW': 'info',
|
||||
'MEDIUM': 'warning',
|
||||
'HIGH': 'danger'
|
||||
};
|
||||
return map[level] || 'info';
|
||||
};
|
||||
|
||||
const getStatusTagType = (status: string) => {
|
||||
const map: Record<string, 'info' | 'warning' | 'danger' | 'primary' | 'success'> = {
|
||||
'AI_REVIEWING': 'info',
|
||||
'PENDING_REVIEW': 'warning',
|
||||
'REJECTED': 'danger',
|
||||
'CONFIRMED': 'primary',
|
||||
'ASSIGNED': 'primary',
|
||||
'IN_PROGRESS': 'warning',
|
||||
'RESOLVED': 'success',
|
||||
'CLOSED': 'info',
|
||||
'PENDING_ASSIGNMENT': 'warning',
|
||||
'PROCESSED': 'success',
|
||||
'COMPLETED': 'success',
|
||||
'SUBMITTED': 'info'
|
||||
};
|
||||
return map[status] || 'info';
|
||||
};
|
||||
|
||||
const getFeedbackDisplayStatus = (feedback: any): string => {
|
||||
if (!feedback) return '未知状态';
|
||||
if (feedback.task && feedback.task.status === 'COMPLETED') {
|
||||
return '已处理';
|
||||
}
|
||||
return formatStatus(feedback.status);
|
||||
};
|
||||
|
||||
const getFeedbackDisplayTagType = (feedback: any): 'info' | 'warning' | 'danger' | 'primary' | 'success' => {
|
||||
if (!feedback) return 'info';
|
||||
if (feedback.task && feedback.task.status === 'COMPLETED') {
|
||||
return 'success';
|
||||
}
|
||||
return getStatusTagType(feedback.status);
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
try {
|
||||
return new Date(dateStr).toLocaleString();
|
||||
} catch (e) {
|
||||
return dateStr;
|
||||
}
|
||||
};
|
||||
|
||||
const formatRole = (role: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'ADMIN': '管理员',
|
||||
'DECISION_MAKER': '决策人',
|
||||
'SUPERVISOR': '监督员',
|
||||
'GRID_WORKER': '网格员',
|
||||
'PUBLIC_SUPERVISOR': '公众监督员'
|
||||
};
|
||||
return map[role] || role;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.feedbackId,
|
||||
(newId) => {
|
||||
if (newId && props.visible) {
|
||||
feedbackStore.fetchFeedbackDetail(newId);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
// 清空选中的网格员
|
||||
selectedGridWorkerId.value = null;
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const handleProcess = async () => {
|
||||
if (!currentFeedback.value) return;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'此操作将批准反馈并创建一个待分配的任务。',
|
||||
'确认处理反馈',
|
||||
{
|
||||
confirmButtonText: '继续',
|
||||
cancelButtonText: '取消',
|
||||
type: 'success',
|
||||
}
|
||||
);
|
||||
await processFeedback(currentFeedback.value.id, {
|
||||
status: FeedbackStatus.PENDING_ASSIGNMENT,
|
||||
notes: '管理员已批准该反馈,准备分配任务。'
|
||||
});
|
||||
ElMessage.success('反馈已批准,进入待分配状态');
|
||||
emit('refresh');
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('处理反馈失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssign = () => {
|
||||
if (!currentFeedback.value) return;
|
||||
loadGridWorkers();
|
||||
assignDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const confirmAssign = async () => {
|
||||
if (!currentFeedback.value || !selectedGridWorkerId.value) {
|
||||
ElMessage.warning('请选择一个网格员');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.post('/tasks/assign', {
|
||||
feedbackId: currentFeedback.value.id,
|
||||
assigneeId: selectedGridWorkerId.value
|
||||
});
|
||||
|
||||
ElMessage.success('分配成功');
|
||||
assignDialogVisible.value = false;
|
||||
emit('refresh');
|
||||
handleClose();
|
||||
} catch (apiError) {
|
||||
console.error('API分配失败:', apiError);
|
||||
ElMessage.error('分配失败,请稍后重试');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRejectTask = async () => {
|
||||
const taskId = currentFeedback.value?.task?.id;
|
||||
if (!taskId) {
|
||||
ElMessage.error('无法获取任务ID,操作取消');
|
||||
return;
|
||||
}
|
||||
|
||||
ElMessageBox.prompt('请输入打回任务的理由:', '打回任务', {
|
||||
confirmButtonText: '确认打回',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /.+/,
|
||||
inputErrorMessage: '理由不能为空',
|
||||
})
|
||||
.then(async ({ value }) => {
|
||||
rejecting.value = true;
|
||||
try {
|
||||
await rejectTask(taskId, value);
|
||||
ElMessage.success('任务已打回');
|
||||
emit('refresh');
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
console.error('打回任务失败:', error);
|
||||
ElMessage.error('操作失败,请重试');
|
||||
} finally {
|
||||
rejecting.value = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消操作');
|
||||
});
|
||||
};
|
||||
|
||||
const handleApprove = async () => {
|
||||
if (!currentFeedback.value?.task) return;
|
||||
approving.value = true;
|
||||
try {
|
||||
await approveTask(currentFeedback.value.task.id);
|
||||
ElMessage.success('任务已批准');
|
||||
emit('refresh');
|
||||
} catch (error) {
|
||||
ElMessage.error('批准任务失败');
|
||||
} finally {
|
||||
approving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRejectFeedback = async () => {
|
||||
if (!currentFeedback.value) return;
|
||||
if (props.feedbackId) {
|
||||
ElMessageBox.prompt('请输入拒绝此反馈的理由:', '拒绝反馈', {
|
||||
confirmButtonText: '确认拒绝',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
.then(async ({ value }) => {
|
||||
try {
|
||||
await rejectFeedback(props.feedbackId!, { notes: value || '无明确理由' });
|
||||
ElMessage.success('反馈已拒绝');
|
||||
emit('refresh');
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
console.error('拒绝反馈失败:', error);
|
||||
ElMessage.error('操作失败');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消拒绝操作');
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.feedback-detail-dialog .loading-container {
|
||||
height: 300px;
|
||||
}
|
||||
.detail-container {
|
||||
max-height: 75vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.mt-4 {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.mt-1 {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.description-content {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.image-gallery {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
.feedback-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
333
ems-frontend/ems-monitoring-system/src/components/GridMap.vue
Normal file
333
ems-frontend/ems-monitoring-system/src/components/GridMap.vue
Normal file
@@ -0,0 +1,333 @@
|
||||
<template>
|
||||
<div class="grid-map-container">
|
||||
<div class="map-header">
|
||||
<h3>环境监测网格地图</h3>
|
||||
<div class="map-controls">
|
||||
<el-select v-model="selectedCity" placeholder="选择城市" @change="handleCityChange" size="small">
|
||||
<el-option v-for="city in cities" :key="city" :label="city" :value="city" />
|
||||
</el-select>
|
||||
<el-button type="primary" size="small" :icon="Loading" :loading="loading" @click="refreshData">
|
||||
刷新数据
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="map-content">
|
||||
<div class="map-container" ref="mapContainer">
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<el-icon class="loading-icon" :size="32"><Loading /></el-icon>
|
||||
<div class="loading-text">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="map-legend">
|
||||
<div v-for="city in Object.keys(cityColorPalette)" :key="city" class="legend-item">
|
||||
<div class="legend-color" :style="{ backgroundColor: cityColorPalette[city] }"></div>
|
||||
<span>{{ city }}</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background-color: #333333;"></div>
|
||||
<span>障碍区域</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 网格编辑/详情一体化对话框 -->
|
||||
<el-dialog
|
||||
v-model="gridEditDialogVisible"
|
||||
:title="editingGrid ? `编辑网格 (${editingGrid.gridX}, ${editingGrid.gridY})` : '网格详情'"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form v-if="editingGrid" :model="editFormData" label-width="120px">
|
||||
<el-descriptions :column="1" border class="detail-view" v-if="!isEditMode">
|
||||
<el-descriptions-item label="城市">{{ editingGrid.cityName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="区域">{{ editingGrid.districtName || '未指定' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="editingGrid.isObstacle ? 'danger' : currentWorkerInDialog ? 'success' : 'warning'" effect="dark">
|
||||
{{ editingGrid.isObstacle ? '障碍区域' : currentWorkerInDialog ? '已分配' : '未分配' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="描述">{{ editFormData.description || '无' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="网格员">
|
||||
<div v-if="currentWorkerInDialog">
|
||||
<strong>{{ currentWorkerInDialog.name }}</strong> (ID: {{ currentWorkerInDialog.id }})
|
||||
</div>
|
||||
<div v-else>未分配</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div v-if="isEditMode">
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="editFormData.description" type="textarea" :rows="3" placeholder="请输入网格描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否为障碍物">
|
||||
<el-switch v-model="editFormData.isObstacle" />
|
||||
</el-form-item>
|
||||
<div v-if="!editFormData.isObstacle">
|
||||
<el-divider>网格员分配</el-divider>
|
||||
<el-form-item label="分配网格员">
|
||||
<el-select v-model="editFormData.workerId" placeholder="选择或搜索网格员" clearable filterable style="width: 100%;">
|
||||
<el-option
|
||||
v-for="worker in availableGridWorkers"
|
||||
:key="worker.id"
|
||||
:label="`${worker.name} (ID: ${worker.id})`"
|
||||
:value="worker.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="isEditMode = !isEditMode">{{ isEditMode ? '返回详情' : '编辑网格' }}</el-button>
|
||||
<el-button @click="gridEditDialogVisible = false">关闭</el-button>
|
||||
<el-button v-if="isEditMode" type="primary" @click="handleUpdateGrid" :loading="loading">保存</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, computed } from 'vue';
|
||||
import {
|
||||
getGrids,
|
||||
getGridWorkers,
|
||||
updateGrid,
|
||||
assignGridWorkerByCoordinates,
|
||||
removeGridWorkerByCoordinates
|
||||
} from '@/api/grid';
|
||||
import type { GridData, GridWorker } from '@/api/grid';
|
||||
import {
|
||||
ElMessage,
|
||||
ElDialog,
|
||||
ElDescriptions,
|
||||
ElDescriptionsItem,
|
||||
ElButton,
|
||||
ElDivider,
|
||||
ElTag,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElSwitch,
|
||||
ElAlert
|
||||
} from 'element-plus';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
|
||||
const loading = ref(false);
|
||||
const selectedCity = ref<string>('北京市');
|
||||
const cities = ref<string[]>(['北京市']);
|
||||
const mapContainer = ref<HTMLDivElement | null>(null);
|
||||
const gridData = ref<GridData[]>([]);
|
||||
const gridWorkers = ref<GridWorker[]>([]);
|
||||
const availableGridWorkers = ref<GridWorker[]>([]);
|
||||
|
||||
const gridEditDialogVisible = ref(false);
|
||||
const editingGrid = ref<GridData | null>(null);
|
||||
const isEditMode = ref(false);
|
||||
|
||||
const editFormData = ref({
|
||||
description: '',
|
||||
isObstacle: false,
|
||||
workerId: null as number | string | null,
|
||||
});
|
||||
|
||||
const currentWorkerInDialog = computed(() => {
|
||||
if (!editingGrid.value) return null;
|
||||
return gridWorkers.value.find(w => w.gridX === editingGrid.value!.gridX && w.gridY === editingGrid.value!.gridY) || null;
|
||||
});
|
||||
|
||||
let gridSize = 20;
|
||||
const cityColorPalette: Record<string, string> = {};
|
||||
|
||||
const getCityColor = (cityName: string): string => {
|
||||
if (!cityColorPalette[cityName]) {
|
||||
const predefinedColors = ['#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ac'];
|
||||
const allCityNames = [...new Set(gridData.value.map(g => g.cityName))];
|
||||
const cityIndex = allCityNames.indexOf(cityName);
|
||||
cityColorPalette[cityName] = predefinedColors[cityIndex % predefinedColors.length];
|
||||
}
|
||||
return cityColorPalette[cityName];
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [grids, workers] = await Promise.all([
|
||||
getGrids(),
|
||||
getGridWorkers()
|
||||
]);
|
||||
|
||||
if (Array.isArray(grids)) {
|
||||
gridData.value = grids;
|
||||
const uniqueCities = [...new Set(grids.map(g => g.cityName))];
|
||||
if (uniqueCities.length > 0) {
|
||||
cities.value = uniqueCities;
|
||||
selectedCity.value = uniqueCities[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(workers)) {
|
||||
gridWorkers.value = workers;
|
||||
}
|
||||
|
||||
renderMap();
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
ElMessage.error('加载数据失败,请检查后端服务');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAvailableGridWorkers = async () => {
|
||||
try {
|
||||
const response = await getGridWorkers({ unassigned: true });
|
||||
availableGridWorkers.value = Array.isArray(response) ? response : [];
|
||||
} catch (error) {
|
||||
console.error('获取可用网格员数据失败:', error);
|
||||
ElMessage.error('获取可用网格员列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
const renderMap = () => {
|
||||
const container = mapContainer.value;
|
||||
if (!container || !gridData.value || gridData.value.length === 0) {
|
||||
if (container) container.innerHTML = '<div class="empty-state">没有可显示的网格数据</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
const allX = gridData.value.map(g => g.gridX);
|
||||
const allY = gridData.value.map(g => g.gridY);
|
||||
const minX = Math.min(...allX);
|
||||
const maxX = Math.max(...allX);
|
||||
const minY = Math.min(...allY);
|
||||
const maxY = Math.max(...allY);
|
||||
const worldWidth = maxX - minX + 1;
|
||||
const worldHeight = maxY - minY + 1;
|
||||
gridSize = Math.max(5, Math.floor(Math.min(container.clientWidth / worldWidth, container.clientHeight / worldHeight)));
|
||||
const svgWidth = worldWidth * gridSize;
|
||||
const svgHeight = worldHeight * gridSize;
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('width', `${svgWidth}px`);
|
||||
svg.setAttribute('height', `${svgHeight}px`);
|
||||
svg.setAttribute('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
|
||||
|
||||
gridData.value.forEach(grid => {
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||
rect.setAttribute('x', `${(grid.gridX - minX) * gridSize}`);
|
||||
rect.setAttribute('y', `${(grid.gridY - minY) * gridSize}`);
|
||||
rect.setAttribute('width', `${gridSize}`);
|
||||
rect.setAttribute('height', `${gridSize}`);
|
||||
rect.setAttribute('fill', grid.isObstacle ? '#333333' : getCityColor(grid.cityName));
|
||||
rect.setAttribute('stroke-width', '0.5');
|
||||
rect.setAttribute('stroke', '#fff');
|
||||
rect.setAttribute('cursor', 'pointer');
|
||||
rect.addEventListener('click', () => showGridDialog(grid));
|
||||
svg.appendChild(rect);
|
||||
});
|
||||
container.appendChild(svg);
|
||||
};
|
||||
|
||||
const showGridDialog = async (grid: GridData) => {
|
||||
isEditMode.value = false;
|
||||
editingGrid.value = grid;
|
||||
const currentWorker = gridWorkers.value.find(w => w.gridX === grid.gridX && w.gridY === grid.gridY);
|
||||
|
||||
editFormData.value = {
|
||||
description: grid.description || '',
|
||||
isObstacle: grid.isObstacle,
|
||||
workerId: currentWorker ? currentWorker.id : '',
|
||||
};
|
||||
|
||||
await fetchAvailableGridWorkers();
|
||||
gridEditDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleUpdateGrid = async () => {
|
||||
if (!editingGrid.value) return;
|
||||
|
||||
loading.value = true;
|
||||
const grid = editingGrid.value;
|
||||
const { isObstacle, description, workerId } = editFormData.value;
|
||||
const newWorkerId = workerId ? Number(workerId) : null;
|
||||
|
||||
try {
|
||||
await updateGrid({ ...grid, isObstacle, description });
|
||||
|
||||
const workerAssignmentChanged = (currentWorkerInDialog.value?.id ?? null) !== newWorkerId;
|
||||
|
||||
if (isObstacle && currentWorkerInDialog.value) {
|
||||
await removeGridWorkerByCoordinates(grid.gridX, grid.gridY);
|
||||
} else if (!isObstacle && workerAssignmentChanged) {
|
||||
if (newWorkerId) {
|
||||
await assignGridWorkerByCoordinates(grid.gridX, grid.gridY, newWorkerId);
|
||||
} else if (currentWorkerInDialog.value) {
|
||||
await removeGridWorkerByCoordinates(grid.gridX, grid.gridY);
|
||||
}
|
||||
}
|
||||
ElMessage.success('网格信息更新成功');
|
||||
gridEditDialogVisible.value = false;
|
||||
await loadData();
|
||||
} catch (error: any) {
|
||||
ElMessage.error('更新网格失败: ' + (error?.response?.data?.message || error.message));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshData = () => loadData();
|
||||
const handleCityChange = () => { /* Future implementation for city-specific view */ };
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
window.addEventListener('resize', renderMap);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', renderMap);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.grid-map-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.map-header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.map-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.map-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
overflow: auto;
|
||||
}
|
||||
.map-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
.legend-item { display: flex; align-items: center; }
|
||||
.legend-color { width: 16px; height: 16px; margin-right: 8px; border-radius: 4px; }
|
||||
.dialog-footer { text-align: right; }
|
||||
.detail-view { margin-bottom: 20px; }
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
title="提交任务进度"
|
||||
width="50%"
|
||||
@update:model-value="$emit('update:visible', $event)"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" label-width="80px">
|
||||
<el-form-item label="进度说明" prop="comments">
|
||||
<el-input
|
||||
v-model="form.comments"
|
||||
type="textarea"
|
||||
rows="4"
|
||||
placeholder="请输入详细的进度说明..."
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="上传图片" prop="files">
|
||||
<el-upload
|
||||
v-model:file-list="form.files"
|
||||
action="#"
|
||||
list-type="picture-card"
|
||||
:auto-upload="false"
|
||||
multiple
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="$emit('update:visible', false)">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="loading">
|
||||
提交
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import type { FormInstance, UploadUserFile } from 'element-plus';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import apiClient from '@/api';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
taskId: number | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:visible', 'submitted']);
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const loading = ref(false);
|
||||
const form = reactive({
|
||||
comments: '',
|
||||
files: [] as UploadUserFile[],
|
||||
});
|
||||
|
||||
watch(() => props.visible, (newValue) => {
|
||||
if (newValue) {
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!props.taskId) {
|
||||
ElMessage.error('任务ID无效');
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('comments', form.comments);
|
||||
form.files.forEach(file => {
|
||||
if (file.raw) {
|
||||
formData.append('files', file.raw);
|
||||
}
|
||||
});
|
||||
|
||||
await apiClient.post(`/worker/${props.taskId}/submit`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
|
||||
ElMessage.success('进度提交成功');
|
||||
emit('submitted');
|
||||
emit('update:visible', false);
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
ElMessage.error('提交失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @file Pollution related constants, aligned with backend enums.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents the types of pollution, consistent with the `PollutionType` enum in the backend.
|
||||
* This ensures that frontend displays and data submissions are aligned with backend expectations.
|
||||
*/
|
||||
export const POLLUTION_TYPES = ['PM25', 'O3', 'NO2', 'SO2', 'OTHER'] as const;
|
||||
|
||||
/**
|
||||
* Type definition for a single pollution type.
|
||||
* Ensures type safety when working with pollution constants.
|
||||
*/
|
||||
export type PollutionType = typeof POLLUTION_TYPES[number];
|
||||
|
||||
/**
|
||||
* A map to provide human-readable names for pollution types.
|
||||
* Useful for displaying in UI components like chart legends or labels.
|
||||
*/
|
||||
export const POLLUTION_TYPE_MAP: Record<PollutionType, string> = {
|
||||
PM25: 'PM2.5',
|
||||
O3: 'O₃',
|
||||
NO2: 'NO₂',
|
||||
SO2: 'SO₂',
|
||||
OTHER: '其他',
|
||||
};
|
||||
428
ems-frontend/ems-monitoring-system/src/layouts/MainLayout.vue
Normal file
428
ems-frontend/ems-monitoring-system/src/layouts/MainLayout.vue
Normal file
@@ -0,0 +1,428 @@
|
||||
<template>
|
||||
<el-container class="layout-container" :class="layoutClass">
|
||||
<el-header class="layout-header">
|
||||
<div class="header-title">环境监督系统</div>
|
||||
<div class="header-right">
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span class="el-dropdown-link">
|
||||
{{ userInfoDisplay }}
|
||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-container class="main-body-container">
|
||||
<el-aside width="220px" class="layout-aside">
|
||||
<el-menu
|
||||
:default-active="$route.path"
|
||||
class="el-menu-vertical"
|
||||
@select="handleMenuSelect"
|
||||
>
|
||||
<template v-for="menu in visibleMenu" :key="menu.path">
|
||||
<el-sub-menu v-if="menu.children && menu.children.length > 0" :index="menu.path">
|
||||
<template #title>
|
||||
<el-icon><component :is="menu.meta.icon" /></el-icon>
|
||||
<span>{{ menu.meta.title }}</span>
|
||||
</template>
|
||||
<el-menu-item v-for="child in menu.children" :key="child.path" :index="child.path">
|
||||
{{ child.meta.title }}
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-menu-item v-else :index="menu.path">
|
||||
<el-icon><component :is="menu.meta.icon" /></el-icon>
|
||||
<span>{{ menu.meta.title }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-main class="layout-main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade-transform" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
HomeFilled,
|
||||
InfoFilled,
|
||||
ArrowDown,
|
||||
Location,
|
||||
List,
|
||||
ChatDotSquare,
|
||||
Setting,
|
||||
User,
|
||||
DataAnalysis,
|
||||
Grid,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { computed, ref, markRaw, watch } from 'vue';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const userRole = computed(() => authStore.user?.role);
|
||||
|
||||
const layoutClass = computed(() => {
|
||||
if (userRole.value) {
|
||||
// a simple convention would be `role-<role-name-in-lowercase>`
|
||||
return `role-${userRole.value.toLowerCase()}`;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
(newPath, oldPath) => {
|
||||
console.log(`路由从 ${oldPath} 切换到 ${newPath}`);
|
||||
}
|
||||
);
|
||||
|
||||
const allMenus = ref([
|
||||
{
|
||||
path: '/dashboard',
|
||||
meta: { title: '仪表盘', icon: markRaw(DataAnalysis), roles: ['DECISION_MAKER', 'ADMIN'] }
|
||||
},
|
||||
{
|
||||
path: '/map',
|
||||
meta: { title: '网格地图', icon: markRaw(Grid), roles: ['ADMIN', 'DECISION_MAKER', 'SUPERVISOR', 'GRID_WORKER'] }
|
||||
},
|
||||
{
|
||||
path: '/my-tasks',
|
||||
meta: { title: '我的任务', icon: markRaw(List), roles: ['GRID_WORKER'] }
|
||||
},
|
||||
{
|
||||
path: '/worker-map',
|
||||
meta: { title: '地图管理', icon: markRaw(Grid), roles: ['GRID_WORKER'] }
|
||||
},
|
||||
{
|
||||
path: '/submit-feedback',
|
||||
meta: { title: '提交反馈', icon: markRaw(ChatDotSquare), roles: ['PUBLIC_SUPERVISOR'] }
|
||||
},
|
||||
{
|
||||
path: '/feedback',
|
||||
meta: { title: '我的反馈', icon: markRaw(List), roles: ['PUBLIC_SUPERVISOR'] }
|
||||
},
|
||||
{
|
||||
path: '/feedback',
|
||||
meta: { title: '反馈管理', icon: markRaw(ChatDotSquare), roles: ['ADMIN', 'SUPERVISOR'] }
|
||||
},
|
||||
{
|
||||
path: '/system',
|
||||
meta: { title: '系统管理', icon: markRaw(Setting), roles: ['ADMIN'] },
|
||||
children: [
|
||||
{ path: '/system/users', meta: { title: '人员管理', roles: ['ADMIN'] } },
|
||||
{ path: '/system/roles', meta: { title: '操作日志', roles: ['ADMIN'] } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
meta: { title: '设置', icon: markRaw(User), roles: ['ADMIN', 'DECISION_MAKER', 'SUPERVISOR', 'GRID_WORKER', 'PUBLIC_SUPERVISOR'] },
|
||||
children: [
|
||||
{ path: '/settings/profile', meta: { title: '个人中心', roles: ['ADMIN', 'DECISION_MAKER', 'SUPERVISOR', 'GRID_WORKER', 'PUBLIC_SUPERVISOR'] } },
|
||||
{ path: '/settings/my-logs', meta: { title: '我的操作日志', roles: ['ADMIN', 'DECISION_MAKER', 'SUPERVISOR', 'GRID_WORKER', 'PUBLIC_SUPERVISOR'] } },
|
||||
{ path: '/settings/system', meta: { title: '系统设置', roles: ['ADMIN'] } },
|
||||
]
|
||||
},
|
||||
]);
|
||||
|
||||
const visibleMenu = computed(() => {
|
||||
if (!userRole.value) {
|
||||
return [];
|
||||
}
|
||||
const role = userRole.value;
|
||||
|
||||
function filterMenu(menuItems: any[]) {
|
||||
const accessibleMenu: any[] = [];
|
||||
for (const item of menuItems) {
|
||||
if (item.meta && item.meta.roles && item.meta.roles.includes(role)) {
|
||||
const newItem = { ...item };
|
||||
if (item.children) {
|
||||
newItem.children = filterMenu(item.children);
|
||||
if (newItem.children.length > 0 || newItem.path === '/settings') {
|
||||
accessibleMenu.push(newItem);
|
||||
}
|
||||
} else {
|
||||
accessibleMenu.push(newItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
return accessibleMenu;
|
||||
}
|
||||
|
||||
return filterMenu(allMenus.value);
|
||||
});
|
||||
|
||||
const userInfoDisplay = computed(() => {
|
||||
if (authStore.user) {
|
||||
return `${authStore.user.name} (${authStore.user.role})`;
|
||||
}
|
||||
return '未登录';
|
||||
});
|
||||
|
||||
const handleCommand = (command: string | number | object) => {
|
||||
if (command === 'logout') {
|
||||
authStore.logout();
|
||||
router.push('/login');
|
||||
}
|
||||
}
|
||||
|
||||
const handleMenuSelect = (path: string) => {
|
||||
console.log('菜单选择:', path);
|
||||
if (route.path === '/grid-management' && path !== '/grid-management') {
|
||||
console.log('从网格管理页面切换到其他页面,使用location.href');
|
||||
window.location.href = path;
|
||||
} else {
|
||||
router.push(path);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* The background color will be set by role-specific styles */
|
||||
}
|
||||
|
||||
.layout-header {
|
||||
/* background: #ffffff; */ /* Will be set by role-specific styles */
|
||||
color: #303133;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 22px;
|
||||
box-shadow: 0 1px 4px rgba(0,21,41,.08);
|
||||
z-index: 10;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
/* color: #004d40; */ /* Will be set by role-specific styles */
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-dropdown-link {
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.main-body-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout-aside {
|
||||
/* background-color: #ffffff; */ /* Will be set by role-specific styles */
|
||||
box-shadow: 2px 0 6px rgba(0,21,41,.08);
|
||||
transition: width 0.3s;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.el-menu-item, .el-sub-menu__title {
|
||||
/* color: #303133; */ /* Will be set by role-specific styles */
|
||||
font-size: 15px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
.el-menu-item:hover, .el-sub-menu__title:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
/* background-color: #ecf5ff; */ /* Will be set by role-specific styles */
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
padding: 20px;
|
||||
background-color: #f0f2f5;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* fade-transform */
|
||||
.fade-transform-leave-active,
|
||||
.fade-transform-enter-active {
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
.fade-transform-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.fade-transform-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Role-based theming */
|
||||
|
||||
/* ADMIN THEME (Dark Green) */
|
||||
.role-admin .layout-header {
|
||||
background-color: #2E7D32; /* Dark Green */
|
||||
color: white;
|
||||
}
|
||||
.role-admin .header-title {
|
||||
color: white;
|
||||
}
|
||||
.role-admin .el-dropdown-link {
|
||||
color: white;
|
||||
}
|
||||
.role-admin .layout-aside {
|
||||
background-color: #388E3C; /* Slightly lighter green */
|
||||
}
|
||||
.role-admin .el-menu-item,
|
||||
.role-admin .el-sub-menu__title {
|
||||
color: #E8F5E9; /* Muted green text */
|
||||
}
|
||||
.role-admin .el-menu-item:hover,
|
||||
.role-admin .el-sub-menu__title:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
.role-admin .el-menu-item.is-active {
|
||||
background-color: #2E7D32; /* Dark Green */
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
/* DECISION_MAKER THEME (Corporate Green) */
|
||||
.role-decision_maker .layout-header {
|
||||
background-color: #00695C; /* Teal/Corporate Green */
|
||||
color: white;
|
||||
}
|
||||
.role-decision_maker .header-title {
|
||||
color: white;
|
||||
}
|
||||
.role-decision_maker .el-dropdown-link {
|
||||
color: white;
|
||||
}
|
||||
.role-decision_maker .layout-aside {
|
||||
background-color: #00796B;
|
||||
}
|
||||
.role-decision_maker .el-menu-item,
|
||||
.role-decision_maker .el-sub-menu__title {
|
||||
color: #B2DFDB;
|
||||
}
|
||||
.role-decision_maker .el-menu-item:hover,
|
||||
.role-decision_maker .el-sub-menu__title:hover {
|
||||
background-color: #00897b;
|
||||
}
|
||||
.role-decision_maker .el-menu-item.is-active {
|
||||
background-color: #00695C;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
/* GRID_WORKER THEME (Vibrant Green) */
|
||||
.role-grid_worker .layout-header {
|
||||
background-color: #689F38; /* Vibrant Green */
|
||||
color: white;
|
||||
}
|
||||
.role-grid_worker .header-title {
|
||||
color: white;
|
||||
}
|
||||
.role-grid_worker .el-dropdown-link {
|
||||
color: white;
|
||||
}
|
||||
.role-grid_worker .layout-aside {
|
||||
background-color: #7CB342;
|
||||
}
|
||||
.role-grid_worker .el-menu-item,
|
||||
.role-grid_worker .el-sub-menu__title {
|
||||
color: #F1F8E9;
|
||||
}
|
||||
.role-grid_worker .el-menu-item:hover,
|
||||
.role-grid_worker .el-sub-menu__title:hover {
|
||||
background-color: #8bc34a;
|
||||
}
|
||||
.role-grid_worker .el-menu-item.is-active {
|
||||
background-color: #689F38;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* SUPERVISOR THEME (Calming Green) */
|
||||
.role-supervisor .layout-header {
|
||||
background-color: #39796b; /* Calming Green */
|
||||
color: white;
|
||||
}
|
||||
.role-supervisor .header-title {
|
||||
color: white;
|
||||
}
|
||||
.role-supervisor .el-dropdown-link {
|
||||
color: white;
|
||||
}
|
||||
.role-supervisor .layout-aside {
|
||||
background-color: #4DB6AC;
|
||||
}
|
||||
.role-supervisor .el-menu-item,
|
||||
.role-supervisor .el-sub-menu__title {
|
||||
color: #E0F2F1;
|
||||
}
|
||||
.role-supervisor .el-menu-item:hover,
|
||||
.role-supervisor .el-sub-menu__title:hover {
|
||||
background-color: #26a69a;
|
||||
}
|
||||
.role-supervisor .el-menu-item.is-active {
|
||||
background-color: #39796b;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* PUBLIC_SUPERVISOR THEME (Light Green) */
|
||||
.role-public_supervisor .layout-header {
|
||||
background-color: #AED581; /* Light Green */
|
||||
color: #333;
|
||||
}
|
||||
.role-public_supervisor .header-title {
|
||||
color: #333;
|
||||
}
|
||||
.role-public_supervisor .el-dropdown-link {
|
||||
color: #333;
|
||||
}
|
||||
.role-public_supervisor .layout-aside {
|
||||
background-color: #DCEDC8;
|
||||
}
|
||||
.role-public_supervisor .el-menu-item,
|
||||
.role-public_supervisor .el-sub-menu__title {
|
||||
color: #556B2F;
|
||||
}
|
||||
.role-public_supervisor .el-menu-item:hover,
|
||||
.role-public_supervisor .el-sub-menu__title:hover {
|
||||
background-color: #9ccc65;
|
||||
}
|
||||
.role-public_supervisor .el-menu-item.is-active {
|
||||
background-color: #AED581;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
31
ems-frontend/ems-monitoring-system/src/main.ts
Normal file
31
ems-frontend/ems-monitoring-system/src/main.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import './assets/main.css'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import 'animate.css'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(ElementPlus, {
|
||||
locale: zhCn,
|
||||
zIndex: 3000,
|
||||
popperOptions: {
|
||||
modifiers: [
|
||||
{
|
||||
name: 'computeStyles',
|
||||
options: {
|
||||
gpuAcceleration: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
app.mount('#app')
|
||||
163
ems-frontend/ems-monitoring-system/src/router/index.ts
Normal file
163
ems-frontend/ems-monitoring-system/src/router/index.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import MainLayout from '../layouts/MainLayout.vue'
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('../views/LoginView.vue')
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'register',
|
||||
component: () => import('../views/RegisterView.vue')
|
||||
},
|
||||
{
|
||||
path: '/forgot-password',
|
||||
name: 'forgot-password',
|
||||
component: () => import('../views/ForgotPasswordView.vue')
|
||||
},
|
||||
{
|
||||
path: '/reset-password',
|
||||
name: 'reset-password',
|
||||
component: () => import('../views/ResetPasswordView.vue')
|
||||
},
|
||||
{
|
||||
path: '/public-feedback',
|
||||
name: 'public-feedback',
|
||||
component: () => import('../views/PublicFeedbackSubmitView.vue')
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: MainLayout,
|
||||
redirect: '/dashboard',
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'dashboard',
|
||||
component: () => import('../views/HomeView.vue'),
|
||||
meta: { roles: ['ADMIN', 'DECISION_MAKER', 'SUPERVISOR'] }
|
||||
},
|
||||
{
|
||||
path: 'map',
|
||||
name: 'map',
|
||||
component: () => import('../views/GridManagementView.vue'),
|
||||
meta: { roles: ['ADMIN', 'DECISION_MAKER', 'SUPERVISOR', 'GRID_WORKER'] }
|
||||
},
|
||||
{
|
||||
path: 'tasks',
|
||||
name: 'tasks',
|
||||
component: () => import('../views/TaskView.vue'),
|
||||
meta: { roles: ['ADMIN', 'SUPERVISOR'] }
|
||||
},
|
||||
{
|
||||
path: 'my-tasks',
|
||||
name: 'my-tasks',
|
||||
component: () => import('../views/MyTasksView.vue'),
|
||||
meta: { roles: ['GRID_WORKER'] }
|
||||
},
|
||||
{
|
||||
path: 'worker-map',
|
||||
name: 'worker-map',
|
||||
component: () => import('../views/WorkerMapView.vue'),
|
||||
meta: { requiresAuth: true, roles: ['GRID_WORKER'] }
|
||||
},
|
||||
{
|
||||
path: 'my-tasks/:id',
|
||||
name: 'my-task-detail',
|
||||
component: () => import('../views/TaskDetailView.vue'),
|
||||
props: true,
|
||||
meta: { roles: ['GRID_WORKER'] }
|
||||
},
|
||||
{
|
||||
path: 'feedback',
|
||||
name: 'feedback',
|
||||
component: () => import('../views/FeedbackView.vue'),
|
||||
meta: { roles: ['PUBLIC_SUPERVISOR', 'ADMIN', 'SUPERVISOR'] }
|
||||
},
|
||||
{
|
||||
path: 'submit-feedback',
|
||||
name: 'submit-feedback',
|
||||
component: () => import('../views/SubmitFeedbackView.vue'),
|
||||
meta: { roles: ['PUBLIC_SUPERVISOR'] }
|
||||
},
|
||||
{
|
||||
path: 'system/users',
|
||||
name: 'system-users',
|
||||
component: () => import('../views/UserManagementView.vue'),
|
||||
meta: { roles: ['ADMIN'] }
|
||||
},
|
||||
{
|
||||
path: 'system/roles',
|
||||
name: 'system-roles',
|
||||
component: () => import('../views/OperationLogView.vue'),
|
||||
meta: { roles: ['ADMIN'] }
|
||||
},
|
||||
{
|
||||
path: 'settings/system',
|
||||
name: 'settings-system',
|
||||
component: () => import('../views/SystemSettingsView.vue'),
|
||||
meta: { roles: ['ADMIN'] }
|
||||
},
|
||||
{
|
||||
path: 'settings/profile',
|
||||
name: 'settings-profile',
|
||||
component: () => import('../views/UserProfileView.vue'),
|
||||
meta: { roles: ['ADMIN', 'DECISION_MAKER', 'SUPERVISOR', 'GRID_WORKER', 'PUBLIC_SUPERVISOR'] }
|
||||
},
|
||||
{
|
||||
path: 'settings/my-logs',
|
||||
name: 'settings-my-logs',
|
||||
component: () => import('../views/MyOperationLogView.vue'),
|
||||
meta: { roles: ['ADMIN', 'DECISION_MAKER', 'SUPERVISOR', 'GRID_WORKER', 'PUBLIC_SUPERVISOR'] }
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
name: 'about',
|
||||
component: () => import('../views/AboutView.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const publicPages = ['/login', '/register', '/forgot-password', '/reset-password', '/public-feedback'];
|
||||
const authRequired = !publicPages.includes(to.path);
|
||||
const authStore = useAuthStore();
|
||||
|
||||
if (authRequired && !authStore.isLoggedIn) {
|
||||
return next('/login');
|
||||
}
|
||||
|
||||
// 检查路由是否需要特定角色
|
||||
if (to.meta.roles) {
|
||||
const userRole = authStore.user?.role;
|
||||
if (userRole && (to.meta.roles as string[]).includes(userRole)) {
|
||||
next();
|
||||
} else {
|
||||
// 如果用户角色不被允许,可以重定向到 403 页面或主页
|
||||
// 这里我们简单地重定向到用户各自的主页
|
||||
if(userRole) {
|
||||
switch (userRole) {
|
||||
case 'GRID_WORKER':
|
||||
return next('/my-tasks');
|
||||
case 'PUBLIC_SUPERVISOR':
|
||||
return next('/submit-feedback');
|
||||
default:
|
||||
return next('/dashboard');
|
||||
}
|
||||
}
|
||||
return next('/login'); // 如果没有角色信息,则返回登录页
|
||||
}
|
||||
} else {
|
||||
// 如果路由没有 meta.roles,则允许所有已登录用户访问
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
export default router
|
||||
5
ems-frontend/ems-monitoring-system/src/shims-vue.d.ts
vendored
Normal file
5
ems-frontend/ems-monitoring-system/src/shims-vue.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
124
ems-frontend/ems-monitoring-system/src/store/feedback.ts
Normal file
124
ems-frontend/ems-monitoring-system/src/store/feedback.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import {
|
||||
getFeedbackList,
|
||||
getFeedbackDetail,
|
||||
processFeedback,
|
||||
assignFeedback,
|
||||
resolveFeedback,
|
||||
closeFeedback,
|
||||
rejectFeedback,
|
||||
confirmFeedback,
|
||||
getFeedbackStats,
|
||||
submitFeedback,
|
||||
FeedbackStatus
|
||||
} from '@/api/feedback';
|
||||
import type {
|
||||
FeedbackResponse,
|
||||
FeedbackDetail,
|
||||
FeedbackFilters,
|
||||
Page
|
||||
} from '@/api/feedback';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
|
||||
interface FeedbackState {
|
||||
feedbackList: FeedbackResponse[];
|
||||
total: number;
|
||||
loading: boolean;
|
||||
currentFeedbackDetail: FeedbackDetail | null;
|
||||
detailLoading: boolean;
|
||||
stats: {
|
||||
total: number;
|
||||
pending: number;
|
||||
confirmed: number;
|
||||
assigned: number;
|
||||
inProgress: number;
|
||||
resolved: number;
|
||||
closed: number;
|
||||
rejected: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const useFeedbackStore = defineStore('feedback', {
|
||||
state: (): FeedbackState => ({
|
||||
feedbackList: [],
|
||||
total: 0,
|
||||
loading: false,
|
||||
currentFeedbackDetail: null,
|
||||
detailLoading: false,
|
||||
stats: {
|
||||
total: 0,
|
||||
pending: 0,
|
||||
confirmed: 0,
|
||||
assigned: 0,
|
||||
inProgress: 0,
|
||||
resolved: 0,
|
||||
closed: 0,
|
||||
rejected: 0
|
||||
}
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async fetchFeedbackList(params: FeedbackFilters) {
|
||||
this.loading = true;
|
||||
const authStore = useAuthStore();
|
||||
const user = authStore.user;
|
||||
|
||||
let apiParams: FeedbackFilters = { ...params };
|
||||
|
||||
if (user && user.role === 'PUBLIC_SUPERVISOR') {
|
||||
apiParams.submitterId = user.id;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await getFeedbackList(apiParams);
|
||||
this.feedbackList = response.content;
|
||||
this.total = response.totalElements;
|
||||
} catch (error) {
|
||||
console.error('获取反馈列表失败:', error);
|
||||
ElMessage.error('获取反馈列表失败,请检查网络或联系管理员');
|
||||
this.feedbackList = [];
|
||||
this.total = 0;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchFeedbackDetail(id: number) {
|
||||
this.detailLoading = true;
|
||||
try {
|
||||
this.currentFeedbackDetail = await getFeedbackDetail(id);
|
||||
} catch (error) {
|
||||
console.error('获取反馈详情失败:', error);
|
||||
ElMessage.error('无法加载反馈详情');
|
||||
this.currentFeedbackDetail = null;
|
||||
} finally {
|
||||
this.detailLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchFeedbackStats() {
|
||||
try {
|
||||
const statsData = await getFeedbackStats();
|
||||
this.stats = { ...this.stats, ...statsData };
|
||||
} catch (error) {
|
||||
console.error('获取反馈统计数据API错误:', error);
|
||||
ElMessage.error('获取反馈统计数据失败');
|
||||
}
|
||||
},
|
||||
|
||||
async submitFeedback(formData: FormData) {
|
||||
try {
|
||||
await submitFeedback(formData);
|
||||
this.fetchFeedbackStats();
|
||||
} catch (error) {
|
||||
console.error('提交反馈失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 可以在这里添加更多处理反馈的 actions, 例如:
|
||||
// async process(id: number, data: ProcessFeedbackRequest) { ... }
|
||||
// async assign(id: number, data: AssignFeedbackRequest) { ... }
|
||||
}
|
||||
});
|
||||
127
ems-frontend/ems-monitoring-system/src/stores/auth.ts
Normal file
127
ems-frontend/ems-monitoring-system/src/stores/auth.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import type { LoginRequest, User } from '@/api/types';
|
||||
import apiClient from '@/api';
|
||||
import { loginWithFetch, logout as apiLogout } from '@/api/auth';
|
||||
import router from '@/router'; // 导入 Vue Router 实例
|
||||
|
||||
// 定义从JWT解码出的负载的类型, 它继承自 User 类型
|
||||
interface JwtPayload extends User {
|
||||
iat: number; // Issued At
|
||||
exp: number; // Expiration Time
|
||||
sub: string; // Subject, typically the user's email or ID
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const token = ref<string | null>(localStorage.getItem('token'));
|
||||
const user = ref<User | null>(null);
|
||||
|
||||
/**
|
||||
* 从 token 解码并设置 user state
|
||||
* @param tokenValue - JWT token string
|
||||
*/
|
||||
function setUserFromToken(tokenValue: string) {
|
||||
try {
|
||||
const decoded = jwtDecode<JwtPayload>(tokenValue);
|
||||
// 解码出的信息直接赋值给 user state
|
||||
user.value = decoded;
|
||||
} catch (error) {
|
||||
console.error('Failed to decode token or token is invalid:', error);
|
||||
// 解码失败时, 清理状态
|
||||
user.value = null;
|
||||
token.value = null;
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时,如果localStorage中存在 token,则从中恢复用户信息
|
||||
if (token.value) {
|
||||
setUserFromToken(token.value);
|
||||
}
|
||||
|
||||
const isLoggedIn = computed(() => !!token.value && !!user.value);
|
||||
|
||||
/**
|
||||
* 设置认证信息
|
||||
* @param newToken - JWT
|
||||
*/
|
||||
function setAuth(newToken: string) {
|
||||
token.value = newToken;
|
||||
localStorage.setItem('token', newToken);
|
||||
setUserFromToken(newToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
* @param credentials - 登录凭据
|
||||
*/
|
||||
async function login(credentials: LoginRequest) {
|
||||
try {
|
||||
const response = await loginWithFetch(credentials);
|
||||
const newToken = response.accessToken;
|
||||
|
||||
if (!newToken) {
|
||||
throw new Error('accessToken not found in login response');
|
||||
}
|
||||
|
||||
setAuth(newToken);
|
||||
|
||||
// 登录成功后,根据用户角色进行重定向
|
||||
if (user.value) {
|
||||
switch (user.value.role) {
|
||||
case 'GRID_WORKER':
|
||||
router.push('/my-tasks');
|
||||
break;
|
||||
case 'ADMIN':
|
||||
case 'DECISION_MAKER':
|
||||
router.push('/dashboard');
|
||||
break;
|
||||
case 'SUPERVISOR':
|
||||
case 'PUBLIC_SUPERVISOR':
|
||||
router.push('/feedback');
|
||||
break;
|
||||
default:
|
||||
// 对于其他未知角色,可以重定向到登录页或一个通用的欢迎页
|
||||
router.push('/login');
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// 如果用户信息未设置,则默认重定向到主页
|
||||
router.push('/');
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Login process failed:', error);
|
||||
delete apiClient.defaults.headers.common['Authorization'];
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
async function logout() {
|
||||
try {
|
||||
// 调用后端的登出接口以记录日志
|
||||
await apiLogout();
|
||||
} catch (error) {
|
||||
console.error('Failed to call logout API, but proceeding with client-side logout:', error);
|
||||
} finally {
|
||||
// 无论后端调用是否成功,都清理前端状态
|
||||
user.value = null;
|
||||
token.value = null;
|
||||
localStorage.removeItem('token');
|
||||
// 登出后重定向到登录页
|
||||
router.push('/login');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
user,
|
||||
isLoggedIn,
|
||||
login,
|
||||
logout,
|
||||
};
|
||||
});
|
||||
12
ems-frontend/ems-monitoring-system/src/stores/counter.ts
Normal file
12
ems-frontend/ems-monitoring-system/src/stores/counter.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
||||
20
ems-frontend/ems-monitoring-system/src/utils/formatter.ts
Normal file
20
ems-frontend/ems-monitoring-system/src/utils/formatter.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { format } from 'date-fns';
|
||||
|
||||
/**
|
||||
* Formats a date-time string or Date object into a standardized "yyyy-MM-dd HH:mm:ss" format.
|
||||
* If the input is invalid or null, it returns an empty string.
|
||||
*
|
||||
* @param dateTime The date-time to format (string, Date, or null/undefined).
|
||||
* @returns The formatted date-time string or an empty string.
|
||||
*/
|
||||
export function formatDateTime(dateTime: string | Date | null | undefined): string {
|
||||
if (!dateTime) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return format(new Date(dateTime), 'yyyy-MM-dd HH:mm:ss');
|
||||
} catch (error) {
|
||||
console.error('Error formatting date:', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
12
ems-frontend/ems-monitoring-system/src/views/AboutView.vue
Normal file
12
ems-frontend/ems-monitoring-system/src/views/AboutView.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>关于页面</h1>
|
||||
<p>这是一个使用 Vue 3 和 Element Plus 构建的环境监督系统。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
459
ems-frontend/ems-monitoring-system/src/views/FeedbackView.vue
Normal file
459
ems-frontend/ems-monitoring-system/src/views/FeedbackView.vue
Normal file
@@ -0,0 +1,459 @@
|
||||
<template>
|
||||
<div class="feedback-management-page">
|
||||
<!-- 统计面板 -->
|
||||
<el-row :gutter="20" class="stats-row">
|
||||
<el-col :span="4">
|
||||
<el-card shadow="hover" class="stats-card">
|
||||
<div class="stats-item">
|
||||
<div class="stats-value">{{ stats.total }}</div>
|
||||
<div class="stats-label">总反馈</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-card shadow="hover" class="stats-card pending">
|
||||
<div class="stats-item">
|
||||
<div class="stats-value">{{ stats.pending }}</div>
|
||||
<div class="stats-label">待处理</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-card shadow="hover" class="stats-card confirmed">
|
||||
<div class="stats-item">
|
||||
<div class="stats-value">{{ stats.confirmed }}</div>
|
||||
<div class="stats-label">已确认</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-card shadow="hover" class="stats-card assigned">
|
||||
<div class="stats-item">
|
||||
<div class="stats-value">{{ stats.assigned }}</div>
|
||||
<div class="stats-label">已分配</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-card shadow="hover" class="stats-card resolved">
|
||||
<div class="stats-item">
|
||||
<div class="stats-value">{{ stats.resolved }}</div>
|
||||
<div class="stats-label">已解决</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-card shadow="hover" class="stats-card rejected">
|
||||
<div class="stats-item">
|
||||
<div class="stats-value">{{ stats.rejected }}</div>
|
||||
<div class="stats-label">已拒绝</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card class="page-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>反馈管理</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 筛选区域 -->
|
||||
<div class="filter-container">
|
||||
<el-form :inline="true" :model="filters" class="filter-form">
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" placeholder="请选择状态" clearable>
|
||||
<el-option
|
||||
v-for="item in filterOptions.status"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="污染类型">
|
||||
<el-select v-model="filters.pollutionType" placeholder="请选择污染类型" clearable>
|
||||
<el-option
|
||||
v-for="item in filterOptions.pollutionType"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="严重程度">
|
||||
<el-select
|
||||
v-model="filters.severityLevel"
|
||||
placeholder="所有程度"
|
||||
clearable
|
||||
:popper-append-to-body="false"
|
||||
>
|
||||
<template #default>
|
||||
<el-option
|
||||
v-for="(text, key) in severityLevelMap"
|
||||
:key="key"
|
||||
:label="text"
|
||||
:value="key"
|
||||
/>
|
||||
</template>
|
||||
<template #prefix>{{ selectedSeverityLabel }}</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="提交时间">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="filters.keyword" placeholder="标题、描述、报告人" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 反馈列表 -->
|
||||
<el-table :data="feedbackList" v-loading="loading" stripe class="feedback-table">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="title" label="标题" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="statusTagMap[scope.row.status] || 'info'">
|
||||
{{ statusMap[scope.row.status] || '未知状态' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="pollutionType" label="污染类型" width="120">
|
||||
<template #default="{ row }: { row: FeedbackResponse }">
|
||||
<span>{{ pollutionTypeMap[row.pollutionType] }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="severityLevel" label="严重程度" width="100">
|
||||
<template #default="{ row }: { row: FeedbackResponse }">
|
||||
<el-tag :type="severityTagMap[row.severityLevel]">{{ severityLevelMap[row.severityLevel] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="网格坐标" width="120">
|
||||
<template #default="{ row }: { row: FeedbackResponse }">
|
||||
<span v-if="row.gridX !== null && row.gridY !== null">
|
||||
{{ `X: ${row.gridX}, Y: ${row.gridY}` }}
|
||||
</span>
|
||||
<span v-else>--</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="处理人" width="120">
|
||||
<template #default="{ row }: { row: FeedbackResponse }">
|
||||
<span v-if="row.task && row.task.assignee">
|
||||
{{ row.task.assignee.name }}
|
||||
</span>
|
||||
<span v-else>--</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="user.name" label="报告人" width="120">
|
||||
<template #default="{ row }: { row: FeedbackResponse }">
|
||||
<div>
|
||||
<span>{{ row.user ? row.user.name : '未知' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="提交时间" width="180">
|
||||
<template #default="{ row }">
|
||||
<span>{{ formatDate(row.createdAt) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="handleViewDetails(row)">查看详情</el-button>
|
||||
<!-- 其他操作按钮,如处理、分配等 -->
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-if="total > 0"
|
||||
class="pagination-container"
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<FeedbackDetailDialog
|
||||
:visible="detailDialogVisible"
|
||||
:feedback-id="selectedFeedbackId"
|
||||
@close="detailDialogVisible = false"
|
||||
@refresh="fetchFeedbackList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch, computed } from 'vue';
|
||||
import { useFeedbackStore } from '@/store/feedback';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { FeedbackStatus, SeverityLevel } from '@/api/feedback';
|
||||
import { PollutionType } from '@/api/types';
|
||||
import type { FeedbackResponse, FeedbackFilters } from '@/api/feedback';
|
||||
import FeedbackDetailDialog from '@/components/FeedbackDetailDialog.vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const feedbackStore = useFeedbackStore();
|
||||
const { feedbackList, total, loading, stats } = storeToRefs(feedbackStore);
|
||||
|
||||
// 使用 reactive 创建过滤条件对象
|
||||
const filters = reactive<Omit<FeedbackFilters, 'page' | 'size'>>({
|
||||
status: undefined,
|
||||
pollutionType: undefined,
|
||||
severityLevel: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
keyword: undefined,
|
||||
});
|
||||
|
||||
// 用于筛选器的状态子集
|
||||
const filterableStatusMap: Partial<Record<FeedbackStatus, string>> = {
|
||||
[FeedbackStatus.AI_REVIEWING]: 'AI审核中',
|
||||
[FeedbackStatus.IN_PROGRESS]: '处理中',
|
||||
[FeedbackStatus.PENDING_ASSIGNMENT]: '待分配',
|
||||
[FeedbackStatus.PROCESSED]: '已处理',
|
||||
};
|
||||
|
||||
// 使用计算属性来确保状态的显示名称
|
||||
const selectedStatusLabel = computed(() => {
|
||||
return filters.status ? statusMap[filters.status] : '全部状态';
|
||||
});
|
||||
|
||||
const selectedPollutionTypeLabel = computed(() => {
|
||||
return filters.pollutionType ? pollutionTypeMap[filters.pollutionType] : '所有类型';
|
||||
});
|
||||
|
||||
const selectedSeverityLabel = computed(() => {
|
||||
return filters.severityLevel ? severityLevelMap[filters.severityLevel] : '所有程度';
|
||||
});
|
||||
|
||||
// 日期范围
|
||||
const dateRange = ref<[string, string] | null>(null);
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
});
|
||||
|
||||
const detailDialogVisible = ref(false);
|
||||
const selectedFeedbackId = ref<number | null>(null);
|
||||
|
||||
// 映射关系
|
||||
const statusMap: Record<string, string> = {
|
||||
'AI_REVIEWING': 'AI审核中',
|
||||
'AI_REVIEW_FAILED': 'AI审核失败',
|
||||
'PENDING_REVIEW': '待人工审核',
|
||||
'PENDING_ASSIGNMENT': '待分配',
|
||||
'ASSIGNED': '已分配',
|
||||
'CONFIRMED': '已确认',
|
||||
'CLOSED_INVALID': '无效关闭',
|
||||
'PROCESSED': '已处理',
|
||||
'IN_PROGRESS': '处理中',
|
||||
'RESOLVED': '已处理',
|
||||
'COMPLETED': '已处理'
|
||||
};
|
||||
const statusTagMap: Record<string, 'info' | 'warning' | 'danger' | 'success'> = {
|
||||
'AI_REVIEWING': 'info',
|
||||
'AI_REVIEW_FAILED': 'danger',
|
||||
'PENDING_REVIEW': 'warning',
|
||||
'PENDING_ASSIGNMENT': 'warning',
|
||||
'ASSIGNED': 'warning',
|
||||
'CONFIRMED': 'success',
|
||||
'CLOSED_INVALID': 'info',
|
||||
'PROCESSED': 'success',
|
||||
'IN_PROGRESS': 'warning',
|
||||
'RESOLVED': 'success',
|
||||
'COMPLETED': 'success'
|
||||
};
|
||||
const pollutionTypeMap: Record<PollutionType, string> = {
|
||||
|
||||
[PollutionType.PM25]: 'PM2.5',
|
||||
[PollutionType.O3]: '臭氧',
|
||||
[PollutionType.SO2]: '二氧化硫',
|
||||
[PollutionType.NO2]: '二氧化氮',
|
||||
[PollutionType.OTHER]: '其他'
|
||||
};
|
||||
const severityLevelMap: Record<SeverityLevel, string> = {
|
||||
[SeverityLevel.LOW]: '低',
|
||||
[SeverityLevel.MEDIUM]: '中',
|
||||
[SeverityLevel.HIGH]: '高',
|
||||
};
|
||||
const severityTagMap: Record<SeverityLevel, 'info' | 'warning' | 'danger'> = {
|
||||
[SeverityLevel.LOW]: 'info',
|
||||
[SeverityLevel.MEDIUM]: 'warning',
|
||||
[SeverityLevel.HIGH]: 'danger',
|
||||
};
|
||||
|
||||
const formatDate = (date: string | null) => {
|
||||
if (!date) return 'N/A';
|
||||
return new Date(date).toLocaleString();
|
||||
};
|
||||
|
||||
const fetchFeedbackList = () => {
|
||||
const params = {
|
||||
...filters,
|
||||
page: pagination.page - 1, // 后端分页从0开始
|
||||
size: pagination.size,
|
||||
};
|
||||
feedbackStore.fetchFeedbackList(params);
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1;
|
||||
fetchFeedbackList();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(filters, {
|
||||
status: undefined,
|
||||
pollutionType: undefined,
|
||||
severityLevel: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
keyword: undefined,
|
||||
});
|
||||
dateRange.value = null;
|
||||
fetchFeedbackList();
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.size = size;
|
||||
fetchFeedbackList();
|
||||
};
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page;
|
||||
fetchFeedbackList();
|
||||
};
|
||||
|
||||
const handleViewDetails = (row: FeedbackResponse) => {
|
||||
selectedFeedbackId.value = row.id;
|
||||
detailDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const fetchFeedbackStats = () => {
|
||||
feedbackStore.fetchFeedbackStats();
|
||||
};
|
||||
|
||||
const filterOptions = {
|
||||
status: [
|
||||
{ value: 'PENDING_REVIEW', label: '待人工审核' },
|
||||
{ value: 'PENDING_ASSIGNMENT', label: '待分配' },
|
||||
{ value: 'ASSIGNED', label: '已分配' },
|
||||
{ value: 'IN_PROGRESS', label: '处理中' },
|
||||
{ value: 'PROCESSED', label: '已处理' },
|
||||
{ value: 'CLOSED_INVALID', label: '无效关闭' }
|
||||
],
|
||||
pollutionType: Object.entries(pollutionTypeMap).map(([value, label]) => ({ value, label })),
|
||||
// ... more filter options
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchFeedbackList();
|
||||
fetchFeedbackStats();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.feedback-management-page {
|
||||
padding: 20px;
|
||||
}
|
||||
.stats-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.stats-card {
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.stats-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
.stats-item {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
.stats-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.stats-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
.pending .stats-value {
|
||||
color: #e6a23c;
|
||||
}
|
||||
.confirmed .stats-value {
|
||||
color: #409eff;
|
||||
}
|
||||
.assigned .stats-value {
|
||||
color: #67c23a;
|
||||
}
|
||||
.resolved .stats-value {
|
||||
color: #67c23a;
|
||||
}
|
||||
.rejected .stats-value {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.page-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.card-header {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.filter-form .el-form-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.feedback-table {
|
||||
width: 100%;
|
||||
}
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 选中的过滤器样式 */
|
||||
:deep(.el-select) .el-input__prefix {
|
||||
font-weight: bold;
|
||||
color: #409eff !important;
|
||||
}
|
||||
:deep(.el-input--prefix .el-input__inner) {
|
||||
color: #409eff;
|
||||
font-weight: bold;
|
||||
}
|
||||
:deep(.el-date-editor.is-active) {
|
||||
color: #409eff;
|
||||
font-weight: bold;
|
||||
}
|
||||
:deep(.el-input.is-active .el-input__inner) {
|
||||
color: #409eff;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div class="forgot-password-container">
|
||||
<el-card class="forgot-password-card">
|
||||
<div class="card-header">
|
||||
<h2>找回密码</h2>
|
||||
<p>请输入您的注册邮箱以接收密码重置邮件</p>
|
||||
</div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
hide-required-asterisk
|
||||
@submit.prevent="handleSubmit"
|
||||
autocomplete="off"
|
||||
>
|
||||
<el-form-item label="邮箱地址" prop="email">
|
||||
<el-input
|
||||
v-model="form.email"
|
||||
placeholder="请输入您注册时使用的邮箱"
|
||||
size="large"
|
||||
autocomplete="off"
|
||||
>
|
||||
<template #append>
|
||||
<el-button
|
||||
:disabled="isSending"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
<div class="card-footer">
|
||||
<el-link @click="$router.push('/login')">返回登录</el-link>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { sendPasswordResetCode } from '@/api/auth';
|
||||
|
||||
const router = useRouter();
|
||||
const formRef = ref<FormInstance>();
|
||||
const isSending = ref(false);
|
||||
const countdown = ref(0);
|
||||
|
||||
const form = reactive({
|
||||
email: '',
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址', trigger: ['blur', 'change'] }
|
||||
],
|
||||
});
|
||||
|
||||
const buttonText = computed(() => {
|
||||
if (isSending.value) {
|
||||
return `${countdown.value}s 后可重发`;
|
||||
}
|
||||
return '发送重置邮件';
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formRef.value) return;
|
||||
formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
isSending.value = true;
|
||||
countdown.value = 60;
|
||||
try {
|
||||
await sendPasswordResetCode(form.email);
|
||||
ElMessage.success('密码重置邮件已发送,即将跳转...');
|
||||
|
||||
const timer = setInterval(() => {
|
||||
if (countdown.value > 0) {
|
||||
countdown.value--;
|
||||
} else {
|
||||
clearInterval(timer);
|
||||
isSending.value = false;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
setTimeout(() => {
|
||||
router.push({ path: '/reset-password', query: { email: form.email } });
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
ElMessage.error('邮件发送失败,请检查邮箱地址是否正确。');
|
||||
isSending.value = false;
|
||||
countdown.value = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.forgot-password-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-image: url('https://images.unsplash.com/photo-1506744038136-46273834b3fb?ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.forgot-password-card {
|
||||
width: 480px;
|
||||
border-radius: 25px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(15px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
color: #004d40;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-header p {
|
||||
color: #607d8b;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
margin-top: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
background-color: #004d40;
|
||||
border-color: #004d40;
|
||||
transition: background-color 0.3s ease;
|
||||
font-size: 1rem;
|
||||
padding: 1.2rem 0;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
background-color: #00695c;
|
||||
border-color: #00695c;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
border-top-left-radius: 15px;
|
||||
border-bottom-left-radius: 15px;
|
||||
}
|
||||
|
||||
:deep(.el-input-group__append) {
|
||||
background-color: #004d40;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-top-right-radius: 15px;
|
||||
border-bottom-right-radius: 15px;
|
||||
}
|
||||
|
||||
:deep(.el-input-group__append:hover) {
|
||||
background-color: #00695c;
|
||||
}
|
||||
|
||||
:deep(.el-input-group__append .el-button) {
|
||||
background: transparent;
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,731 @@
|
||||
<template>
|
||||
<div class="grid-management-container">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h2>网格地图</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="refreshData" :loading="loading || isLoadingWorkers" icon="Refresh">
|
||||
刷新数据
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content-grid">
|
||||
<!-- Grid Map Area -->
|
||||
<div class="map-area" v-loading="loading">
|
||||
<div v-if="error" class="error-message">
|
||||
<el-alert title="地图数据加载失败" :description="error" type="error" show-icon :closable="false" />
|
||||
<el-button @click="fetchAllGrids" type="primary" class="retry-button">重试</el-button>
|
||||
</div>
|
||||
<svg v-else-if="displayGrids.length" :width="svgDimensions.width" :height="svgDimensions.height" class="grid-svg">
|
||||
<defs>
|
||||
<pattern id="grid-pattern" :width="CELL_SIZE" :height="CELL_SIZE" patternUnits="userSpaceOnUse">
|
||||
<path :d="`M ${CELL_SIZE} 0 L 0 0 0 ${CELL_SIZE}`" fill="none" stroke="#e9e9e9" stroke-width="0.5" />
|
||||
</pattern>
|
||||
|
||||
<!-- Softer Gradient for the path -->
|
||||
<linearGradient id="soft-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#89f7fe; stop-opacity:1" /> <!-- Light Sky Blue -->
|
||||
<stop offset="100%" style="stop-color:#66a6ff; stop-opacity:1" /> <!-- Soft Lavender Blue -->
|
||||
</linearGradient>
|
||||
|
||||
</defs>
|
||||
<rect :width="svgDimensions.width" :height="svgDimensions.height" fill="url(#grid-pattern)" />
|
||||
|
||||
<g v-for="grid in displayGrids" :key="grid.id">
|
||||
<!-- Replace rect with foreignObject to allow for CSS pseudo-elements -->
|
||||
<foreignObject
|
||||
:x="grid.displayX * CELL_SIZE"
|
||||
:y="grid.displayY * CELL_SIZE"
|
||||
:width="CELL_SIZE"
|
||||
:height="CELL_SIZE"
|
||||
>
|
||||
<div
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
class="grid-cell-wrapper"
|
||||
@click="selectGrid(grid)"
|
||||
>
|
||||
<div
|
||||
:class="{
|
||||
'grid-cell': true,
|
||||
'selected': selectedGrid && selectedGrid.id === grid.id,
|
||||
'is-path': pathInfo.pathSet.has(`${grid.gridX},${grid.gridY}`),
|
||||
'is-start': pathInfo.start === `${grid.gridX},${grid.gridY}`,
|
||||
'is-end': pathInfo.end === `${grid.gridX},${grid.gridY}`
|
||||
}"
|
||||
:style="{
|
||||
'animation-delay': `${(pathInfo.pathMap.get(`${grid.gridX},${grid.gridY}`) || 0) * 50}ms`,
|
||||
'background-color': getGridFillColor(grid)
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
|
||||
<!-- Path Number Text remains as SVG text -->
|
||||
<text
|
||||
v-if="pathInfo.pathSet.has(`${grid.gridX},${grid.gridY}`)"
|
||||
:x="grid.displayX * CELL_SIZE + CELL_SIZE / 2"
|
||||
:y="grid.displayY * CELL_SIZE + CELL_SIZE / 2"
|
||||
class="path-text"
|
||||
:style="{ 'animation-delay': `${(pathInfo.pathMap.get(`${grid.gridX},${grid.gridY}`) || 0) * 50}ms` }"
|
||||
>
|
||||
{{ (pathInfo.pathMap.get(`${grid.gridX},${grid.gridY}`) || 0) + 1 }}
|
||||
</text>
|
||||
<!-- Border Rendering -->
|
||||
<line v-if="getBorder(grid, 'top')" :x1="grid.displayX * CELL_SIZE" :y1="grid.displayY * CELL_SIZE" :x2="(grid.displayX + 1) * CELL_SIZE" :y2="grid.displayY * CELL_SIZE" class="border-line" />
|
||||
<line v-if="getBorder(grid, 'right')" :x1="(grid.displayX + 1) * CELL_SIZE" :y1="grid.displayY * CELL_SIZE" :x2="(grid.displayX + 1) * CELL_SIZE" :y2="(grid.displayY + 1) * CELL_SIZE" class="border-line" />
|
||||
<line v-if="getBorder(grid, 'bottom')" :x1="grid.displayX * CELL_SIZE" :y1="(grid.displayY + 1) * CELL_SIZE" :x2="(grid.displayX + 1) * CELL_SIZE" :y2="(grid.displayY + 1) * CELL_SIZE" class="border-line" />
|
||||
<line v-if="getBorder(grid, 'left')" :x1="grid.displayX * CELL_SIZE" :y1="grid.displayY * CELL_SIZE" :x2="grid.displayX * CELL_SIZE" :y2="(grid.displayY + 1) * CELL_SIZE" class="border-line" />
|
||||
</g>
|
||||
</svg>
|
||||
<el-empty v-else description="没有可显示的网格数据" />
|
||||
</div>
|
||||
|
||||
<!-- Details Sidebar -->
|
||||
<div class="sidebar-area">
|
||||
<el-card shadow="never" class="details-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>网格详情</span>
|
||||
<el-button v-if="selectedGrid && !isEditing && canEdit" type="primary" link @click="enterEditMode" icon="Edit">
|
||||
编辑
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="isSaving" v-loading="true" class="details-content-loading"></div>
|
||||
|
||||
<div v-else-if="selectedGrid" class="details-content">
|
||||
<!-- Display Mode -->
|
||||
<template v-if="!isEditing">
|
||||
<p><strong>网格ID:</strong> {{ selectedGrid.id }}</p>
|
||||
<p><strong>城市:</strong> <el-tag :color="getCityColor(selectedGrid.cityName)" effect="light">{{ selectedGrid.cityName }}</el-tag></p>
|
||||
<p><strong>区县:</strong> {{ selectedGrid.districtName || 'N/A' }}</p>
|
||||
<p><strong>原始坐标:</strong> ({{ selectedGrid.gridX }}, {{ selectedGrid.gridY }})</p>
|
||||
<p><strong>是否为障碍物:</strong> <el-tag :type="selectedGrid.isObstacle ? 'danger' : 'success'">{{ selectedGrid.isObstacle ? '是' : '否' }}</el-tag></p>
|
||||
<p><strong>描述:</strong> {{ selectedGrid.description || '无' }}</p>
|
||||
<el-divider />
|
||||
<p><strong>负责人:</strong></p>
|
||||
<div v-if="selectedGridWorker">
|
||||
<el-tag effect="plain" type="info">
|
||||
<el-icon><User /></el-icon>
|
||||
{{ selectedGridWorker.name }} ({{ selectedGridWorker.phone }})
|
||||
</el-tag>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-tag type="warning">未分配</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Editing Mode -->
|
||||
<template v-else-if="canEdit">
|
||||
<el-form label-position="top" label-width="100px">
|
||||
<el-form-item label="是否为障碍物">
|
||||
<el-switch v-model="editableIsObstacle" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input type="textarea" v-model="editableDescription" :rows="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分配网格员">
|
||||
<el-select
|
||||
v-model="selectedWorkerIdToAssign"
|
||||
placeholder="请选择网格员"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-option label="-- 未分配 --" value="unassigned"></el-option>
|
||||
<el-option
|
||||
v-for="worker in allWorkers"
|
||||
:key="worker.id"
|
||||
:label="`${worker.name} (${worker.phone})`"
|
||||
:value="worker.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="edit-actions">
|
||||
<el-button @click="cancelEdit">取消</el-button>
|
||||
<el-button type="primary" @click="saveChanges" :loading="isSaving">保存</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<el-empty v-else description="请在左侧地图上选择一个网格" />
|
||||
</el-card>
|
||||
|
||||
<!-- Pathfinding Card -->
|
||||
<el-card shadow="never" class="details-card pathfinding-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>路径规划</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="!authStore.user" class="pathfinding-content">
|
||||
<el-empty description="请先登录以使用路径规划功能" />
|
||||
</div>
|
||||
<div v-else class="pathfinding-content">
|
||||
<el-form label-position="top" :model="pathfinding" @submit.prevent="handleFindPath">
|
||||
<el-form-item label="起点坐标 (X, Y)">
|
||||
<div style="display: flex; gap: 8px; width: 100%;">
|
||||
<el-input-number v-model="pathfinding.startX" :controls="false" placeholder="X" style="width: 100%" />
|
||||
<el-input-number v-model="pathfinding.startY" :controls="false" placeholder="Y" style="width: 100%" />
|
||||
</div>
|
||||
<small class="form-help-text">默认为您分配的网格,或 (0,0)</small>
|
||||
</el-form-item>
|
||||
<el-form-item label="终点坐标 (X, Y)">
|
||||
<div style="display: flex; gap: 8px; width: 100%;">
|
||||
<el-input-number v-model="pathfinding.endX" :controls="false" placeholder="X" style="width: 100%" />
|
||||
<el-input-number v-model="pathfinding.endY" :controls="false" placeholder="Y" style="width: 100%" />
|
||||
</div>
|
||||
<small class="form-help-text">点击地图上的网格可快速设置终点</small>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="pathfinding-actions">
|
||||
<el-button @click="clearPath" :disabled="!calculatedPath.length">清除路径</el-button>
|
||||
<el-button type="primary" @click="handleFindPath" :loading="isPathfindingLoading">规划路径</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import type { Grid, UserAccount } from '@/api/types';
|
||||
import { getGrids } from '@/api/dashboard';
|
||||
import { getAllGridWorkers } from '@/api/personnel';
|
||||
import { updateGrid, assignWorkerByCoordinates, unassignWorkerByCoordinates } from '@/api/grid';
|
||||
import { findPath, type Point, type PathfindingRequest } from '@/api/pathfinding';
|
||||
import { User } from '@element-plus/icons-vue';
|
||||
|
||||
const CELL_SIZE = 25;
|
||||
const PADDING_CELLS = 2;
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// --- State ---
|
||||
const gridData = ref<Grid[]>([]);
|
||||
const allWorkers = ref<UserAccount[]>([]);
|
||||
const loading = ref(false); // For main grid loading
|
||||
const isLoadingWorkers = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const selectedGrid = ref<DisplayGrid | null>(null);
|
||||
const isEditing = ref(false);
|
||||
|
||||
// --- Editing State ---
|
||||
const editableDescription = ref('');
|
||||
const editableIsObstacle = ref(false);
|
||||
const selectedWorkerIdToAssign = ref<number | 'unassigned' | null>(null);
|
||||
|
||||
// --- Pathfinding State ---
|
||||
const pathfinding = ref({
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
endX: undefined as number | undefined,
|
||||
endY: undefined as number | undefined
|
||||
});
|
||||
const calculatedPath = ref<Point[]>([]);
|
||||
const isPathfindingLoading = ref(false);
|
||||
|
||||
// --- Data Fetching & Refresh ---
|
||||
const fetchAllGrids = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
selectedGrid.value = null; // Reset selection
|
||||
try {
|
||||
const response = await getGrids();
|
||||
gridData.value = Array.isArray(response) ? response : (response as any).content || [];
|
||||
} catch (e: any) {
|
||||
const errorMessage = e.message || '获取网格数据时发生未知错误';
|
||||
error.value = errorMessage;
|
||||
ElMessage.error(errorMessage);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAllWorkers = async () => {
|
||||
const userRole = authStore.user?.role;
|
||||
// 只有管理员和主管需要获取完整的网格员列表用于管理
|
||||
if (userRole !== 'ADMIN' && userRole !== 'SUPERVISOR') {
|
||||
allWorkers.value = []; // 其他角色无需加载网格员数据
|
||||
return;
|
||||
}
|
||||
|
||||
isLoadingWorkers.value = true;
|
||||
try {
|
||||
allWorkers.value = await getAllGridWorkers();
|
||||
} catch (e: any) {
|
||||
ElMessage.error('获取网格员列表失败: ' + e.message);
|
||||
} finally {
|
||||
isLoadingWorkers.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshData = async () => {
|
||||
await Promise.all([fetchAllGrids(), fetchAllWorkers()]);
|
||||
ElMessage.success('数据已刷新');
|
||||
// Set default start point for pathfinding if user has one
|
||||
const user = authStore.user;
|
||||
if (user && typeof user.gridX === 'number' && typeof user.gridY === 'number') {
|
||||
pathfinding.value.startX = user.gridX;
|
||||
pathfinding.value.startY = user.gridY;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
refreshData();
|
||||
});
|
||||
|
||||
// --- Computed Properties ---
|
||||
const canEdit = computed(() => {
|
||||
const role = authStore.user?.role;
|
||||
return role === 'ADMIN' || role === 'SUPERVISOR';
|
||||
});
|
||||
|
||||
const workersByCoord = computed<Map<string, UserAccount>>(() => {
|
||||
const map = new Map<string, UserAccount>();
|
||||
allWorkers.value.forEach(worker => {
|
||||
if (worker.gridX !== null && worker.gridY !== null) {
|
||||
map.set(`${worker.gridX},${worker.gridY}`, worker);
|
||||
}
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
const selectedGridWorker = computed<UserAccount | undefined>(() => {
|
||||
if (!selectedGrid.value || !allWorkers.value.length) {
|
||||
return undefined;
|
||||
}
|
||||
return allWorkers.value.find(
|
||||
worker => worker.gridX === selectedGrid.value?.gridX && worker.gridY === selectedGrid.value?.gridY
|
||||
);
|
||||
});
|
||||
|
||||
const pathInfo = computed(() => {
|
||||
const pathSet = new Set<string>();
|
||||
const pathMap = new Map<string, number>(); // Stores coordinate -> index for animation delay
|
||||
let start = '';
|
||||
let end = '';
|
||||
|
||||
if (calculatedPath.value.length > 0) {
|
||||
calculatedPath.value.forEach((point, index) => {
|
||||
const coord = `${point.x},${point.y}`;
|
||||
pathSet.add(coord);
|
||||
pathMap.set(coord, index);
|
||||
});
|
||||
const startPoint = calculatedPath.value[0];
|
||||
const endPoint = calculatedPath.value[calculatedPath.value.length - 1];
|
||||
start = `${startPoint.x},${startPoint.y}`;
|
||||
end = `${endPoint.x},${endPoint.y}`;
|
||||
}
|
||||
|
||||
return { pathSet, pathMap, start, end };
|
||||
});
|
||||
|
||||
// --- City Color Logic ---
|
||||
const cityColors = ref<Map<string, string>>(new Map());
|
||||
const getCityColor = (cityName: string): string => {
|
||||
if (!cityColors.value.has(cityName)) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < cityName.length; i++) {
|
||||
hash = cityName.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
const color = `hsl(${hash % 360}, 80%, 85%)`;
|
||||
cityColors.value.set(cityName, color);
|
||||
}
|
||||
return cityColors.value.get(cityName)!;
|
||||
};
|
||||
|
||||
const getGridFillColor = (grid: Grid): string => {
|
||||
const isPath = pathInfo.value.pathSet.has(`${grid.gridX},${grid.gridY}`);
|
||||
// If it's part of the path, let the CSS class handle the gradient background
|
||||
if (isPath) {
|
||||
return 'transparent';
|
||||
}
|
||||
if (grid.isObstacle) {
|
||||
return '#000000'; // Black for obstacles
|
||||
}
|
||||
if (workersByCoord.value.has(`${grid.gridX},${grid.gridY}`)) {
|
||||
return '#FF0000'; // Red for grids with workers
|
||||
}
|
||||
return getCityColor(grid.cityName);
|
||||
};
|
||||
|
||||
// --- Display Logic ---
|
||||
interface DisplayGrid extends Grid {
|
||||
displayX: number;
|
||||
displayY: number;
|
||||
}
|
||||
|
||||
const displayGrids = computed<DisplayGrid[]>(() => {
|
||||
if (!gridData.value.length) return [];
|
||||
|
||||
// Determine the top-left corner of the entire map to normalize coordinates
|
||||
const minX = Math.min(...gridData.value.map(g => g.gridX));
|
||||
const minY = Math.min(...gridData.value.map(g => g.gridY));
|
||||
|
||||
// Render all grids based on their absolute positions, normalized to start at (0,0)
|
||||
return gridData.value.map(grid => ({
|
||||
...grid,
|
||||
displayX: grid.gridX - minX,
|
||||
displayY: grid.gridY - minY,
|
||||
}));
|
||||
});
|
||||
|
||||
const gridMap = computed(() => {
|
||||
const map = new Map<string, DisplayGrid>();
|
||||
displayGrids.value.forEach(grid => {
|
||||
map.set(`${grid.displayX},${grid.displayY}`, grid);
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
const svgDimensions = computed(() => {
|
||||
if (!displayGrids.value.length) return { width: 0, height: 0 };
|
||||
const maxX = Math.max(...displayGrids.value.map(g => g.displayX));
|
||||
const maxY = Math.max(...displayGrids.value.map(g => g.displayY));
|
||||
return {
|
||||
width: (maxX + 1) * CELL_SIZE,
|
||||
height: (maxY + 1) * CELL_SIZE,
|
||||
};
|
||||
});
|
||||
|
||||
const getBorder = (grid: DisplayGrid, side: 'top' | 'right' | 'bottom' | 'left'): boolean => {
|
||||
const { displayX, displayY, cityName } = grid;
|
||||
let neighbor: DisplayGrid | undefined;
|
||||
switch (side) {
|
||||
case 'top':
|
||||
neighbor = gridMap.value.get(`${displayX},${displayY - 1}`);
|
||||
break;
|
||||
case 'right':
|
||||
neighbor = gridMap.value.get(`${displayX + 1},${displayY}`);
|
||||
break;
|
||||
case 'bottom':
|
||||
neighbor = gridMap.value.get(`${displayX},${displayY + 1}`);
|
||||
break;
|
||||
case 'left':
|
||||
neighbor = gridMap.value.get(`${displayX - 1},${displayY}`);
|
||||
break;
|
||||
}
|
||||
return !neighbor || neighbor.cityName !== cityName;
|
||||
};
|
||||
|
||||
const selectGrid = (grid: DisplayGrid) => {
|
||||
selectedGrid.value = grid;
|
||||
|
||||
// Also set pathfinding endpoint for convenience
|
||||
pathfinding.value.endX = grid.gridX;
|
||||
pathfinding.value.endY = grid.gridY;
|
||||
|
||||
// If in edit mode, populate fields
|
||||
if (isEditing.value) {
|
||||
// populateEditFields(grid); // This function was removed, keep it commented or remove it if not needed
|
||||
}
|
||||
};
|
||||
|
||||
// --- Editing Logic ---
|
||||
const enterEditMode = () => {
|
||||
if (!selectedGrid.value) return;
|
||||
isEditing.value = true;
|
||||
editableDescription.value = selectedGrid.value.description || '';
|
||||
editableIsObstacle.value = selectedGrid.value.isObstacle;
|
||||
selectedWorkerIdToAssign.value = selectedGridWorker.value?.id ?? 'unassigned';
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
isEditing.value = false;
|
||||
};
|
||||
|
||||
const saveChanges = async () => {
|
||||
if (!selectedGrid.value) return;
|
||||
isSaving.value = true;
|
||||
|
||||
try {
|
||||
const { id, gridX, gridY } = selectedGrid.value;
|
||||
let needsGridRefresh = false;
|
||||
let needsWorkerRefresh = false;
|
||||
|
||||
// 1. Update grid details (obstacle, description)
|
||||
const originalDescription = selectedGrid.value.description || '';
|
||||
if (editableIsObstacle.value !== selectedGrid.value.isObstacle || editableDescription.value !== originalDescription) {
|
||||
await updateGrid(id, {
|
||||
isObstacle: editableIsObstacle.value,
|
||||
description: editableDescription.value,
|
||||
});
|
||||
needsGridRefresh = true;
|
||||
}
|
||||
|
||||
// 2. Update worker assignment
|
||||
const originalWorkerId = selectedGridWorker.value?.id;
|
||||
const newWorkerId = selectedWorkerIdToAssign.value;
|
||||
|
||||
if (newWorkerId !== (originalWorkerId ?? 'unassigned')) {
|
||||
if (newWorkerId === 'unassigned') {
|
||||
await unassignWorkerByCoordinates(gridX, gridY);
|
||||
} else if (newWorkerId) {
|
||||
await assignWorkerByCoordinates(gridX, gridY, newWorkerId);
|
||||
}
|
||||
needsWorkerRefresh = true;
|
||||
}
|
||||
|
||||
ElMessage.success('网格信息更新成功!');
|
||||
|
||||
// Refresh data
|
||||
if (needsGridRefresh) await fetchAllGrids();
|
||||
if (needsWorkerRefresh) await fetchAllWorkers();
|
||||
|
||||
isEditing.value = false;
|
||||
} catch (e: any) {
|
||||
ElMessage.error('保存失败: ' + (e.response?.data?.message || e.message));
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Pathfinding Methods ---
|
||||
const handleFindPath = async () => {
|
||||
if (typeof pathfinding.value.endX !== 'number' || typeof pathfinding.value.endY !== 'number') {
|
||||
ElMessage.warning('请输入有效的终点坐标。');
|
||||
return;
|
||||
}
|
||||
|
||||
isPathfindingLoading.value = true;
|
||||
calculatedPath.value = []; // Clear previous path
|
||||
|
||||
const request: PathfindingRequest = {
|
||||
startX: pathfinding.value.startX,
|
||||
startY: pathfinding.value.startY,
|
||||
endX: pathfinding.value.endX,
|
||||
endY: pathfinding.value.endY,
|
||||
};
|
||||
|
||||
try {
|
||||
const path = await findPath(request);
|
||||
if (path && path.length > 0) {
|
||||
calculatedPath.value = path;
|
||||
ElMessage.success(`路径规划成功,共 ${path.length} 步。`);
|
||||
} else {
|
||||
ElMessage.error('未找到有效路径,请检查起终点或地图障碍物。');
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error('路径规划失败: ' + (e.response?.data?.message || e.message));
|
||||
} finally {
|
||||
isPathfindingLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const clearPath = () => {
|
||||
calculatedPath.value = [];
|
||||
ElMessage.info('路径已清除。');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.grid-management-container {
|
||||
padding: 20px;
|
||||
background-color: #f7f8fa;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.main-content-grid {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.map-area {
|
||||
flex: 3;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
border: 1px solid #e9e9e9;
|
||||
}
|
||||
|
||||
.sidebar-area {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.details-card .card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.grid-svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.grid-cell-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.grid-cell {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: transparent; /* Default state */
|
||||
border: 0.5px solid #dcdfe6;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
overflow: hidden; /* Important for pseudo-element animation */
|
||||
}
|
||||
|
||||
.grid-cell:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
.grid-cell.selected {
|
||||
border: 2px solid #ff4d4f;
|
||||
}
|
||||
|
||||
.grid-cell.is-start {
|
||||
background-color: #00e676;
|
||||
box-shadow: 0 0 8px #00e676, inset 0 0 5px rgba(255,255,255,0.7);
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.grid-cell.is-end {
|
||||
background-color: #ff5252;
|
||||
box-shadow: 0 0 8px #ff5252, inset 0 0 5px rgba(255,255,255,0.7);
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.grid-cell.is-path {
|
||||
background-image: linear-gradient(to bottom right, #89f7fe, #66a6ff);
|
||||
border-color: #66a6ff;
|
||||
opacity: 0;
|
||||
animation: draw-path-reveal 0.4s forwards ease-out;
|
||||
}
|
||||
|
||||
.grid-cell.is-path::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -150%;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0) 100%);
|
||||
transform: skewX(-25deg);
|
||||
animation: light-sweep 1.2s ease-in-out 3 forwards; /* Play 3 times and stop */
|
||||
animation-delay: inherit; /* Inherit delay from parent */
|
||||
}
|
||||
|
||||
@keyframes light-sweep {
|
||||
100% {
|
||||
left: 150%;
|
||||
}
|
||||
}
|
||||
|
||||
.path-text {
|
||||
font-size: 10px;
|
||||
fill: white;
|
||||
font-weight: bold;
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
pointer-events: none;
|
||||
animation: draw-path-reveal 0.4s forwards ease-out;
|
||||
}
|
||||
|
||||
.border-line {
|
||||
stroke: #000;
|
||||
stroke-width: 2.5px;
|
||||
margin-top: 15px;
|
||||
width: 100%; /* Ensure it takes full width */
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.retry-button {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.details-content p {
|
||||
margin: 0 0 12px;
|
||||
color: #606266;
|
||||
margin-top: 4px;
|
||||
line-height: 1.2;
|
||||
width: 100%; /* Ensure it takes full width */
|
||||
}
|
||||
|
||||
.details-content p strong {
|
||||
color: #303133;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.details-content-loading {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.edit-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pathfinding-card {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pathfinding-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.form-help-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
line-height: 1.2;
|
||||
width: 100%; /* Ensure it takes full width */
|
||||
}
|
||||
|
||||
@keyframes draw-path-reveal {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
filter: brightness(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
filter: brightness(1.7);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1791
ems-frontend/ems-monitoring-system/src/views/HomeView.vue
Normal file
1791
ems-frontend/ems-monitoring-system/src/views/HomeView.vue
Normal file
@@ -0,0 +1,1791 @@
|
||||
<template>
|
||||
<div v-if="canViewDashboard" class="dashboard-container">
|
||||
<div class="dashboard-header">
|
||||
<h1>环境监测系统仪表盘</h1>
|
||||
<div class="dashboard-actions">
|
||||
<el-button type="primary" :icon="Refresh" @click="refreshAllData">刷新全部</el-button>
|
||||
<el-button type="success" :icon="Setting">设置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-grid">
|
||||
<!-- Top Row: Key Stats -->
|
||||
<el-card shadow="hover" class="grid-item kpi-card" v-for="kpi in kpiStats" :key="kpi.title">
|
||||
<div class="kpi-content">
|
||||
<div class="kpi-icon" :style="{ backgroundColor: kpi.color }">
|
||||
<component :is="kpi.icon" />
|
||||
</div>
|
||||
<div class="kpi-details">
|
||||
<div class="kpi-title">{{ kpi.title }}</div>
|
||||
<div class="kpi-value">
|
||||
<span v-if="!loading.stats">{{ kpi.value }}</span>
|
||||
<span v-else class="loading-text">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- AQI Distribution Chart -->
|
||||
<el-card shadow="never" class="grid-item chart-card aqi-distribution" :body-style="{ padding: '16px', height: 'calc(100% - 53px)' }">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon class="header-icon"><DataAnalysis /></el-icon>
|
||||
<span>AQI 级别分布</span>
|
||||
<div class="card-actions">
|
||||
<el-tooltip content="查看详细数据" placement="top">
|
||||
<el-button type="success" :icon="View" circle size="small" @click="showAqiDetailData" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="刷新数据" placement="top">
|
||||
<el-button type="primary" :icon="Refresh" circle size="small" @click="refreshAqiData" :loading="loading.aqi" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="aqiDistributionChartRef" class="chart" v-if="!loading.aqi" />
|
||||
<el-skeleton :rows="5" animated v-else />
|
||||
</el-card>
|
||||
|
||||
<!-- Task Status Chart -->
|
||||
<el-card shadow="never" class="grid-item chart-card task-status" :body-style="{ padding: '16px', height: 'calc(100% - 53px)' }">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon class="header-icon"><Finished /></el-icon>
|
||||
<span>任务状态分布</span>
|
||||
<div class="card-actions">
|
||||
<el-tooltip content="查看详细数据" placement="top">
|
||||
<el-button type="success" :icon="View" circle size="small" @click="showTaskDetailData" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="刷新数据" placement="top">
|
||||
<el-button type="primary" :icon="Refresh" circle size="small" @click="refreshTaskData" :loading="loading.tasks" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="taskStatusChartRef" class="chart" v-if="!loading.tasks" />
|
||||
<el-skeleton :rows="5" animated v-else />
|
||||
</el-card>
|
||||
|
||||
<!-- Exceedance Trend Chart -->
|
||||
<el-card shadow="never" class="grid-item chart-card exceedance-trend" :body-style="{ padding: '16px', height: 'calc(100% - 53px)' }">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon class="header-icon"><TrendCharts /></el-icon>
|
||||
<span>月度超标趋势</span>
|
||||
<div class="card-actions">
|
||||
<el-tooltip content="查看详细数据" placement="top">
|
||||
<el-button type="success" :icon="View" circle size="small" @click="showTrendDetailData" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="刷新数据" placement="top">
|
||||
<el-button type="primary" :icon="Refresh" circle size="small" @click="refreshTrendData" :loading="loading.trend" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="exceedanceTrendChartRef" class="chart" v-if="!loading.trend" />
|
||||
<el-skeleton :rows="8" animated v-else />
|
||||
</el-card>
|
||||
|
||||
<!-- Pollutant Trend Chart -->
|
||||
<el-card shadow="never" class="grid-item chart-card pollutant-trend" :body-style="{ padding: '16px', height: 'calc(100% - 53px)' }">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon class="header-icon"><DataLine /></el-icon>
|
||||
<span>污染物趋势</span>
|
||||
<div class="card-actions">
|
||||
<el-select v-model="selectedPollutantType" placeholder="选择污染物" size="small" style="width: 120px; margin-right: 10px;" @change="handlePollutantTypeChange">
|
||||
<el-option v-for="item in pollutantTypes" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<el-tooltip content="查看详细数据" placement="top">
|
||||
<el-button type="success" :icon="View" circle size="small" @click="showPollutantDetailData" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="刷新数据" placement="top">
|
||||
<el-button type="primary" :icon="Refresh" circle size="small" @click="refreshPollutantTrendData" :loading="loading.pollutantTrend" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="pollutantTrendChartRef" class="chart" v-if="!loading.pollutantTrend && !errors.pollutantTrend" />
|
||||
<el-skeleton :rows="8" animated v-else-if="loading.pollutantTrend" />
|
||||
<el-result v-else icon="error" title="数据加载失败" sub-title="无法加载污染物趋势数据,请稍后重试。">
|
||||
<template #extra>
|
||||
<el-button type="primary" @click="refreshPollutantTrendData">重试</el-button>
|
||||
</template>
|
||||
</el-result>
|
||||
</el-card>
|
||||
|
||||
<!-- Grid Coverage Table -->
|
||||
<el-card
|
||||
shadow="never"
|
||||
class="grid-item table-card grid-coverage"
|
||||
body-style="padding: 16px; height: calc(100% - 53px); box-sizing: border-box;"
|
||||
header-class="coverage-header"
|
||||
body-class="coverage-body"
|
||||
footer-class=""
|
||||
>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon class="header-icon"><Grid /></el-icon>
|
||||
<span>网格覆盖率 (按城市)</span>
|
||||
<div class="card-actions">
|
||||
<el-tooltip content="刷新数据" placement="top">
|
||||
<el-button type="primary" :icon="Refresh" circle size="small" @click="refreshCoverageData" :loading="loading.coverage" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
:data="gridCoverageData"
|
||||
style="width: 100%"
|
||||
height="100%"
|
||||
v-if="!loading.coverage"
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }"
|
||||
:row-class-name="coverageTableRowClassName"
|
||||
border
|
||||
>
|
||||
<el-table-column prop="city" label="城市" />
|
||||
<el-table-column prop="coverageRate" label="覆盖率" width="220">
|
||||
<template #default="{ row }">
|
||||
<el-progress
|
||||
:percentage="row.coverageRate"
|
||||
:color="getCoverageColor(row.coverageRate)"
|
||||
:stroke-width="18"
|
||||
:show-text="true"
|
||||
:format="percentageFormat"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="coveredGrids" label="已覆盖" align="center" />
|
||||
<el-table-column prop="totalGrids" label="总网格" align="center" />
|
||||
</el-table>
|
||||
<el-skeleton :rows="5" animated v-else />
|
||||
</el-card>
|
||||
</div>
|
||||
<div class="dashboard-footer">
|
||||
<p>© 2025 环境监测系统 - 实时数据更新于 {{ currentTime }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 详细数据对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
:title="detailDialogTitle"
|
||||
width="80%"
|
||||
:before-close="closeDetailDialog"
|
||||
destroy-on-close
|
||||
class="detail-dialog"
|
||||
>
|
||||
<div class="detail-table-container">
|
||||
<el-table
|
||||
:data="detailTableData"
|
||||
border
|
||||
style="width: 100%"
|
||||
:header-cell-style="{
|
||||
background: 'linear-gradient(to right, #409EFF, #67C23A)',
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '16px',
|
||||
padding: '12px 8px',
|
||||
textAlign: 'center'
|
||||
}"
|
||||
:cell-style="{
|
||||
textAlign: 'center',
|
||||
padding: '12px 8px',
|
||||
fontSize: '14px'
|
||||
}"
|
||||
:row-class-name="tableRowClassName"
|
||||
highlight-current-row
|
||||
>
|
||||
<el-table-column
|
||||
v-for="column in detailTableColumns"
|
||||
:key="column.prop"
|
||||
:prop="column.prop"
|
||||
:label="column.label"
|
||||
:width="column.width"
|
||||
:align="column.align || 'center'"
|
||||
/>
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="detail-dialog-footer">
|
||||
<el-button @click="closeDetailDialog" plain>关闭</el-button>
|
||||
<el-button type="primary" @click="exportDetailData" :icon="Download">导出数据</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
<div v-else class="welcome-container">
|
||||
<el-result
|
||||
icon="success"
|
||||
title="欢迎回来"
|
||||
:sub-title="`您好, ${user?.name || '用户'}!您当前的角色是 ${user?.role || '未知'},没有权限访问仪表盘。`"
|
||||
>
|
||||
<template #extra>
|
||||
<el-button type="primary" @click="goBack">返回上一页</el-button>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed, nextTick, onBeforeUnmount, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { PieChart, LineChart } from 'echarts/charts';
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent,
|
||||
} from 'echarts/components';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getDashboardStats, getAqiDistribution, getTaskCompletionStats, getMonthlyExceedanceTrend, getGridCoverageByCity, getPollutantMonthlyTrends } from '@/api/dashboard';
|
||||
import type { DashboardStatsDTO, AqiDistributionDTO, TaskStatsDTO, TrendDataPointDTO, GridCoverageDTO } from '@/api/types';
|
||||
import { Briefcase, CircleCheck, WarnTriangleFilled, Grid, DataAnalysis, Finished, TrendCharts, Refresh, Setting, DataLine, View, Download } from '@element-plus/icons-vue';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const user = computed(() => authStore.user);
|
||||
const canViewDashboard = computed(() => {
|
||||
if (!user.value) return false;
|
||||
return ['ADMIN', 'DECISION_MAKER'].includes(user.value.role);
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
echarts.use([
|
||||
CanvasRenderer,
|
||||
PieChart,
|
||||
LineChart,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent,
|
||||
]);
|
||||
|
||||
// --- Chart Element Refs ---
|
||||
const aqiDistributionChartRef = ref<HTMLElement | null>(null);
|
||||
const taskStatusChartRef = ref<HTMLElement | null>(null);
|
||||
const exceedanceTrendChartRef = ref<HTMLElement | null>(null);
|
||||
const pollutantTrendChartRef = ref<HTMLElement | null>(null);
|
||||
|
||||
// --- Timer Ref ---
|
||||
const timer = ref<number | null>(null);
|
||||
|
||||
// --- Loading & Data State ---
|
||||
const loading = reactive({
|
||||
stats: false,
|
||||
aqi: false,
|
||||
tasks: false,
|
||||
trend: false,
|
||||
coverage: false,
|
||||
pollutantTrend: false,
|
||||
});
|
||||
|
||||
const errors = reactive({
|
||||
stats: false,
|
||||
aqi: false,
|
||||
tasks: false,
|
||||
trend: false,
|
||||
coverage: false,
|
||||
pollutantTrend: false,
|
||||
});
|
||||
|
||||
const statsData = ref<Partial<DashboardStatsDTO & TaskStatsDTO>>({});
|
||||
const aqiDistributionData = ref<AqiDistributionDTO[]>([]);
|
||||
const taskStatusData = ref<TaskStatsDTO>({ totalTasks: 0, completedTasks: 0, completionRate: 0 });
|
||||
const exceedanceTrendData = ref<TrendDataPointDTO[]>([]);
|
||||
const gridCoverageData = ref<GridCoverageDTO[]>([]);
|
||||
const pollutantTrendData = ref<Record<string, TrendDataPointDTO[]>>({});
|
||||
const currentTime = ref(new Date().toLocaleString());
|
||||
|
||||
// 污染物类型选择
|
||||
const selectedPollutantType = ref<string>('ALL');
|
||||
const pollutantTypes = [
|
||||
{ label: '全部污染物', value: 'ALL' },
|
||||
{ label: 'PM2.5', value: 'PM25' },
|
||||
{ label: '臭氧(O₃)', value: 'O3' },
|
||||
{ label: '二氧化氮(NO₂)', value: 'NO2' },
|
||||
{ label: '二氧化硫(SO₂)', value: 'SO2' },
|
||||
{ label: '其他污染物', value: 'OTHER' }
|
||||
];
|
||||
|
||||
// 处理污染物类型变化
|
||||
const handlePollutantTypeChange = () => {
|
||||
console.log('选择的污染物类型:', selectedPollutantType.value);
|
||||
// 先销毁旧的图表实例
|
||||
if (pollutantTrendChartRef.value) {
|
||||
const oldChart = echarts.getInstanceByDom(pollutantTrendChartRef.value);
|
||||
if (oldChart) {
|
||||
oldChart.dispose();
|
||||
}
|
||||
}
|
||||
// 然后重新初始化图表
|
||||
nextTick(() => {
|
||||
initPollutantTrendChart();
|
||||
});
|
||||
};
|
||||
|
||||
// --- Timer for current time ---
|
||||
const timeInterval = setInterval(() => {
|
||||
currentTime.value = new Date().toLocaleString();
|
||||
}, 1000);
|
||||
|
||||
// --- KPI Stats ---
|
||||
const kpiStats = computed(() => [
|
||||
{
|
||||
title: '待办任务总数',
|
||||
value: statsData.value?.totalTasks ?? 0,
|
||||
icon: Briefcase,
|
||||
color: '#409EFF',
|
||||
},
|
||||
{
|
||||
title: '待处理任务',
|
||||
value: (statsData.value?.totalTasks ?? 0) - (statsData.value?.completedTasks ?? 0),
|
||||
icon: CircleCheck,
|
||||
color: '#67C23A',
|
||||
},
|
||||
{
|
||||
title: '待人工审核反馈',
|
||||
value: statsData.value?.totalFeedbacks ?? 0,
|
||||
icon: WarnTriangleFilled,
|
||||
color: '#E6A23C',
|
||||
},
|
||||
{
|
||||
title: '活动用户数',
|
||||
value: statsData.value?.activeGridWorkers ?? 0,
|
||||
icon: Grid,
|
||||
color: '#F56C6C',
|
||||
}
|
||||
]);
|
||||
|
||||
// --- Chart Logic ---
|
||||
|
||||
const getAqiColor = (level: string): string => {
|
||||
const colorMap: { [key: string]: string } = {
|
||||
'Good': '#67c23a',
|
||||
'Moderate': '#f5c85b',
|
||||
'Unhealthy for Sensitive Groups': '#f5a623',
|
||||
'Unhealthy': '#d0021b',
|
||||
'Very Unhealthy': '#8b0000',
|
||||
'Hazardous': '#4a0000',
|
||||
};
|
||||
return colorMap[level] || '#999';
|
||||
};
|
||||
|
||||
const initAqiChart = () => {
|
||||
if (aqiDistributionChartRef.value && aqiDistributionData.value.length > 0) {
|
||||
const chart = echarts.init(aqiDistributionChartRef.value);
|
||||
chart.setOption({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
top: '85%',
|
||||
left: 'center',
|
||||
textStyle: { fontWeight: 'bold' },
|
||||
formatter: function(name: string) {
|
||||
// 如果名称太长,截断并添加省略号
|
||||
return name.length > 10 ? name.slice(0, 10) + '...' : name;
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: 'AQI 分布',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '40%'],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 },
|
||||
label: {
|
||||
show: true,
|
||||
position: 'outside',
|
||||
formatter: '{d}%',
|
||||
overflow: 'truncate',
|
||||
width: 80
|
||||
},
|
||||
labelLine: {
|
||||
length: 15,
|
||||
length2: 10,
|
||||
smooth: true
|
||||
},
|
||||
emphasis: {
|
||||
label: { show: true, fontSize: '18', fontWeight: 'bold' },
|
||||
itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' }
|
||||
},
|
||||
data: aqiDistributionData.value.map(item => ({
|
||||
value: item.count,
|
||||
name: item.level,
|
||||
itemStyle: { color: getAqiColor(item.level) }
|
||||
})),
|
||||
}]
|
||||
});
|
||||
|
||||
// 响应式调整
|
||||
window.addEventListener('resize', () => chart.resize());
|
||||
}
|
||||
};
|
||||
|
||||
const initTaskChart = () => {
|
||||
if (taskStatusChartRef.value && taskStatusData.value.totalTasks > 0) {
|
||||
const chart = echarts.init(taskStatusChartRef.value);
|
||||
const notCompleted = taskStatusData.value.totalTasks - taskStatusData.value.completedTasks;
|
||||
chart.setOption({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
top: '85%',
|
||||
left: 'center',
|
||||
textStyle: { fontWeight: 'bold' }
|
||||
},
|
||||
series: [{
|
||||
name: '任务状态',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '40%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'inside',
|
||||
formatter: '{d}%',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
emphasis: {
|
||||
label: { show: true, fontSize: '20', fontWeight: 'bold' },
|
||||
itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' }
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: taskStatusData.value.completedTasks,
|
||||
name: '已完成',
|
||||
itemStyle: {
|
||||
color: '#67c23a',
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
value: notCompleted,
|
||||
name: '未完成',
|
||||
itemStyle: {
|
||||
color: '#f56c6c',
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}
|
||||
},
|
||||
],
|
||||
}]
|
||||
});
|
||||
|
||||
// 响应式调整
|
||||
window.addEventListener('resize', () => chart.resize());
|
||||
}
|
||||
};
|
||||
|
||||
const initTrendChart = () => {
|
||||
if (exceedanceTrendChartRef.value && exceedanceTrendData.value.length > 0) {
|
||||
const chart = echarts.init(exceedanceTrendChartRef.value);
|
||||
|
||||
// 确保数据格式正确
|
||||
console.log("趋势数据详情:", JSON.stringify(exceedanceTrendData.value));
|
||||
|
||||
// 准备数据
|
||||
const xAxisData = exceedanceTrendData.value.map(p => p.yearMonth || '未知');
|
||||
const seriesData = exceedanceTrendData.value.map(p => p.count || 0);
|
||||
|
||||
console.log("X轴数据:", xAxisData);
|
||||
console.log("系列数据:", seriesData);
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '过去12个月污染超标情况',
|
||||
left: 'center',
|
||||
top: 0,
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'normal',
|
||||
color: '#303133'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
shadowStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.05)'
|
||||
}
|
||||
},
|
||||
formatter: function(params: any) {
|
||||
const data = params[0];
|
||||
return `${data.name}<br/>${data.seriesName}: <strong>${data.value}</strong> 次`;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: xAxisData,
|
||||
axisLabel: {
|
||||
interval: 'auto',
|
||||
rotate: 45,
|
||||
formatter: function(value: string) {
|
||||
if (!value || value === '未知') return '未知';
|
||||
// 将 yyyy-MM 格式转换为 yyyy年MM月
|
||||
const parts = value.split('-');
|
||||
if (parts.length === 2) {
|
||||
return `${parts[0]}年${parts[1]}月`;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
margin: 14,
|
||||
hideOverlap: true
|
||||
},
|
||||
name: '',
|
||||
nameTextStyle: {
|
||||
fontWeight: 'bold',
|
||||
color: '#606266'
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#DCDFE6'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
alignWithLabel: true
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '',
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#DCDFE6'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
color: '#EBEEF5'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
formatter: '{value} 次'
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: '超标次数',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: seriesData,
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', name: '最大值' },
|
||||
{ type: 'min', name: '最小值' }
|
||||
],
|
||||
symbolSize: 60,
|
||||
itemStyle: {
|
||||
color: '#F56C6C',
|
||||
shadowBlur: 10,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)'
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
markLine: {
|
||||
data: [{ type: 'average', name: '平均值' }],
|
||||
lineStyle: {
|
||||
color: '#E6A23C',
|
||||
type: 'dashed',
|
||||
width: 2
|
||||
},
|
||||
label: {
|
||||
formatter: '平均: {c} 次',
|
||||
position: 'middle',
|
||||
color: '#E6A23C',
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
padding: [4, 8],
|
||||
borderRadius: 4
|
||||
}
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [{
|
||||
offset: 0, color: 'rgba(245, 108, 108, 0.7)'
|
||||
}, {
|
||||
offset: 0.5, color: 'rgba(245, 108, 108, 0.3)'
|
||||
}, {
|
||||
offset: 1, color: 'rgba(245, 108, 108, 0.05)'
|
||||
}]
|
||||
},
|
||||
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
||||
shadowBlur: 10
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#f56c6c',
|
||||
width: 4,
|
||||
shadowColor: 'rgba(245, 108, 108, 0.5)',
|
||||
shadowBlur: 10
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#f56c6c',
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff',
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
shadowBlur: 5
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
borderWidth: 6,
|
||||
shadowBlur: 20
|
||||
},
|
||||
lineStyle: {
|
||||
width: 6
|
||||
}
|
||||
},
|
||||
// 增加动画效果,但速度更慢
|
||||
animationDuration: 3000,
|
||||
animationEasing: 'cubicOut',
|
||||
animationDelay: function (idx: number) {
|
||||
return idx * 300;
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
// 应用配置
|
||||
chart.setOption(option);
|
||||
|
||||
// 响应式调整
|
||||
window.addEventListener('resize', () => chart.resize());
|
||||
} else {
|
||||
console.warn("无法初始化趋势图表,元素不存在或数据为空",
|
||||
{ hasRef: !!exceedanceTrendChartRef.value, dataLength: exceedanceTrendData.value.length });
|
||||
}
|
||||
};
|
||||
|
||||
const initPollutantTrendChart = () => {
|
||||
nextTick(() => {
|
||||
if (pollutantTrendChartRef.value && Object.keys(pollutantTrendData.value).length > 0) {
|
||||
const chart = echarts.init(pollutantTrendChartRef.value);
|
||||
|
||||
// 确保数据格式正确
|
||||
console.log("污染物趋势数据详情:", JSON.stringify(pollutantTrendData.value));
|
||||
console.log("当前选择的污染物类型:", selectedPollutantType.value);
|
||||
|
||||
// 污染物类型对应的颜色
|
||||
const colorMap: Record<string, string> = {
|
||||
'PM25': '#ff7f50',
|
||||
'O3': '#6a5acd',
|
||||
'NO2': '#32cd32',
|
||||
'SO2': '#ffd700',
|
||||
'OTHER': '#808080'
|
||||
};
|
||||
|
||||
// 获取所有月份
|
||||
const allMonths = new Set<string>();
|
||||
Object.keys(pollutantTrendData.value).forEach(type => {
|
||||
pollutantTrendData.value[type].forEach(point => {
|
||||
if (point.yearMonth) {
|
||||
allMonths.add(point.yearMonth);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 转换为数组并排序
|
||||
const xAxisData = Array.from(allMonths).sort();
|
||||
|
||||
// 准备系列数据
|
||||
let series = [];
|
||||
|
||||
// 设置动画时间,全部显示时稍慢,单个显示时较快
|
||||
const animationDuration = selectedPollutantType.value === 'ALL' ? 2000 : 1200;
|
||||
const animationDelayTime = selectedPollutantType.value === 'ALL' ? 200 : 100;
|
||||
|
||||
if (selectedPollutantType.value === 'ALL') {
|
||||
// 显示所有污染物
|
||||
series = Object.keys(pollutantTrendData.value).map(type => {
|
||||
// 创建一个映射,方便查找
|
||||
const dataMap = new Map<string, number>();
|
||||
pollutantTrendData.value[type].forEach(point => {
|
||||
if (point.yearMonth) {
|
||||
dataMap.set(point.yearMonth, point.count / 100); // 除以100,因为后端乘以了100
|
||||
}
|
||||
});
|
||||
|
||||
// 生成系列数据
|
||||
const seriesData = xAxisData.map(month => dataMap.get(month) || 0);
|
||||
|
||||
return {
|
||||
name: getPollutantDisplayName(type),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
data: seriesData,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: colorMap[type] || '#5470c6'
|
||||
},
|
||||
itemStyle: {
|
||||
color: colorMap[type] || '#5470c6'
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
lineStyle: {
|
||||
width: 5
|
||||
}
|
||||
},
|
||||
animationDelay: (idx: number) => idx * animationDelayTime
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// 检查选择的污染物类型是否有数据
|
||||
if (!pollutantTrendData.value[selectedPollutantType.value]) {
|
||||
chart.setOption({
|
||||
title: {
|
||||
text: `暂无${getPollutantDisplayName(selectedPollutantType.value)}趋势数据`,
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
color: '#909399'
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 准备数据
|
||||
const trendData = pollutantTrendData.value[selectedPollutantType.value];
|
||||
|
||||
// 获取月份和数据
|
||||
const singleXAxisData = trendData.map(point => point.yearMonth || '未知').sort();
|
||||
const seriesData = trendData.map(point => point.count / 100); // 除以100,因为后端乘以了100
|
||||
|
||||
const color = colorMap[selectedPollutantType.value] || '#5470c6';
|
||||
|
||||
series = [{
|
||||
name: getPollutantDisplayName(selectedPollutantType.value),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
data: seriesData,
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', name: '最大值' },
|
||||
{ type: 'min', name: '最小值' }
|
||||
],
|
||||
symbolSize: 60,
|
||||
itemStyle: {
|
||||
color: color,
|
||||
shadowBlur: 10,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)'
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
markLine: {
|
||||
data: [{ type: 'average', name: '平均值' }],
|
||||
lineStyle: {
|
||||
color: '#E6A23C',
|
||||
type: 'dashed',
|
||||
width: 2
|
||||
},
|
||||
label: {
|
||||
formatter: '平均: {c}',
|
||||
position: 'middle',
|
||||
color: '#E6A23C',
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
padding: [4, 8],
|
||||
borderRadius: 4
|
||||
}
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [{
|
||||
offset: 0, color: `${color}CC` // 80% 透明度
|
||||
}, {
|
||||
offset: 0.5, color: `${color}66` // 40% 透明度
|
||||
}, {
|
||||
offset: 1, color: `${color}11` // 10% 透明度
|
||||
}]
|
||||
},
|
||||
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
||||
shadowBlur: 10
|
||||
},
|
||||
lineStyle: {
|
||||
color: color,
|
||||
width: 4,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
shadowBlur: 10
|
||||
},
|
||||
itemStyle: {
|
||||
color: color,
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff',
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
shadowBlur: 5
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
borderWidth: 6,
|
||||
shadowBlur: 20
|
||||
},
|
||||
lineStyle: {
|
||||
width: 6
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: selectedPollutantType.value === 'ALL' ? '所有污染物浓度月度趋势' : `${getPollutantDisplayName(selectedPollutantType.value)}浓度月度趋势`,
|
||||
left: 'center',
|
||||
top: 0,
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'normal',
|
||||
color: '#303133'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
formatter: function(params: any) {
|
||||
if (params.length === 0) return '';
|
||||
|
||||
let result = params[0].name + '<br/>';
|
||||
params.forEach((param: any) => {
|
||||
result += `${param.marker} ${param.seriesName}: <strong>${param.value.toFixed(2)}</strong><br/>`;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: selectedPollutantType.value === 'ALL'
|
||||
? Object.keys(pollutantTrendData.value).map(type => getPollutantDisplayName(type))
|
||||
: [getPollutantDisplayName(selectedPollutantType.value)],
|
||||
top: selectedPollutantType.value === 'ALL' ? '10%' : '5%',
|
||||
type: 'scroll',
|
||||
textStyle: {
|
||||
color: '#666'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
top: selectedPollutantType.value === 'ALL' ? '25%' : '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: xAxisData,
|
||||
axisLabel: {
|
||||
interval: 'auto',
|
||||
rotate: 45,
|
||||
formatter: function(value: string) {
|
||||
if (!value || value === '未知') return '未知';
|
||||
// 将 yyyy-MM 格式转换为 yyyy年MM月
|
||||
const parts = value.split('-');
|
||||
if (parts.length === 2) {
|
||||
return `${parts[0]}年${parts[1]}月`;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
margin: 14,
|
||||
hideOverlap: true
|
||||
},
|
||||
name: '',
|
||||
nameTextStyle: {
|
||||
fontWeight: 'bold',
|
||||
color: '#606266'
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '',
|
||||
axisLine: {
|
||||
show: true
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dashed'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
formatter: '{value} μg/m³',
|
||||
margin: 16
|
||||
}
|
||||
},
|
||||
series: series,
|
||||
animationDuration: animationDuration,
|
||||
animationEasing: 'cubicOut' as const,
|
||||
animationDelay: function(idx: number) {
|
||||
return idx * animationDelayTime;
|
||||
}
|
||||
};
|
||||
|
||||
// 应用配置
|
||||
chart.setOption(option);
|
||||
|
||||
// 响应式调整
|
||||
window.addEventListener('resize', () => chart.resize());
|
||||
} else {
|
||||
console.warn("无法初始化污染物趋势图表,元素不存在或数据为空",
|
||||
{ hasRef: !!pollutantTrendChartRef.value, dataLength: Object.keys(pollutantTrendData.value).length });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// --- Utility Functions ---
|
||||
const getCoverageColor = (percentage: number) => {
|
||||
if (percentage < 50) return '#f56c6c';
|
||||
if (percentage < 80) return '#e6a23c';
|
||||
return '#67c23a';
|
||||
};
|
||||
|
||||
// 获取污染物显示名称
|
||||
const getPollutantDisplayName = (type: string): string => {
|
||||
const nameMap: Record<string, string> = {
|
||||
'PM25': 'PM2.5',
|
||||
'O3': '臭氧(O₃)',
|
||||
'NO2': '二氧化氮(NO₂)',
|
||||
'SO2': '二氧化硫(SO₂)',
|
||||
'OTHER': '其他污染物'
|
||||
};
|
||||
|
||||
return nameMap[type] || type;
|
||||
};
|
||||
|
||||
const percentageFormat = (percentage: number) => {
|
||||
return `${percentage}%`;
|
||||
};
|
||||
|
||||
const tableRowClassName = ({ row, rowIndex }: { row: any, rowIndex: number }) => {
|
||||
if (rowIndex % 2 === 0) {
|
||||
return 'even-row';
|
||||
}
|
||||
return 'odd-row';
|
||||
};
|
||||
|
||||
const coverageTableRowClassName = ({ row }: { row: any }) => {
|
||||
if (row.coverageRate < 50) return 'coverage-row-low';
|
||||
if (row.coverageRate < 80) return 'coverage-row-medium';
|
||||
return 'coverage-row-high';
|
||||
};
|
||||
|
||||
// --- Data Fetching ---
|
||||
const refreshAqiData = () => {
|
||||
loading.aqi = true;
|
||||
getAqiDistribution().then(data => {
|
||||
aqiDistributionData.value = data;
|
||||
}).catch(e => {
|
||||
console.error('获取AQI分布数据失败:', e);
|
||||
ElMessage.error('获取AQI分布数据失败');
|
||||
}).finally(() => {
|
||||
loading.aqi = false;
|
||||
nextTick(initAqiChart);
|
||||
});
|
||||
};
|
||||
|
||||
const refreshTaskData = () => {
|
||||
loading.tasks = true;
|
||||
getTaskCompletionStats().then(data => {
|
||||
taskStatusData.value = data;
|
||||
}).catch(e => {
|
||||
console.error('获取任务统计数据失败:', e);
|
||||
ElMessage.error('获取任务统计数据失败');
|
||||
}).finally(() => {
|
||||
loading.tasks = false;
|
||||
nextTick(initTaskChart);
|
||||
});
|
||||
};
|
||||
|
||||
const refreshTrendData = () => {
|
||||
loading.trend = true;
|
||||
getMonthlyExceedanceTrend().then(data => {
|
||||
console.log("获取到的月度趋势数据:", JSON.stringify(data));
|
||||
exceedanceTrendData.value = data;
|
||||
}).catch(e => {
|
||||
console.error('获取月度趋势数据失败:', e);
|
||||
ElMessage.error('获取月度趋势数据失败');
|
||||
}).finally(() => {
|
||||
loading.trend = false;
|
||||
nextTick(() => {
|
||||
console.log("准备初始化趋势图,数据:", JSON.stringify(exceedanceTrendData.value));
|
||||
initTrendChart();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const refreshCoverageData = () => {
|
||||
loading.coverage = true;
|
||||
getGridCoverageByCity().then(data => {
|
||||
gridCoverageData.value = data;
|
||||
}).catch(e => {
|
||||
console.error('获取网格覆盖率数据失败:', e);
|
||||
ElMessage.error('获取网格覆盖率数据失败');
|
||||
}).finally(() => {
|
||||
loading.coverage = false;
|
||||
});
|
||||
};
|
||||
|
||||
const refreshPollutantTrendData = async () => {
|
||||
console.log('开始获取污染物趋势数据...');
|
||||
loading.pollutantTrend = true;
|
||||
errors.pollutantTrend = false;
|
||||
try {
|
||||
const data = await getPollutantMonthlyTrends(selectedPollutantType.value);
|
||||
console.log('获取到的污染物趋势数据:', data);
|
||||
pollutantTrendData.value = data;
|
||||
} catch (error) {
|
||||
console.error('获取污染物趋势数据失败:', error);
|
||||
errors.pollutantTrend = true;
|
||||
pollutantTrendData.value = {}; // Clear data on error, ensure it's an object
|
||||
} finally {
|
||||
loading.pollutantTrend = false;
|
||||
console.log('污染物趋势数据获取流程结束。');
|
||||
}
|
||||
};
|
||||
|
||||
const refreshAllData = () => {
|
||||
console.log("开始刷新所有数据...");
|
||||
|
||||
loading.stats = true;
|
||||
loading.aqi = true;
|
||||
loading.tasks = true;
|
||||
loading.trend = true;
|
||||
loading.coverage = true;
|
||||
loading.pollutantTrend = true;
|
||||
|
||||
getDashboardStats().then(data => {
|
||||
console.log("获取到的关键指标数据:", data);
|
||||
statsData.value = data;
|
||||
}).catch(e => {
|
||||
console.error('获取关键指标失败:', e);
|
||||
ElMessage.error('获取关键指标失败');
|
||||
}).finally(() => {
|
||||
loading.stats = false;
|
||||
});
|
||||
|
||||
refreshAqiData();
|
||||
refreshTaskData();
|
||||
refreshTrendData();
|
||||
refreshCoverageData();
|
||||
refreshPollutantTrendData();
|
||||
|
||||
console.log("所有数据刷新请求已发送");
|
||||
};
|
||||
|
||||
// --- Lifecycle Hooks ---
|
||||
watch(pollutantTrendData, (newData) => {
|
||||
if (newData && Object.keys(newData).length > 0) {
|
||||
initPollutantTrendChart();
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
onMounted(() => {
|
||||
console.log("组件挂载,开始加载数据...");
|
||||
if (canViewDashboard.value) {
|
||||
refreshAllData();
|
||||
timer.value = setInterval(refreshAllData, 300000); // 5分钟刷新一次
|
||||
} else {
|
||||
console.log(`用户角色: ${user.value?.role}, 无需加载仪表盘数据。`);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value);
|
||||
}
|
||||
clearInterval(timeInterval);
|
||||
});
|
||||
|
||||
// --- Navigation ---
|
||||
const goBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
// 详细数据对话框
|
||||
const detailDialogVisible = ref(false);
|
||||
const detailDialogTitle = ref('');
|
||||
const detailTableData = ref<any[]>([]);
|
||||
const detailTableColumns = ref<any[]>([]);
|
||||
|
||||
// 显示AQI详细数据
|
||||
const showAqiDetailData = () => {
|
||||
detailDialogTitle.value = 'AQI级别分布详细数据';
|
||||
detailTableData.value = aqiDistributionData.value.map(item => ({
|
||||
level: item.level,
|
||||
count: item.count,
|
||||
percentage: ((item.count / aqiDistributionData.value.reduce((sum, i) => sum + i.count, 0)) * 100).toFixed(2) + '%'
|
||||
}));
|
||||
detailTableColumns.value = [
|
||||
{ prop: 'level', label: 'AQI级别', width: '180' },
|
||||
{ prop: 'count', label: '记录数量', width: '120' },
|
||||
{ prop: 'percentage', label: '百分比', width: '120' }
|
||||
];
|
||||
detailDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 显示任务详细数据
|
||||
const showTaskDetailData = () => {
|
||||
detailDialogTitle.value = '任务状态分布详细数据';
|
||||
detailTableData.value = [
|
||||
{ status: '已完成', count: taskStatusData.value.completedTasks, percentage: taskStatusData.value.completionRate.toFixed(2) + '%' },
|
||||
{ status: '未完成', count: taskStatusData.value.totalTasks - taskStatusData.value.completedTasks, percentage: (100 - taskStatusData.value.completionRate).toFixed(2) + '%' }
|
||||
];
|
||||
detailTableColumns.value = [
|
||||
{ prop: 'status', label: '任务状态', width: '180' },
|
||||
{ prop: 'count', label: '任务数量', width: '120' },
|
||||
{ prop: 'percentage', label: '百分比', width: '120' }
|
||||
];
|
||||
detailDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 显示超标趋势详细数据
|
||||
const showTrendDetailData = () => {
|
||||
detailDialogTitle.value = '月度超标趋势详细数据';
|
||||
detailTableData.value = exceedanceTrendData.value.map(item => ({
|
||||
yearMonth: item.yearMonth || '未知',
|
||||
count: item.count
|
||||
})).sort((a, b) => a.yearMonth.localeCompare(b.yearMonth));
|
||||
detailTableColumns.value = [
|
||||
{ prop: 'yearMonth', label: '年月', width: '180' },
|
||||
{ prop: 'count', label: '超标次数', width: '120' }
|
||||
];
|
||||
detailDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 显示污染物趋势详细数据
|
||||
const showPollutantDetailData = () => {
|
||||
detailDialogTitle.value = '污染物趋势详细数据';
|
||||
|
||||
if (selectedPollutantType.value === 'ALL') {
|
||||
// 获取所有月份
|
||||
const allMonths = new Set<string>();
|
||||
Object.keys(pollutantTrendData.value).forEach(type => {
|
||||
pollutantTrendData.value[type].forEach(point => {
|
||||
if (point.yearMonth) {
|
||||
allMonths.add(point.yearMonth);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 转换为数组并排序
|
||||
const months = Array.from(allMonths).sort();
|
||||
|
||||
// 创建数据映射
|
||||
const dataMap: Record<string, Record<string, number>> = {};
|
||||
months.forEach(month => {
|
||||
dataMap[month] = {};
|
||||
Object.keys(pollutantTrendData.value).forEach(type => {
|
||||
dataMap[month][type] = 0;
|
||||
});
|
||||
});
|
||||
|
||||
// 填充数据
|
||||
Object.keys(pollutantTrendData.value).forEach(type => {
|
||||
pollutantTrendData.value[type].forEach(point => {
|
||||
if (point.yearMonth) {
|
||||
dataMap[point.yearMonth][type] = point.count / 100;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 转换为表格数据
|
||||
detailTableData.value = months.map(month => {
|
||||
const row: any = { yearMonth: month };
|
||||
Object.keys(pollutantTrendData.value).forEach(type => {
|
||||
row[type] = dataMap[month][type].toFixed(2);
|
||||
});
|
||||
return row;
|
||||
});
|
||||
|
||||
// 创建表格列
|
||||
detailTableColumns.value = [
|
||||
{ prop: 'yearMonth', label: '年月', width: '120' },
|
||||
...Object.keys(pollutantTrendData.value).map(type => ({
|
||||
prop: type,
|
||||
label: getPollutantDisplayName(type),
|
||||
width: '120'
|
||||
}))
|
||||
];
|
||||
} else {
|
||||
// 单个污染物
|
||||
const type = selectedPollutantType.value;
|
||||
detailTableData.value = pollutantTrendData.value[type].map(item => ({
|
||||
yearMonth: item.yearMonth || '未知',
|
||||
value: (item.count / 100).toFixed(2)
|
||||
})).sort((a, b) => a.yearMonth.localeCompare(b.yearMonth));
|
||||
|
||||
detailTableColumns.value = [
|
||||
{ prop: 'yearMonth', label: '年月', width: '180' },
|
||||
{ prop: 'value', label: `${getPollutantDisplayName(type)}浓度 (μg/m³)`, width: '180' }
|
||||
];
|
||||
}
|
||||
|
||||
detailDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 关闭详细数据对话框
|
||||
const closeDetailDialog = () => {
|
||||
detailDialogVisible.value = false;
|
||||
};
|
||||
|
||||
// 导出详细数据
|
||||
const exportDetailData = () => {
|
||||
// 创建CSV内容
|
||||
let csvContent = '';
|
||||
|
||||
// 添加表头
|
||||
csvContent += detailTableColumns.value.map(col => `"${col.label}"`).join(',') + '\n';
|
||||
|
||||
// 添加数据行
|
||||
detailTableData.value.forEach(row => {
|
||||
const rowData = detailTableColumns.value.map(col => {
|
||||
const value = row[col.prop];
|
||||
return `"${value !== undefined ? value : ''}"`;
|
||||
});
|
||||
csvContent += rowData.join(',') + '\n';
|
||||
});
|
||||
|
||||
// 创建Blob对象
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
|
||||
// 创建下载链接
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `${detailDialogTitle.value}.csv`);
|
||||
link.style.visibility = 'hidden';
|
||||
|
||||
// 添加到DOM并触发点击
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// 清理
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
ElMessage.success('数据导出成功');
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e4ebf5 100%);
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.dashboard-header h1 {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dashboard-header h1::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 24px;
|
||||
background: linear-gradient(to bottom, #409EFF, #67C23A);
|
||||
margin-right: 12px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.dashboard-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-rows: auto auto auto auto;
|
||||
gap: 20px;
|
||||
padding: 24px;
|
||||
background-color: #f5f7fa;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.grid-item:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
grid-column: span 1;
|
||||
grid-row: 1 / 2;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.kpi-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, rgba(103, 194, 58, 0.6) 0%, rgba(103, 194, 58, 1) 100%);
|
||||
transform: scaleX(0.7);
|
||||
transform-origin: left;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.kpi-card:hover::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
grid-column: span 2;
|
||||
height: 350px; /* Ensure charts have a consistent height */
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chart-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
height: 6px;
|
||||
background: linear-gradient(90deg, #409EFF, #67C23A);
|
||||
opacity: 0.7;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 0 16px 0;
|
||||
border-bottom: 1px dashed #EBEEF5;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.chart-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
position: relative;
|
||||
height: calc(100% - 50px);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.loading-overlay .el-icon {
|
||||
font-size: 24px;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.aqi-distribution {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 2 / 3;
|
||||
}
|
||||
|
||||
.task-status {
|
||||
grid-column: 3 / 5;
|
||||
grid-row: 2 / 3;
|
||||
}
|
||||
|
||||
.exceedance-trend {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 3 / 4;
|
||||
}
|
||||
|
||||
.pollutant-trend {
|
||||
grid-column: 3 / 5;
|
||||
grid-row: 3 / 4;
|
||||
}
|
||||
|
||||
.grid-coverage {
|
||||
grid-column: 1 / 5;
|
||||
grid-row: 4 / 5;
|
||||
}
|
||||
|
||||
.kpi-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.kpi-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: white;
|
||||
font-size: 28px;
|
||||
margin-right: 20px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.kpi-icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0) 70%);
|
||||
}
|
||||
|
||||
.kpi-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.kpi-title {
|
||||
font-size: 16px;
|
||||
color: #606266;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.kpi-value::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
left: 0;
|
||||
width: 30px;
|
||||
height: 3px;
|
||||
background-color: #409EFF;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #303133;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card-header span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 20px;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-card__header) {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
background-color: #f8f9fb;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px;
|
||||
height: calc(100% - 53px); /* Adjust based on header height */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.coverage-header {
|
||||
background-color: #f8f9fb;
|
||||
color: #303133;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.coverage-body {
|
||||
padding: 16px;
|
||||
height: calc(100% - 53px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.coverage-row-low {
|
||||
color: #f56c6c;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.coverage-row-medium {
|
||||
color: #e6a23c;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.coverage-row-high {
|
||||
color: #67c23a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.coverage-row-low td,
|
||||
.coverage-row-medium td,
|
||||
.coverage-row-high td {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
:deep(.el-progress-bar__outer) {
|
||||
border-radius: 10px;
|
||||
background-color: #ebeef5;
|
||||
}
|
||||
|
||||
:deep(.el-progress-bar__inner) {
|
||||
border-radius: 10px;
|
||||
transition: all 0.6s ease;
|
||||
}
|
||||
|
||||
:deep(.el-progress__text) {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:deep(.el-table th) {
|
||||
background-color: #f8f9fb !important;
|
||||
color: #303133;
|
||||
font-weight: bold;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
:deep(.el-table td) {
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
:deep(.el-table--border) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-footer {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.even-row {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.odd-row {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.even-row td,
|
||||
.odd-row td {
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.even-row:hover td,
|
||||
.odd-row:hover td {
|
||||
background-color: #ecf5ff !important;
|
||||
}
|
||||
|
||||
.detail-dialog {
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-dialog :deep(.el-dialog__header) {
|
||||
background: linear-gradient(135deg, #409EFF, #67C23A);
|
||||
padding: 20px;
|
||||
border-bottom: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.detail-dialog :deep(.el-dialog__title) {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.detail-dialog :deep(.el-dialog__headerbtn) {
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.detail-dialog :deep(.el-dialog__headerbtn .el-dialog__close) {
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.detail-dialog :deep(.el-dialog__body) {
|
||||
padding: 24px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
background-color: #f9fafc;
|
||||
}
|
||||
|
||||
.detail-table-container {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.detail-table-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 6px;
|
||||
background: linear-gradient(90deg, #409EFF, #67C23A);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.detail-dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
background-color: #f9fafc;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.even-row {
|
||||
background-color: #f9fafc;
|
||||
}
|
||||
|
||||
.odd-row {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.even-row td,
|
||||
.odd-row td {
|
||||
transition: all 0.3s;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.even-row:hover td,
|
||||
.odd-row:hover td {
|
||||
background-color: #ecf5ff !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.welcome-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 80vh;
|
||||
}
|
||||
</style>
|
||||
240
ems-frontend/ems-monitoring-system/src/views/LoginView.vue
Normal file
240
ems-frontend/ems-monitoring-system/src/views/LoginView.vue
Normal file
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<el-card class="login-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>环境监督系统</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" class="login-form" label-position="top" autocomplete="off" @submit.prevent="handleLogin">
|
||||
<el-form-item label="邮箱" prop="email" class="form-item-tight">
|
||||
<el-input v-model="loginForm.email" placeholder="请输入邮箱" size="large" :prefix-icon="User" clearable autocomplete="off"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password" class="form-item-tight">
|
||||
<el-input type="password" v-model="loginForm.password" placeholder="请输入密码" size="large" :prefix-icon="Lock" show-password autocomplete="new-password"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item class="login-btn-item">
|
||||
<el-button type="primary" @click="handleLogin" :loading="loading" class="login-button" size="large">登 录</el-button>
|
||||
</el-form-item>
|
||||
<div class="extra-links">
|
||||
<el-link type="info" @click="$router.push('/register')">注册账号</el-link>
|
||||
<el-link type="info" @click="$router.push('/forgot-password')">找回密码</el-link>
|
||||
</div>
|
||||
|
||||
<el-form-item class="guest-btn-item">
|
||||
<el-button @click="handleGuestAccess" class="guest-button" size="large" :icon="Promotion">作为游客提交反馈</el-button>
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { User, Lock, Promotion } from '@element-plus/icons-vue';
|
||||
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
const loginFormRef = ref<FormInstance>();
|
||||
const loading = ref(false);
|
||||
|
||||
const loginForm = reactive({
|
||||
email: '',
|
||||
password: '',
|
||||
});
|
||||
|
||||
const loginRules = reactive<FormRules>({
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址', trigger: ['blur', 'change'] }
|
||||
],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||||
});
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return;
|
||||
await loginFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
try {
|
||||
await authStore.login(loginForm);
|
||||
ElMessage.success('登录成功,正在跳转...');
|
||||
} catch (error) {
|
||||
ElMessage.error('登录失败,请检查您的邮箱和密码。');
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleGuestAccess = () => {
|
||||
router.push('/public-feedback');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden; /* Prevent scrolling */
|
||||
background-image: url('https://images.unsplash.com/photo-1506744038136-46273834b3fb?ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
position: relative;
|
||||
width: 480px;
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
|
||||
animation: fadeIn 0.7s ease-in-out;
|
||||
background: rgba(245, 245, 245, 0.85); /* Light warm gray */
|
||||
backdrop-filter: blur(15px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
color: #004d40; /* Teal Gold color */
|
||||
padding: 25px 0 0 0; /* Removed bottom padding */
|
||||
border-bottom: 1px solid rgba(0,77,64,0.2);
|
||||
}
|
||||
|
||||
.login-form {
|
||||
padding: 10px 35px 20px 35px; /* Minimal top padding */
|
||||
}
|
||||
|
||||
.form-item-tight {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: #004d40; /* Teal Gold color */
|
||||
color: white;
|
||||
letter-spacing: 5px;
|
||||
padding-left: 10px; /* to compensate letter-spacing */
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(0, 77, 64, 0.4);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
background: #00695c;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 77, 64, 0.5);
|
||||
}
|
||||
|
||||
.login-btn-item {
|
||||
margin-top: 25px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.el-input__wrapper {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.el-input__wrapper:hover {
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary) inset !important;
|
||||
}
|
||||
|
||||
.el-input__wrapper.is-focus {
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary) inset !important;
|
||||
}
|
||||
|
||||
.extra-links {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.guest-btn-item {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.guest-button {
|
||||
width: 100%;
|
||||
border-color: #004d40;
|
||||
color: #004d40;
|
||||
}
|
||||
|
||||
.guest-button:hover {
|
||||
background-color: #e0f2f1;
|
||||
border-color: #00695c;
|
||||
color: #00695c;
|
||||
}
|
||||
|
||||
.extra-links .el-link {
|
||||
color: #bbb;
|
||||
font-size: 15px; /* Increased font size */
|
||||
}
|
||||
.extra-links .el-link:hover {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* This is a global style override to achieve the desired hover/focus effect */
|
||||
.el-input__wrapper {
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
.el-input__wrapper:hover {
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary, #409eff) inset !important;
|
||||
}
|
||||
|
||||
.el-input__wrapper.is-focus {
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary, #409eff) inset !important;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
color: #555 !important;
|
||||
font-weight: 500 !important;
|
||||
letter-spacing: 0.5px !important;
|
||||
font-size: 15px !important;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
color: #333 !important;
|
||||
font-size: 15px !important; /* Matched font size to label */
|
||||
}
|
||||
|
||||
.el-input__icon {
|
||||
font-size: 18px !important; /* Icon size can be slightly larger */
|
||||
}
|
||||
|
||||
.el-input__wrapper {
|
||||
background-color: rgba(255, 255, 255, 0.9) !important;
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
</style>
|
||||
12
ems-frontend/ems-monitoring-system/src/views/MapView.vue
Normal file
12
ems-frontend/ems-monitoring-system/src/views/MapView.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>地图模块</h1>
|
||||
<p>这里是地图模块页面。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>我的操作日志</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="logs" v-loading="loading" style="width: 100%" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="ipAddress" label="IP地址" width="150"></el-table-column>
|
||||
<el-table-column prop="operationTypeDesc" label="操作类型" width="120"></el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="200"></el-table-column>
|
||||
<el-table-column prop="targetId" label="对象ID" width="100"></el-table-column>
|
||||
<el-table-column prop="targetType" label="对象类型" width="120"></el-table-column>
|
||||
<el-table-column prop="createdAt" label="操作时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatTime(scope.row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getMyOperationLogs } from '@/api/log';
|
||||
import type { OperationLog } from '@/api/types';
|
||||
|
||||
const loading = ref(true);
|
||||
const logs = ref<OperationLog[]>([]);
|
||||
|
||||
const fetchLogs = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
logs.value = await getMyOperationLogs();
|
||||
} catch (error) {
|
||||
ElMessage.error('获取操作日志失败');
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (timeStr: string) => {
|
||||
if (!timeStr) return '';
|
||||
return new Date(timeStr).toLocaleString();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchLogs();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* You can add styles if needed */
|
||||
</style>
|
||||
228
ems-frontend/ems-monitoring-system/src/views/MyTasksView.vue
Normal file
228
ems-frontend/ems-monitoring-system/src/views/MyTasksView.vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<div class="my-tasks-view">
|
||||
<el-card class="page-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>我的任务</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 筛选区域 -->
|
||||
<div class="filter-container">
|
||||
<el-radio-group v-model="selectedStatus" @change="fetchTasks">
|
||||
<el-radio-button value="">全部</el-radio-button>
|
||||
<el-radio-button value="ASSIGNED">待接受</el-radio-button>
|
||||
<el-radio-button value="IN_PROGRESS">进行中</el-radio-button>
|
||||
<el-radio-button value="SUBMITTED">已提交</el-radio-button>
|
||||
<el-radio-button value="COMPLETED">已完成</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<el-table :data="tasks" v-loading="loading" stripe class="task-table" @row-click="handleRowClick">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusTagType(row.status)">{{ formatStatus(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="textAddress" label="地址" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column prop="severity" label="严重程度" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getSeverityTagType(row.severity)">{{ formatSeverity(row.severity) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="分配时间" width="180">
|
||||
<template #default="{ row }">
|
||||
<span>{{ new Date(row.createdAt).toLocaleString() }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="viewTaskDetails(row.id)">查看详情</el-button>
|
||||
<el-button v-if="row.status === 'ASSIGNED'" type="success" link size="small" @click="acceptTask(row.id)">接受</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-if="total > 0"
|
||||
class="pagination-container"
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive } from 'vue';
|
||||
import apiClient from '@/api';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import type { Page } from '@/api/types';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
// --- 类型定义 ---
|
||||
interface TaskSummary {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
status: string;
|
||||
assigneeName: string | null;
|
||||
createdAt: string;
|
||||
textAddress: string;
|
||||
imageUrl: string;
|
||||
severity: string;
|
||||
}
|
||||
|
||||
// --- 响应式状态 ---
|
||||
const tasks = ref<TaskSummary[]>([]);
|
||||
const loading = ref(false);
|
||||
const selectedStatus = ref<string | null>(null);
|
||||
const total = ref(0);
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
});
|
||||
const router = useRouter();
|
||||
|
||||
// --- API 调用 ---
|
||||
const fetchTasks = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: any = {
|
||||
page: pagination.page - 1,
|
||||
size: pagination.size,
|
||||
sort: 'createdAt,desc'
|
||||
};
|
||||
if (selectedStatus.value) {
|
||||
params.status = selectedStatus.value;
|
||||
}
|
||||
const response = await apiClient.get<Page<TaskSummary>>('/worker', { params });
|
||||
tasks.value = response.content;
|
||||
total.value = response.totalElements;
|
||||
} catch (error) {
|
||||
console.error('获取任务列表失败:', error);
|
||||
ElMessage.error('获取任务列表失败');
|
||||
tasks.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const acceptTask = async (taskId: number) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要接受这个任务吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info',
|
||||
});
|
||||
await apiClient.post(`/worker/${taskId}/accept`);
|
||||
ElMessage.success('任务已接受');
|
||||
fetchTasks(); // 刷新列表
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('接受任务失败:', error);
|
||||
ElMessage.error('接受任务失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --- 辅助函数 ---
|
||||
const formatStatus = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'ASSIGNED': '待接受',
|
||||
'IN_PROGRESS': '进行中',
|
||||
'SUBMITTED': '已提交',
|
||||
'COMPLETED': '已完成',
|
||||
'REJECTED': '已拒绝',
|
||||
};
|
||||
return map[status] || status;
|
||||
};
|
||||
|
||||
const getStatusTagType = (status: string): 'primary' | 'warning' | 'info' | 'success' | 'danger' => {
|
||||
const map: Record<string, 'primary' | 'warning' | 'info' | 'success' | 'danger'> = {
|
||||
'ASSIGNED': 'primary',
|
||||
'IN_PROGRESS': 'warning',
|
||||
'SUBMITTED': 'info',
|
||||
'COMPLETED': 'success',
|
||||
'REJECTED': 'danger',
|
||||
};
|
||||
return map[status] || 'info';
|
||||
};
|
||||
|
||||
const formatSeverity = (severity: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'LOW': '低',
|
||||
'MEDIUM': '中',
|
||||
'HIGH': '高',
|
||||
};
|
||||
return map[severity] || severity;
|
||||
};
|
||||
|
||||
const getSeverityTagType = (severity: string): 'info' | 'warning' | 'danger' => {
|
||||
const map: Record<string, 'info' | 'warning' | 'danger'> = {
|
||||
'LOW': 'info',
|
||||
'MEDIUM': 'warning',
|
||||
'HIGH': 'danger',
|
||||
};
|
||||
return map[severity] || 'info';
|
||||
};
|
||||
|
||||
// --- 事件处理 ---
|
||||
const viewTaskDetails = (taskId: number) => {
|
||||
router.push(`/my-tasks/${taskId}`);
|
||||
};
|
||||
|
||||
const handleRowClick = (row: TaskSummary) => {
|
||||
viewTaskDetails(row.id);
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.size = size;
|
||||
fetchTasks();
|
||||
};
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page;
|
||||
fetchTasks();
|
||||
};
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
onMounted(() => {
|
||||
fetchTasks();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.my-tasks-view {
|
||||
padding: 20px;
|
||||
}
|
||||
.page-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.card-header {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.task-table {
|
||||
width: 100%;
|
||||
}
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>操作日志</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="filter-container">
|
||||
<el-input v-model="filters.userId" placeholder="按用户ID搜索" style="width: 200px;"></el-input>
|
||||
<el-select v-model="filters.operationType" placeholder="按操作类型筛选" clearable>
|
||||
<el-option v-for="item in operationTypes" :key="item.value" :label="item.label" :value="item.value"></el-option>
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-model="filters.timeRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期">
|
||||
</el-date-picker>
|
||||
<el-button type="primary" :icon="Search" @click="fetchLogs">搜索</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="logs" v-loading="loading" style="width: 100%" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="userName" label="用户名" width="120"></el-table-column>
|
||||
<el-table-column prop="ipAddress" label="IP地址" width="150"></el-table-column>
|
||||
<el-table-column prop="operationTypeDesc" label="操作类型" width="120"></el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="200"></el-table-column>
|
||||
<el-table-column prop="targetId" label="对象ID" width="100"></el-table-column>
|
||||
<el-table-column prop="targetType" label="对象类型" width="120"></el-table-column>
|
||||
<el-table-column prop="createdAt" label="操作时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatTime(scope.row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import { getOperationLogs } from '@/api/log';
|
||||
import type { OperationLog } from '@/api/types';
|
||||
|
||||
const loading = ref(true);
|
||||
const logs = ref<OperationLog[]>([]);
|
||||
const filters = reactive({
|
||||
userId: '',
|
||||
operationType: '',
|
||||
timeRange: [] as [Date, Date] | [],
|
||||
});
|
||||
|
||||
const operationTypes = [
|
||||
{ label: "登录", value: "LOGIN" },
|
||||
{ label: "登出", value: "LOGOUT" },
|
||||
{ label: "创建", value: "CREATE" },
|
||||
{ label: "更新", value: "UPDATE" },
|
||||
{ label: "删除", value: "DELETE" },
|
||||
{ label: "分配任务", value: "ASSIGN_TASK" },
|
||||
{ label: "提交任务", value: "SUBMIT_TASK" },
|
||||
{ label: "审批任务", value: "APPROVE_TASK" },
|
||||
{ label: "拒绝任务", value: "REJECT_TASK" },
|
||||
{ label: "提交反馈", value: "SUBMIT_FEEDBACK" },
|
||||
{ label: "审批反馈", value: "APPROVE_FEEDBACK" },
|
||||
{ label: "拒绝反馈", value: "REJECT_FEEDBACK" },
|
||||
{ label: "其他操作", value: "OTHER" }
|
||||
];
|
||||
|
||||
const fetchLogs = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: any = {
|
||||
userId: filters.userId || undefined,
|
||||
operationType: filters.operationType || undefined,
|
||||
startTime: filters.timeRange?.[0]?.toISOString(),
|
||||
endTime: filters.timeRange?.[1]?.toISOString(),
|
||||
};
|
||||
logs.value = await getOperationLogs(params);
|
||||
} catch (error) {
|
||||
ElMessage.error('获取操作日志失败');
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (timeStr: string) => {
|
||||
if (!timeStr) return '';
|
||||
return new Date(timeStr).toLocaleString();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchLogs();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div class="public-submit-container">
|
||||
<el-card class="box-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>公共环境问题反馈</span>
|
||||
<p>感谢您为环境保护出一份力!您可以在此提交发现的环境问题,无需登录。</p>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :model="form" :rules="rules" ref="feedbackForm" label-width="120px">
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="form.title" placeholder="请输入反馈标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="问题描述" prop="description">
|
||||
<el-input type="textarea" :rows="4" v-model="form.description" placeholder="请详细描述您发现的问题"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="污染类型" prop="pollutionType">
|
||||
<el-select v-model="form.pollutionType" placeholder="请选择污染类型">
|
||||
<el-option v-for="(label, key) in pollutionTypeMap" :key="key" :label="label" :value="key"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="严重程度" prop="severityLevel">
|
||||
<el-rate v-model="rating" :max="3" :texts="['低', '中', '高']" show-text></el-rate>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系方式" prop="contactInfo">
|
||||
<el-input v-model="form.contactInfo" placeholder="选填,便于我们与您联系(邮箱或电话)"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="问题地址" prop="textAddress">
|
||||
<el-input v-model="form.textAddress" placeholder="请输入问题发生的地址"></el-input>
|
||||
</el-form-item>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="经度" prop="longitude">
|
||||
<el-input-number v-model="form.longitude" :precision="6" :step="0.000001" placeholder="请输入经度"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="纬度" prop="latitude">
|
||||
<el-input-number v-model="form.latitude" :precision="6" :step="0.000001" placeholder="请输入纬度"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="网格X" prop="gridX">
|
||||
<el-input-number v-model="form.gridX" :precision="0" :step="1" placeholder="请输入网格X坐标"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="网格Y" prop="gridY">
|
||||
<el-input-number v-model="form.gridY" :precision="0" :step="1" placeholder="请输入网格Y坐标"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="照片/视频附件">
|
||||
<el-upload
|
||||
ref="upload"
|
||||
class="upload-demo"
|
||||
action="#"
|
||||
:on-remove="handleRemove"
|
||||
:on-change="handleChange"
|
||||
:file-list="fileList"
|
||||
list-type="picture-card"
|
||||
:auto-upload="false"
|
||||
multiple
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm" :loading="loading">立即提交</el-button>
|
||||
<el-button @click="resetForm">重置表单</el-button>
|
||||
<el-button @click="$router.push('/login')">返回登录</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { PollutionType, SeverityLevel } from '@/api/feedback';
|
||||
import { submitPublicFeedback } from '@/api/public'; // Assuming the function will be in a new api/public.ts
|
||||
|
||||
const feedbackForm = ref<FormInstance>();
|
||||
const loading = ref(false);
|
||||
const fileList = ref([]);
|
||||
const rating = ref(2);
|
||||
|
||||
interface PublicFeedbackForm {
|
||||
title: string;
|
||||
description: string;
|
||||
pollutionType: PollutionType;
|
||||
severityLevel: SeverityLevel;
|
||||
contactInfo?: string;
|
||||
textAddress: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
gridX?: number;
|
||||
gridY?: number;
|
||||
}
|
||||
|
||||
const form = reactive({
|
||||
title: '',
|
||||
description: '',
|
||||
pollutionType: PollutionType.OTHER,
|
||||
contactInfo: '',
|
||||
textAddress: '',
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
gridX: 0,
|
||||
gridY: 0,
|
||||
});
|
||||
|
||||
const pollutionTypeMap: Record<PollutionType, string> = {
|
||||
[PollutionType.PM25]: 'PM2.5',
|
||||
[PollutionType.O3]: '臭氧 (O3)',
|
||||
[PollutionType.NO2]: '二氧化氮 (NO2)',
|
||||
[PollutionType.SO2]: '二氧化硫 (SO2)',
|
||||
[PollutionType.OTHER]: '其他',
|
||||
};
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '请输入问题描述', trigger: 'blur' }],
|
||||
pollutionType: [{ required: true, message: '请选择污染类型', trigger: 'change' }],
|
||||
textAddress: [{ required: true, message: '请输入问题地址', trigger: 'blur' }],
|
||||
gridX: [{ required: true, type: 'number', message: '网格X必须为数字' }],
|
||||
gridY: [{ required: true, type: 'number', message: '网格Y必须为数字' }],
|
||||
});
|
||||
|
||||
const handleRemove = (file, fileListUpdated) => {
|
||||
fileList.value = fileListUpdated;
|
||||
};
|
||||
|
||||
const handleChange = (file, fileListUpdated) => {
|
||||
fileList.value = fileListUpdated;
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!feedbackForm.value) return;
|
||||
await feedbackForm.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
try {
|
||||
let severity: SeverityLevel;
|
||||
if (rating.value === 1) severity = SeverityLevel.LOW;
|
||||
else if (rating.value === 2) severity = SeverityLevel.MEDIUM;
|
||||
else severity = SeverityLevel.HIGH;
|
||||
|
||||
const submissionData: PublicFeedbackForm = { ...form, severityLevel: severity };
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('feedback', new Blob([JSON.stringify(submissionData)], { type: 'application/json' }));
|
||||
|
||||
fileList.value.forEach(file => {
|
||||
formData.append('files', file.raw);
|
||||
});
|
||||
|
||||
await submitPublicFeedback(formData);
|
||||
ElMessage.success('反馈提交成功!感谢您的贡献。');
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
ElMessage.error('提交失败,请稍后再试。');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
feedbackForm.value?.resetFields();
|
||||
fileList.value = [];
|
||||
rating.value = 2;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.public-submit-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
.box-card {
|
||||
width: 800px;
|
||||
}
|
||||
.card-header {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
.card-header p {
|
||||
font-size: 0.8em;
|
||||
font-weight: normal;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
||||
299
ems-frontend/ems-monitoring-system/src/views/RegisterView.vue
Normal file
299
ems-frontend/ems-monitoring-system/src/views/RegisterView.vue
Normal file
@@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<div class="register-container">
|
||||
<el-card class="register-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>创建您的账户</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" label-position="top" hide-required-asterisk autocomplete="off">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="registerForm.name" placeholder="您的真实姓名" size="large" autocomplete="off"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="registerForm.phone" placeholder="您的手机号" size="large" autocomplete="off"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="registerForm.email" placeholder="您的邮箱" size="large" autocomplete="off"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="验证码" prop="verificationCode">
|
||||
<el-row :gutter="10" style="width: 100%;">
|
||||
<el-col :span="15" class="verification-code-input">
|
||||
<el-input v-model="registerForm.verificationCode" placeholder="请输入6位验证码" size="large"></el-input>
|
||||
</el-col>
|
||||
<el-col :span="9">
|
||||
<el-button @click="sendVerificationCode" :disabled="isSendingCode || countdown > 0" :loading="isSendingCode" class="send-code-button" size="large">
|
||||
{{ countdown > 0 ? `${countdown}秒后重发` : '发送验证码' }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input type="password" v-model="registerForm.password" placeholder="设置您的密码" show-password size="large" autocomplete="new-password"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input type="password" v-model="registerForm.confirmPassword" placeholder="请再次输入您的密码" show-password size="large" autocomplete="new-password"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="注册身份" prop="role">
|
||||
<el-select v-model="registerForm.role" placeholder="请选择您的身份" style="width: 100%;" size="large">
|
||||
<el-option label="公众监督员" value="PUBLIC_SUPERVISOR"></el-option>
|
||||
<el-option label="网格员" value="GRID_WORKER"></el-option>
|
||||
<el-option label="业务主管" value="SUPERVISOR"></el-option>
|
||||
<el-option label="系统管理员" value="ADMIN"></el-option>
|
||||
<el-option label="决策者" value="DECISION_MAKER"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleRegister" :loading="isRegistering" class="register-button">立即注册</el-button>
|
||||
</el-form-item>
|
||||
<div class="extra-links">
|
||||
<el-link type="info" @click="$router.push('/login')">已有账户?返回登录</el-link>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { signup, sendVerificationCode as apiSendCode } from '@/api/auth';
|
||||
|
||||
const router = useRouter();
|
||||
const registerFormRef = ref<FormInstance>();
|
||||
const isRegistering = ref(false);
|
||||
|
||||
const registerForm = reactive({
|
||||
name: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
role: '',
|
||||
verificationCode: '',
|
||||
});
|
||||
|
||||
const validatePass2 = (rule: any, value: any, callback: any) => {
|
||||
if (value === '') {
|
||||
callback(new Error('请再次输入密码'));
|
||||
} else if (value !== registerForm.password) {
|
||||
callback(new Error("两次输入的密码不一致!"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const validatePhone = (rule: any, value: any, callback: any) => {
|
||||
if (value === '') {
|
||||
callback(new Error('请输入手机号'));
|
||||
} else if (!/^1\d{10}$/.test(value)) {
|
||||
callback(new Error('手机号必须是以1开头的11位数字'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const registerRules = reactive<FormRules>({
|
||||
email: [{ required: true, message: '请输入邮箱', trigger: 'blur' }, { type: 'email', message: '请输入有效的邮箱地址', trigger: ['blur', 'change'] }],
|
||||
phone: [{ required: true, validator: validatePhone, trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||||
confirmPassword: [{ validator: validatePass2, trigger: 'blur' }],
|
||||
role: [{ required: true, message: '请选择注册身份', trigger: 'change' }],
|
||||
verificationCode: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
|
||||
});
|
||||
|
||||
const isSendingCode = ref(false);
|
||||
const countdown = ref(0);
|
||||
let timer: number | null = null;
|
||||
|
||||
const sendVerificationCode = async () => {
|
||||
if (!registerForm.email) {
|
||||
ElMessage.warning('请输入您的邮箱地址');
|
||||
return;
|
||||
}
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(registerForm.email)) {
|
||||
ElMessage.warning('请输入有效的邮箱地址');
|
||||
return;
|
||||
}
|
||||
|
||||
isSendingCode.value = true;
|
||||
try {
|
||||
await apiSendCode(registerForm.email);
|
||||
ElMessage.success('验证码已发送,请注意查收');
|
||||
|
||||
countdown.value = 60;
|
||||
if(timer) clearInterval(timer);
|
||||
timer = setInterval(() => {
|
||||
if (countdown.value > 0) {
|
||||
countdown.value--;
|
||||
} else {
|
||||
if(timer) clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
ElMessage.error('验证码发送失败,请稍后重试');
|
||||
console.error(error);
|
||||
} finally {
|
||||
isSendingCode.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegister = async () => {
|
||||
if (!registerFormRef.value) return;
|
||||
await registerFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
isRegistering.value = true;
|
||||
try {
|
||||
const { confirmPassword, ...signupData } = registerForm;
|
||||
await signup(signupData);
|
||||
ElMessage.success('注册成功!正在跳转到登录页面...');
|
||||
setTimeout(() => {
|
||||
router.push('/login');
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
ElMessage.error('注册失败,请检查您填写的信息。');
|
||||
console.error(error);
|
||||
} finally {
|
||||
isRegistering.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.register-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
overflow-y: auto; /* Allow scrolling only if card is too tall */
|
||||
padding: 40px 0;
|
||||
background-image: url('https://images.unsplash.com/photo-1506744038136-46273834b3fb?ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.register-card {
|
||||
width: 550px; /* Wider card for more fields */
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
|
||||
background: rgba(245, 245, 245, 0.85);
|
||||
backdrop-filter: blur(15px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
color: #004d40;
|
||||
padding: 25px 0 15px 0;
|
||||
border-bottom: 1px solid rgba(0,77,64,0.2);
|
||||
}
|
||||
|
||||
.register-button {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: #004d40;
|
||||
color: white;
|
||||
letter-spacing: 5px;
|
||||
padding-left: 10px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(0, 77, 64, 0.4);
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
.register-button:hover {
|
||||
background: #00695c;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 77, 64, 0.5);
|
||||
}
|
||||
|
||||
.extra-links {
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.extra-links .el-link {
|
||||
color: #555;
|
||||
font-size: 15px;
|
||||
}
|
||||
.extra-links .el-link:hover {
|
||||
color: #004d40;
|
||||
}
|
||||
|
||||
/* Add scoped styles for the verification code input and button */
|
||||
:deep(.verification-code-input .el-input__wrapper) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.send-code-button {
|
||||
width: 100%;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
background-color: #004d40;
|
||||
color: white;
|
||||
border-color: #004d40;
|
||||
}
|
||||
|
||||
.send-code-button:hover {
|
||||
background-color: #00695c;
|
||||
border-color: #00695c;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
/* Global overrides to match login page style */
|
||||
:deep(.register-container .el-input__wrapper),
|
||||
:deep(.register-container .el-select .el-input__wrapper) {
|
||||
background-color: rgba(255, 255, 255, 0.9) !important;
|
||||
border-radius: 10px !important;
|
||||
height: 45px;
|
||||
box-shadow: none !important;
|
||||
border: 1px solid transparent !important;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.register-container .el-input__wrapper:hover),
|
||||
:deep(.register-container .el-select .el-input__wrapper:hover) {
|
||||
border-color: #ccc !important;
|
||||
}
|
||||
|
||||
:deep(.register-container .el-input__wrapper.is-focus),
|
||||
:deep(.register-container .el-select .el-input__wrapper.is-focused) {
|
||||
border-color: #004d40 !important;
|
||||
box-shadow: 0 0 0 1px rgba(0, 77, 64, 0.2) !important;
|
||||
}
|
||||
|
||||
.register-container .el-form-item__label {
|
||||
color: #555 !important;
|
||||
font-weight: 500 !important;
|
||||
letter-spacing: 0.5px !important;
|
||||
font-size: 15px !important;
|
||||
}
|
||||
|
||||
.register-container .el-input__inner {
|
||||
color: #333 !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.register-container .el-button {
|
||||
height: 45px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div class="reset-password-container">
|
||||
<el-card class="reset-password-card">
|
||||
<div class="card-header">
|
||||
<h2>重置密码</h2>
|
||||
<p>请输入您的验证码和新密码</p>
|
||||
</div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
hide-required-asterisk
|
||||
@submit.prevent="handleSubmit"
|
||||
autocomplete="off"
|
||||
>
|
||||
<el-form-item label="邮箱地址" prop="email">
|
||||
<el-input
|
||||
v-model="form.email"
|
||||
placeholder="请输入您的邮箱地址"
|
||||
size="large"
|
||||
autocomplete="email"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="验证码" prop="code">
|
||||
<el-input
|
||||
v-model="form.code"
|
||||
placeholder="请输入6位邮箱验证码"
|
||||
size="large"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="新密码" prop="password">
|
||||
<el-input
|
||||
type="password"
|
||||
v-model="form.password"
|
||||
placeholder="请输入您的新密码"
|
||||
show-password
|
||||
size="large"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="确认新密码" prop="confirmPassword">
|
||||
<el-input
|
||||
type="password"
|
||||
v-model="form.confirmPassword"
|
||||
placeholder="请再次输入新密码"
|
||||
show-password
|
||||
size="large"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
native-type="submit"
|
||||
:loading="isLoading"
|
||||
class="submit-button"
|
||||
size="large"
|
||||
>
|
||||
确认重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="card-footer">
|
||||
<el-link @click="$router.push('/login')">返回登录</el-link>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { resetPasswordWithCode } from '@/api/auth';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const formRef = ref<FormInstance>();
|
||||
const isLoading = ref(false);
|
||||
|
||||
const form = reactive({
|
||||
email: '',
|
||||
code: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query.email) {
|
||||
form.email = route.query.email as string;
|
||||
} else {
|
||||
ElMessage.error('无效的重置链接,请返回重试。');
|
||||
router.push('/forgot-password');
|
||||
}
|
||||
});
|
||||
|
||||
const validatePass = (rule: any, value: any, callback: any) => {
|
||||
if (value === '') {
|
||||
callback(new Error('请再次输入密码'));
|
||||
} else if (value !== form.password) {
|
||||
callback(new Error("两次输入的密码不一致!"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址', trigger: ['blur', 'change'] }
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入验证码', trigger: 'blur' },
|
||||
{ len: 6, message: '验证码必须是6位', trigger: 'blur' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' },
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请确认新密码', trigger: 'blur' },
|
||||
{ validator: validatePass, trigger: 'blur' }
|
||||
],
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formRef.value) return;
|
||||
formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await resetPasswordWithCode({
|
||||
email: form.email,
|
||||
code: form.code,
|
||||
newPassword: form.password,
|
||||
});
|
||||
ElMessage.success('密码重置成功!即将跳转到登录页...');
|
||||
setTimeout(() => router.push('/login'), 2000);
|
||||
} catch (error) {
|
||||
ElMessage.error('密码重置失败,请检查验证码是否正确。');
|
||||
console.error(error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.reset-password-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-image: url('https://images.unsplash.com/photo-1506744038136-46273834b3fb?ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.reset-password-card {
|
||||
width: 480px;
|
||||
border-radius: 25px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(15px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
color: #004d40;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-header p {
|
||||
color: #607d8b;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 1.8rem;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
background-color: #004d40;
|
||||
border-color: #004d40;
|
||||
transition: background-color 0.3s ease;
|
||||
font-size: 1rem;
|
||||
padding: 1.2rem 0;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
background-color: #00695c;
|
||||
border-color: #00695c;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<div class="submit-feedback-container">
|
||||
<el-card class="box-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>提交新的环境问题反馈</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :model="form" :rules="rules" ref="feedbackForm" label-width="120px">
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="form.title" placeholder="请输入反馈标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="问题描述" prop="description">
|
||||
<el-input type="textarea" :rows="4" v-model="form.description" placeholder="请详细描述您发现的问题"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="污染类型" prop="pollutionType">
|
||||
<el-select v-model="form.pollutionType" placeholder="请选择污染类型">
|
||||
<el-option v-for="(label, key) in pollutionTypeMap" :key="key" :label="label" :value="key"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="严重程度" prop="severityLevel">
|
||||
<el-rate v-model="rating" :max="3" :texts="['低', '中', '高']" show-text></el-rate>
|
||||
</el-form-item>
|
||||
<el-form-item label="问题地址" prop="location.textAddress">
|
||||
<el-input v-model="form.location.textAddress" placeholder="请输入问题发生的地址"></el-input>
|
||||
</el-form-item>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="经度" prop="location.longitude">
|
||||
<el-input-number v-model="form.location.longitude" :precision="6" :step="0.000001" placeholder="请输入经度"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="纬度" prop="location.latitude">
|
||||
<el-input-number v-model="form.location.latitude" :precision="6" :step="0.000001" placeholder="请输入纬度"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="网格X" prop="location.gridX">
|
||||
<el-input-number v-model="form.location.gridX" :precision="0" :step="1" placeholder="请输入网格X坐标"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="网格Y" prop="location.gridY">
|
||||
<el-input-number v-model="form.location.gridY" :precision="0" :step="1" placeholder="请输入网格Y坐标"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="照片/视频附件">
|
||||
<el-upload
|
||||
ref="upload"
|
||||
class="upload-demo"
|
||||
action="#"
|
||||
:on-preview="handlePreview"
|
||||
:on-remove="handleRemove"
|
||||
:on-change="handleChange"
|
||||
:file-list="fileList"
|
||||
list-type="picture-card"
|
||||
:auto-upload="false"
|
||||
multiple
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm" :loading="loading">立即提交</el-button>
|
||||
<el-button @click="resetForm">重置表单</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { useFeedbackStore } from '@/store/feedback';
|
||||
import { PollutionType } from '@/api/types';
|
||||
import { SeverityLevel } from '@/api/feedback';
|
||||
import type { FeedbackSubmissionRequest, LocationInfo } from '@/api/feedback';
|
||||
|
||||
const feedbackForm = ref<FormInstance>();
|
||||
const loading = ref(false);
|
||||
const fileList = ref([]);
|
||||
const rating = ref(2); // For el-rate which uses a number. 1=LOW, 2=MEDIUM, 3=HIGH
|
||||
|
||||
const form = reactive<Omit<FeedbackSubmissionRequest, 'severityLevel'>>({
|
||||
title: '',
|
||||
description: '',
|
||||
pollutionType: PollutionType.PM25,
|
||||
location: {
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
textAddress: '',
|
||||
gridX: 0,
|
||||
gridY: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const pollutionTypeMap: Record<PollutionType, string> = {
|
||||
[PollutionType.PM25]: 'PM2.5',
|
||||
[PollutionType.O3]: '臭氧 (O3)',
|
||||
[PollutionType.NO2]: '二氧化氮 (NO2)',
|
||||
[PollutionType.SO2]: '二氧化硫 (SO2)',
|
||||
[PollutionType.OTHER]: '其他',
|
||||
};
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '请输入问题描述', trigger: 'blur' }],
|
||||
pollutionType: [{ required: true, message: '请选择污染类型', trigger: 'change' }],
|
||||
'location.textAddress': [{ required: true, message: '请输入问题地址', trigger: 'blur' }],
|
||||
'location.latitude': [{ required: true, type: 'number', message: '纬度必须为数字' }],
|
||||
'location.longitude': [{ required: true, type: 'number', message: '经度必须为数字' }],
|
||||
'location.gridX': [{ required: true, type: 'number', message: '网格X必须为数字' }],
|
||||
'location.gridY': [{ required: true, type: 'number', message: '网格Y必须为数字' }],
|
||||
});
|
||||
|
||||
const feedbackStore = useFeedbackStore();
|
||||
|
||||
const handleRemove = (file, fileListUpdated) => {
|
||||
fileList.value = fileListUpdated;
|
||||
};
|
||||
|
||||
const handlePreview = (file) => {
|
||||
console.log(file);
|
||||
};
|
||||
|
||||
const handleChange = (file, fileListUpdated) => {
|
||||
fileList.value = fileListUpdated;
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!feedbackForm.value) return;
|
||||
await feedbackForm.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
try {
|
||||
let severity: SeverityLevel;
|
||||
if (rating.value === 1) {
|
||||
severity = SeverityLevel.LOW;
|
||||
} else if (rating.value === 2) {
|
||||
severity = SeverityLevel.MEDIUM;
|
||||
} else {
|
||||
severity = SeverityLevel.HIGH;
|
||||
}
|
||||
|
||||
const submissionData: FeedbackSubmissionRequest = {
|
||||
...form,
|
||||
severityLevel: severity,
|
||||
};
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('feedback', new Blob([JSON.stringify(submissionData)], { type: 'application/json' }));
|
||||
|
||||
fileList.value.forEach(file => {
|
||||
formData.append('files', file.raw);
|
||||
});
|
||||
|
||||
await feedbackStore.submitFeedback(formData);
|
||||
ElMessage.success('反馈提交成功!感谢您的贡献。');
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
ElMessage.error('提交失败,请稍后再试。');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
feedbackForm.value?.resetFields();
|
||||
fileList.value = [];
|
||||
rating.value = 2;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.submit-feedback-container {
|
||||
padding: 20px;
|
||||
}
|
||||
.card-header {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="supervisor-view">
|
||||
<el-card class="box-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>待审核反馈</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="pendingFeedback" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="title" label="标题"></el-table-column>
|
||||
<el-table-column prop="description" label="描述" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="pollutionType" label="污染类型" width="120"></el-table-column>
|
||||
<el-table-column prop="severityLevel" label="严重等级" width="120"></el-table-column>
|
||||
<el-table-column prop="createdAt" label="提交时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatDateTime(scope.row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleCreateTask(scope.row)">创建任务</el-button>
|
||||
<el-button size="small" type="danger" @click="handleRejectFeedback(scope.row)">拒绝</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- TODO: Add Task Management Section -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import type { Feedback } from '@/api/types';
|
||||
import { getPendingFeedback, processFeedback } from '@/api/feedback'; // Assuming processFeedback exists
|
||||
import { formatDateTime } from '@/utils/formatter';
|
||||
|
||||
const pendingFeedback = ref<Feedback[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const fetchPendingFeedback = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await getPendingFeedback();
|
||||
pendingFeedback.value = response.data;
|
||||
} catch (error) {
|
||||
ElMessage.error('获取待审核反馈失败');
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateTask = (feedback: Feedback) => {
|
||||
// TODO: Implement task creation logic
|
||||
console.log('Create task from feedback:', feedback.id);
|
||||
ElMessage.info(`准备为反馈 #${feedback.id} 创建任务`);
|
||||
};
|
||||
|
||||
const handleRejectFeedback = async (feedback: Feedback) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要拒绝这条反馈吗? (ID: ${feedback.id})`, '确认操作', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
|
||||
// This assumes a 'processFeedback' function exists to update feedback status
|
||||
// to something like 'REJECTED' or 'CLOSED_INVALID'.
|
||||
// We may need to create or adjust this backend logic.
|
||||
await processFeedback(feedback.id, { status: 'CLOSED_INVALID' }); // Placeholder status
|
||||
ElMessage.success('反馈已拒绝');
|
||||
fetchPendingFeedback(); // Refresh list
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('操作失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchPendingFeedback();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.supervisor-view {
|
||||
padding: 20px;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>系统设置</h1>
|
||||
<p>这里是系统设置页面。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
289
ems-frontend/ems-monitoring-system/src/views/TaskDetailView.vue
Normal file
289
ems-frontend/ems-monitoring-system/src/views/TaskDetailView.vue
Normal file
@@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<div class="task-detail-view" v-if="task">
|
||||
<el-card class="page-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>任务详情: {{ task.title }}</span>
|
||||
<el-tag :type="getStatusTagType(task.status)">{{ formatStatus(task.status) }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="任务ID">{{ task.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="严重程度">
|
||||
<el-tag :type="getSeverityTagType(task.severityLevel)">{{ formatSeverity(task.severityLevel) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="污染类型">{{ task.pollutionType || '未指定' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="网格坐标">X: {{ task.gridX }}, Y: {{ task.gridY }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分配时间">{{ task.assignedAt ? new Date(task.assignedAt).toLocaleString() : 'N/A' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="完成时间">{{ task.completedAt ? new Date(task.completedAt).toLocaleString() : 'N/A' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="地址" :span="2">{{ task.textAddress || '未提供' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="任务描述" :span="2">{{ task.description }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div v-if="task.feedback">
|
||||
<el-divider />
|
||||
<h3>关联反馈信息</h3>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="反馈ID">{{ task.feedback.feedbackId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="提交人">{{ task.feedback.submitterName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="反馈时间">{{ new Date(task.feedback.createdAt).toLocaleString() }}</el-descriptions-item>
|
||||
<el-descriptions-item label="反馈标题">{{ task.feedback.title }}</el-descriptions-item>
|
||||
<el-descriptions-item label="反馈描述" :span="2">{{ task.feedback.description }}</el-descriptions-item>
|
||||
<el-descriptions-item label="现场图片" :span="2" v-if="task.feedback.imageUrls && task.feedback.imageUrls.length > 0">
|
||||
<el-image
|
||||
v-for="url in task.feedback.imageUrls"
|
||||
:key="url"
|
||||
:src="url"
|
||||
:preview-src-list="task.feedback.imageUrls"
|
||||
style="width: 100px; height: 100px; margin-right: 10px;"
|
||||
fit="cover"
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h3>任务历史</h3>
|
||||
<div v-if="task.history && task.history.length > 0">
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="item in task.history"
|
||||
:key="item.id"
|
||||
:timestamp="new Date(item.changedAt).toLocaleString()"
|
||||
:type="getHistoryType(item.newStatus)"
|
||||
>
|
||||
<strong>{{ formatStatus(item.newStatus) }}</strong>
|
||||
<p>{{ item.comments }}</p>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
<el-empty v-else description="暂无历史记录" />
|
||||
|
||||
<div v-if="task.submissionInfo">
|
||||
<el-divider />
|
||||
<h3>任务提交详情</h3>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="提交说明">{{ task.submissionInfo.comments }}</el-descriptions-item>
|
||||
<el-descriptions-item label="提交附件" v-if="task.submissionInfo.attachments && task.submissionInfo.attachments.length > 0">
|
||||
<div v-for="att in task.submissionInfo.attachments" :key="att.id">
|
||||
<el-link :href="att.url" type="primary" target="_blank">{{ att.fileName }}</el-link>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<el-button v-if="task.status === 'ASSIGNED'" type="success" @click="acceptTask" :loading="actionLoading">接受任务</el-button>
|
||||
<el-button v-if="task.status === 'IN_PROGRESS'" type="primary" @click="openSubmitDialog" :loading="actionLoading">提交进度</el-button>
|
||||
<el-button @click="$router.back()">返回列表</el-button>
|
||||
</div>
|
||||
|
||||
</el-card>
|
||||
</div>
|
||||
<div v-else-if="loading">
|
||||
<el-card class="page-card" v-loading="true" style="min-height: 400px;"></el-card>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-card class="page-card">
|
||||
<el-empty description="无法加载任务详情" />
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<SubmitProgressDialog
|
||||
v-if="task"
|
||||
v-model:visible="isSubmitDialogVisible"
|
||||
:task-id="task.id"
|
||||
@submitted="handleSubmitted"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import apiClient from '@/api';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import SubmitProgressDialog from '@/components/SubmitProgressDialog.vue';
|
||||
|
||||
// --- 类型定义 ---
|
||||
interface TaskHistoryDTO {
|
||||
id: number;
|
||||
changedAt: string;
|
||||
newStatus: string;
|
||||
comments: string;
|
||||
}
|
||||
interface FeedbackDTO {
|
||||
feedbackId: number;
|
||||
eventId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
severityLevel: string;
|
||||
textAddress: string;
|
||||
submitterId: number;
|
||||
submitterName: string;
|
||||
createdAt: string;
|
||||
imageUrls: string[];
|
||||
}
|
||||
interface AssigneeDTO {
|
||||
id: number;
|
||||
name: string;
|
||||
phone: string;
|
||||
}
|
||||
interface AttachmentDTO {
|
||||
id: number;
|
||||
fileName: string;
|
||||
fileType: string;
|
||||
url: string;
|
||||
}
|
||||
interface SubmissionInfoDTO {
|
||||
comments: string;
|
||||
attachments: AttachmentDTO[];
|
||||
}
|
||||
interface TaskDetail {
|
||||
id: number;
|
||||
feedback: FeedbackDTO;
|
||||
title: string;
|
||||
description: string;
|
||||
severityLevel: string;
|
||||
pollutionType: string;
|
||||
textAddress: string;
|
||||
gridX: number;
|
||||
gridY: number;
|
||||
status: string;
|
||||
assignee: AssigneeDTO;
|
||||
assignedAt: string | null;
|
||||
completedAt: string | null;
|
||||
history: TaskHistoryDTO[];
|
||||
submissionInfo: SubmissionInfoDTO | null;
|
||||
}
|
||||
|
||||
// --- 响应式状态 ---
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const taskId = ref(route.params.id as string);
|
||||
const task = ref<TaskDetail | null>(null);
|
||||
const loading = ref(false);
|
||||
const actionLoading = ref(false);
|
||||
const isSubmitDialogVisible = ref(false);
|
||||
|
||||
// --- API 调用 ---
|
||||
const fetchTaskDetails = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await apiClient.get(`/worker/${taskId.value}`);
|
||||
task.value = response as TaskDetail;
|
||||
} catch (error) {
|
||||
console.error(`获取任务详情失败 (ID: ${taskId.value}):`, error);
|
||||
ElMessage.error('获取任务详情失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const acceptTask = async () => {
|
||||
actionLoading.value = true;
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要接受这个任务吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info',
|
||||
});
|
||||
await apiClient.post(`/worker/${taskId.value}/accept`);
|
||||
ElMessage.success('任务已接受');
|
||||
fetchTaskDetails(); // Refresh details
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('接受任务失败:', error);
|
||||
ElMessage.error('接受任务失败');
|
||||
}
|
||||
} finally {
|
||||
actionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openSubmitDialog = () => {
|
||||
isSubmitDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleSubmitted = () => {
|
||||
fetchTaskDetails();
|
||||
};
|
||||
|
||||
// --- 辅助函数 ---
|
||||
const formatStatus = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'ASSIGNED': '待接受',
|
||||
'IN_PROGRESS': '进行中',
|
||||
'SUBMITTED': '已提交',
|
||||
'COMPLETED': '已完成',
|
||||
'REJECTED': '已拒绝',
|
||||
};
|
||||
return map[status] || status;
|
||||
};
|
||||
|
||||
const getStatusTagType = (status: string): 'primary' | 'warning' | 'info' | 'success' | 'danger' => {
|
||||
const map: Record<string, 'primary' | 'warning' | 'info' | 'success' | 'danger'> = {
|
||||
'ASSIGNED': 'primary',
|
||||
'IN_PROGRESS': 'warning',
|
||||
'SUBMITTED': 'info',
|
||||
'COMPLETED': 'success',
|
||||
'REJECTED': 'danger',
|
||||
};
|
||||
return map[status] || 'info';
|
||||
};
|
||||
|
||||
const formatSeverity = (severity: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'LOW': '低',
|
||||
'MEDIUM': '中',
|
||||
'HIGH': '高',
|
||||
};
|
||||
return map[severity] || severity;
|
||||
};
|
||||
|
||||
const getSeverityTagType = (severity: string): 'info' | 'warning' | 'danger' => {
|
||||
const map: Record<string, 'info' | 'warning' | 'danger'> = {
|
||||
'LOW': 'info',
|
||||
'MEDIUM': 'warning',
|
||||
'HIGH': 'danger',
|
||||
};
|
||||
return map[severity] || 'info';
|
||||
};
|
||||
|
||||
const getHistoryType = (status: string): 'primary' | 'success' | 'info' => {
|
||||
switch (status) {
|
||||
case 'ASSIGNED':
|
||||
case 'IN_PROGRESS':
|
||||
return 'primary';
|
||||
case 'COMPLETED':
|
||||
return 'success';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchTaskDetails();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-detail-view {
|
||||
padding: 20px;
|
||||
}
|
||||
.page-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.action-buttons {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
12
ems-frontend/ems-monitoring-system/src/views/TaskView.vue
Normal file
12
ems-frontend/ems-monitoring-system/src/views/TaskView.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>任务管理</h1>
|
||||
<p>这里是任务管理页面。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,437 @@
|
||||
<template>
|
||||
<el-card class="box-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>人员管理</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="table-header">
|
||||
<el-input v-model="searchName" placeholder="按姓名搜索" clearable @clear="fetchUsers" @keyup.enter="fetchUsers" style="width: 200px;"></el-input>
|
||||
<el-select v-model="searchRole" placeholder="按角色筛选" clearable @change="fetchUsers" style="width: 150px;">
|
||||
<el-option label="管理员" value="ADMIN"></el-option>
|
||||
<el-option label="决策者" value="DECISION_MAKER"></el-option>
|
||||
<el-option label="主管" value="SUPERVISOR"></el-option>
|
||||
<el-option label="网格员" value="GRID_WORKER"></el-option>
|
||||
<el-option label="公众监督员" value="PUBLIC_SUPERVISOR"></el-option>
|
||||
</el-select>
|
||||
<el-button type="primary" @click="fetchUsers" :icon="Search">搜索</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="users" v-loading="loading" style="width: 100%" border stripe table-layout="auto" class="user-table compact-table">
|
||||
<el-table-column prop="id" label="ID" width="60" align="center"></el-table-column>
|
||||
<el-table-column prop="name" label="姓名" width="120" align="center"></el-table-column>
|
||||
<el-table-column prop="email" label="邮箱" min-width="180"></el-table-column>
|
||||
<el-table-column prop="phone" label="手机号" width="120" align="center"></el-table-column>
|
||||
<el-table-column prop="gender" label="性别" width="70" align="center">
|
||||
<template #default="scope">
|
||||
{{ formatGender(scope.row.gender) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="role" label="角色" width="150" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="roleTagType(scope.row.role)" size="small">{{ formatRole(scope.row.role) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="statusTagType(scope.row.status)" size="small" disable-transitions>{{ scope.row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-popconfirm title="确定要删除这位用户吗?" @confirm="handleDelete(scope.row.id)">
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-if="total > 0"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
v-model:page-size="pagination.size"
|
||||
v-model:current-page="pagination.page"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
style="margin-top: 20px; justify-content: flex-end;"
|
||||
></el-pagination>
|
||||
|
||||
<!-- Edit User Dialog -->
|
||||
<el-dialog v-model="editDialogVisible" title="编辑用户信息" width="700px" :show-close="false" class="edit-dialog ultimate-dialog">
|
||||
<template #header="{ close, titleId, titleClass }">
|
||||
<div class="ultimate-dialog-header">
|
||||
<h4 :id="titleId" :class="titleClass">
|
||||
<el-icon class="header-icon"><EditPen /></el-icon>
|
||||
编辑用户信息
|
||||
</h4>
|
||||
<el-button circle :icon="Close" type="danger" class="header-close-btn" @click="close"></el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :model="editForm" ref="editFormRef" label-width="90px" label-position="top">
|
||||
|
||||
<div class="form-section">
|
||||
<h3 class="section-title">身份凭证</h3>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="form-label-icon">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>用户ID</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model="editForm.id" disabled class="readonly-input"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="form-label-icon">
|
||||
<el-icon><Avatar /></el-icon>
|
||||
<span>姓名</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model="editForm.name" disabled class="readonly-input"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h3 class="section-title">详细信息</h3>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="phone">
|
||||
<template #label>
|
||||
<div class="form-label-icon">
|
||||
<el-icon><Phone /></el-icon>
|
||||
<span>手机号</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model="editForm.phone" placeholder="请输入手机号"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="gender">
|
||||
<template #label>
|
||||
<div class="form-label-icon">
|
||||
<el-icon><Male /></el-icon>
|
||||
<span>性别</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-radio-group v-model="editForm.gender">
|
||||
<el-radio label="MALE">男</el-radio>
|
||||
<el-radio label="FEMALE">女</el-radio>
|
||||
<el-radio label="OTHER">其他</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="role">
|
||||
<template #label>
|
||||
<div class="form-label-icon">
|
||||
<el-icon><Key /></el-icon>
|
||||
<span>角色</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-select v-model="editForm.role" placeholder="选择角色" style="width:100%;">
|
||||
<el-option label="管理员" value="ADMIN"></el-option>
|
||||
<el-option label="决策者" value="DECISION_MAKER"></el-option>
|
||||
<el-option label="主管" value="SUPERVISOR"></el-option>
|
||||
<el-option label="网格员" value="GRID_WORKER"></el-option>
|
||||
<el-option label="公众监督员" value="PUBLIC_SUPERVISOR"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="status">
|
||||
<template #label>
|
||||
<div class="form-label-icon">
|
||||
<el-icon><Switch /></el-icon>
|
||||
<span>状态</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-select v-model="editForm.status" placeholder="选择状态" style="width:100%;">
|
||||
<el-option label="ACTIVE" value="ACTIVE"></el-option>
|
||||
<el-option label="INACTIVE" value="INACTIVE"></el-option>
|
||||
<el-option label="SUSPENDED" value="SUSPENDED"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="editDialogVisible = false" plain>取 消</el-button>
|
||||
<el-button type="primary" @click="submitEdit" :loading="isSaving" :icon="Check">确 认 保 存</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Search, User, Avatar, Phone, Male, Key, Switch, Close, Check, EditPen } from '@element-plus/icons-vue';
|
||||
import { getUsers, updateUser, deleteUser } from '@/api/personnel';
|
||||
import type { UserAccount, UserUpdateRequest } from '@/api/types';
|
||||
|
||||
const users = ref<UserAccount[]>([]);
|
||||
const loading = ref(true);
|
||||
const total = ref(0);
|
||||
const searchName = ref('');
|
||||
const searchRole = ref('');
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
});
|
||||
|
||||
const editDialogVisible = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const editFormRef = ref();
|
||||
const editForm = reactive<Partial<UserAccount>>({});
|
||||
|
||||
const roleMap: Record<string, string> = {
|
||||
ADMIN: '管理员',
|
||||
DECISION_MAKER: '决策者',
|
||||
SUPERVISOR: '主管',
|
||||
GRID_WORKER: '网格员',
|
||||
PUBLIC_SUPERVISOR: '公众监督员'
|
||||
};
|
||||
|
||||
const formatRole = (role: string) => roleMap[role] || role;
|
||||
const formatGender = (gender: 'MALE' | 'FEMALE' | 'OTHER') => {
|
||||
if (gender === 'MALE') return '男';
|
||||
if (gender === 'FEMALE') return '女';
|
||||
return '未知';
|
||||
}
|
||||
|
||||
const roleTagType = (role: string) => {
|
||||
switch (role) {
|
||||
case 'ADMIN': return 'danger';
|
||||
case 'DECISION_MAKER': return 'warning';
|
||||
case 'SUPERVISOR': return 'primary';
|
||||
default: return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
const statusTagType = (status: string) => {
|
||||
if (status === 'ACTIVE') return 'success';
|
||||
if (status === 'INACTIVE') return 'info';
|
||||
if (status === 'SUSPENDED') return 'warning';
|
||||
return '';
|
||||
};
|
||||
|
||||
const fetchUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page - 1,
|
||||
size: pagination.size,
|
||||
name: searchName.value || undefined,
|
||||
role: searchRole.value || undefined,
|
||||
};
|
||||
const response = await getUsers(params);
|
||||
users.value = response.content;
|
||||
total.value = response.totalElements;
|
||||
} catch (error) {
|
||||
console.error("获取用户列表时出错:", error);
|
||||
ElMessage.error('获取用户列表失败,请检查网络或联系管理员。');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (user: UserAccount) => {
|
||||
Object.assign(editForm, user);
|
||||
editDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const submitEdit = async () => {
|
||||
if (!editForm.id) return;
|
||||
isSaving.value = true;
|
||||
|
||||
const updateData: UserUpdateRequest = {
|
||||
phone: editForm.phone,
|
||||
gender: editForm.gender as 'MALE' | 'FEMALE' | 'OTHER',
|
||||
role: editForm.role,
|
||||
status: editForm.status,
|
||||
};
|
||||
|
||||
try {
|
||||
await updateUser(editForm.id, updateData);
|
||||
ElMessage.success('用户信息更新成功');
|
||||
editDialogVisible.value = false;
|
||||
await fetchUsers();
|
||||
} catch (error) {
|
||||
console.error(`更新用户 #${editForm.id} 失败:`, error);
|
||||
ElMessage.error('更新失败,请重试。');
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (userId: number) => {
|
||||
try {
|
||||
await deleteUser(userId);
|
||||
ElMessage.success('删除成功');
|
||||
await fetchUsers();
|
||||
} catch (error) {
|
||||
console.error(`删除用户 #${userId} 失败:`, error);
|
||||
ElMessage.error('删除失败,请重试。');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.size = size;
|
||||
fetchUsers();
|
||||
};
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page;
|
||||
fetchUsers();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchUsers();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.box-card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.user-table {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.compact-table :deep(.el-table__cell) {
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.user-table .el-button {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.el-pagination {
|
||||
margin-top: 20px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
:deep(.el-table__row .el-button) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:hover .el-button) {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Ultimate Dialog Styles */
|
||||
.ultimate-dialog .el-dialog__header {
|
||||
display: none; /* Hide original header */
|
||||
}
|
||||
|
||||
.ultimate-dialog {
|
||||
--el-dialog-padding-primary: 0;
|
||||
background: linear-gradient(to top right, #e0f2f1, #ffffff);
|
||||
}
|
||||
|
||||
.ultimate-dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.ultimate-dialog-header h4 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.header-close-btn {
|
||||
--el-button-bg-color: transparent;
|
||||
--el-button-border-color: transparent;
|
||||
--el-button-hover-bg-color: var(--el-color-danger-light-8);
|
||||
--el-button-hover-border-color: var(--el-color-danger-light-8);
|
||||
}
|
||||
|
||||
.ultimate-dialog .el-dialog__body {
|
||||
padding: 24px 30px;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%);
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 24px 0 rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 25px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-top: 0;
|
||||
margin-bottom: 24px;
|
||||
border-left: 4px solid var(--el-color-primary);
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.form-label-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
color: #343a40;
|
||||
}
|
||||
|
||||
.readonly-input {
|
||||
--el-input-disabled-bg-color: #eef1f6;
|
||||
--el-input-disabled-text-color: #303133;
|
||||
--el-input-disabled-border-color: #dcdfe6;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
padding: 10px 30px 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="profile-container">
|
||||
<el-card class="profile-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>个人信息</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="user" class="profile-content">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="用户ID" label-align="right" align="center" label-class-name="my-label">
|
||||
{{ user.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名" label-align="right" align="center">
|
||||
{{ user.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱" label-align="right" align="center">
|
||||
{{ user.email }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="电话" label-align="right" align="center">
|
||||
{{ user.phone || 'N/A' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="角色" label-align="right" align="center">
|
||||
<el-tag>{{ user.role }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态" label-align="right" align="center">
|
||||
<el-tag :type="user.status === 'ACTIVE' ? 'success' : 'danger'">{{ user.status }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="所属区域" label-align="right" align="center" :span="2">
|
||||
{{ user.region || 'N/A' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="技能" label-align="right" align="center" :span="2">
|
||||
<template v-if="user.skills && user.skills.length">
|
||||
<el-tag v-for="skill in user.skills" :key="skill" type="info" style="margin-right: 8px;">
|
||||
{{ skill }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<span v-else>N/A</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>正在加载用户信息...</p>
|
||||
</div>
|
||||
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
// 从 Pinia store 中获取用户信息
|
||||
const authStore = useAuthStore();
|
||||
// 使用 storeToRefs 来保持响应性
|
||||
const { user } = storeToRefs(authStore);
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.profile-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.profile-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.my-label {
|
||||
background: var(--el-color-success-light-9) !important;
|
||||
}
|
||||
</style>
|
||||
337
ems-frontend/ems-monitoring-system/src/views/WorkerMapView.vue
Normal file
337
ems-frontend/ems-monitoring-system/src/views/WorkerMapView.vue
Normal file
@@ -0,0 +1,337 @@
|
||||
<template>
|
||||
<div class="grid-management-container">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h2>我的网格地图</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="refreshData" :loading="loading || isLoadingWorkers" icon="Refresh">
|
||||
刷新数据
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content-grid">
|
||||
<!-- Grid Map Area -->
|
||||
<div class="map-area" v-loading="loading">
|
||||
<div v-if="error" class="error-message">
|
||||
<el-alert title="地图数据加载失败" :description="error" type="error" show-icon :closable="false" />
|
||||
<el-button @click="fetchAllGrids" type="primary" class="retry-button">重试</el-button>
|
||||
</div>
|
||||
<svg v-else-if="displayGrids.length" :width="svgDimensions.width" :height="svgDimensions.height" class="grid-svg">
|
||||
<defs>
|
||||
<pattern id="grid-pattern-worker" :width="CELL_SIZE" :height="CELL_SIZE" patternUnits="userSpaceOnUse">
|
||||
<path :d="`M ${CELL_SIZE} 0 L 0 0 0 ${CELL_SIZE}`" fill="none" stroke="#e9e9e9" stroke-width="0.5" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect :width="svgDimensions.width" :height="svgDimensions.height" fill="url(#grid-pattern-worker)" />
|
||||
|
||||
<g v-for="grid in displayGrids" :key="grid.id">
|
||||
<rect
|
||||
:x="grid.displayX * CELL_SIZE"
|
||||
:y="grid.displayY * CELL_SIZE"
|
||||
:width="CELL_SIZE"
|
||||
:height="CELL_SIZE"
|
||||
:fill="getGridFillColor(grid)"
|
||||
@click="selectGrid(grid)"
|
||||
:class="{ 'selected': selectedGrid && selectedGrid.id === grid.id }"
|
||||
class="grid-cell"
|
||||
/>
|
||||
<!-- Border Rendering -->
|
||||
<line v-if="getBorder(grid, 'top')" :x1="grid.displayX * CELL_SIZE" :y1="grid.displayY * CELL_SIZE" :x2="(grid.displayX + 1) * CELL_SIZE" :y2="grid.displayY * CELL_SIZE" class="border-line" />
|
||||
<line v-if="getBorder(grid, 'right')" :x1="(grid.displayX + 1) * CELL_SIZE" :y1="grid.displayY * CELL_SIZE" :x2="(grid.displayX + 1) * CELL_SIZE" :y2="(grid.displayY + 1) * CELL_SIZE" class="border-line" />
|
||||
<line v-if="getBorder(grid, 'bottom')" :x1="grid.displayX * CELL_SIZE" :y1="(grid.displayY + 1) * CELL_SIZE" :x2="(grid.displayX + 1) * CELL_SIZE" :y2="(grid.displayY + 1) * CELL_SIZE" class="border-line" />
|
||||
<line v-if="getBorder(grid, 'left')" :x1="grid.displayX * CELL_SIZE" :y1="grid.displayY * CELL_SIZE" :x2="grid.displayX * CELL_SIZE" :y2="(grid.displayY + 1) * CELL_SIZE" class="border-line" />
|
||||
</g>
|
||||
</svg>
|
||||
<el-empty v-else description="没有可显示的网格数据" />
|
||||
</div>
|
||||
|
||||
<!-- Details Sidebar -->
|
||||
<div class="sidebar-area">
|
||||
<el-card shadow="never" class="details-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>网格详情</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="selectedGrid" class="details-content">
|
||||
<p><strong>网格ID:</strong> {{ selectedGrid.id }}</p>
|
||||
<p><strong>城市:</strong> <el-tag :color="getCityColor(selectedGrid.cityName)" effect="light">{{ selectedGrid.cityName }}</el-tag></p>
|
||||
<p><strong>区县:</strong> {{ selectedGrid.districtName || 'N/A' }}</p>
|
||||
<p><strong>原始坐标:</strong> ({{ selectedGrid.gridX }}, {{ selectedGrid.gridY }})</p>
|
||||
<p><strong>是否为障碍物:</strong> <el-tag :type="selectedGrid.isObstacle ? 'danger' : 'success'">{{ selectedGrid.isObstacle ? '是' : '否' }}</el-tag></p>
|
||||
<p><strong>描述:</strong> {{ selectedGrid.description || '无' }}</p>
|
||||
<el-divider />
|
||||
<p><strong>负责人:</strong></p>
|
||||
<div v-if="selectedGridWorker">
|
||||
<el-tag effect="plain" type="info">
|
||||
<el-icon><User /></el-icon>
|
||||
{{ selectedGridWorker.name }} ({{ selectedGridWorker.phone }})
|
||||
</el-tag>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-tag type="warning">未分配</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="请在左侧地图上选择一个网格" />
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import type { Grid, UserAccount } from '@/api/types';
|
||||
import { getGrids } from '@/api/dashboard';
|
||||
import { getAllGridWorkers } from '@/api/personnel';
|
||||
import { User } from '@element-plus/icons-vue';
|
||||
|
||||
const CELL_SIZE = 25;
|
||||
const PADDING_CELLS = 2;
|
||||
|
||||
// --- State ---
|
||||
const gridData = ref<Grid[]>([]);
|
||||
const allWorkers = ref<UserAccount[]>([]);
|
||||
const loading = ref(false);
|
||||
const isLoadingWorkers = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const selectedGrid = ref<DisplayGrid | null>(null);
|
||||
|
||||
// --- Data Fetching & Refresh ---
|
||||
const fetchAllGrids = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
selectedGrid.value = null;
|
||||
try {
|
||||
const response = await getGrids();
|
||||
gridData.value = Array.isArray(response) ? response : (response as any).content || [];
|
||||
} catch (e: any) {
|
||||
error.value = e.message || '获取网格数据时发生未知错误';
|
||||
ElMessage.error(error.value);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAllWorkers = async () => {
|
||||
isLoadingWorkers.value = true;
|
||||
try {
|
||||
allWorkers.value = await getAllGridWorkers();
|
||||
} catch (e: any) {
|
||||
ElMessage.error('获取网格员列表失败: ' + e.message);
|
||||
} finally {
|
||||
isLoadingWorkers.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshData = async () => {
|
||||
await Promise.all([fetchAllGrids(), fetchAllWorkers()]);
|
||||
ElMessage.success('数据已刷新');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refreshData();
|
||||
});
|
||||
|
||||
// --- Computed Properties ---
|
||||
const workersByCoord = computed<Map<string, UserAccount>>(() => {
|
||||
const map = new Map<string, UserAccount>();
|
||||
allWorkers.value.forEach(worker => {
|
||||
if (worker.gridX !== null && worker.gridY !== null) {
|
||||
map.set(`${worker.gridX},${worker.gridY}`, worker);
|
||||
}
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
const selectedGridWorker = computed<UserAccount | undefined>(() => {
|
||||
if (!selectedGrid.value || !allWorkers.value.length) {
|
||||
return undefined;
|
||||
}
|
||||
return allWorkers.value.find(
|
||||
worker => worker.gridX === selectedGrid.value?.gridX && worker.gridY === selectedGrid.value?.gridY
|
||||
);
|
||||
});
|
||||
|
||||
// --- City Color Logic ---
|
||||
const cityColors = ref<Map<string, string>>(new Map());
|
||||
const getCityColor = (cityName: string): string => {
|
||||
if (!cityColors.value.has(cityName)) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < cityName.length; i++) {
|
||||
hash = cityName.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
const color = `hsl(${hash % 360}, 80%, 85%)`;
|
||||
cityColors.value.set(cityName, color);
|
||||
}
|
||||
return cityColors.value.get(cityName)!;
|
||||
};
|
||||
|
||||
const getGridFillColor = (grid: Grid): string => {
|
||||
if (grid.isObstacle) {
|
||||
return '#000000';
|
||||
}
|
||||
if (workersByCoord.value.has(`${grid.gridX},${grid.gridY}`)) {
|
||||
return '#FF0000';
|
||||
}
|
||||
return getCityColor(grid.cityName);
|
||||
};
|
||||
|
||||
// --- Display Logic ---
|
||||
interface DisplayGrid extends Grid {
|
||||
displayX: number;
|
||||
displayY: number;
|
||||
}
|
||||
|
||||
const displayGrids = computed<DisplayGrid[]>(() => {
|
||||
if (!gridData.value.length) return [];
|
||||
|
||||
// Determine the top-left corner of the entire map to normalize coordinates
|
||||
const minX = Math.min(...gridData.value.map(g => g.gridX));
|
||||
const minY = Math.min(...gridData.value.map(g => g.gridY));
|
||||
|
||||
// Render all grids based on their absolute positions, normalized to start at (0,0)
|
||||
return gridData.value.map(grid => ({
|
||||
...grid,
|
||||
displayX: grid.gridX - minX,
|
||||
displayY: grid.gridY - minY,
|
||||
}));
|
||||
});
|
||||
|
||||
const gridMap = computed(() => {
|
||||
const map = new Map<string, DisplayGrid>();
|
||||
displayGrids.value.forEach(grid => {
|
||||
map.set(`${grid.displayX},${grid.displayY}`, grid);
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
const svgDimensions = computed(() => {
|
||||
if (!displayGrids.value.length) return { width: 0, height: 0 };
|
||||
const maxX = Math.max(...displayGrids.value.map(g => g.displayX));
|
||||
const maxY = Math.max(...displayGrids.value.map(g => g.displayY));
|
||||
return {
|
||||
width: (maxX + 1) * CELL_SIZE,
|
||||
height: (maxY + 1) * CELL_SIZE,
|
||||
};
|
||||
});
|
||||
|
||||
const getBorder = (grid: DisplayGrid, side: 'top' | 'right' | 'bottom' | 'left'): boolean => {
|
||||
const { displayX, displayY, cityName } = grid;
|
||||
let neighbor: DisplayGrid | undefined;
|
||||
switch (side) {
|
||||
case 'top':
|
||||
neighbor = gridMap.value.get(`${displayX},${displayY - 1}`);
|
||||
break;
|
||||
case 'right':
|
||||
neighbor = gridMap.value.get(`${displayX + 1},${displayY}`);
|
||||
break;
|
||||
case 'bottom':
|
||||
neighbor = gridMap.value.get(`${displayX},${displayY + 1}`);
|
||||
break;
|
||||
case 'left':
|
||||
neighbor = gridMap.value.get(`${displayX - 1},${displayY}`);
|
||||
break;
|
||||
}
|
||||
return !neighbor || neighbor.cityName !== cityName;
|
||||
};
|
||||
|
||||
const selectGrid = (grid: DisplayGrid) => {
|
||||
selectedGrid.value = grid;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.grid-management-container {
|
||||
padding: 20px;
|
||||
background-color: #f7f8fa;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.main-content-grid {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.map-area {
|
||||
flex: 3;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
border: 1px solid #e9e9e9;
|
||||
}
|
||||
|
||||
.sidebar-area {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.details-card .card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.grid-svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.grid-cell {
|
||||
stroke: #fff;
|
||||
stroke-width: 1;
|
||||
transition: filter 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.grid-cell:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
.grid-cell.selected {
|
||||
stroke: #ff4d4f;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.border-line {
|
||||
stroke: #333;
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.retry-button {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.details-content p {
|
||||
margin: 0 0 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.details-content p strong {
|
||||
color: #303133;
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
224
ems-frontend/ems-monitoring-system/src/views/WorkerTaskView.vue
Normal file
224
ems-frontend/ems-monitoring-system/src/views/WorkerTaskView.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div class="worker-task-view">
|
||||
<el-card class="page-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>我的任务</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 筛选区域 -->
|
||||
<div class="filter-container">
|
||||
<el-radio-group v-model="selectedStatus" @change="fetchTasks">
|
||||
<el-radio-button :label="null">全部</el-radio-button>
|
||||
<el-radio-button label="ASSIGNED">已分配</el-radio-button>
|
||||
<el-radio-button label="IN_PROGRESS">进行中</el-radio-button>
|
||||
<el-radio-button label="SUBMITTED">已提交</el-radio-button>
|
||||
<el-radio-button label="COMPLETED">已完成</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<el-table :data="tasks" v-loading="loading" stripe class="task-table">
|
||||
<el-table-column prop="id" label="任务ID" width="80" />
|
||||
<el-table-column prop="title" label="任务标题" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusTagType(row.status)">{{ formatStatus(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="textAddress" label="地址" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column prop="severity" label="严重程度" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getSeverityTagType(row.severity)">{{ formatSeverity(row.severity) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="分配时间" width="180">
|
||||
<template #default="{ row }">
|
||||
<span>{{ new Date(row.createdAt).toLocaleString() }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="viewTaskDetails(row.id)">查看详情</el-button>
|
||||
<el-button v-if="row.status === 'ASSIGNED'" type="success" link size="small" @click="acceptTask(row.id)">接受</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-if="total > 0"
|
||||
class="pagination-container"
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive } from 'vue';
|
||||
import apiClient from '@/api';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import type { Pageable } from '@/api/types';
|
||||
|
||||
// --- 类型定义 ---
|
||||
interface TaskSummary {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
status: string;
|
||||
assigneeName: string | null;
|
||||
createdAt: string;
|
||||
textAddress: string;
|
||||
imageUrl: string;
|
||||
severity: string;
|
||||
}
|
||||
|
||||
// --- 响应式状态 ---
|
||||
const tasks = ref<TaskSummary[]>([]);
|
||||
const loading = ref(false);
|
||||
const selectedStatus = ref<string | null>(null);
|
||||
const total = ref(0);
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
});
|
||||
|
||||
// --- API 调用 ---
|
||||
const fetchTasks = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: any = {
|
||||
page: pagination.page - 1,
|
||||
size: pagination.size,
|
||||
};
|
||||
if (selectedStatus.value) {
|
||||
params.status = selectedStatus.value;
|
||||
}
|
||||
// 注意:这里的API返回的是Page<T>,但Controller返回的是List<T>
|
||||
// 需要调整Controller或这里的处理方式。暂时假设Controller返回List
|
||||
const response = await apiClient.get<TaskSummary[]>('/worker', { params });
|
||||
tasks.value = response;
|
||||
// @ts-ignore
|
||||
// total.value = response.totalElements || response.length; // 假设 totalElements
|
||||
} catch (error) {
|
||||
console.error('获取任务列表失败:', error);
|
||||
ElMessage.error('获取任务列表失败');
|
||||
tasks.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const acceptTask = async (taskId: number) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要接受这个任务吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info',
|
||||
});
|
||||
await apiClient.post(`/worker/${taskId}/accept`);
|
||||
ElMessage.success('任务已接受');
|
||||
fetchTasks(); // 刷新列表
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('接受任务失败:', error);
|
||||
ElMessage.error('接受任务失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --- 辅助函数 ---
|
||||
const formatStatus = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'ASSIGNED': '已分配',
|
||||
'IN_PROGRESS': '进行中',
|
||||
'SUBMITTED': '已提交',
|
||||
'COMPLETED': '已完成',
|
||||
'REJECTED': '已拒绝',
|
||||
};
|
||||
return map[status] || status;
|
||||
};
|
||||
|
||||
const getStatusTagType = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'ASSIGNED': 'primary',
|
||||
'IN_PROGRESS': 'warning',
|
||||
'SUBMITTED': 'info',
|
||||
'COMPLETED': 'success',
|
||||
'REJECTED': 'danger',
|
||||
};
|
||||
return map[status] || 'info';
|
||||
};
|
||||
|
||||
const formatSeverity = (severity: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'LOW': '低',
|
||||
'MEDIUM': '中',
|
||||
'HIGH': '高',
|
||||
};
|
||||
return map[severity] || severity;
|
||||
};
|
||||
|
||||
const getSeverityTagType = (severity: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'LOW': 'info',
|
||||
'MEDIUM': 'warning',
|
||||
'HIGH': 'danger',
|
||||
};
|
||||
return map[severity] || 'info';
|
||||
};
|
||||
|
||||
// --- 事件处理 ---
|
||||
const viewTaskDetails = (taskId: number) => {
|
||||
// TODO: 实现跳转到任务详情页或打开详情对话框
|
||||
ElMessage.info(`查看任务详情: ${taskId}`);
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.size = size;
|
||||
fetchTasks();
|
||||
};
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page;
|
||||
fetchTasks();
|
||||
};
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
onMounted(() => {
|
||||
fetchTasks();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.worker-task-view {
|
||||
padding: 20px;
|
||||
}
|
||||
.page-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.card-header {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.task-table {
|
||||
width: 100%;
|
||||
}
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
12
ems-frontend/ems-monitoring-system/tsconfig.app.json
Normal file
12
ems-frontend/ems-monitoring-system/tsconfig.app.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
11
ems-frontend/ems-monitoring-system/tsconfig.json
Normal file
11
ems-frontend/ems-monitoring-system/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
19
ems-frontend/ems-monitoring-system/tsconfig.node.json
Normal file
19
ems-frontend/ems-monitoring-system/tsconfig.node.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*",
|
||||
"eslint.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
40
ems-frontend/ems-monitoring-system/vite.config.ts
Normal file
40
ems-frontend/ems-monitoring-system/vite.config.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
// 字符串简写写法
|
||||
// '/foo': 'http://localhost:4567',
|
||||
// 选项写法
|
||||
'/api': {
|
||||
target: 'http://localhost:8080', // 确保这是您的后端服务地址
|
||||
changeOrigin: true,
|
||||
configure: (proxy, options) => {
|
||||
proxy.on('error', (err, req, res) => {
|
||||
console.log('代理错误:', err);
|
||||
});
|
||||
proxy.on('proxyReq', (proxyReq, req, res) => {
|
||||
console.log('发送代理请求到目标:', req.method, req.url);
|
||||
});
|
||||
proxy.on('proxyRes', (proxyRes, req, res) => {
|
||||
console.log('收到来自目标的代理响应:', proxyRes.statusCode, req.url);
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user