Showing
33 changed files
with
9764 additions
and
0 deletions
.gitignore
0 → 100644
| 1 | +# Logs | ||
| 2 | +logs | ||
| 3 | +*.log | ||
| 4 | +npm-debug.log* | ||
| 5 | +yarn-debug.log* | ||
| 6 | +yarn-error.log* | ||
| 7 | +pnpm-debug.log* | ||
| 8 | +lerna-debug.log* | ||
| 9 | + | ||
| 10 | +node_modules | ||
| 11 | +.DS_Store | ||
| 12 | +dist | ||
| 13 | +dist-ssr | ||
| 14 | +coverage | ||
| 15 | +*.local | ||
| 16 | + | ||
| 17 | +# Editor directories and files | ||
| 18 | +.vscode/* | ||
| 19 | +!.vscode/extensions.json | ||
| 20 | +.idea | ||
| 21 | +*.suo | ||
| 22 | +*.ntvs* | ||
| 23 | +*.njsproj | ||
| 24 | +*.sln | ||
| 25 | +*.sw? | ||
| 26 | + | ||
| 27 | +*.tsbuildinfo | ||
| 28 | + | ||
| 29 | +.eslintcache | ||
| 30 | + | ||
| 31 | +# Cypress | ||
| 32 | +/cypress/videos/ | ||
| 33 | +/cypress/screenshots/ | ||
| 34 | + | ||
| 35 | +# Vitest | ||
| 36 | +__screenshots__/ | ||
| 37 | + | ||
| 38 | +# Vite | ||
| 39 | +*.timestamp-*-*.mjs |
.vscode/extensions.json
0 → 100644
README.md
0 → 100644
| 1 | +# iot-bridge-ui | ||
| 2 | + | ||
| 3 | +This template should help get you started developing with Vue 3 in Vite. | ||
| 4 | + | ||
| 5 | +## Recommended IDE Setup | ||
| 6 | + | ||
| 7 | +[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). | ||
| 8 | + | ||
| 9 | +## Recommended Browser Setup | ||
| 10 | + | ||
| 11 | +- Chromium-based browsers (Chrome, Edge, Brave, etc.): | ||
| 12 | + - [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) | ||
| 13 | + - [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters) | ||
| 14 | +- Firefox: | ||
| 15 | + - [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/) | ||
| 16 | + - [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/) | ||
| 17 | + | ||
| 18 | +## Customize configuration | ||
| 19 | + | ||
| 20 | +See [Vite Configuration Reference](https://vite.dev/config/). | ||
| 21 | + | ||
| 22 | +## Project Setup | ||
| 23 | + | ||
| 24 | +```sh | ||
| 25 | +npm install | ||
| 26 | +``` | ||
| 27 | + | ||
| 28 | +### Compile and Hot-Reload for Development | ||
| 29 | + | ||
| 30 | +```sh | ||
| 31 | +npm run dev | ||
| 32 | +``` | ||
| 33 | + | ||
| 34 | +### Compile and Minify for Production | ||
| 35 | + | ||
| 36 | +```sh | ||
| 37 | +npm run build | ||
| 38 | +``` |
index.html
0 → 100644
| 1 | +<!DOCTYPE html> | ||
| 2 | +<html lang=""> | ||
| 3 | + <head> | ||
| 4 | + <meta charset="UTF-8"> | ||
| 5 | + <link rel="icon" href="/favicon.ico"> | ||
| 6 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| 7 | + <title>Vite App</title> | ||
| 8 | + </head> | ||
| 9 | + <body> | ||
| 10 | + <div id="app"></div> | ||
| 11 | + <script type="module" src="/src/main.js"></script> | ||
| 12 | + </body> | ||
| 13 | +</html> |
jsconfig.json
0 → 100644
package-lock.json
0 → 100644
| 1 | +{ | ||
| 2 | + "name": "iot-bridge-ui", | ||
| 3 | + "version": "0.0.0", | ||
| 4 | + "lockfileVersion": 3, | ||
| 5 | + "requires": true, | ||
| 6 | + "packages": { | ||
| 7 | + "": { | ||
| 8 | + "name": "iot-bridge-ui", | ||
| 9 | + "version": "0.0.0", | ||
| 10 | + "dependencies": { | ||
| 11 | + "@element-plus/icons-vue": "^2.3.2", | ||
| 12 | + "element-plus": "^2.13.7", | ||
| 13 | + "vue": "^3.5.32", | ||
| 14 | + "vue-router": "^4.6.4" | ||
| 15 | + }, | ||
| 16 | + "devDependencies": { | ||
| 17 | + "@vitejs/plugin-vue": "^6.0.6", | ||
| 18 | + "vite": "^8.0.8", | ||
| 19 | + "vite-plugin-vue-devtools": "^8.1.1" | ||
| 20 | + }, | ||
| 21 | + "engines": { | ||
| 22 | + "node": "^20.19.0 || >=22.12.0" | ||
| 23 | + } | ||
| 24 | + }, | ||
| 25 | + "node_modules/@babel/code-frame": { | ||
| 26 | + "version": "7.29.0", | ||
| 27 | + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz", | ||
| 28 | + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", | ||
| 29 | + "dev": true, | ||
| 30 | + "license": "MIT", | ||
| 31 | + "dependencies": { | ||
| 32 | + "@babel/helper-validator-identifier": "^7.28.5", | ||
| 33 | + "js-tokens": "^4.0.0", | ||
| 34 | + "picocolors": "^1.1.1" | ||
| 35 | + }, | ||
| 36 | + "engines": { | ||
| 37 | + "node": ">=6.9.0" | ||
| 38 | + } | ||
| 39 | + }, | ||
| 40 | + "node_modules/@babel/compat-data": { | ||
| 41 | + "version": "7.29.0", | ||
| 42 | + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.0.tgz", | ||
| 43 | + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", | ||
| 44 | + "dev": true, | ||
| 45 | + "license": "MIT", | ||
| 46 | + "engines": { | ||
| 47 | + "node": ">=6.9.0" | ||
| 48 | + } | ||
| 49 | + }, | ||
| 50 | + "node_modules/@babel/core": { | ||
| 51 | + "version": "7.29.0", | ||
| 52 | + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.0.tgz", | ||
| 53 | + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", | ||
| 54 | + "dev": true, | ||
| 55 | + "license": "MIT", | ||
| 56 | + "dependencies": { | ||
| 57 | + "@babel/code-frame": "^7.29.0", | ||
| 58 | + "@babel/generator": "^7.29.0", | ||
| 59 | + "@babel/helper-compilation-targets": "^7.28.6", | ||
| 60 | + "@babel/helper-module-transforms": "^7.28.6", | ||
| 61 | + "@babel/helpers": "^7.28.6", | ||
| 62 | + "@babel/parser": "^7.29.0", | ||
| 63 | + "@babel/template": "^7.28.6", | ||
| 64 | + "@babel/traverse": "^7.29.0", | ||
| 65 | + "@babel/types": "^7.29.0", | ||
| 66 | + "@jridgewell/remapping": "^2.3.5", | ||
| 67 | + "convert-source-map": "^2.0.0", | ||
| 68 | + "debug": "^4.1.0", | ||
| 69 | + "gensync": "^1.0.0-beta.2", | ||
| 70 | + "json5": "^2.2.3", | ||
| 71 | + "semver": "^6.3.1" | ||
| 72 | + }, | ||
| 73 | + "engines": { | ||
| 74 | + "node": ">=6.9.0" | ||
| 75 | + }, | ||
| 76 | + "funding": { | ||
| 77 | + "type": "opencollective", | ||
| 78 | + "url": "https://opencollective.com/babel" | ||
| 79 | + } | ||
| 80 | + }, | ||
| 81 | + "node_modules/@babel/generator": { | ||
| 82 | + "version": "7.29.1", | ||
| 83 | + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.1.tgz", | ||
| 84 | + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", | ||
| 85 | + "dev": true, | ||
| 86 | + "license": "MIT", | ||
| 87 | + "dependencies": { | ||
| 88 | + "@babel/parser": "^7.29.0", | ||
| 89 | + "@babel/types": "^7.29.0", | ||
| 90 | + "@jridgewell/gen-mapping": "^0.3.12", | ||
| 91 | + "@jridgewell/trace-mapping": "^0.3.28", | ||
| 92 | + "jsesc": "^3.0.2" | ||
| 93 | + }, | ||
| 94 | + "engines": { | ||
| 95 | + "node": ">=6.9.0" | ||
| 96 | + } | ||
| 97 | + }, | ||
| 98 | + "node_modules/@babel/helper-annotate-as-pure": { | ||
| 99 | + "version": "7.27.3", | ||
| 100 | + "resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", | ||
| 101 | + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", | ||
| 102 | + "dev": true, | ||
| 103 | + "license": "MIT", | ||
| 104 | + "dependencies": { | ||
| 105 | + "@babel/types": "^7.27.3" | ||
| 106 | + }, | ||
| 107 | + "engines": { | ||
| 108 | + "node": ">=6.9.0" | ||
| 109 | + } | ||
| 110 | + }, | ||
| 111 | + "node_modules/@babel/helper-compilation-targets": { | ||
| 112 | + "version": "7.28.6", | ||
| 113 | + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", | ||
| 114 | + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", | ||
| 115 | + "dev": true, | ||
| 116 | + "license": "MIT", | ||
| 117 | + "dependencies": { | ||
| 118 | + "@babel/compat-data": "^7.28.6", | ||
| 119 | + "@babel/helper-validator-option": "^7.27.1", | ||
| 120 | + "browserslist": "^4.24.0", | ||
| 121 | + "lru-cache": "^5.1.1", | ||
| 122 | + "semver": "^6.3.1" | ||
| 123 | + }, | ||
| 124 | + "engines": { | ||
| 125 | + "node": ">=6.9.0" | ||
| 126 | + } | ||
| 127 | + }, | ||
| 128 | + "node_modules/@babel/helper-create-class-features-plugin": { | ||
| 129 | + "version": "7.28.6", | ||
| 130 | + "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", | ||
| 131 | + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", | ||
| 132 | + "dev": true, | ||
| 133 | + "license": "MIT", | ||
| 134 | + "dependencies": { | ||
| 135 | + "@babel/helper-annotate-as-pure": "^7.27.3", | ||
| 136 | + "@babel/helper-member-expression-to-functions": "^7.28.5", | ||
| 137 | + "@babel/helper-optimise-call-expression": "^7.27.1", | ||
| 138 | + "@babel/helper-replace-supers": "^7.28.6", | ||
| 139 | + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", | ||
| 140 | + "@babel/traverse": "^7.28.6", | ||
| 141 | + "semver": "^6.3.1" | ||
| 142 | + }, | ||
| 143 | + "engines": { | ||
| 144 | + "node": ">=6.9.0" | ||
| 145 | + }, | ||
| 146 | + "peerDependencies": { | ||
| 147 | + "@babel/core": "^7.0.0" | ||
| 148 | + } | ||
| 149 | + }, | ||
| 150 | + "node_modules/@babel/helper-globals": { | ||
| 151 | + "version": "7.28.0", | ||
| 152 | + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", | ||
| 153 | + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", | ||
| 154 | + "dev": true, | ||
| 155 | + "license": "MIT", | ||
| 156 | + "engines": { | ||
| 157 | + "node": ">=6.9.0" | ||
| 158 | + } | ||
| 159 | + }, | ||
| 160 | + "node_modules/@babel/helper-member-expression-to-functions": { | ||
| 161 | + "version": "7.28.5", | ||
| 162 | + "resolved": "https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", | ||
| 163 | + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", | ||
| 164 | + "dev": true, | ||
| 165 | + "license": "MIT", | ||
| 166 | + "dependencies": { | ||
| 167 | + "@babel/traverse": "^7.28.5", | ||
| 168 | + "@babel/types": "^7.28.5" | ||
| 169 | + }, | ||
| 170 | + "engines": { | ||
| 171 | + "node": ">=6.9.0" | ||
| 172 | + } | ||
| 173 | + }, | ||
| 174 | + "node_modules/@babel/helper-module-imports": { | ||
| 175 | + "version": "7.28.6", | ||
| 176 | + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", | ||
| 177 | + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", | ||
| 178 | + "dev": true, | ||
| 179 | + "license": "MIT", | ||
| 180 | + "dependencies": { | ||
| 181 | + "@babel/traverse": "^7.28.6", | ||
| 182 | + "@babel/types": "^7.28.6" | ||
| 183 | + }, | ||
| 184 | + "engines": { | ||
| 185 | + "node": ">=6.9.0" | ||
| 186 | + } | ||
| 187 | + }, | ||
| 188 | + "node_modules/@babel/helper-module-transforms": { | ||
| 189 | + "version": "7.28.6", | ||
| 190 | + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", | ||
| 191 | + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", | ||
| 192 | + "dev": true, | ||
| 193 | + "license": "MIT", | ||
| 194 | + "dependencies": { | ||
| 195 | + "@babel/helper-module-imports": "^7.28.6", | ||
| 196 | + "@babel/helper-validator-identifier": "^7.28.5", | ||
| 197 | + "@babel/traverse": "^7.28.6" | ||
| 198 | + }, | ||
| 199 | + "engines": { | ||
| 200 | + "node": ">=6.9.0" | ||
| 201 | + }, | ||
| 202 | + "peerDependencies": { | ||
| 203 | + "@babel/core": "^7.0.0" | ||
| 204 | + } | ||
| 205 | + }, | ||
| 206 | + "node_modules/@babel/helper-optimise-call-expression": { | ||
| 207 | + "version": "7.27.1", | ||
| 208 | + "resolved": "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", | ||
| 209 | + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", | ||
| 210 | + "dev": true, | ||
| 211 | + "license": "MIT", | ||
| 212 | + "dependencies": { | ||
| 213 | + "@babel/types": "^7.27.1" | ||
| 214 | + }, | ||
| 215 | + "engines": { | ||
| 216 | + "node": ">=6.9.0" | ||
| 217 | + } | ||
| 218 | + }, | ||
| 219 | + "node_modules/@babel/helper-plugin-utils": { | ||
| 220 | + "version": "7.28.6", | ||
| 221 | + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", | ||
| 222 | + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", | ||
| 223 | + "dev": true, | ||
| 224 | + "license": "MIT", | ||
| 225 | + "engines": { | ||
| 226 | + "node": ">=6.9.0" | ||
| 227 | + } | ||
| 228 | + }, | ||
| 229 | + "node_modules/@babel/helper-replace-supers": { | ||
| 230 | + "version": "7.28.6", | ||
| 231 | + "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", | ||
| 232 | + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", | ||
| 233 | + "dev": true, | ||
| 234 | + "license": "MIT", | ||
| 235 | + "dependencies": { | ||
| 236 | + "@babel/helper-member-expression-to-functions": "^7.28.5", | ||
| 237 | + "@babel/helper-optimise-call-expression": "^7.27.1", | ||
| 238 | + "@babel/traverse": "^7.28.6" | ||
| 239 | + }, | ||
| 240 | + "engines": { | ||
| 241 | + "node": ">=6.9.0" | ||
| 242 | + }, | ||
| 243 | + "peerDependencies": { | ||
| 244 | + "@babel/core": "^7.0.0" | ||
| 245 | + } | ||
| 246 | + }, | ||
| 247 | + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { | ||
| 248 | + "version": "7.27.1", | ||
| 249 | + "resolved": "https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", | ||
| 250 | + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", | ||
| 251 | + "dev": true, | ||
| 252 | + "license": "MIT", | ||
| 253 | + "dependencies": { | ||
| 254 | + "@babel/traverse": "^7.27.1", | ||
| 255 | + "@babel/types": "^7.27.1" | ||
| 256 | + }, | ||
| 257 | + "engines": { | ||
| 258 | + "node": ">=6.9.0" | ||
| 259 | + } | ||
| 260 | + }, | ||
| 261 | + "node_modules/@babel/helper-string-parser": { | ||
| 262 | + "version": "7.27.1", | ||
| 263 | + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", | ||
| 264 | + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", | ||
| 265 | + "license": "MIT", | ||
| 266 | + "engines": { | ||
| 267 | + "node": ">=6.9.0" | ||
| 268 | + } | ||
| 269 | + }, | ||
| 270 | + "node_modules/@babel/helper-validator-identifier": { | ||
| 271 | + "version": "7.28.5", | ||
| 272 | + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", | ||
| 273 | + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", | ||
| 274 | + "license": "MIT", | ||
| 275 | + "engines": { | ||
| 276 | + "node": ">=6.9.0" | ||
| 277 | + } | ||
| 278 | + }, | ||
| 279 | + "node_modules/@babel/helper-validator-option": { | ||
| 280 | + "version": "7.27.1", | ||
| 281 | + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", | ||
| 282 | + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", | ||
| 283 | + "dev": true, | ||
| 284 | + "license": "MIT", | ||
| 285 | + "engines": { | ||
| 286 | + "node": ">=6.9.0" | ||
| 287 | + } | ||
| 288 | + }, | ||
| 289 | + "node_modules/@babel/helpers": { | ||
| 290 | + "version": "7.29.2", | ||
| 291 | + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.2.tgz", | ||
| 292 | + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", | ||
| 293 | + "dev": true, | ||
| 294 | + "license": "MIT", | ||
| 295 | + "dependencies": { | ||
| 296 | + "@babel/template": "^7.28.6", | ||
| 297 | + "@babel/types": "^7.29.0" | ||
| 298 | + }, | ||
| 299 | + "engines": { | ||
| 300 | + "node": ">=6.9.0" | ||
| 301 | + } | ||
| 302 | + }, | ||
| 303 | + "node_modules/@babel/parser": { | ||
| 304 | + "version": "7.29.2", | ||
| 305 | + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.2.tgz", | ||
| 306 | + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", | ||
| 307 | + "license": "MIT", | ||
| 308 | + "dependencies": { | ||
| 309 | + "@babel/types": "^7.29.0" | ||
| 310 | + }, | ||
| 311 | + "bin": { | ||
| 312 | + "parser": "bin/babel-parser.js" | ||
| 313 | + }, | ||
| 314 | + "engines": { | ||
| 315 | + "node": ">=6.0.0" | ||
| 316 | + } | ||
| 317 | + }, | ||
| 318 | + "node_modules/@babel/plugin-proposal-decorators": { | ||
| 319 | + "version": "7.29.0", | ||
| 320 | + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz", | ||
| 321 | + "integrity": "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==", | ||
| 322 | + "dev": true, | ||
| 323 | + "license": "MIT", | ||
| 324 | + "dependencies": { | ||
| 325 | + "@babel/helper-create-class-features-plugin": "^7.28.6", | ||
| 326 | + "@babel/helper-plugin-utils": "^7.28.6", | ||
| 327 | + "@babel/plugin-syntax-decorators": "^7.28.6" | ||
| 328 | + }, | ||
| 329 | + "engines": { | ||
| 330 | + "node": ">=6.9.0" | ||
| 331 | + }, | ||
| 332 | + "peerDependencies": { | ||
| 333 | + "@babel/core": "^7.0.0-0" | ||
| 334 | + } | ||
| 335 | + }, | ||
| 336 | + "node_modules/@babel/plugin-syntax-decorators": { | ||
| 337 | + "version": "7.28.6", | ||
| 338 | + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz", | ||
| 339 | + "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==", | ||
| 340 | + "dev": true, | ||
| 341 | + "license": "MIT", | ||
| 342 | + "dependencies": { | ||
| 343 | + "@babel/helper-plugin-utils": "^7.28.6" | ||
| 344 | + }, | ||
| 345 | + "engines": { | ||
| 346 | + "node": ">=6.9.0" | ||
| 347 | + }, | ||
| 348 | + "peerDependencies": { | ||
| 349 | + "@babel/core": "^7.0.0-0" | ||
| 350 | + } | ||
| 351 | + }, | ||
| 352 | + "node_modules/@babel/plugin-syntax-import-attributes": { | ||
| 353 | + "version": "7.28.6", | ||
| 354 | + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", | ||
| 355 | + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", | ||
| 356 | + "dev": true, | ||
| 357 | + "license": "MIT", | ||
| 358 | + "dependencies": { | ||
| 359 | + "@babel/helper-plugin-utils": "^7.28.6" | ||
| 360 | + }, | ||
| 361 | + "engines": { | ||
| 362 | + "node": ">=6.9.0" | ||
| 363 | + }, | ||
| 364 | + "peerDependencies": { | ||
| 365 | + "@babel/core": "^7.0.0-0" | ||
| 366 | + } | ||
| 367 | + }, | ||
| 368 | + "node_modules/@babel/plugin-syntax-import-meta": { | ||
| 369 | + "version": "7.10.4", | ||
| 370 | + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", | ||
| 371 | + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", | ||
| 372 | + "dev": true, | ||
| 373 | + "license": "MIT", | ||
| 374 | + "dependencies": { | ||
| 375 | + "@babel/helper-plugin-utils": "^7.10.4" | ||
| 376 | + }, | ||
| 377 | + "peerDependencies": { | ||
| 378 | + "@babel/core": "^7.0.0-0" | ||
| 379 | + } | ||
| 380 | + }, | ||
| 381 | + "node_modules/@babel/plugin-syntax-jsx": { | ||
| 382 | + "version": "7.28.6", | ||
| 383 | + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", | ||
| 384 | + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", | ||
| 385 | + "dev": true, | ||
| 386 | + "license": "MIT", | ||
| 387 | + "dependencies": { | ||
| 388 | + "@babel/helper-plugin-utils": "^7.28.6" | ||
| 389 | + }, | ||
| 390 | + "engines": { | ||
| 391 | + "node": ">=6.9.0" | ||
| 392 | + }, | ||
| 393 | + "peerDependencies": { | ||
| 394 | + "@babel/core": "^7.0.0-0" | ||
| 395 | + } | ||
| 396 | + }, | ||
| 397 | + "node_modules/@babel/plugin-syntax-typescript": { | ||
| 398 | + "version": "7.28.6", | ||
| 399 | + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", | ||
| 400 | + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", | ||
| 401 | + "dev": true, | ||
| 402 | + "license": "MIT", | ||
| 403 | + "dependencies": { | ||
| 404 | + "@babel/helper-plugin-utils": "^7.28.6" | ||
| 405 | + }, | ||
| 406 | + "engines": { | ||
| 407 | + "node": ">=6.9.0" | ||
| 408 | + }, | ||
| 409 | + "peerDependencies": { | ||
| 410 | + "@babel/core": "^7.0.0-0" | ||
| 411 | + } | ||
| 412 | + }, | ||
| 413 | + "node_modules/@babel/plugin-transform-typescript": { | ||
| 414 | + "version": "7.28.6", | ||
| 415 | + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", | ||
| 416 | + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", | ||
| 417 | + "dev": true, | ||
| 418 | + "license": "MIT", | ||
| 419 | + "dependencies": { | ||
| 420 | + "@babel/helper-annotate-as-pure": "^7.27.3", | ||
| 421 | + "@babel/helper-create-class-features-plugin": "^7.28.6", | ||
| 422 | + "@babel/helper-plugin-utils": "^7.28.6", | ||
| 423 | + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", | ||
| 424 | + "@babel/plugin-syntax-typescript": "^7.28.6" | ||
| 425 | + }, | ||
| 426 | + "engines": { | ||
| 427 | + "node": ">=6.9.0" | ||
| 428 | + }, | ||
| 429 | + "peerDependencies": { | ||
| 430 | + "@babel/core": "^7.0.0-0" | ||
| 431 | + } | ||
| 432 | + }, | ||
| 433 | + "node_modules/@babel/template": { | ||
| 434 | + "version": "7.28.6", | ||
| 435 | + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz", | ||
| 436 | + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", | ||
| 437 | + "dev": true, | ||
| 438 | + "license": "MIT", | ||
| 439 | + "dependencies": { | ||
| 440 | + "@babel/code-frame": "^7.28.6", | ||
| 441 | + "@babel/parser": "^7.28.6", | ||
| 442 | + "@babel/types": "^7.28.6" | ||
| 443 | + }, | ||
| 444 | + "engines": { | ||
| 445 | + "node": ">=6.9.0" | ||
| 446 | + } | ||
| 447 | + }, | ||
| 448 | + "node_modules/@babel/traverse": { | ||
| 449 | + "version": "7.29.0", | ||
| 450 | + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.0.tgz", | ||
| 451 | + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", | ||
| 452 | + "dev": true, | ||
| 453 | + "license": "MIT", | ||
| 454 | + "dependencies": { | ||
| 455 | + "@babel/code-frame": "^7.29.0", | ||
| 456 | + "@babel/generator": "^7.29.0", | ||
| 457 | + "@babel/helper-globals": "^7.28.0", | ||
| 458 | + "@babel/parser": "^7.29.0", | ||
| 459 | + "@babel/template": "^7.28.6", | ||
| 460 | + "@babel/types": "^7.29.0", | ||
| 461 | + "debug": "^4.3.1" | ||
| 462 | + }, | ||
| 463 | + "engines": { | ||
| 464 | + "node": ">=6.9.0" | ||
| 465 | + } | ||
| 466 | + }, | ||
| 467 | + "node_modules/@babel/types": { | ||
| 468 | + "version": "7.29.0", | ||
| 469 | + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", | ||
| 470 | + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", | ||
| 471 | + "license": "MIT", | ||
| 472 | + "dependencies": { | ||
| 473 | + "@babel/helper-string-parser": "^7.27.1", | ||
| 474 | + "@babel/helper-validator-identifier": "^7.28.5" | ||
| 475 | + }, | ||
| 476 | + "engines": { | ||
| 477 | + "node": ">=6.9.0" | ||
| 478 | + } | ||
| 479 | + }, | ||
| 480 | + "node_modules/@ctrl/tinycolor": { | ||
| 481 | + "version": "4.2.0", | ||
| 482 | + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", | ||
| 483 | + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", | ||
| 484 | + "license": "MIT", | ||
| 485 | + "engines": { | ||
| 486 | + "node": ">=14" | ||
| 487 | + } | ||
| 488 | + }, | ||
| 489 | + "node_modules/@element-plus/icons-vue": { | ||
| 490 | + "version": "2.3.2", | ||
| 491 | + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", | ||
| 492 | + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", | ||
| 493 | + "license": "MIT", | ||
| 494 | + "peerDependencies": { | ||
| 495 | + "vue": "^3.2.0" | ||
| 496 | + } | ||
| 497 | + }, | ||
| 498 | + "node_modules/@emnapi/core": { | ||
| 499 | + "version": "1.10.0", | ||
| 500 | + "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz", | ||
| 501 | + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", | ||
| 502 | + "dev": true, | ||
| 503 | + "license": "MIT", | ||
| 504 | + "optional": true, | ||
| 505 | + "dependencies": { | ||
| 506 | + "@emnapi/wasi-threads": "1.2.1", | ||
| 507 | + "tslib": "^2.4.0" | ||
| 508 | + } | ||
| 509 | + }, | ||
| 510 | + "node_modules/@emnapi/runtime": { | ||
| 511 | + "version": "1.10.0", | ||
| 512 | + "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.10.0.tgz", | ||
| 513 | + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", | ||
| 514 | + "dev": true, | ||
| 515 | + "license": "MIT", | ||
| 516 | + "optional": true, | ||
| 517 | + "dependencies": { | ||
| 518 | + "tslib": "^2.4.0" | ||
| 519 | + } | ||
| 520 | + }, | ||
| 521 | + "node_modules/@emnapi/wasi-threads": { | ||
| 522 | + "version": "1.2.1", | ||
| 523 | + "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", | ||
| 524 | + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", | ||
| 525 | + "dev": true, | ||
| 526 | + "license": "MIT", | ||
| 527 | + "optional": true, | ||
| 528 | + "dependencies": { | ||
| 529 | + "tslib": "^2.4.0" | ||
| 530 | + } | ||
| 531 | + }, | ||
| 532 | + "node_modules/@floating-ui/core": { | ||
| 533 | + "version": "1.7.5", | ||
| 534 | + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", | ||
| 535 | + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", | ||
| 536 | + "license": "MIT", | ||
| 537 | + "dependencies": { | ||
| 538 | + "@floating-ui/utils": "^0.2.11" | ||
| 539 | + } | ||
| 540 | + }, | ||
| 541 | + "node_modules/@floating-ui/dom": { | ||
| 542 | + "version": "1.7.6", | ||
| 543 | + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", | ||
| 544 | + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", | ||
| 545 | + "license": "MIT", | ||
| 546 | + "dependencies": { | ||
| 547 | + "@floating-ui/core": "^1.7.5", | ||
| 548 | + "@floating-ui/utils": "^0.2.11" | ||
| 549 | + } | ||
| 550 | + }, | ||
| 551 | + "node_modules/@floating-ui/utils": { | ||
| 552 | + "version": "0.2.11", | ||
| 553 | + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", | ||
| 554 | + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", | ||
| 555 | + "license": "MIT" | ||
| 556 | + }, | ||
| 557 | + "node_modules/@jridgewell/gen-mapping": { | ||
| 558 | + "version": "0.3.13", | ||
| 559 | + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", | ||
| 560 | + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", | ||
| 561 | + "dev": true, | ||
| 562 | + "license": "MIT", | ||
| 563 | + "dependencies": { | ||
| 564 | + "@jridgewell/sourcemap-codec": "^1.5.0", | ||
| 565 | + "@jridgewell/trace-mapping": "^0.3.24" | ||
| 566 | + } | ||
| 567 | + }, | ||
| 568 | + "node_modules/@jridgewell/remapping": { | ||
| 569 | + "version": "2.3.5", | ||
| 570 | + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", | ||
| 571 | + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", | ||
| 572 | + "dev": true, | ||
| 573 | + "license": "MIT", | ||
| 574 | + "dependencies": { | ||
| 575 | + "@jridgewell/gen-mapping": "^0.3.5", | ||
| 576 | + "@jridgewell/trace-mapping": "^0.3.24" | ||
| 577 | + } | ||
| 578 | + }, | ||
| 579 | + "node_modules/@jridgewell/resolve-uri": { | ||
| 580 | + "version": "3.1.2", | ||
| 581 | + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", | ||
| 582 | + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", | ||
| 583 | + "dev": true, | ||
| 584 | + "license": "MIT", | ||
| 585 | + "engines": { | ||
| 586 | + "node": ">=6.0.0" | ||
| 587 | + } | ||
| 588 | + }, | ||
| 589 | + "node_modules/@jridgewell/sourcemap-codec": { | ||
| 590 | + "version": "1.5.5", | ||
| 591 | + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", | ||
| 592 | + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", | ||
| 593 | + "license": "MIT" | ||
| 594 | + }, | ||
| 595 | + "node_modules/@jridgewell/trace-mapping": { | ||
| 596 | + "version": "0.3.31", | ||
| 597 | + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", | ||
| 598 | + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", | ||
| 599 | + "dev": true, | ||
| 600 | + "license": "MIT", | ||
| 601 | + "dependencies": { | ||
| 602 | + "@jridgewell/resolve-uri": "^3.1.0", | ||
| 603 | + "@jridgewell/sourcemap-codec": "^1.4.14" | ||
| 604 | + } | ||
| 605 | + }, | ||
| 606 | + "node_modules/@napi-rs/wasm-runtime": { | ||
| 607 | + "version": "1.1.4", | ||
| 608 | + "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", | ||
| 609 | + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", | ||
| 610 | + "dev": true, | ||
| 611 | + "license": "MIT", | ||
| 612 | + "optional": true, | ||
| 613 | + "dependencies": { | ||
| 614 | + "@tybys/wasm-util": "^0.10.1" | ||
| 615 | + }, | ||
| 616 | + "funding": { | ||
| 617 | + "type": "github", | ||
| 618 | + "url": "https://github.com/sponsors/Brooooooklyn" | ||
| 619 | + }, | ||
| 620 | + "peerDependencies": { | ||
| 621 | + "@emnapi/core": "^1.7.1", | ||
| 622 | + "@emnapi/runtime": "^1.7.1" | ||
| 623 | + } | ||
| 624 | + }, | ||
| 625 | + "node_modules/@oxc-project/types": { | ||
| 626 | + "version": "0.127.0", | ||
| 627 | + "resolved": "https://registry.npmmirror.com/@oxc-project/types/-/types-0.127.0.tgz", | ||
| 628 | + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", | ||
| 629 | + "dev": true, | ||
| 630 | + "license": "MIT", | ||
| 631 | + "funding": { | ||
| 632 | + "url": "https://github.com/sponsors/Boshen" | ||
| 633 | + } | ||
| 634 | + }, | ||
| 635 | + "node_modules/@polka/url": { | ||
| 636 | + "version": "1.0.0-next.29", | ||
| 637 | + "resolved": "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.29.tgz", | ||
| 638 | + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", | ||
| 639 | + "dev": true, | ||
| 640 | + "license": "MIT" | ||
| 641 | + }, | ||
| 642 | + "node_modules/@popperjs/core": { | ||
| 643 | + "name": "@sxzz/popperjs-es", | ||
| 644 | + "version": "2.11.8", | ||
| 645 | + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", | ||
| 646 | + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", | ||
| 647 | + "license": "MIT", | ||
| 648 | + "funding": { | ||
| 649 | + "type": "opencollective", | ||
| 650 | + "url": "https://opencollective.com/popperjs" | ||
| 651 | + } | ||
| 652 | + }, | ||
| 653 | + "node_modules/@rolldown/binding-android-arm64": { | ||
| 654 | + "version": "1.0.0-rc.17", | ||
| 655 | + "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", | ||
| 656 | + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", | ||
| 657 | + "cpu": [ | ||
| 658 | + "arm64" | ||
| 659 | + ], | ||
| 660 | + "dev": true, | ||
| 661 | + "license": "MIT", | ||
| 662 | + "optional": true, | ||
| 663 | + "os": [ | ||
| 664 | + "android" | ||
| 665 | + ], | ||
| 666 | + "engines": { | ||
| 667 | + "node": "^20.19.0 || >=22.12.0" | ||
| 668 | + } | ||
| 669 | + }, | ||
| 670 | + "node_modules/@rolldown/binding-darwin-arm64": { | ||
| 671 | + "version": "1.0.0-rc.17", | ||
| 672 | + "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", | ||
| 673 | + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", | ||
| 674 | + "cpu": [ | ||
| 675 | + "arm64" | ||
| 676 | + ], | ||
| 677 | + "dev": true, | ||
| 678 | + "license": "MIT", | ||
| 679 | + "optional": true, | ||
| 680 | + "os": [ | ||
| 681 | + "darwin" | ||
| 682 | + ], | ||
| 683 | + "engines": { | ||
| 684 | + "node": "^20.19.0 || >=22.12.0" | ||
| 685 | + } | ||
| 686 | + }, | ||
| 687 | + "node_modules/@rolldown/binding-darwin-x64": { | ||
| 688 | + "version": "1.0.0-rc.17", | ||
| 689 | + "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", | ||
| 690 | + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", | ||
| 691 | + "cpu": [ | ||
| 692 | + "x64" | ||
| 693 | + ], | ||
| 694 | + "dev": true, | ||
| 695 | + "license": "MIT", | ||
| 696 | + "optional": true, | ||
| 697 | + "os": [ | ||
| 698 | + "darwin" | ||
| 699 | + ], | ||
| 700 | + "engines": { | ||
| 701 | + "node": "^20.19.0 || >=22.12.0" | ||
| 702 | + } | ||
| 703 | + }, | ||
| 704 | + "node_modules/@rolldown/binding-freebsd-x64": { | ||
| 705 | + "version": "1.0.0-rc.17", | ||
| 706 | + "resolved": "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", | ||
| 707 | + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", | ||
| 708 | + "cpu": [ | ||
| 709 | + "x64" | ||
| 710 | + ], | ||
| 711 | + "dev": true, | ||
| 712 | + "license": "MIT", | ||
| 713 | + "optional": true, | ||
| 714 | + "os": [ | ||
| 715 | + "freebsd" | ||
| 716 | + ], | ||
| 717 | + "engines": { | ||
| 718 | + "node": "^20.19.0 || >=22.12.0" | ||
| 719 | + } | ||
| 720 | + }, | ||
| 721 | + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { | ||
| 722 | + "version": "1.0.0-rc.17", | ||
| 723 | + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", | ||
| 724 | + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", | ||
| 725 | + "cpu": [ | ||
| 726 | + "arm" | ||
| 727 | + ], | ||
| 728 | + "dev": true, | ||
| 729 | + "license": "MIT", | ||
| 730 | + "optional": true, | ||
| 731 | + "os": [ | ||
| 732 | + "linux" | ||
| 733 | + ], | ||
| 734 | + "engines": { | ||
| 735 | + "node": "^20.19.0 || >=22.12.0" | ||
| 736 | + } | ||
| 737 | + }, | ||
| 738 | + "node_modules/@rolldown/binding-linux-arm64-gnu": { | ||
| 739 | + "version": "1.0.0-rc.17", | ||
| 740 | + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", | ||
| 741 | + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", | ||
| 742 | + "cpu": [ | ||
| 743 | + "arm64" | ||
| 744 | + ], | ||
| 745 | + "dev": true, | ||
| 746 | + "license": "MIT", | ||
| 747 | + "optional": true, | ||
| 748 | + "os": [ | ||
| 749 | + "linux" | ||
| 750 | + ], | ||
| 751 | + "engines": { | ||
| 752 | + "node": "^20.19.0 || >=22.12.0" | ||
| 753 | + } | ||
| 754 | + }, | ||
| 755 | + "node_modules/@rolldown/binding-linux-arm64-musl": { | ||
| 756 | + "version": "1.0.0-rc.17", | ||
| 757 | + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", | ||
| 758 | + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", | ||
| 759 | + "cpu": [ | ||
| 760 | + "arm64" | ||
| 761 | + ], | ||
| 762 | + "dev": true, | ||
| 763 | + "license": "MIT", | ||
| 764 | + "optional": true, | ||
| 765 | + "os": [ | ||
| 766 | + "linux" | ||
| 767 | + ], | ||
| 768 | + "engines": { | ||
| 769 | + "node": "^20.19.0 || >=22.12.0" | ||
| 770 | + } | ||
| 771 | + }, | ||
| 772 | + "node_modules/@rolldown/binding-linux-ppc64-gnu": { | ||
| 773 | + "version": "1.0.0-rc.17", | ||
| 774 | + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", | ||
| 775 | + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", | ||
| 776 | + "cpu": [ | ||
| 777 | + "ppc64" | ||
| 778 | + ], | ||
| 779 | + "dev": true, | ||
| 780 | + "license": "MIT", | ||
| 781 | + "optional": true, | ||
| 782 | + "os": [ | ||
| 783 | + "linux" | ||
| 784 | + ], | ||
| 785 | + "engines": { | ||
| 786 | + "node": "^20.19.0 || >=22.12.0" | ||
| 787 | + } | ||
| 788 | + }, | ||
| 789 | + "node_modules/@rolldown/binding-linux-s390x-gnu": { | ||
| 790 | + "version": "1.0.0-rc.17", | ||
| 791 | + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", | ||
| 792 | + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", | ||
| 793 | + "cpu": [ | ||
| 794 | + "s390x" | ||
| 795 | + ], | ||
| 796 | + "dev": true, | ||
| 797 | + "license": "MIT", | ||
| 798 | + "optional": true, | ||
| 799 | + "os": [ | ||
| 800 | + "linux" | ||
| 801 | + ], | ||
| 802 | + "engines": { | ||
| 803 | + "node": "^20.19.0 || >=22.12.0" | ||
| 804 | + } | ||
| 805 | + }, | ||
| 806 | + "node_modules/@rolldown/binding-linux-x64-gnu": { | ||
| 807 | + "version": "1.0.0-rc.17", | ||
| 808 | + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", | ||
| 809 | + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", | ||
| 810 | + "cpu": [ | ||
| 811 | + "x64" | ||
| 812 | + ], | ||
| 813 | + "dev": true, | ||
| 814 | + "license": "MIT", | ||
| 815 | + "optional": true, | ||
| 816 | + "os": [ | ||
| 817 | + "linux" | ||
| 818 | + ], | ||
| 819 | + "engines": { | ||
| 820 | + "node": "^20.19.0 || >=22.12.0" | ||
| 821 | + } | ||
| 822 | + }, | ||
| 823 | + "node_modules/@rolldown/binding-linux-x64-musl": { | ||
| 824 | + "version": "1.0.0-rc.17", | ||
| 825 | + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", | ||
| 826 | + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", | ||
| 827 | + "cpu": [ | ||
| 828 | + "x64" | ||
| 829 | + ], | ||
| 830 | + "dev": true, | ||
| 831 | + "license": "MIT", | ||
| 832 | + "optional": true, | ||
| 833 | + "os": [ | ||
| 834 | + "linux" | ||
| 835 | + ], | ||
| 836 | + "engines": { | ||
| 837 | + "node": "^20.19.0 || >=22.12.0" | ||
| 838 | + } | ||
| 839 | + }, | ||
| 840 | + "node_modules/@rolldown/binding-openharmony-arm64": { | ||
| 841 | + "version": "1.0.0-rc.17", | ||
| 842 | + "resolved": "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", | ||
| 843 | + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", | ||
| 844 | + "cpu": [ | ||
| 845 | + "arm64" | ||
| 846 | + ], | ||
| 847 | + "dev": true, | ||
| 848 | + "license": "MIT", | ||
| 849 | + "optional": true, | ||
| 850 | + "os": [ | ||
| 851 | + "openharmony" | ||
| 852 | + ], | ||
| 853 | + "engines": { | ||
| 854 | + "node": "^20.19.0 || >=22.12.0" | ||
| 855 | + } | ||
| 856 | + }, | ||
| 857 | + "node_modules/@rolldown/binding-wasm32-wasi": { | ||
| 858 | + "version": "1.0.0-rc.17", | ||
| 859 | + "resolved": "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", | ||
| 860 | + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", | ||
| 861 | + "cpu": [ | ||
| 862 | + "wasm32" | ||
| 863 | + ], | ||
| 864 | + "dev": true, | ||
| 865 | + "license": "MIT", | ||
| 866 | + "optional": true, | ||
| 867 | + "dependencies": { | ||
| 868 | + "@emnapi/core": "1.10.0", | ||
| 869 | + "@emnapi/runtime": "1.10.0", | ||
| 870 | + "@napi-rs/wasm-runtime": "^1.1.4" | ||
| 871 | + }, | ||
| 872 | + "engines": { | ||
| 873 | + "node": "^20.19.0 || >=22.12.0" | ||
| 874 | + } | ||
| 875 | + }, | ||
| 876 | + "node_modules/@rolldown/binding-win32-arm64-msvc": { | ||
| 877 | + "version": "1.0.0-rc.17", | ||
| 878 | + "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", | ||
| 879 | + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", | ||
| 880 | + "cpu": [ | ||
| 881 | + "arm64" | ||
| 882 | + ], | ||
| 883 | + "dev": true, | ||
| 884 | + "license": "MIT", | ||
| 885 | + "optional": true, | ||
| 886 | + "os": [ | ||
| 887 | + "win32" | ||
| 888 | + ], | ||
| 889 | + "engines": { | ||
| 890 | + "node": "^20.19.0 || >=22.12.0" | ||
| 891 | + } | ||
| 892 | + }, | ||
| 893 | + "node_modules/@rolldown/binding-win32-x64-msvc": { | ||
| 894 | + "version": "1.0.0-rc.17", | ||
| 895 | + "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", | ||
| 896 | + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", | ||
| 897 | + "cpu": [ | ||
| 898 | + "x64" | ||
| 899 | + ], | ||
| 900 | + "dev": true, | ||
| 901 | + "license": "MIT", | ||
| 902 | + "optional": true, | ||
| 903 | + "os": [ | ||
| 904 | + "win32" | ||
| 905 | + ], | ||
| 906 | + "engines": { | ||
| 907 | + "node": "^20.19.0 || >=22.12.0" | ||
| 908 | + } | ||
| 909 | + }, | ||
| 910 | + "node_modules/@rolldown/pluginutils": { | ||
| 911 | + "version": "1.0.0-rc.13", | ||
| 912 | + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz", | ||
| 913 | + "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==", | ||
| 914 | + "dev": true, | ||
| 915 | + "license": "MIT" | ||
| 916 | + }, | ||
| 917 | + "node_modules/@tybys/wasm-util": { | ||
| 918 | + "version": "0.10.1", | ||
| 919 | + "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", | ||
| 920 | + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", | ||
| 921 | + "dev": true, | ||
| 922 | + "license": "MIT", | ||
| 923 | + "optional": true, | ||
| 924 | + "dependencies": { | ||
| 925 | + "tslib": "^2.4.0" | ||
| 926 | + } | ||
| 927 | + }, | ||
| 928 | + "node_modules/@types/lodash": { | ||
| 929 | + "version": "4.17.24", | ||
| 930 | + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz", | ||
| 931 | + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", | ||
| 932 | + "license": "MIT" | ||
| 933 | + }, | ||
| 934 | + "node_modules/@types/lodash-es": { | ||
| 935 | + "version": "4.17.12", | ||
| 936 | + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", | ||
| 937 | + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", | ||
| 938 | + "license": "MIT", | ||
| 939 | + "dependencies": { | ||
| 940 | + "@types/lodash": "*" | ||
| 941 | + } | ||
| 942 | + }, | ||
| 943 | + "node_modules/@types/web-bluetooth": { | ||
| 944 | + "version": "0.0.20", | ||
| 945 | + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", | ||
| 946 | + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", | ||
| 947 | + "license": "MIT" | ||
| 948 | + }, | ||
| 949 | + "node_modules/@vitejs/plugin-vue": { | ||
| 950 | + "version": "6.0.6", | ||
| 951 | + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz", | ||
| 952 | + "integrity": "sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==", | ||
| 953 | + "dev": true, | ||
| 954 | + "license": "MIT", | ||
| 955 | + "dependencies": { | ||
| 956 | + "@rolldown/pluginutils": "1.0.0-rc.13" | ||
| 957 | + }, | ||
| 958 | + "engines": { | ||
| 959 | + "node": "^20.19.0 || >=22.12.0" | ||
| 960 | + }, | ||
| 961 | + "peerDependencies": { | ||
| 962 | + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", | ||
| 963 | + "vue": "^3.2.25" | ||
| 964 | + } | ||
| 965 | + }, | ||
| 966 | + "node_modules/@vue/babel-helper-vue-transform-on": { | ||
| 967 | + "version": "1.5.0", | ||
| 968 | + "resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", | ||
| 969 | + "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==", | ||
| 970 | + "dev": true, | ||
| 971 | + "license": "MIT" | ||
| 972 | + }, | ||
| 973 | + "node_modules/@vue/babel-plugin-jsx": { | ||
| 974 | + "version": "1.5.0", | ||
| 975 | + "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz", | ||
| 976 | + "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==", | ||
| 977 | + "dev": true, | ||
| 978 | + "license": "MIT", | ||
| 979 | + "dependencies": { | ||
| 980 | + "@babel/helper-module-imports": "^7.27.1", | ||
| 981 | + "@babel/helper-plugin-utils": "^7.27.1", | ||
| 982 | + "@babel/plugin-syntax-jsx": "^7.27.1", | ||
| 983 | + "@babel/template": "^7.27.2", | ||
| 984 | + "@babel/traverse": "^7.28.0", | ||
| 985 | + "@babel/types": "^7.28.2", | ||
| 986 | + "@vue/babel-helper-vue-transform-on": "1.5.0", | ||
| 987 | + "@vue/babel-plugin-resolve-type": "1.5.0", | ||
| 988 | + "@vue/shared": "^3.5.18" | ||
| 989 | + }, | ||
| 990 | + "peerDependencies": { | ||
| 991 | + "@babel/core": "^7.0.0-0" | ||
| 992 | + }, | ||
| 993 | + "peerDependenciesMeta": { | ||
| 994 | + "@babel/core": { | ||
| 995 | + "optional": true | ||
| 996 | + } | ||
| 997 | + } | ||
| 998 | + }, | ||
| 999 | + "node_modules/@vue/babel-plugin-resolve-type": { | ||
| 1000 | + "version": "1.5.0", | ||
| 1001 | + "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz", | ||
| 1002 | + "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==", | ||
| 1003 | + "dev": true, | ||
| 1004 | + "license": "MIT", | ||
| 1005 | + "dependencies": { | ||
| 1006 | + "@babel/code-frame": "^7.27.1", | ||
| 1007 | + "@babel/helper-module-imports": "^7.27.1", | ||
| 1008 | + "@babel/helper-plugin-utils": "^7.27.1", | ||
| 1009 | + "@babel/parser": "^7.28.0", | ||
| 1010 | + "@vue/compiler-sfc": "^3.5.18" | ||
| 1011 | + }, | ||
| 1012 | + "funding": { | ||
| 1013 | + "url": "https://github.com/sponsors/sxzz" | ||
| 1014 | + }, | ||
| 1015 | + "peerDependencies": { | ||
| 1016 | + "@babel/core": "^7.0.0-0" | ||
| 1017 | + } | ||
| 1018 | + }, | ||
| 1019 | + "node_modules/@vue/compiler-core": { | ||
| 1020 | + "version": "3.5.33", | ||
| 1021 | + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.33.tgz", | ||
| 1022 | + "integrity": "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==", | ||
| 1023 | + "license": "MIT", | ||
| 1024 | + "dependencies": { | ||
| 1025 | + "@babel/parser": "^7.29.2", | ||
| 1026 | + "@vue/shared": "3.5.33", | ||
| 1027 | + "entities": "^7.0.1", | ||
| 1028 | + "estree-walker": "^2.0.2", | ||
| 1029 | + "source-map-js": "^1.2.1" | ||
| 1030 | + } | ||
| 1031 | + }, | ||
| 1032 | + "node_modules/@vue/compiler-dom": { | ||
| 1033 | + "version": "3.5.33", | ||
| 1034 | + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.33.tgz", | ||
| 1035 | + "integrity": "sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==", | ||
| 1036 | + "license": "MIT", | ||
| 1037 | + "dependencies": { | ||
| 1038 | + "@vue/compiler-core": "3.5.33", | ||
| 1039 | + "@vue/shared": "3.5.33" | ||
| 1040 | + } | ||
| 1041 | + }, | ||
| 1042 | + "node_modules/@vue/compiler-sfc": { | ||
| 1043 | + "version": "3.5.33", | ||
| 1044 | + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.33.tgz", | ||
| 1045 | + "integrity": "sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==", | ||
| 1046 | + "license": "MIT", | ||
| 1047 | + "dependencies": { | ||
| 1048 | + "@babel/parser": "^7.29.2", | ||
| 1049 | + "@vue/compiler-core": "3.5.33", | ||
| 1050 | + "@vue/compiler-dom": "3.5.33", | ||
| 1051 | + "@vue/compiler-ssr": "3.5.33", | ||
| 1052 | + "@vue/shared": "3.5.33", | ||
| 1053 | + "estree-walker": "^2.0.2", | ||
| 1054 | + "magic-string": "^0.30.21", | ||
| 1055 | + "postcss": "^8.5.10", | ||
| 1056 | + "source-map-js": "^1.2.1" | ||
| 1057 | + } | ||
| 1058 | + }, | ||
| 1059 | + "node_modules/@vue/compiler-ssr": { | ||
| 1060 | + "version": "3.5.33", | ||
| 1061 | + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.33.tgz", | ||
| 1062 | + "integrity": "sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==", | ||
| 1063 | + "license": "MIT", | ||
| 1064 | + "dependencies": { | ||
| 1065 | + "@vue/compiler-dom": "3.5.33", | ||
| 1066 | + "@vue/shared": "3.5.33" | ||
| 1067 | + } | ||
| 1068 | + }, | ||
| 1069 | + "node_modules/@vue/devtools-api": { | ||
| 1070 | + "version": "6.6.4", | ||
| 1071 | + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", | ||
| 1072 | + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", | ||
| 1073 | + "license": "MIT" | ||
| 1074 | + }, | ||
| 1075 | + "node_modules/@vue/devtools-core": { | ||
| 1076 | + "version": "8.1.1", | ||
| 1077 | + "resolved": "https://registry.npmmirror.com/@vue/devtools-core/-/devtools-core-8.1.1.tgz", | ||
| 1078 | + "integrity": "sha512-bCCsSABp1/ot4j8xJEycM6Mtt2wbuucfByr6hMgjbYhrtlscOJypZKvy8f1FyWLYrLTchB5Qz216Lm92wfbq0A==", | ||
| 1079 | + "dev": true, | ||
| 1080 | + "license": "MIT", | ||
| 1081 | + "dependencies": { | ||
| 1082 | + "@vue/devtools-kit": "^8.1.1", | ||
| 1083 | + "@vue/devtools-shared": "^8.1.1" | ||
| 1084 | + }, | ||
| 1085 | + "peerDependencies": { | ||
| 1086 | + "vue": "^3.0.0" | ||
| 1087 | + } | ||
| 1088 | + }, | ||
| 1089 | + "node_modules/@vue/devtools-kit": { | ||
| 1090 | + "version": "8.1.1", | ||
| 1091 | + "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-8.1.1.tgz", | ||
| 1092 | + "integrity": "sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==", | ||
| 1093 | + "dev": true, | ||
| 1094 | + "license": "MIT", | ||
| 1095 | + "dependencies": { | ||
| 1096 | + "@vue/devtools-shared": "^8.1.1", | ||
| 1097 | + "birpc": "^2.6.1", | ||
| 1098 | + "hookable": "^5.5.3", | ||
| 1099 | + "perfect-debounce": "^2.0.0" | ||
| 1100 | + } | ||
| 1101 | + }, | ||
| 1102 | + "node_modules/@vue/devtools-shared": { | ||
| 1103 | + "version": "8.1.1", | ||
| 1104 | + "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-8.1.1.tgz", | ||
| 1105 | + "integrity": "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==", | ||
| 1106 | + "dev": true, | ||
| 1107 | + "license": "MIT" | ||
| 1108 | + }, | ||
| 1109 | + "node_modules/@vue/reactivity": { | ||
| 1110 | + "version": "3.5.33", | ||
| 1111 | + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.33.tgz", | ||
| 1112 | + "integrity": "sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A==", | ||
| 1113 | + "license": "MIT", | ||
| 1114 | + "dependencies": { | ||
| 1115 | + "@vue/shared": "3.5.33" | ||
| 1116 | + } | ||
| 1117 | + }, | ||
| 1118 | + "node_modules/@vue/runtime-core": { | ||
| 1119 | + "version": "3.5.33", | ||
| 1120 | + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.33.tgz", | ||
| 1121 | + "integrity": "sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ==", | ||
| 1122 | + "license": "MIT", | ||
| 1123 | + "dependencies": { | ||
| 1124 | + "@vue/reactivity": "3.5.33", | ||
| 1125 | + "@vue/shared": "3.5.33" | ||
| 1126 | + } | ||
| 1127 | + }, | ||
| 1128 | + "node_modules/@vue/runtime-dom": { | ||
| 1129 | + "version": "3.5.33", | ||
| 1130 | + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.33.tgz", | ||
| 1131 | + "integrity": "sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw==", | ||
| 1132 | + "license": "MIT", | ||
| 1133 | + "dependencies": { | ||
| 1134 | + "@vue/reactivity": "3.5.33", | ||
| 1135 | + "@vue/runtime-core": "3.5.33", | ||
| 1136 | + "@vue/shared": "3.5.33", | ||
| 1137 | + "csstype": "^3.2.3" | ||
| 1138 | + } | ||
| 1139 | + }, | ||
| 1140 | + "node_modules/@vue/server-renderer": { | ||
| 1141 | + "version": "3.5.33", | ||
| 1142 | + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.33.tgz", | ||
| 1143 | + "integrity": "sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw==", | ||
| 1144 | + "license": "MIT", | ||
| 1145 | + "dependencies": { | ||
| 1146 | + "@vue/compiler-ssr": "3.5.33", | ||
| 1147 | + "@vue/shared": "3.5.33" | ||
| 1148 | + }, | ||
| 1149 | + "peerDependencies": { | ||
| 1150 | + "vue": "3.5.33" | ||
| 1151 | + } | ||
| 1152 | + }, | ||
| 1153 | + "node_modules/@vue/shared": { | ||
| 1154 | + "version": "3.5.33", | ||
| 1155 | + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.33.tgz", | ||
| 1156 | + "integrity": "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==", | ||
| 1157 | + "license": "MIT" | ||
| 1158 | + }, | ||
| 1159 | + "node_modules/@vueuse/core": { | ||
| 1160 | + "version": "12.0.0", | ||
| 1161 | + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-12.0.0.tgz", | ||
| 1162 | + "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==", | ||
| 1163 | + "license": "MIT", | ||
| 1164 | + "dependencies": { | ||
| 1165 | + "@types/web-bluetooth": "^0.0.20", | ||
| 1166 | + "@vueuse/metadata": "12.0.0", | ||
| 1167 | + "@vueuse/shared": "12.0.0", | ||
| 1168 | + "vue": "^3.5.13" | ||
| 1169 | + }, | ||
| 1170 | + "funding": { | ||
| 1171 | + "url": "https://github.com/sponsors/antfu" | ||
| 1172 | + } | ||
| 1173 | + }, | ||
| 1174 | + "node_modules/@vueuse/metadata": { | ||
| 1175 | + "version": "12.0.0", | ||
| 1176 | + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-12.0.0.tgz", | ||
| 1177 | + "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==", | ||
| 1178 | + "license": "MIT", | ||
| 1179 | + "funding": { | ||
| 1180 | + "url": "https://github.com/sponsors/antfu" | ||
| 1181 | + } | ||
| 1182 | + }, | ||
| 1183 | + "node_modules/@vueuse/shared": { | ||
| 1184 | + "version": "12.0.0", | ||
| 1185 | + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-12.0.0.tgz", | ||
| 1186 | + "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==", | ||
| 1187 | + "license": "MIT", | ||
| 1188 | + "dependencies": { | ||
| 1189 | + "vue": "^3.5.13" | ||
| 1190 | + }, | ||
| 1191 | + "funding": { | ||
| 1192 | + "url": "https://github.com/sponsors/antfu" | ||
| 1193 | + } | ||
| 1194 | + }, | ||
| 1195 | + "node_modules/ansis": { | ||
| 1196 | + "version": "4.2.0", | ||
| 1197 | + "resolved": "https://registry.npmmirror.com/ansis/-/ansis-4.2.0.tgz", | ||
| 1198 | + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", | ||
| 1199 | + "dev": true, | ||
| 1200 | + "license": "ISC", | ||
| 1201 | + "engines": { | ||
| 1202 | + "node": ">=14" | ||
| 1203 | + } | ||
| 1204 | + }, | ||
| 1205 | + "node_modules/async-validator": { | ||
| 1206 | + "version": "4.2.5", | ||
| 1207 | + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", | ||
| 1208 | + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", | ||
| 1209 | + "license": "MIT" | ||
| 1210 | + }, | ||
| 1211 | + "node_modules/baseline-browser-mapping": { | ||
| 1212 | + "version": "2.10.23", | ||
| 1213 | + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz", | ||
| 1214 | + "integrity": "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==", | ||
| 1215 | + "dev": true, | ||
| 1216 | + "license": "Apache-2.0", | ||
| 1217 | + "bin": { | ||
| 1218 | + "baseline-browser-mapping": "dist/cli.cjs" | ||
| 1219 | + }, | ||
| 1220 | + "engines": { | ||
| 1221 | + "node": ">=6.0.0" | ||
| 1222 | + } | ||
| 1223 | + }, | ||
| 1224 | + "node_modules/birpc": { | ||
| 1225 | + "version": "2.9.0", | ||
| 1226 | + "resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz", | ||
| 1227 | + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", | ||
| 1228 | + "dev": true, | ||
| 1229 | + "license": "MIT", | ||
| 1230 | + "funding": { | ||
| 1231 | + "url": "https://github.com/sponsors/antfu" | ||
| 1232 | + } | ||
| 1233 | + }, | ||
| 1234 | + "node_modules/browserslist": { | ||
| 1235 | + "version": "4.28.2", | ||
| 1236 | + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", | ||
| 1237 | + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", | ||
| 1238 | + "dev": true, | ||
| 1239 | + "funding": [ | ||
| 1240 | + { | ||
| 1241 | + "type": "opencollective", | ||
| 1242 | + "url": "https://opencollective.com/browserslist" | ||
| 1243 | + }, | ||
| 1244 | + { | ||
| 1245 | + "type": "tidelift", | ||
| 1246 | + "url": "https://tidelift.com/funding/github/npm/browserslist" | ||
| 1247 | + }, | ||
| 1248 | + { | ||
| 1249 | + "type": "github", | ||
| 1250 | + "url": "https://github.com/sponsors/ai" | ||
| 1251 | + } | ||
| 1252 | + ], | ||
| 1253 | + "license": "MIT", | ||
| 1254 | + "dependencies": { | ||
| 1255 | + "baseline-browser-mapping": "^2.10.12", | ||
| 1256 | + "caniuse-lite": "^1.0.30001782", | ||
| 1257 | + "electron-to-chromium": "^1.5.328", | ||
| 1258 | + "node-releases": "^2.0.36", | ||
| 1259 | + "update-browserslist-db": "^1.2.3" | ||
| 1260 | + }, | ||
| 1261 | + "bin": { | ||
| 1262 | + "browserslist": "cli.js" | ||
| 1263 | + }, | ||
| 1264 | + "engines": { | ||
| 1265 | + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" | ||
| 1266 | + } | ||
| 1267 | + }, | ||
| 1268 | + "node_modules/bundle-name": { | ||
| 1269 | + "version": "4.1.0", | ||
| 1270 | + "resolved": "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz", | ||
| 1271 | + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", | ||
| 1272 | + "dev": true, | ||
| 1273 | + "license": "MIT", | ||
| 1274 | + "dependencies": { | ||
| 1275 | + "run-applescript": "^7.0.0" | ||
| 1276 | + }, | ||
| 1277 | + "engines": { | ||
| 1278 | + "node": ">=18" | ||
| 1279 | + }, | ||
| 1280 | + "funding": { | ||
| 1281 | + "url": "https://github.com/sponsors/sindresorhus" | ||
| 1282 | + } | ||
| 1283 | + }, | ||
| 1284 | + "node_modules/caniuse-lite": { | ||
| 1285 | + "version": "1.0.30001791", | ||
| 1286 | + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", | ||
| 1287 | + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", | ||
| 1288 | + "dev": true, | ||
| 1289 | + "funding": [ | ||
| 1290 | + { | ||
| 1291 | + "type": "opencollective", | ||
| 1292 | + "url": "https://opencollective.com/browserslist" | ||
| 1293 | + }, | ||
| 1294 | + { | ||
| 1295 | + "type": "tidelift", | ||
| 1296 | + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" | ||
| 1297 | + }, | ||
| 1298 | + { | ||
| 1299 | + "type": "github", | ||
| 1300 | + "url": "https://github.com/sponsors/ai" | ||
| 1301 | + } | ||
| 1302 | + ], | ||
| 1303 | + "license": "CC-BY-4.0" | ||
| 1304 | + }, | ||
| 1305 | + "node_modules/convert-source-map": { | ||
| 1306 | + "version": "2.0.0", | ||
| 1307 | + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", | ||
| 1308 | + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", | ||
| 1309 | + "dev": true, | ||
| 1310 | + "license": "MIT" | ||
| 1311 | + }, | ||
| 1312 | + "node_modules/csstype": { | ||
| 1313 | + "version": "3.2.3", | ||
| 1314 | + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", | ||
| 1315 | + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", | ||
| 1316 | + "license": "MIT" | ||
| 1317 | + }, | ||
| 1318 | + "node_modules/dayjs": { | ||
| 1319 | + "version": "1.11.20", | ||
| 1320 | + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz", | ||
| 1321 | + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", | ||
| 1322 | + "license": "MIT" | ||
| 1323 | + }, | ||
| 1324 | + "node_modules/debug": { | ||
| 1325 | + "version": "4.4.3", | ||
| 1326 | + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", | ||
| 1327 | + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", | ||
| 1328 | + "dev": true, | ||
| 1329 | + "license": "MIT", | ||
| 1330 | + "dependencies": { | ||
| 1331 | + "ms": "^2.1.3" | ||
| 1332 | + }, | ||
| 1333 | + "engines": { | ||
| 1334 | + "node": ">=6.0" | ||
| 1335 | + }, | ||
| 1336 | + "peerDependenciesMeta": { | ||
| 1337 | + "supports-color": { | ||
| 1338 | + "optional": true | ||
| 1339 | + } | ||
| 1340 | + } | ||
| 1341 | + }, | ||
| 1342 | + "node_modules/default-browser": { | ||
| 1343 | + "version": "5.5.0", | ||
| 1344 | + "resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.5.0.tgz", | ||
| 1345 | + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", | ||
| 1346 | + "dev": true, | ||
| 1347 | + "license": "MIT", | ||
| 1348 | + "dependencies": { | ||
| 1349 | + "bundle-name": "^4.1.0", | ||
| 1350 | + "default-browser-id": "^5.0.0" | ||
| 1351 | + }, | ||
| 1352 | + "engines": { | ||
| 1353 | + "node": ">=18" | ||
| 1354 | + }, | ||
| 1355 | + "funding": { | ||
| 1356 | + "url": "https://github.com/sponsors/sindresorhus" | ||
| 1357 | + } | ||
| 1358 | + }, | ||
| 1359 | + "node_modules/default-browser-id": { | ||
| 1360 | + "version": "5.0.1", | ||
| 1361 | + "resolved": "https://registry.npmmirror.com/default-browser-id/-/default-browser-id-5.0.1.tgz", | ||
| 1362 | + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", | ||
| 1363 | + "dev": true, | ||
| 1364 | + "license": "MIT", | ||
| 1365 | + "engines": { | ||
| 1366 | + "node": ">=18" | ||
| 1367 | + }, | ||
| 1368 | + "funding": { | ||
| 1369 | + "url": "https://github.com/sponsors/sindresorhus" | ||
| 1370 | + } | ||
| 1371 | + }, | ||
| 1372 | + "node_modules/define-lazy-prop": { | ||
| 1373 | + "version": "3.0.0", | ||
| 1374 | + "resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", | ||
| 1375 | + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", | ||
| 1376 | + "dev": true, | ||
| 1377 | + "license": "MIT", | ||
| 1378 | + "engines": { | ||
| 1379 | + "node": ">=12" | ||
| 1380 | + }, | ||
| 1381 | + "funding": { | ||
| 1382 | + "url": "https://github.com/sponsors/sindresorhus" | ||
| 1383 | + } | ||
| 1384 | + }, | ||
| 1385 | + "node_modules/detect-libc": { | ||
| 1386 | + "version": "2.1.2", | ||
| 1387 | + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", | ||
| 1388 | + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", | ||
| 1389 | + "dev": true, | ||
| 1390 | + "license": "Apache-2.0", | ||
| 1391 | + "engines": { | ||
| 1392 | + "node": ">=8" | ||
| 1393 | + } | ||
| 1394 | + }, | ||
| 1395 | + "node_modules/electron-to-chromium": { | ||
| 1396 | + "version": "1.5.344", | ||
| 1397 | + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", | ||
| 1398 | + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", | ||
| 1399 | + "dev": true, | ||
| 1400 | + "license": "ISC" | ||
| 1401 | + }, | ||
| 1402 | + "node_modules/element-plus": { | ||
| 1403 | + "version": "2.13.7", | ||
| 1404 | + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.13.7.tgz", | ||
| 1405 | + "integrity": "sha512-XdHATFZOyzVFL1DaHQ90IOJQSg9UnSAV+bhDW+YB5UoZ0Hxs50mwqjqfwXkuwpSag+VXXizVcErBR6Movo5daw==", | ||
| 1406 | + "license": "MIT", | ||
| 1407 | + "dependencies": { | ||
| 1408 | + "@ctrl/tinycolor": "^4.2.0", | ||
| 1409 | + "@element-plus/icons-vue": "^2.3.2", | ||
| 1410 | + "@floating-ui/dom": "^1.0.1", | ||
| 1411 | + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", | ||
| 1412 | + "@types/lodash": "^4.17.20", | ||
| 1413 | + "@types/lodash-es": "^4.17.12", | ||
| 1414 | + "@vueuse/core": "12.0.0", | ||
| 1415 | + "async-validator": "^4.2.5", | ||
| 1416 | + "dayjs": "^1.11.19", | ||
| 1417 | + "lodash": "^4.17.23", | ||
| 1418 | + "lodash-es": "^4.17.23", | ||
| 1419 | + "lodash-unified": "^1.0.3", | ||
| 1420 | + "memoize-one": "^6.0.0", | ||
| 1421 | + "normalize-wheel-es": "^1.2.0", | ||
| 1422 | + "vue-component-type-helpers": "^3.2.4" | ||
| 1423 | + }, | ||
| 1424 | + "peerDependencies": { | ||
| 1425 | + "vue": "^3.3.0" | ||
| 1426 | + } | ||
| 1427 | + }, | ||
| 1428 | + "node_modules/entities": { | ||
| 1429 | + "version": "7.0.1", | ||
| 1430 | + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", | ||
| 1431 | + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", | ||
| 1432 | + "license": "BSD-2-Clause", | ||
| 1433 | + "engines": { | ||
| 1434 | + "node": ">=0.12" | ||
| 1435 | + }, | ||
| 1436 | + "funding": { | ||
| 1437 | + "url": "https://github.com/fb55/entities?sponsor=1" | ||
| 1438 | + } | ||
| 1439 | + }, | ||
| 1440 | + "node_modules/error-stack-parser-es": { | ||
| 1441 | + "version": "1.0.5", | ||
| 1442 | + "resolved": "https://registry.npmmirror.com/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", | ||
| 1443 | + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", | ||
| 1444 | + "dev": true, | ||
| 1445 | + "license": "MIT", | ||
| 1446 | + "funding": { | ||
| 1447 | + "url": "https://github.com/sponsors/antfu" | ||
| 1448 | + } | ||
| 1449 | + }, | ||
| 1450 | + "node_modules/escalade": { | ||
| 1451 | + "version": "3.2.0", | ||
| 1452 | + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", | ||
| 1453 | + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", | ||
| 1454 | + "dev": true, | ||
| 1455 | + "license": "MIT", | ||
| 1456 | + "engines": { | ||
| 1457 | + "node": ">=6" | ||
| 1458 | + } | ||
| 1459 | + }, | ||
| 1460 | + "node_modules/estree-walker": { | ||
| 1461 | + "version": "2.0.2", | ||
| 1462 | + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", | ||
| 1463 | + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", | ||
| 1464 | + "license": "MIT" | ||
| 1465 | + }, | ||
| 1466 | + "node_modules/fdir": { | ||
| 1467 | + "version": "6.5.0", | ||
| 1468 | + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", | ||
| 1469 | + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", | ||
| 1470 | + "dev": true, | ||
| 1471 | + "license": "MIT", | ||
| 1472 | + "engines": { | ||
| 1473 | + "node": ">=12.0.0" | ||
| 1474 | + }, | ||
| 1475 | + "peerDependencies": { | ||
| 1476 | + "picomatch": "^3 || ^4" | ||
| 1477 | + }, | ||
| 1478 | + "peerDependenciesMeta": { | ||
| 1479 | + "picomatch": { | ||
| 1480 | + "optional": true | ||
| 1481 | + } | ||
| 1482 | + } | ||
| 1483 | + }, | ||
| 1484 | + "node_modules/fsevents": { | ||
| 1485 | + "version": "2.3.3", | ||
| 1486 | + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", | ||
| 1487 | + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", | ||
| 1488 | + "dev": true, | ||
| 1489 | + "hasInstallScript": true, | ||
| 1490 | + "license": "MIT", | ||
| 1491 | + "optional": true, | ||
| 1492 | + "os": [ | ||
| 1493 | + "darwin" | ||
| 1494 | + ], | ||
| 1495 | + "engines": { | ||
| 1496 | + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" | ||
| 1497 | + } | ||
| 1498 | + }, | ||
| 1499 | + "node_modules/gensync": { | ||
| 1500 | + "version": "1.0.0-beta.2", | ||
| 1501 | + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", | ||
| 1502 | + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", | ||
| 1503 | + "dev": true, | ||
| 1504 | + "license": "MIT", | ||
| 1505 | + "engines": { | ||
| 1506 | + "node": ">=6.9.0" | ||
| 1507 | + } | ||
| 1508 | + }, | ||
| 1509 | + "node_modules/hookable": { | ||
| 1510 | + "version": "5.5.3", | ||
| 1511 | + "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz", | ||
| 1512 | + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", | ||
| 1513 | + "dev": true, | ||
| 1514 | + "license": "MIT" | ||
| 1515 | + }, | ||
| 1516 | + "node_modules/is-docker": { | ||
| 1517 | + "version": "3.0.0", | ||
| 1518 | + "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz", | ||
| 1519 | + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", | ||
| 1520 | + "dev": true, | ||
| 1521 | + "license": "MIT", | ||
| 1522 | + "bin": { | ||
| 1523 | + "is-docker": "cli.js" | ||
| 1524 | + }, | ||
| 1525 | + "engines": { | ||
| 1526 | + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" | ||
| 1527 | + }, | ||
| 1528 | + "funding": { | ||
| 1529 | + "url": "https://github.com/sponsors/sindresorhus" | ||
| 1530 | + } | ||
| 1531 | + }, | ||
| 1532 | + "node_modules/is-inside-container": { | ||
| 1533 | + "version": "1.0.0", | ||
| 1534 | + "resolved": "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz", | ||
| 1535 | + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", | ||
| 1536 | + "dev": true, | ||
| 1537 | + "license": "MIT", | ||
| 1538 | + "dependencies": { | ||
| 1539 | + "is-docker": "^3.0.0" | ||
| 1540 | + }, | ||
| 1541 | + "bin": { | ||
| 1542 | + "is-inside-container": "cli.js" | ||
| 1543 | + }, | ||
| 1544 | + "engines": { | ||
| 1545 | + "node": ">=14.16" | ||
| 1546 | + }, | ||
| 1547 | + "funding": { | ||
| 1548 | + "url": "https://github.com/sponsors/sindresorhus" | ||
| 1549 | + } | ||
| 1550 | + }, | ||
| 1551 | + "node_modules/is-wsl": { | ||
| 1552 | + "version": "3.1.1", | ||
| 1553 | + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.1.tgz", | ||
| 1554 | + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", | ||
| 1555 | + "dev": true, | ||
| 1556 | + "license": "MIT", | ||
| 1557 | + "dependencies": { | ||
| 1558 | + "is-inside-container": "^1.0.0" | ||
| 1559 | + }, | ||
| 1560 | + "engines": { | ||
| 1561 | + "node": ">=16" | ||
| 1562 | + }, | ||
| 1563 | + "funding": { | ||
| 1564 | + "url": "https://github.com/sponsors/sindresorhus" | ||
| 1565 | + } | ||
| 1566 | + }, | ||
| 1567 | + "node_modules/js-tokens": { | ||
| 1568 | + "version": "4.0.0", | ||
| 1569 | + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", | ||
| 1570 | + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", | ||
| 1571 | + "dev": true, | ||
| 1572 | + "license": "MIT" | ||
| 1573 | + }, | ||
| 1574 | + "node_modules/jsesc": { | ||
| 1575 | + "version": "3.1.0", | ||
| 1576 | + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", | ||
| 1577 | + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", | ||
| 1578 | + "dev": true, | ||
| 1579 | + "license": "MIT", | ||
| 1580 | + "bin": { | ||
| 1581 | + "jsesc": "bin/jsesc" | ||
| 1582 | + }, | ||
| 1583 | + "engines": { | ||
| 1584 | + "node": ">=6" | ||
| 1585 | + } | ||
| 1586 | + }, | ||
| 1587 | + "node_modules/json5": { | ||
| 1588 | + "version": "2.2.3", | ||
| 1589 | + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", | ||
| 1590 | + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", | ||
| 1591 | + "dev": true, | ||
| 1592 | + "license": "MIT", | ||
| 1593 | + "bin": { | ||
| 1594 | + "json5": "lib/cli.js" | ||
| 1595 | + }, | ||
| 1596 | + "engines": { | ||
| 1597 | + "node": ">=6" | ||
| 1598 | + } | ||
| 1599 | + }, | ||
| 1600 | + "node_modules/kolorist": { | ||
| 1601 | + "version": "1.8.0", | ||
| 1602 | + "resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz", | ||
| 1603 | + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", | ||
| 1604 | + "dev": true, | ||
| 1605 | + "license": "MIT" | ||
| 1606 | + }, | ||
| 1607 | + "node_modules/lightningcss": { | ||
| 1608 | + "version": "1.32.0", | ||
| 1609 | + "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", | ||
| 1610 | + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", | ||
| 1611 | + "dev": true, | ||
| 1612 | + "license": "MPL-2.0", | ||
| 1613 | + "dependencies": { | ||
| 1614 | + "detect-libc": "^2.0.3" | ||
| 1615 | + }, | ||
| 1616 | + "engines": { | ||
| 1617 | + "node": ">= 12.0.0" | ||
| 1618 | + }, | ||
| 1619 | + "funding": { | ||
| 1620 | + "type": "opencollective", | ||
| 1621 | + "url": "https://opencollective.com/parcel" | ||
| 1622 | + }, | ||
| 1623 | + "optionalDependencies": { | ||
| 1624 | + "lightningcss-android-arm64": "1.32.0", | ||
| 1625 | + "lightningcss-darwin-arm64": "1.32.0", | ||
| 1626 | + "lightningcss-darwin-x64": "1.32.0", | ||
| 1627 | + "lightningcss-freebsd-x64": "1.32.0", | ||
| 1628 | + "lightningcss-linux-arm-gnueabihf": "1.32.0", | ||
| 1629 | + "lightningcss-linux-arm64-gnu": "1.32.0", | ||
| 1630 | + "lightningcss-linux-arm64-musl": "1.32.0", | ||
| 1631 | + "lightningcss-linux-x64-gnu": "1.32.0", | ||
| 1632 | + "lightningcss-linux-x64-musl": "1.32.0", | ||
| 1633 | + "lightningcss-win32-arm64-msvc": "1.32.0", | ||
| 1634 | + "lightningcss-win32-x64-msvc": "1.32.0" | ||
| 1635 | + } | ||
| 1636 | + }, | ||
| 1637 | + "node_modules/lightningcss-android-arm64": { | ||
| 1638 | + "version": "1.32.0", | ||
| 1639 | + "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", | ||
| 1640 | + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", | ||
| 1641 | + "cpu": [ | ||
| 1642 | + "arm64" | ||
| 1643 | + ], | ||
| 1644 | + "dev": true, | ||
| 1645 | + "license": "MPL-2.0", | ||
| 1646 | + "optional": true, | ||
| 1647 | + "os": [ | ||
| 1648 | + "android" | ||
| 1649 | + ], | ||
| 1650 | + "engines": { | ||
| 1651 | + "node": ">= 12.0.0" | ||
| 1652 | + }, | ||
| 1653 | + "funding": { | ||
| 1654 | + "type": "opencollective", | ||
| 1655 | + "url": "https://opencollective.com/parcel" | ||
| 1656 | + } | ||
| 1657 | + }, | ||
| 1658 | + "node_modules/lightningcss-darwin-arm64": { | ||
| 1659 | + "version": "1.32.0", | ||
| 1660 | + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", | ||
| 1661 | + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", | ||
| 1662 | + "cpu": [ | ||
| 1663 | + "arm64" | ||
| 1664 | + ], | ||
| 1665 | + "dev": true, | ||
| 1666 | + "license": "MPL-2.0", | ||
| 1667 | + "optional": true, | ||
| 1668 | + "os": [ | ||
| 1669 | + "darwin" | ||
| 1670 | + ], | ||
| 1671 | + "engines": { | ||
| 1672 | + "node": ">= 12.0.0" | ||
| 1673 | + }, | ||
| 1674 | + "funding": { | ||
| 1675 | + "type": "opencollective", | ||
| 1676 | + "url": "https://opencollective.com/parcel" | ||
| 1677 | + } | ||
| 1678 | + }, | ||
| 1679 | + "node_modules/lightningcss-darwin-x64": { | ||
| 1680 | + "version": "1.32.0", | ||
| 1681 | + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", | ||
| 1682 | + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", | ||
| 1683 | + "cpu": [ | ||
| 1684 | + "x64" | ||
| 1685 | + ], | ||
| 1686 | + "dev": true, | ||
| 1687 | + "license": "MPL-2.0", | ||
| 1688 | + "optional": true, | ||
| 1689 | + "os": [ | ||
| 1690 | + "darwin" | ||
| 1691 | + ], | ||
| 1692 | + "engines": { | ||
| 1693 | + "node": ">= 12.0.0" | ||
| 1694 | + }, | ||
| 1695 | + "funding": { | ||
| 1696 | + "type": "opencollective", | ||
| 1697 | + "url": "https://opencollective.com/parcel" | ||
| 1698 | + } | ||
| 1699 | + }, | ||
| 1700 | + "node_modules/lightningcss-freebsd-x64": { | ||
| 1701 | + "version": "1.32.0", | ||
| 1702 | + "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", | ||
| 1703 | + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", | ||
| 1704 | + "cpu": [ | ||
| 1705 | + "x64" | ||
| 1706 | + ], | ||
| 1707 | + "dev": true, | ||
| 1708 | + "license": "MPL-2.0", | ||
| 1709 | + "optional": true, | ||
| 1710 | + "os": [ | ||
| 1711 | + "freebsd" | ||
| 1712 | + ], | ||
| 1713 | + "engines": { | ||
| 1714 | + "node": ">= 12.0.0" | ||
| 1715 | + }, | ||
| 1716 | + "funding": { | ||
| 1717 | + "type": "opencollective", | ||
| 1718 | + "url": "https://opencollective.com/parcel" | ||
| 1719 | + } | ||
| 1720 | + }, | ||
| 1721 | + "node_modules/lightningcss-linux-arm-gnueabihf": { | ||
| 1722 | + "version": "1.32.0", | ||
| 1723 | + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", | ||
| 1724 | + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", | ||
| 1725 | + "cpu": [ | ||
| 1726 | + "arm" | ||
| 1727 | + ], | ||
| 1728 | + "dev": true, | ||
| 1729 | + "license": "MPL-2.0", | ||
| 1730 | + "optional": true, | ||
| 1731 | + "os": [ | ||
| 1732 | + "linux" | ||
| 1733 | + ], | ||
| 1734 | + "engines": { | ||
| 1735 | + "node": ">= 12.0.0" | ||
| 1736 | + }, | ||
| 1737 | + "funding": { | ||
| 1738 | + "type": "opencollective", | ||
| 1739 | + "url": "https://opencollective.com/parcel" | ||
| 1740 | + } | ||
| 1741 | + }, | ||
| 1742 | + "node_modules/lightningcss-linux-arm64-gnu": { | ||
| 1743 | + "version": "1.32.0", | ||
| 1744 | + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", | ||
| 1745 | + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", | ||
| 1746 | + "cpu": [ | ||
| 1747 | + "arm64" | ||
| 1748 | + ], | ||
| 1749 | + "dev": true, | ||
| 1750 | + "license": "MPL-2.0", | ||
| 1751 | + "optional": true, | ||
| 1752 | + "os": [ | ||
| 1753 | + "linux" | ||
| 1754 | + ], | ||
| 1755 | + "engines": { | ||
| 1756 | + "node": ">= 12.0.0" | ||
| 1757 | + }, | ||
| 1758 | + "funding": { | ||
| 1759 | + "type": "opencollective", | ||
| 1760 | + "url": "https://opencollective.com/parcel" | ||
| 1761 | + } | ||
| 1762 | + }, | ||
| 1763 | + "node_modules/lightningcss-linux-arm64-musl": { | ||
| 1764 | + "version": "1.32.0", | ||
| 1765 | + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", | ||
| 1766 | + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", | ||
| 1767 | + "cpu": [ | ||
| 1768 | + "arm64" | ||
| 1769 | + ], | ||
| 1770 | + "dev": true, | ||
| 1771 | + "license": "MPL-2.0", | ||
| 1772 | + "optional": true, | ||
| 1773 | + "os": [ | ||
| 1774 | + "linux" | ||
| 1775 | + ], | ||
| 1776 | + "engines": { | ||
| 1777 | + "node": ">= 12.0.0" | ||
| 1778 | + }, | ||
| 1779 | + "funding": { | ||
| 1780 | + "type": "opencollective", | ||
| 1781 | + "url": "https://opencollective.com/parcel" | ||
| 1782 | + } | ||
| 1783 | + }, | ||
| 1784 | + "node_modules/lightningcss-linux-x64-gnu": { | ||
| 1785 | + "version": "1.32.0", | ||
| 1786 | + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", | ||
| 1787 | + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", | ||
| 1788 | + "cpu": [ | ||
| 1789 | + "x64" | ||
| 1790 | + ], | ||
| 1791 | + "dev": true, | ||
| 1792 | + "license": "MPL-2.0", | ||
| 1793 | + "optional": true, | ||
| 1794 | + "os": [ | ||
| 1795 | + "linux" | ||
| 1796 | + ], | ||
| 1797 | + "engines": { | ||
| 1798 | + "node": ">= 12.0.0" | ||
| 1799 | + }, | ||
| 1800 | + "funding": { | ||
| 1801 | + "type": "opencollective", | ||
| 1802 | + "url": "https://opencollective.com/parcel" | ||
| 1803 | + } | ||
| 1804 | + }, | ||
| 1805 | + "node_modules/lightningcss-linux-x64-musl": { | ||
| 1806 | + "version": "1.32.0", | ||
| 1807 | + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", | ||
| 1808 | + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", | ||
| 1809 | + "cpu": [ | ||
| 1810 | + "x64" | ||
| 1811 | + ], | ||
| 1812 | + "dev": true, | ||
| 1813 | + "license": "MPL-2.0", | ||
| 1814 | + "optional": true, | ||
| 1815 | + "os": [ | ||
| 1816 | + "linux" | ||
| 1817 | + ], | ||
| 1818 | + "engines": { | ||
| 1819 | + "node": ">= 12.0.0" | ||
| 1820 | + }, | ||
| 1821 | + "funding": { | ||
| 1822 | + "type": "opencollective", | ||
| 1823 | + "url": "https://opencollective.com/parcel" | ||
| 1824 | + } | ||
| 1825 | + }, | ||
| 1826 | + "node_modules/lightningcss-win32-arm64-msvc": { | ||
| 1827 | + "version": "1.32.0", | ||
| 1828 | + "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", | ||
| 1829 | + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", | ||
| 1830 | + "cpu": [ | ||
| 1831 | + "arm64" | ||
| 1832 | + ], | ||
| 1833 | + "dev": true, | ||
| 1834 | + "license": "MPL-2.0", | ||
| 1835 | + "optional": true, | ||
| 1836 | + "os": [ | ||
| 1837 | + "win32" | ||
| 1838 | + ], | ||
| 1839 | + "engines": { | ||
| 1840 | + "node": ">= 12.0.0" | ||
| 1841 | + }, | ||
| 1842 | + "funding": { | ||
| 1843 | + "type": "opencollective", | ||
| 1844 | + "url": "https://opencollective.com/parcel" | ||
| 1845 | + } | ||
| 1846 | + }, | ||
| 1847 | + "node_modules/lightningcss-win32-x64-msvc": { | ||
| 1848 | + "version": "1.32.0", | ||
| 1849 | + "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", | ||
| 1850 | + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", | ||
| 1851 | + "cpu": [ | ||
| 1852 | + "x64" | ||
| 1853 | + ], | ||
| 1854 | + "dev": true, | ||
| 1855 | + "license": "MPL-2.0", | ||
| 1856 | + "optional": true, | ||
| 1857 | + "os": [ | ||
| 1858 | + "win32" | ||
| 1859 | + ], | ||
| 1860 | + "engines": { | ||
| 1861 | + "node": ">= 12.0.0" | ||
| 1862 | + }, | ||
| 1863 | + "funding": { | ||
| 1864 | + "type": "opencollective", | ||
| 1865 | + "url": "https://opencollective.com/parcel" | ||
| 1866 | + } | ||
| 1867 | + }, | ||
| 1868 | + "node_modules/lodash": { | ||
| 1869 | + "version": "4.18.1", | ||
| 1870 | + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", | ||
| 1871 | + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", | ||
| 1872 | + "license": "MIT" | ||
| 1873 | + }, | ||
| 1874 | + "node_modules/lodash-es": { | ||
| 1875 | + "version": "4.18.1", | ||
| 1876 | + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", | ||
| 1877 | + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", | ||
| 1878 | + "license": "MIT" | ||
| 1879 | + }, | ||
| 1880 | + "node_modules/lodash-unified": { | ||
| 1881 | + "version": "1.0.3", | ||
| 1882 | + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", | ||
| 1883 | + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", | ||
| 1884 | + "license": "MIT", | ||
| 1885 | + "peerDependencies": { | ||
| 1886 | + "@types/lodash-es": "*", | ||
| 1887 | + "lodash": "*", | ||
| 1888 | + "lodash-es": "*" | ||
| 1889 | + } | ||
| 1890 | + }, | ||
| 1891 | + "node_modules/lru-cache": { | ||
| 1892 | + "version": "5.1.1", | ||
| 1893 | + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", | ||
| 1894 | + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", | ||
| 1895 | + "dev": true, | ||
| 1896 | + "license": "ISC", | ||
| 1897 | + "dependencies": { | ||
| 1898 | + "yallist": "^3.0.2" | ||
| 1899 | + } | ||
| 1900 | + }, | ||
| 1901 | + "node_modules/magic-string": { | ||
| 1902 | + "version": "0.30.21", | ||
| 1903 | + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", | ||
| 1904 | + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", | ||
| 1905 | + "license": "MIT", | ||
| 1906 | + "dependencies": { | ||
| 1907 | + "@jridgewell/sourcemap-codec": "^1.5.5" | ||
| 1908 | + } | ||
| 1909 | + }, | ||
| 1910 | + "node_modules/memoize-one": { | ||
| 1911 | + "version": "6.0.0", | ||
| 1912 | + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", | ||
| 1913 | + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", | ||
| 1914 | + "license": "MIT" | ||
| 1915 | + }, | ||
| 1916 | + "node_modules/mrmime": { | ||
| 1917 | + "version": "2.0.1", | ||
| 1918 | + "resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz", | ||
| 1919 | + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", | ||
| 1920 | + "dev": true, | ||
| 1921 | + "license": "MIT", | ||
| 1922 | + "engines": { | ||
| 1923 | + "node": ">=10" | ||
| 1924 | + } | ||
| 1925 | + }, | ||
| 1926 | + "node_modules/ms": { | ||
| 1927 | + "version": "2.1.3", | ||
| 1928 | + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", | ||
| 1929 | + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", | ||
| 1930 | + "dev": true, | ||
| 1931 | + "license": "MIT" | ||
| 1932 | + }, | ||
| 1933 | + "node_modules/nanoid": { | ||
| 1934 | + "version": "3.3.11", | ||
| 1935 | + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", | ||
| 1936 | + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", | ||
| 1937 | + "funding": [ | ||
| 1938 | + { | ||
| 1939 | + "type": "github", | ||
| 1940 | + "url": "https://github.com/sponsors/ai" | ||
| 1941 | + } | ||
| 1942 | + ], | ||
| 1943 | + "license": "MIT", | ||
| 1944 | + "bin": { | ||
| 1945 | + "nanoid": "bin/nanoid.cjs" | ||
| 1946 | + }, | ||
| 1947 | + "engines": { | ||
| 1948 | + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" | ||
| 1949 | + } | ||
| 1950 | + }, | ||
| 1951 | + "node_modules/node-releases": { | ||
| 1952 | + "version": "2.0.38", | ||
| 1953 | + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.38.tgz", | ||
| 1954 | + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", | ||
| 1955 | + "dev": true, | ||
| 1956 | + "license": "MIT" | ||
| 1957 | + }, | ||
| 1958 | + "node_modules/normalize-wheel-es": { | ||
| 1959 | + "version": "1.2.0", | ||
| 1960 | + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", | ||
| 1961 | + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", | ||
| 1962 | + "license": "BSD-3-Clause" | ||
| 1963 | + }, | ||
| 1964 | + "node_modules/ohash": { | ||
| 1965 | + "version": "2.0.11", | ||
| 1966 | + "resolved": "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz", | ||
| 1967 | + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", | ||
| 1968 | + "dev": true, | ||
| 1969 | + "license": "MIT" | ||
| 1970 | + }, | ||
| 1971 | + "node_modules/open": { | ||
| 1972 | + "version": "10.2.0", | ||
| 1973 | + "resolved": "https://registry.npmmirror.com/open/-/open-10.2.0.tgz", | ||
| 1974 | + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", | ||
| 1975 | + "dev": true, | ||
| 1976 | + "license": "MIT", | ||
| 1977 | + "dependencies": { | ||
| 1978 | + "default-browser": "^5.2.1", | ||
| 1979 | + "define-lazy-prop": "^3.0.0", | ||
| 1980 | + "is-inside-container": "^1.0.0", | ||
| 1981 | + "wsl-utils": "^0.1.0" | ||
| 1982 | + }, | ||
| 1983 | + "engines": { | ||
| 1984 | + "node": ">=18" | ||
| 1985 | + }, | ||
| 1986 | + "funding": { | ||
| 1987 | + "url": "https://github.com/sponsors/sindresorhus" | ||
| 1988 | + } | ||
| 1989 | + }, | ||
| 1990 | + "node_modules/pathe": { | ||
| 1991 | + "version": "2.0.3", | ||
| 1992 | + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", | ||
| 1993 | + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", | ||
| 1994 | + "dev": true, | ||
| 1995 | + "license": "MIT" | ||
| 1996 | + }, | ||
| 1997 | + "node_modules/perfect-debounce": { | ||
| 1998 | + "version": "2.1.0", | ||
| 1999 | + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.1.0.tgz", | ||
| 2000 | + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", | ||
| 2001 | + "dev": true, | ||
| 2002 | + "license": "MIT" | ||
| 2003 | + }, | ||
| 2004 | + "node_modules/picocolors": { | ||
| 2005 | + "version": "1.1.1", | ||
| 2006 | + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", | ||
| 2007 | + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", | ||
| 2008 | + "license": "ISC" | ||
| 2009 | + }, | ||
| 2010 | + "node_modules/picomatch": { | ||
| 2011 | + "version": "4.0.4", | ||
| 2012 | + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", | ||
| 2013 | + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", | ||
| 2014 | + "dev": true, | ||
| 2015 | + "license": "MIT", | ||
| 2016 | + "engines": { | ||
| 2017 | + "node": ">=12" | ||
| 2018 | + }, | ||
| 2019 | + "funding": { | ||
| 2020 | + "url": "https://github.com/sponsors/jonschlinkert" | ||
| 2021 | + } | ||
| 2022 | + }, | ||
| 2023 | + "node_modules/postcss": { | ||
| 2024 | + "version": "8.5.12", | ||
| 2025 | + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.12.tgz", | ||
| 2026 | + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", | ||
| 2027 | + "funding": [ | ||
| 2028 | + { | ||
| 2029 | + "type": "opencollective", | ||
| 2030 | + "url": "https://opencollective.com/postcss/" | ||
| 2031 | + }, | ||
| 2032 | + { | ||
| 2033 | + "type": "tidelift", | ||
| 2034 | + "url": "https://tidelift.com/funding/github/npm/postcss" | ||
| 2035 | + }, | ||
| 2036 | + { | ||
| 2037 | + "type": "github", | ||
| 2038 | + "url": "https://github.com/sponsors/ai" | ||
| 2039 | + } | ||
| 2040 | + ], | ||
| 2041 | + "license": "MIT", | ||
| 2042 | + "dependencies": { | ||
| 2043 | + "nanoid": "^3.3.11", | ||
| 2044 | + "picocolors": "^1.1.1", | ||
| 2045 | + "source-map-js": "^1.2.1" | ||
| 2046 | + }, | ||
| 2047 | + "engines": { | ||
| 2048 | + "node": "^10 || ^12 || >=14" | ||
| 2049 | + } | ||
| 2050 | + }, | ||
| 2051 | + "node_modules/rolldown": { | ||
| 2052 | + "version": "1.0.0-rc.17", | ||
| 2053 | + "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.17.tgz", | ||
| 2054 | + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", | ||
| 2055 | + "dev": true, | ||
| 2056 | + "license": "MIT", | ||
| 2057 | + "dependencies": { | ||
| 2058 | + "@oxc-project/types": "=0.127.0", | ||
| 2059 | + "@rolldown/pluginutils": "1.0.0-rc.17" | ||
| 2060 | + }, | ||
| 2061 | + "bin": { | ||
| 2062 | + "rolldown": "bin/cli.mjs" | ||
| 2063 | + }, | ||
| 2064 | + "engines": { | ||
| 2065 | + "node": "^20.19.0 || >=22.12.0" | ||
| 2066 | + }, | ||
| 2067 | + "optionalDependencies": { | ||
| 2068 | + "@rolldown/binding-android-arm64": "1.0.0-rc.17", | ||
| 2069 | + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", | ||
| 2070 | + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", | ||
| 2071 | + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", | ||
| 2072 | + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", | ||
| 2073 | + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", | ||
| 2074 | + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", | ||
| 2075 | + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", | ||
| 2076 | + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", | ||
| 2077 | + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", | ||
| 2078 | + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", | ||
| 2079 | + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", | ||
| 2080 | + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", | ||
| 2081 | + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", | ||
| 2082 | + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" | ||
| 2083 | + } | ||
| 2084 | + }, | ||
| 2085 | + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { | ||
| 2086 | + "version": "1.0.0-rc.17", | ||
| 2087 | + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", | ||
| 2088 | + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", | ||
| 2089 | + "dev": true, | ||
| 2090 | + "license": "MIT" | ||
| 2091 | + }, | ||
| 2092 | + "node_modules/run-applescript": { | ||
| 2093 | + "version": "7.1.0", | ||
| 2094 | + "resolved": "https://registry.npmmirror.com/run-applescript/-/run-applescript-7.1.0.tgz", | ||
| 2095 | + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", | ||
| 2096 | + "dev": true, | ||
| 2097 | + "license": "MIT", | ||
| 2098 | + "engines": { | ||
| 2099 | + "node": ">=18" | ||
| 2100 | + }, | ||
| 2101 | + "funding": { | ||
| 2102 | + "url": "https://github.com/sponsors/sindresorhus" | ||
| 2103 | + } | ||
| 2104 | + }, | ||
| 2105 | + "node_modules/semver": { | ||
| 2106 | + "version": "6.3.1", | ||
| 2107 | + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", | ||
| 2108 | + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", | ||
| 2109 | + "dev": true, | ||
| 2110 | + "license": "ISC", | ||
| 2111 | + "bin": { | ||
| 2112 | + "semver": "bin/semver.js" | ||
| 2113 | + } | ||
| 2114 | + }, | ||
| 2115 | + "node_modules/sirv": { | ||
| 2116 | + "version": "3.0.2", | ||
| 2117 | + "resolved": "https://registry.npmmirror.com/sirv/-/sirv-3.0.2.tgz", | ||
| 2118 | + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", | ||
| 2119 | + "dev": true, | ||
| 2120 | + "license": "MIT", | ||
| 2121 | + "dependencies": { | ||
| 2122 | + "@polka/url": "^1.0.0-next.24", | ||
| 2123 | + "mrmime": "^2.0.0", | ||
| 2124 | + "totalist": "^3.0.0" | ||
| 2125 | + }, | ||
| 2126 | + "engines": { | ||
| 2127 | + "node": ">=18" | ||
| 2128 | + } | ||
| 2129 | + }, | ||
| 2130 | + "node_modules/source-map-js": { | ||
| 2131 | + "version": "1.2.1", | ||
| 2132 | + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", | ||
| 2133 | + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", | ||
| 2134 | + "license": "BSD-3-Clause", | ||
| 2135 | + "engines": { | ||
| 2136 | + "node": ">=0.10.0" | ||
| 2137 | + } | ||
| 2138 | + }, | ||
| 2139 | + "node_modules/tinyglobby": { | ||
| 2140 | + "version": "0.2.16", | ||
| 2141 | + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", | ||
| 2142 | + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", | ||
| 2143 | + "dev": true, | ||
| 2144 | + "license": "MIT", | ||
| 2145 | + "dependencies": { | ||
| 2146 | + "fdir": "^6.5.0", | ||
| 2147 | + "picomatch": "^4.0.4" | ||
| 2148 | + }, | ||
| 2149 | + "engines": { | ||
| 2150 | + "node": ">=12.0.0" | ||
| 2151 | + }, | ||
| 2152 | + "funding": { | ||
| 2153 | + "url": "https://github.com/sponsors/SuperchupuDev" | ||
| 2154 | + } | ||
| 2155 | + }, | ||
| 2156 | + "node_modules/totalist": { | ||
| 2157 | + "version": "3.0.1", | ||
| 2158 | + "resolved": "https://registry.npmmirror.com/totalist/-/totalist-3.0.1.tgz", | ||
| 2159 | + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", | ||
| 2160 | + "dev": true, | ||
| 2161 | + "license": "MIT", | ||
| 2162 | + "engines": { | ||
| 2163 | + "node": ">=6" | ||
| 2164 | + } | ||
| 2165 | + }, | ||
| 2166 | + "node_modules/tslib": { | ||
| 2167 | + "version": "2.8.1", | ||
| 2168 | + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", | ||
| 2169 | + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", | ||
| 2170 | + "dev": true, | ||
| 2171 | + "license": "0BSD", | ||
| 2172 | + "optional": true | ||
| 2173 | + }, | ||
| 2174 | + "node_modules/unplugin-utils": { | ||
| 2175 | + "version": "0.3.1", | ||
| 2176 | + "resolved": "https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.3.1.tgz", | ||
| 2177 | + "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", | ||
| 2178 | + "dev": true, | ||
| 2179 | + "license": "MIT", | ||
| 2180 | + "dependencies": { | ||
| 2181 | + "pathe": "^2.0.3", | ||
| 2182 | + "picomatch": "^4.0.3" | ||
| 2183 | + }, | ||
| 2184 | + "engines": { | ||
| 2185 | + "node": ">=20.19.0" | ||
| 2186 | + }, | ||
| 2187 | + "funding": { | ||
| 2188 | + "url": "https://github.com/sponsors/sxzz" | ||
| 2189 | + } | ||
| 2190 | + }, | ||
| 2191 | + "node_modules/update-browserslist-db": { | ||
| 2192 | + "version": "1.2.3", | ||
| 2193 | + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", | ||
| 2194 | + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", | ||
| 2195 | + "dev": true, | ||
| 2196 | + "funding": [ | ||
| 2197 | + { | ||
| 2198 | + "type": "opencollective", | ||
| 2199 | + "url": "https://opencollective.com/browserslist" | ||
| 2200 | + }, | ||
| 2201 | + { | ||
| 2202 | + "type": "tidelift", | ||
| 2203 | + "url": "https://tidelift.com/funding/github/npm/browserslist" | ||
| 2204 | + }, | ||
| 2205 | + { | ||
| 2206 | + "type": "github", | ||
| 2207 | + "url": "https://github.com/sponsors/ai" | ||
| 2208 | + } | ||
| 2209 | + ], | ||
| 2210 | + "license": "MIT", | ||
| 2211 | + "dependencies": { | ||
| 2212 | + "escalade": "^3.2.0", | ||
| 2213 | + "picocolors": "^1.1.1" | ||
| 2214 | + }, | ||
| 2215 | + "bin": { | ||
| 2216 | + "update-browserslist-db": "cli.js" | ||
| 2217 | + }, | ||
| 2218 | + "peerDependencies": { | ||
| 2219 | + "browserslist": ">= 4.21.0" | ||
| 2220 | + } | ||
| 2221 | + }, | ||
| 2222 | + "node_modules/vite": { | ||
| 2223 | + "version": "8.0.10", | ||
| 2224 | + "resolved": "https://registry.npmmirror.com/vite/-/vite-8.0.10.tgz", | ||
| 2225 | + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", | ||
| 2226 | + "dev": true, | ||
| 2227 | + "license": "MIT", | ||
| 2228 | + "dependencies": { | ||
| 2229 | + "lightningcss": "^1.32.0", | ||
| 2230 | + "picomatch": "^4.0.4", | ||
| 2231 | + "postcss": "^8.5.10", | ||
| 2232 | + "rolldown": "1.0.0-rc.17", | ||
| 2233 | + "tinyglobby": "^0.2.16" | ||
| 2234 | + }, | ||
| 2235 | + "bin": { | ||
| 2236 | + "vite": "bin/vite.js" | ||
| 2237 | + }, | ||
| 2238 | + "engines": { | ||
| 2239 | + "node": "^20.19.0 || >=22.12.0" | ||
| 2240 | + }, | ||
| 2241 | + "funding": { | ||
| 2242 | + "url": "https://github.com/vitejs/vite?sponsor=1" | ||
| 2243 | + }, | ||
| 2244 | + "optionalDependencies": { | ||
| 2245 | + "fsevents": "~2.3.3" | ||
| 2246 | + }, | ||
| 2247 | + "peerDependencies": { | ||
| 2248 | + "@types/node": "^20.19.0 || >=22.12.0", | ||
| 2249 | + "@vitejs/devtools": "^0.1.0", | ||
| 2250 | + "esbuild": "^0.27.0 || ^0.28.0", | ||
| 2251 | + "jiti": ">=1.21.0", | ||
| 2252 | + "less": "^4.0.0", | ||
| 2253 | + "sass": "^1.70.0", | ||
| 2254 | + "sass-embedded": "^1.70.0", | ||
| 2255 | + "stylus": ">=0.54.8", | ||
| 2256 | + "sugarss": "^5.0.0", | ||
| 2257 | + "terser": "^5.16.0", | ||
| 2258 | + "tsx": "^4.8.1", | ||
| 2259 | + "yaml": "^2.4.2" | ||
| 2260 | + }, | ||
| 2261 | + "peerDependenciesMeta": { | ||
| 2262 | + "@types/node": { | ||
| 2263 | + "optional": true | ||
| 2264 | + }, | ||
| 2265 | + "@vitejs/devtools": { | ||
| 2266 | + "optional": true | ||
| 2267 | + }, | ||
| 2268 | + "esbuild": { | ||
| 2269 | + "optional": true | ||
| 2270 | + }, | ||
| 2271 | + "jiti": { | ||
| 2272 | + "optional": true | ||
| 2273 | + }, | ||
| 2274 | + "less": { | ||
| 2275 | + "optional": true | ||
| 2276 | + }, | ||
| 2277 | + "sass": { | ||
| 2278 | + "optional": true | ||
| 2279 | + }, | ||
| 2280 | + "sass-embedded": { | ||
| 2281 | + "optional": true | ||
| 2282 | + }, | ||
| 2283 | + "stylus": { | ||
| 2284 | + "optional": true | ||
| 2285 | + }, | ||
| 2286 | + "sugarss": { | ||
| 2287 | + "optional": true | ||
| 2288 | + }, | ||
| 2289 | + "terser": { | ||
| 2290 | + "optional": true | ||
| 2291 | + }, | ||
| 2292 | + "tsx": { | ||
| 2293 | + "optional": true | ||
| 2294 | + }, | ||
| 2295 | + "yaml": { | ||
| 2296 | + "optional": true | ||
| 2297 | + } | ||
| 2298 | + } | ||
| 2299 | + }, | ||
| 2300 | + "node_modules/vite-plugin-vue-devtools": { | ||
| 2301 | + "version": "8.1.1", | ||
| 2302 | + "resolved": "https://registry.npmmirror.com/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.1.1.tgz", | ||
| 2303 | + "integrity": "sha512-9qTpOmZ2vHpvlI9hdVXAQ1Ry4I8GcBArU7aPi0qfIaV7fQIXy0L1nb6X4mFY2Gw0dYshHuLbIl0Ulb572SCjsQ==", | ||
| 2304 | + "dev": true, | ||
| 2305 | + "license": "MIT", | ||
| 2306 | + "dependencies": { | ||
| 2307 | + "@vue/devtools-core": "^8.1.1", | ||
| 2308 | + "@vue/devtools-kit": "^8.1.1", | ||
| 2309 | + "@vue/devtools-shared": "^8.1.1", | ||
| 2310 | + "sirv": "^3.0.2", | ||
| 2311 | + "vite-plugin-inspect": "^11.3.3", | ||
| 2312 | + "vite-plugin-vue-inspector": "^5.3.2" | ||
| 2313 | + }, | ||
| 2314 | + "engines": { | ||
| 2315 | + "node": ">=v14.21.3" | ||
| 2316 | + }, | ||
| 2317 | + "peerDependencies": { | ||
| 2318 | + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" | ||
| 2319 | + } | ||
| 2320 | + }, | ||
| 2321 | + "node_modules/vite-plugin-vue-devtools/node_modules/vite-plugin-inspect": { | ||
| 2322 | + "version": "11.3.3", | ||
| 2323 | + "resolved": "https://registry.npmmirror.com/vite-plugin-inspect/-/vite-plugin-inspect-11.3.3.tgz", | ||
| 2324 | + "integrity": "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==", | ||
| 2325 | + "dev": true, | ||
| 2326 | + "license": "MIT", | ||
| 2327 | + "dependencies": { | ||
| 2328 | + "ansis": "^4.1.0", | ||
| 2329 | + "debug": "^4.4.1", | ||
| 2330 | + "error-stack-parser-es": "^1.0.5", | ||
| 2331 | + "ohash": "^2.0.11", | ||
| 2332 | + "open": "^10.2.0", | ||
| 2333 | + "perfect-debounce": "^2.0.0", | ||
| 2334 | + "sirv": "^3.0.1", | ||
| 2335 | + "unplugin-utils": "^0.3.0", | ||
| 2336 | + "vite-dev-rpc": "^1.1.0" | ||
| 2337 | + }, | ||
| 2338 | + "engines": { | ||
| 2339 | + "node": ">=14" | ||
| 2340 | + }, | ||
| 2341 | + "funding": { | ||
| 2342 | + "url": "https://github.com/sponsors/antfu" | ||
| 2343 | + }, | ||
| 2344 | + "peerDependencies": { | ||
| 2345 | + "vite": "^6.0.0 || ^7.0.0-0" | ||
| 2346 | + }, | ||
| 2347 | + "peerDependenciesMeta": { | ||
| 2348 | + "@nuxt/kit": { | ||
| 2349 | + "optional": true | ||
| 2350 | + } | ||
| 2351 | + } | ||
| 2352 | + }, | ||
| 2353 | + "node_modules/vite-plugin-vue-devtools/node_modules/vite-plugin-inspect/node_modules/vite-dev-rpc": { | ||
| 2354 | + "version": "1.1.0", | ||
| 2355 | + "resolved": "https://registry.npmmirror.com/vite-dev-rpc/-/vite-dev-rpc-1.1.0.tgz", | ||
| 2356 | + "integrity": "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==", | ||
| 2357 | + "dev": true, | ||
| 2358 | + "license": "MIT", | ||
| 2359 | + "dependencies": { | ||
| 2360 | + "birpc": "^2.4.0", | ||
| 2361 | + "vite-hot-client": "^2.1.0" | ||
| 2362 | + }, | ||
| 2363 | + "funding": { | ||
| 2364 | + "url": "https://github.com/sponsors/antfu" | ||
| 2365 | + }, | ||
| 2366 | + "peerDependencies": { | ||
| 2367 | + "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0" | ||
| 2368 | + } | ||
| 2369 | + }, | ||
| 2370 | + "node_modules/vite-plugin-vue-devtools/node_modules/vite-plugin-inspect/node_modules/vite-dev-rpc/node_modules/vite-hot-client": { | ||
| 2371 | + "version": "2.1.0", | ||
| 2372 | + "resolved": "https://registry.npmmirror.com/vite-hot-client/-/vite-hot-client-2.1.0.tgz", | ||
| 2373 | + "integrity": "sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==", | ||
| 2374 | + "dev": true, | ||
| 2375 | + "license": "MIT", | ||
| 2376 | + "funding": { | ||
| 2377 | + "url": "https://github.com/sponsors/antfu" | ||
| 2378 | + }, | ||
| 2379 | + "peerDependencies": { | ||
| 2380 | + "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" | ||
| 2381 | + } | ||
| 2382 | + }, | ||
| 2383 | + "node_modules/vite-plugin-vue-inspector": { | ||
| 2384 | + "version": "5.4.0", | ||
| 2385 | + "resolved": "https://registry.npmmirror.com/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.4.0.tgz", | ||
| 2386 | + "integrity": "sha512-Iq/024CydcE46FZqWPU4t4lw4uYOdLnFSO1RNxJVt2qY9zxIjmnkBqhHnYaReWM82kmNnaXs7OkfgRrV2GEjyw==", | ||
| 2387 | + "dev": true, | ||
| 2388 | + "license": "MIT", | ||
| 2389 | + "dependencies": { | ||
| 2390 | + "@babel/core": "^7.23.0", | ||
| 2391 | + "@babel/plugin-proposal-decorators": "^7.23.0", | ||
| 2392 | + "@babel/plugin-syntax-import-attributes": "^7.22.5", | ||
| 2393 | + "@babel/plugin-syntax-import-meta": "^7.10.4", | ||
| 2394 | + "@babel/plugin-transform-typescript": "^7.22.15", | ||
| 2395 | + "@vue/babel-plugin-jsx": "^1.1.5", | ||
| 2396 | + "@vue/compiler-dom": "^3.3.4", | ||
| 2397 | + "kolorist": "^1.8.0", | ||
| 2398 | + "magic-string": "^0.30.4" | ||
| 2399 | + }, | ||
| 2400 | + "peerDependencies": { | ||
| 2401 | + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" | ||
| 2402 | + } | ||
| 2403 | + }, | ||
| 2404 | + "node_modules/vue": { | ||
| 2405 | + "version": "3.5.33", | ||
| 2406 | + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.33.tgz", | ||
| 2407 | + "integrity": "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ==", | ||
| 2408 | + "license": "MIT", | ||
| 2409 | + "dependencies": { | ||
| 2410 | + "@vue/compiler-dom": "3.5.33", | ||
| 2411 | + "@vue/compiler-sfc": "3.5.33", | ||
| 2412 | + "@vue/runtime-dom": "3.5.33", | ||
| 2413 | + "@vue/server-renderer": "3.5.33", | ||
| 2414 | + "@vue/shared": "3.5.33" | ||
| 2415 | + }, | ||
| 2416 | + "peerDependencies": { | ||
| 2417 | + "typescript": "*" | ||
| 2418 | + }, | ||
| 2419 | + "peerDependenciesMeta": { | ||
| 2420 | + "typescript": { | ||
| 2421 | + "optional": true | ||
| 2422 | + } | ||
| 2423 | + } | ||
| 2424 | + }, | ||
| 2425 | + "node_modules/vue-component-type-helpers": { | ||
| 2426 | + "version": "3.2.7", | ||
| 2427 | + "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.2.7.tgz", | ||
| 2428 | + "integrity": "sha512-+gPp5YGmhfsj1IN+xUo7y0fb4clfnOiiUA39y07yW1VzCRjzVgwLbtmdWlghh7mXrPsEaYc7rrIir/HT6C8vYQ==", | ||
| 2429 | + "license": "MIT" | ||
| 2430 | + }, | ||
| 2431 | + "node_modules/vue-router": { | ||
| 2432 | + "version": "4.6.4", | ||
| 2433 | + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", | ||
| 2434 | + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", | ||
| 2435 | + "license": "MIT", | ||
| 2436 | + "dependencies": { | ||
| 2437 | + "@vue/devtools-api": "^6.6.4" | ||
| 2438 | + }, | ||
| 2439 | + "funding": { | ||
| 2440 | + "url": "https://github.com/sponsors/posva" | ||
| 2441 | + }, | ||
| 2442 | + "peerDependencies": { | ||
| 2443 | + "vue": "^3.5.0" | ||
| 2444 | + } | ||
| 2445 | + }, | ||
| 2446 | + "node_modules/wsl-utils": { | ||
| 2447 | + "version": "0.1.0", | ||
| 2448 | + "resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.1.0.tgz", | ||
| 2449 | + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", | ||
| 2450 | + "dev": true, | ||
| 2451 | + "license": "MIT", | ||
| 2452 | + "dependencies": { | ||
| 2453 | + "is-wsl": "^3.1.0" | ||
| 2454 | + }, | ||
| 2455 | + "engines": { | ||
| 2456 | + "node": ">=18" | ||
| 2457 | + }, | ||
| 2458 | + "funding": { | ||
| 2459 | + "url": "https://github.com/sponsors/sindresorhus" | ||
| 2460 | + } | ||
| 2461 | + }, | ||
| 2462 | + "node_modules/yallist": { | ||
| 2463 | + "version": "3.1.1", | ||
| 2464 | + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", | ||
| 2465 | + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", | ||
| 2466 | + "dev": true, | ||
| 2467 | + "license": "ISC" | ||
| 2468 | + } | ||
| 2469 | + } | ||
| 2470 | +} |
package.json
0 → 100644
| 1 | +{ | ||
| 2 | + "name": "iot-bridge-ui", | ||
| 3 | + "version": "0.0.0", | ||
| 4 | + "private": true, | ||
| 5 | + "type": "module", | ||
| 6 | + "scripts": { | ||
| 7 | + "dev": "vite", | ||
| 8 | + "build": "vite build", | ||
| 9 | + "preview": "vite preview" | ||
| 10 | + }, | ||
| 11 | + "dependencies": { | ||
| 12 | + "@element-plus/icons-vue": "^2.3.2", | ||
| 13 | + "element-plus": "^2.13.7", | ||
| 14 | + "vue": "^3.5.32", | ||
| 15 | + "vue-router": "^4.6.4" | ||
| 16 | + }, | ||
| 17 | + "devDependencies": { | ||
| 18 | + "@vitejs/plugin-vue": "^6.0.6", | ||
| 19 | + "vite": "^8.0.8", | ||
| 20 | + "vite-plugin-vue-devtools": "^8.1.1" | ||
| 21 | + }, | ||
| 22 | + "engines": { | ||
| 23 | + "node": "^20.19.0 || >=22.12.0" | ||
| 24 | + } | ||
| 25 | +} |
public/favicon.ico
0 → 100644
No preview for this file type
src/App.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="app-container"> | ||
| 3 | + <el-container> | ||
| 4 | + <el-aside width="180px" class="sidebar"> | ||
| 5 | + <div class="logo-area"> | ||
| 6 | + <span>云物联网平台</span> | ||
| 7 | + </div> | ||
| 8 | + <el-menu | ||
| 9 | + :default-active="currentRoute" | ||
| 10 | + class="sidebar-menu" | ||
| 11 | + background-color="#304156" | ||
| 12 | + text-color="#bfcbd9" | ||
| 13 | + active-text-color="#409eff" | ||
| 14 | + :router="true" | ||
| 15 | + > | ||
| 16 | + <el-menu-item index="/smart-light"> | ||
| 17 | + <el-icon><Monitor /></el-icon> | ||
| 18 | + <span>智能灯</span> | ||
| 19 | + </el-menu-item> | ||
| 20 | + <el-menu-item index="/energy"> | ||
| 21 | + <el-icon><Lightning /></el-icon> | ||
| 22 | + <span>能耗</span> | ||
| 23 | + </el-menu-item> | ||
| 24 | + </el-menu> | ||
| 25 | + </el-aside> | ||
| 26 | + <el-main class="main-content"> | ||
| 27 | + <router-view /> | ||
| 28 | + </el-main> | ||
| 29 | + </el-container> | ||
| 30 | + </div> | ||
| 31 | +</template> | ||
| 32 | + | ||
| 33 | +<script setup> | ||
| 34 | +import { computed } from 'vue' | ||
| 35 | +import { useRoute } from 'vue-router' | ||
| 36 | + | ||
| 37 | +const route = useRoute() | ||
| 38 | +const currentRoute = computed(() => route.path) | ||
| 39 | +</script> | ||
| 40 | + | ||
| 41 | +<style scoped> | ||
| 42 | +.app-container { | ||
| 43 | + height: 100vh; | ||
| 44 | + overflow: hidden; | ||
| 45 | +} | ||
| 46 | +.sidebar { | ||
| 47 | + background-color: #304156; | ||
| 48 | + color: #fff; | ||
| 49 | +} | ||
| 50 | +.logo-area { | ||
| 51 | + padding: 18px 16px; | ||
| 52 | + color: #fff; | ||
| 53 | + font-size: 15px; | ||
| 54 | + font-weight: bold; | ||
| 55 | + text-align: center; | ||
| 56 | + border-bottom: 1px solid rgba(255,255,255,0.08); | ||
| 57 | + background-color: #263445; | ||
| 58 | +} | ||
| 59 | +.sidebar-menu { | ||
| 60 | + border-right: none !important; | ||
| 61 | + padding-top: 8px; | ||
| 62 | +} | ||
| 63 | +.main-content { | ||
| 64 | + background-color: #f0f2f5; | ||
| 65 | + padding: 0; | ||
| 66 | + overflow-y: auto; | ||
| 67 | +} | ||
| 68 | +</style> |
src/assets/base.css
0 → 100644
| 1 | +/* color palette from <https://github.com/vuejs/theme> */ | ||
| 2 | +:root { | ||
| 3 | + --vt-c-white: #ffffff; | ||
| 4 | + --vt-c-white-soft: #f8f8f8; | ||
| 5 | + --vt-c-white-mute: #f2f2f2; | ||
| 6 | + | ||
| 7 | + --vt-c-black: #181818; | ||
| 8 | + --vt-c-black-soft: #222222; | ||
| 9 | + --vt-c-black-mute: #282828; | ||
| 10 | + | ||
| 11 | + --vt-c-indigo: #2c3e50; | ||
| 12 | + | ||
| 13 | + --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); | ||
| 14 | + --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); | ||
| 15 | + --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); | ||
| 16 | + --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); | ||
| 17 | + | ||
| 18 | + --vt-c-text-light-1: var(--vt-c-indigo); | ||
| 19 | + --vt-c-text-light-2: rgba(60, 60, 60, 0.66); | ||
| 20 | + --vt-c-text-dark-1: var(--vt-c-white); | ||
| 21 | + --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); | ||
| 22 | +} | ||
| 23 | + | ||
| 24 | +/* semantic color variables for this project */ | ||
| 25 | +:root { | ||
| 26 | + --color-background: var(--vt-c-white); | ||
| 27 | + --color-background-soft: var(--vt-c-white-soft); | ||
| 28 | + --color-background-mute: var(--vt-c-white-mute); | ||
| 29 | + | ||
| 30 | + --color-border: var(--vt-c-divider-light-2); | ||
| 31 | + --color-border-hover: var(--vt-c-divider-light-1); | ||
| 32 | + | ||
| 33 | + --color-heading: var(--vt-c-text-light-1); | ||
| 34 | + --color-text: var(--vt-c-text-light-1); | ||
| 35 | + | ||
| 36 | + --section-gap: 160px; | ||
| 37 | +} | ||
| 38 | + | ||
| 39 | +@media (prefers-color-scheme: dark) { | ||
| 40 | + :root { | ||
| 41 | + --color-background: var(--vt-c-black); | ||
| 42 | + --color-background-soft: var(--vt-c-black-soft); | ||
| 43 | + --color-background-mute: var(--vt-c-black-mute); | ||
| 44 | + | ||
| 45 | + --color-border: var(--vt-c-divider-dark-2); | ||
| 46 | + --color-border-hover: var(--vt-c-divider-dark-1); | ||
| 47 | + | ||
| 48 | + --color-heading: var(--vt-c-text-dark-1); | ||
| 49 | + --color-text: var(--vt-c-text-dark-2); | ||
| 50 | + } | ||
| 51 | +} | ||
| 52 | + | ||
| 53 | +*, | ||
| 54 | +*::before, | ||
| 55 | +*::after { | ||
| 56 | + box-sizing: border-box; | ||
| 57 | + margin: 0; | ||
| 58 | + font-weight: normal; | ||
| 59 | +} | ||
| 60 | + | ||
| 61 | +body { | ||
| 62 | + min-height: 100vh; | ||
| 63 | + color: var(--color-text); | ||
| 64 | + background: var(--color-background); | ||
| 65 | + transition: | ||
| 66 | + color 0.5s, | ||
| 67 | + background-color 0.5s; | ||
| 68 | + line-height: 1.6; | ||
| 69 | + font-family: | ||
| 70 | + Inter, | ||
| 71 | + -apple-system, | ||
| 72 | + BlinkMacSystemFont, | ||
| 73 | + 'Segoe UI', | ||
| 74 | + Roboto, | ||
| 75 | + Oxygen, | ||
| 76 | + Ubuntu, | ||
| 77 | + Cantarell, | ||
| 78 | + 'Fira Sans', | ||
| 79 | + 'Droid Sans', | ||
| 80 | + 'Helvetica Neue', | ||
| 81 | + sans-serif; | ||
| 82 | + font-size: 15px; | ||
| 83 | + text-rendering: optimizeLegibility; | ||
| 84 | + -webkit-font-smoothing: antialiased; | ||
| 85 | + -moz-osx-font-smoothing: grayscale; | ||
| 86 | +} |
src/assets/logo.svg
0 → 100644
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg> |
src/assets/main.css
0 → 100644
| 1 | +* { | ||
| 2 | + margin: 0; | ||
| 3 | + padding: 0; | ||
| 4 | + box-sizing: border-box; | ||
| 5 | +} | ||
| 6 | + | ||
| 7 | +html, body { | ||
| 8 | + width: 100%; | ||
| 9 | + height: 100%; | ||
| 10 | + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | ||
| 11 | +} | ||
| 12 | + | ||
| 13 | +#app { | ||
| 14 | + width: 100%; | ||
| 15 | + height: 100vh; | ||
| 16 | + max-width: none; | ||
| 17 | + margin: 0; | ||
| 18 | + padding: 0; | ||
| 19 | +} |
src/components/CountDialog.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <el-dialog | ||
| 3 | + :model-value="visible" | ||
| 4 | + @update:model-value="$emit('update:visible', $event)" | ||
| 5 | + title="" | ||
| 6 | + width="calc(100vw - 40px)" | ||
| 7 | + :style="{ maxWidth: '1400px' }" | ||
| 8 | + top="3vh" | ||
| 9 | + destroy-on-close | ||
| 10 | + class="count-dialog" | ||
| 11 | + > | ||
| 12 | + <template #header> | ||
| 13 | + <div class="dialog-header"> | ||
| 14 | + <span class="title-text">中速纸杯机24号机 计数明细</span> | ||
| 15 | + <span class="query-label">查询方式:</span> | ||
| 16 | + <div class="header-center"> | ||
| 17 | + <el-radio-group v-model="queryMode" size="small"> | ||
| 18 | + <el-radio-button value="day">日查询</el-radio-button> | ||
| 19 | + <el-radio-button value="week">周查询</el-radio-button> | ||
| 20 | + <el-radio-button value="month">月查询</el-radio-button> | ||
| 21 | + </el-radio-group> | ||
| 22 | + <el-date-picker v-model="dateRange" type="daterange" size="small" range-separator="-" | ||
| 23 | + start-placeholder="2026-04-21" end-placeholder="2026-04-28" style="width: 240px; margin-left: 8px;" /> | ||
| 24 | + </div> | ||
| 25 | + <div class="header-right"> | ||
| 26 | + </div> | ||
| 27 | + </div> | ||
| 28 | + </template> | ||
| 29 | + | ||
| 30 | + <div class="count-body"> | ||
| 31 | + <!-- 折线图 --> | ||
| 32 | + <div class="chart-section"> | ||
| 33 | + <svg viewBox="0 0 900 250" xmlns="http://www.w3.org/2000/svg"> | ||
| 34 | + <!-- Y轴 --> | ||
| 35 | + <g font-size="11" fill="#999" text-anchor="end"> | ||
| 36 | + <text x="35" y="215">0</text> | ||
| 37 | + <text x="35" y="175">0.2</text> | ||
| 38 | + <text x="35" y="135">0.4</text> | ||
| 39 | + <text x="35" y="95">0.6</text> | ||
| 40 | + <text x="35" y="55">0.8</text> | ||
| 41 | + <text x="35" y="18">1</text> | ||
| 42 | + </g> | ||
| 43 | + <!-- 网格横线 --> | ||
| 44 | + <g stroke="#f0f0f0" stroke-width="1"> | ||
| 45 | + <line x1="45" y1="213" x2="880" y2="213"/> | ||
| 46 | + <line x1="45" y1="173" x2="880" y2="173"/> | ||
| 47 | + <line x1="45" y1="133" x2="880" y2="133"/> | ||
| 48 | + <line x1="45" y1="93" x2="880" y2="93"/> | ||
| 49 | + <line x1="45" y1="53" x2="880" y2="53"/> | ||
| 50 | + <line x1="45" y1="16" x2="880" y2="16"/> | ||
| 51 | + </g> | ||
| 52 | + <!-- X轴日期 --> | ||
| 53 | + <g font-size="11" fill="#666" text-anchor="middle"> | ||
| 54 | + <text x="105" y="235">2026-04-21</text> | ||
| 55 | + <text x="225" y="235">2026-04-22</text> | ||
| 56 | + <text x="345" y="235">2026-04-23</text> | ||
| 57 | + <text x="465" y="235">2026-04-24</text> | ||
| 58 | + <text x="585" y="235">2026-04-25</text> | ||
| 59 | + <text x="705" y="235">2026-04-26</text> | ||
| 60 | + <text x="825" y="235">2026-04-27</text> | ||
| 61 | + </g> | ||
| 62 | + <!-- 数据点(全部为0) --> | ||
| 63 | + <g fill="#409eff"> | ||
| 64 | + <circle cx="105" cy="213" r="3"/><text x="105" y="205" text-anchor="middle" font-size="11" fill="#409eff">0</text> | ||
| 65 | + <circle cx="225" cy="213" r="3"/><text x="225" y="205" text-anchor="middle" font-size="11" fill="#409eff">0</text> | ||
| 66 | + <circle cx="345" cy="213" r="3"/><text x="345" y="205" text-anchor="middle" font-size="11" fill="#409eff">0</text> | ||
| 67 | + <circle cx="465" cy="213" r="3"/><text x="465" y="205" text-anchor="middle" font-size="11" fill="#409eff">0</text> | ||
| 68 | + <circle cx="585" cy="213" r="3"/><text x="585" y="205" text-anchor="middle" font-size="11" fill="#409eff">0</text> | ||
| 69 | + <circle cx="705" cy="213" r="3"/><text x="705" y="205" text-anchor="middle" font-size="11" fill="#409eff">0</text> | ||
| 70 | + <circle cx="825" cy="213" r="3"/><text x="825" y="205" text-anchor="middle" font-size="11" fill="#409eff">0</text> | ||
| 71 | + </g> | ||
| 72 | + <!-- 连接线 --> | ||
| 73 | + <polyline points="105,213 225,213 345,213 465,213 585,213 705,213 825,213" | ||
| 74 | + fill="none" stroke="#409eff" stroke-width="2"/> | ||
| 75 | + <!-- 坐标轴线 --> | ||
| 76 | + <line x1="45" y1="213" x2="880" y2="213" stroke="#ccc" stroke-width="1"/> | ||
| 77 | + </svg> | ||
| 78 | + </div> | ||
| 79 | + | ||
| 80 | + <!-- 表格 --> | ||
| 81 | + <div class="table-section"> | ||
| 82 | + <el-table :data="[]" size="small" stripe border style="width: 100%; font-size: 12px;" empty-text="暂无数据"> | ||
| 83 | + <el-table-column prop="date" label="日期" width="140" align="center" /> | ||
| 84 | + <el-table-column prop="totalCount" label="总个数" align="center" /> | ||
| 85 | + <el-table-column prop="lightOnDuration" label="亮灯时长" align="center" /> | ||
| 86 | + <el-table-column prop="avgDuration" label="平均时长" align="center" /> | ||
| 87 | + <el-table-column prop="maxDuration" label="理论时长" align="center" /> | ||
| 88 | + <el-table-column prop="efficiency" label="生产效率" align="center" /> | ||
| 89 | + <el-table-column prop="detail" label="详情" align="center" /> | ||
| 90 | + <el-table-column prop="remark" label="备注" align="center" /> | ||
| 91 | + </el-table> | ||
| 92 | + <div class="empty-hint">暂无数据</div> | ||
| 93 | + </div> | ||
| 94 | + </div> | ||
| 95 | + </el-dialog> | ||
| 96 | +</template> | ||
| 97 | + | ||
| 98 | +<script setup> | ||
| 99 | +import { ref } from 'vue' | ||
| 100 | + | ||
| 101 | +defineProps({ visible: Boolean, device: Object }) | ||
| 102 | +defineEmits(['update:visible']) | ||
| 103 | +const queryMode = ref('day') | ||
| 104 | +const dateRange = ref(null) | ||
| 105 | +</script> | ||
| 106 | + | ||
| 107 | +<style scoped> | ||
| 108 | +.count-dialog :deep(.el-dialog) { | ||
| 109 | + max-height: 92vh; | ||
| 110 | + display: flex; | ||
| 111 | + flex-direction: column; | ||
| 112 | +} | ||
| 113 | +.count-dialog :deep(.el-dialog__header) { | ||
| 114 | + padding: 10px 20px; | ||
| 115 | + border-bottom: 1px solid #e8e8e8; | ||
| 116 | + margin: 0; | ||
| 117 | + flex-shrink: 0; | ||
| 118 | +} | ||
| 119 | +.count-dialog :deep(.el-dialog__body) { | ||
| 120 | + overflow-y: auto; | ||
| 121 | + flex: 1; | ||
| 122 | +} | ||
| 123 | +.dialog-header { | ||
| 124 | + display: flex; | ||
| 125 | + align-items: center; | ||
| 126 | + gap: 12px; | ||
| 127 | +} | ||
| 128 | +.title-text { | ||
| 129 | + font-size: 14px; | ||
| 130 | + font-weight: bold; | ||
| 131 | + color: #333; | ||
| 132 | + white-space: nowrap; | ||
| 133 | +} | ||
| 134 | +.query-label { | ||
| 135 | + font-size: 13px; | ||
| 136 | + color: #666; | ||
| 137 | +} | ||
| 138 | +.header-center { | ||
| 139 | + display: flex; | ||
| 140 | + align-items: center; | ||
| 141 | + flex: 1; | ||
| 142 | +} | ||
| 143 | +.header-right { | ||
| 144 | + display: flex; | ||
| 145 | + align-items: center; | ||
| 146 | + gap: 8px; | ||
| 147 | +} | ||
| 148 | + | ||
| 149 | +.count-body { | ||
| 150 | + padding: 0; | ||
| 151 | +} | ||
| 152 | +.chart-section { | ||
| 153 | + background: #fff; | ||
| 154 | + border: 1px solid #ebeef5; | ||
| 155 | + border-radius: 4px; | ||
| 156 | + padding: 16px 20px; | ||
| 157 | + margin-bottom: 16px; | ||
| 158 | +} | ||
| 159 | +.chart-section svg { | ||
| 160 | + width: 100%; | ||
| 161 | + height: auto; | ||
| 162 | + display: block; | ||
| 163 | +} | ||
| 164 | + | ||
| 165 | +.table-section { | ||
| 166 | + background: #fff; | ||
| 167 | + border: 1px solid #ebeef5; | ||
| 168 | + border-radius: 4px; | ||
| 169 | + overflow: hidden; | ||
| 170 | +} | ||
| 171 | +.empty-hint { | ||
| 172 | + text-align: center; | ||
| 173 | + padding: 30px; | ||
| 174 | + color: #999; | ||
| 175 | + font-size: 13px; | ||
| 176 | +} | ||
| 177 | +</style> |
src/components/EnergyReportDialog.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <el-dialog | ||
| 3 | + :model-value="visible" | ||
| 4 | + @update:model-value="$emit('update:visible', $event)" | ||
| 5 | + title="" | ||
| 6 | + width="calc(100vw - 40px)" | ||
| 7 | + :style="{ maxWidth: '1400px' }" | ||
| 8 | + top="3vh" | ||
| 9 | + destroy-on-close | ||
| 10 | + class="ereport-dialog" | ||
| 11 | + > | ||
| 12 | + <template #header> | ||
| 13 | + <div class="dialog-header"> | ||
| 14 | + <span class="title-text">{{ device?.name || '能耗设备1' }} 能耗报表</span> | ||
| 15 | + <div class="header-right"> | ||
| 16 | + <el-icon :size="16" style="cursor:pointer;color:#409eff;"><FullScreen /></el-icon> | ||
| 17 | + <el-icon :size="16" style="cursor:pointer;color:#409eff;margin-left:8px;" @click="$emit('update:visible', false)"><Close /></el-icon> | ||
| 18 | + </div> | ||
| 19 | + </div> | ||
| 20 | + </template> | ||
| 21 | + | ||
| 22 | + <div class="report-body"> | ||
| 23 | + <!-- 上排:6个能耗卡片 + 碳排放统计 --> | ||
| 24 | + <div class="top-section"> | ||
| 25 | + <div class="energy-cards-grid"> | ||
| 26 | + <div v-for="(card, idx) in energyCards" :key="idx" :class="['energy-card-item', card.color]"> | ||
| 27 | + <span class="card-label">{{ card.label }}</span> | ||
| 28 | + <span class="card-val">{{ card.value }}<small>{{ card.unit }}</small></span> | ||
| 29 | + </div> | ||
| 30 | + </div> | ||
| 31 | + <div class="carbon-panel"> | ||
| 32 | + <div class="carbon-title">碳排放统计 <i style="font-size:12px;">▼</i></div> | ||
| 33 | + <div class="carbon-sub">碳排放系数0</div> | ||
| 34 | + <div v-for="item in carbonItems" :key="item.label" :class="['carbon-row', item.color]"> | ||
| 35 | + {{ item.label }}{{ item.val }} | ||
| 36 | + </div> | ||
| 37 | + </div> | ||
| 38 | + </div> | ||
| 39 | + | ||
| 40 | + <!-- 下排:3个图表 --> | ||
| 41 | + <div class="charts-grid"> | ||
| 42 | + <!-- 时能耗 --> | ||
| 43 | + <div class="chart-card"> | ||
| 44 | + <div class="chart-header"> | ||
| 45 | + <span class="chart-title">时能耗</span> | ||
| 46 | + <div class="chart-tools"> | ||
| 47 | + <label><input type="radio" name="t1" checked /> 2025-04-28</label> | ||
| 48 | + <label><input type="radio" name="t2" checked /> 昨日日期</label> | ||
| 49 | + <el-icon :size="14"><ZoomIn /></el-icon> | ||
| 50 | + </div> | ||
| 51 | + </div> | ||
| 52 | + <div class="chart-body"> | ||
| 53 | + <svg viewBox="0 0 500 220"> | ||
| 54 | + <g font-size="10" fill="#999" text-anchor="end"> | ||
| 55 | + <text x="24" y="20">1</text><text x="24" y="60">0.8</text><text x="24" y="100">0.6</text> | ||
| 56 | + <text x="24" y="140">0.4</text><text x="24" y="180">0.2</text><text x="24" y="210">0</text> | ||
| 57 | + </g> | ||
| 58 | + <line x1="30" y1="206" x2="490" y2="206" stroke="#ddd"/> | ||
| 59 | + <g font-size="9" fill="#666" text-anchor="middle"> | ||
| 60 | + <template v-for="i in 25" :key="'th'+i"><text :x="36+i*18" y="218">{{ i-1 }}</text></template> | ||
| 61 | + </g> | ||
| 62 | + <polyline points="36,206 54,206 72,206 90,206 108,206 126,206 144,206 162,206 180,206 198,206 216,206 234,206 252,206 270,206 288,206 306,206 324,206 342,206 360,206 378,206 396,206 414,206 432,206 450,206 468,206 486,206" | ||
| 63 | + fill="none" stroke="#409eff" stroke-width="1.5"/> | ||
| 64 | + </svg> | ||
| 65 | + </div> | ||
| 66 | + </div> | ||
| 67 | + | ||
| 68 | + <!-- 日能耗 --> | ||
| 69 | + <div class="chart-card"> | ||
| 70 | + <div class="chart-header"> | ||
| 71 | + <span class="chart-title">日能耗</span> | ||
| 72 | + <div class="chart-tools"> | ||
| 73 | + <label><input type="radio" checked/> 2025-04</label> | ||
| 74 | + <label><input type="radio" checked/> 昨日日期</label> | ||
| 75 | + <el-icon :size="14"><ZoomIn /></el-icon> | ||
| 76 | + </div> | ||
| 77 | + </div> | ||
| 78 | + <div class="chart-body"> | ||
| 79 | + <svg viewBox="0 0 500 220"> | ||
| 80 | + <g font-size="10" fill="#999" text-anchor="end"> | ||
| 81 | + <text x="24" y="20">1</text><text x="24" y="60">0.8</text><text x="24" y="100">0.6</text> | ||
| 82 | + <text x="24" y="140">0.4</text><text x="24" y="180">0.2</text><text x="24" y="210">0</text> | ||
| 83 | + </g> | ||
| 84 | + <line x1="30" y1="206" x2="490" y2="206" stroke="#ddd"/> | ||
| 85 | + <g font-size="9" fill="#666" text-anchor="middle"> | ||
| 86 | + <template v-for="i in 31" :key="'dh'+i"><text :x="32+(i-1)*15" y="218">{{ i }}</text></template> | ||
| 87 | + </g> | ||
| 88 | + <polyline fill="none" stroke="#67c23a" stroke-width="1.5"/> | ||
| 89 | + </svg> | ||
| 90 | + </div> | ||
| 91 | + </div> | ||
| 92 | + | ||
| 93 | + <!-- 月能耗 --> | ||
| 94 | + <div class="chart-card full-width"> | ||
| 95 | + <div class="chart-header"> | ||
| 96 | + <span class="chart-title">月能耗</span> | ||
| 97 | + <div class="chart-tools"> | ||
| 98 | + <label><input type="radio" checked/> 2025</label> | ||
| 99 | + <el-icon :size="14"><ZoomIn /></el-icon> | ||
| 100 | + </div> | ||
| 101 | + </div> | ||
| 102 | + <div class="chart-body"> | ||
| 103 | + <svg viewBox="0 0 500 180"> | ||
| 104 | + <g font-size="10" fill="#999" text-anchor="end"> | ||
| 105 | + <text x="22" y="18">1</text><text x="22" y="53">0.8</text><text x="22" y="88">0.6</text> | ||
| 106 | + <text x="22" y="123">0.4</text><text x="22" y="158">0.2</text> | ||
| 107 | + </g> | ||
| 108 | + <line x1="28" y1="164" x2="488" y2="164" stroke="#ddd"/> | ||
| 109 | + <g font-size="9" fill="#666" text-anchor="middle"> | ||
| 110 | + <template v-for="i in 12" :key="'mh'+i"><text :x="34+(i-1)*39" y="177">{{ i }}</text></template> | ||
| 111 | + </g> | ||
| 112 | + </svg> | ||
| 113 | + </div> | ||
| 114 | + </div> | ||
| 115 | + </div> | ||
| 116 | + </div> | ||
| 117 | + </el-dialog> | ||
| 118 | +</template> | ||
| 119 | + | ||
| 120 | +<script setup> | ||
| 121 | +import { ref } from 'vue' | ||
| 122 | +import { FullScreen, Close, ZoomIn } from '@element-plus/icons-vue' | ||
| 123 | + | ||
| 124 | +defineProps({ visible: Boolean, device: Object }) | ||
| 125 | +defineEmits(['update:visible']) | ||
| 126 | + | ||
| 127 | +const energyCards = [ | ||
| 128 | + { label: '本小时能耗', value: '0', unit: 'kw·h', color: 'orange' }, | ||
| 129 | + { label: '本日能耗', value: '0', unit: 'kw·h', color: 'green' }, | ||
| 130 | + { label: '本月能耗', value: '0', unit: 'kw·h', color: 'blue' }, | ||
| 131 | + { label: '上小时能耗', value: '0', unit: 'kw·h', color: 'orange' }, | ||
| 132 | + { label: '昨日能耗', value: '0', unit: 'kw·h', color: 'green' }, | ||
| 133 | + { label: '上月能耗', value: '0', unit: 'kw·h', color: 'blue' } | ||
| 134 | +] | ||
| 135 | +const carbonItems = [ | ||
| 136 | + { label: '累计碳排放:', val: '0', color: 'blue' }, | ||
| 137 | + { label: '时:', val: '0.00', color: 'blue' }, | ||
| 138 | + { label: '日:', val: '0.00', color: 'blue' }, | ||
| 139 | + { label: '月:', val: '0.00', color: 'blue' } | ||
| 140 | +] | ||
| 141 | +</script> | ||
| 142 | + | ||
| 143 | +<style scoped> | ||
| 144 | +.ereport-dialog :deep(.el-dialog) { | ||
| 145 | + max-height: 92vh; | ||
| 146 | + display: flex; flex-direction: column; | ||
| 147 | +} | ||
| 148 | +.ereport-dialog :deep(.el-dialog__header) { padding: 10px 20px; border-bottom: 1px solid #e8e8e8; margin: 0; flex-shrink: 0; } | ||
| 149 | +.ereport-dialog :deep(.el-dialog__body) { overflow-y: auto; flex: 1; } | ||
| 150 | +.dialog-header { display: flex; align-items: center; justify-content: space-between; } | ||
| 151 | +.title-text { font-size: 15px; font-weight: bold; color: #333; } | ||
| 152 | +.header-right { display: flex; align-items: center; } | ||
| 153 | + | ||
| 154 | +.report-body { padding: 12px 0; } | ||
| 155 | + | ||
| 156 | +.top-section { display: grid; grid-template-columns: 1fr 200px; gap: 14px; margin-bottom: 14px; padding: 0 16px; } | ||
| 157 | +.energy-cards-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; } | ||
| 158 | +.energy-card-item { | ||
| 159 | + border-radius: 6px; padding: 16px 12px; display: flex; flex-direction: column; | ||
| 160 | + align-items: center; justify-content: center; color: #fff; | ||
| 161 | +} | ||
| 162 | +.energy-card-item.orange { background: linear-gradient(135deg, #f56c6c, #e74c3c); } | ||
| 163 | +.energy-card-item.green { background: linear-gradient(135deg, #67c23a, #52c41a); } | ||
| 164 | +.energy-card-item.blue { background: linear-gradient(135deg, #409eff, #1890ff); } | ||
| 165 | +.card-label { font-size: 13px; opacity: 0.9; } | ||
| 166 | +.card-val { font-size: 26px; font-weight: bold; margin-top: 6px; } | ||
| 167 | +.card-val small { font-size: 13px; font-weight: normal; margin-left: 2px; } | ||
| 168 | + | ||
| 169 | +.carbon-panel { background: linear-gradient(160deg, #5b9bd5 0%, #2e75b6 100%); border-radius: 6px; padding: 14px; color: #fff; } | ||
| 170 | +.carbon-title { font-size: 13px; font-weight: bold; margin-bottom: 8px; } | ||
| 171 | +.carbon-sub { font-size: 11px; opacity: 0.85; margin-bottom: 10px; text-align: right; } | ||
| 172 | +.carbon-row { padding: 7px 12px; border-radius: 4px; margin-bottom: 4px; font-size: 12px; background: rgba(255,255,255,0.18); } | ||
| 173 | +.carbon-row.blue { background: rgba(64,158,255,0.35); } | ||
| 174 | + | ||
| 175 | +.charts-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; padding: 0 16px; } | ||
| 176 | +.chart-card { background: #fff; border: 1px solid #eee; border-radius: 6px; overflow: hidden; } | ||
| 177 | +.chart-card.full-width { grid-column: 1 / -1; } | ||
| 178 | +.chart-header { padding: 10px 14px; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center; } | ||
| 179 | +.chart-title { font-size: 13px; font-weight: bold; color: #333; } | ||
| 180 | +.chart-tools { display: flex; align-items: center; gap: 6px; font-size: 11px; color: #666; } | ||
| 181 | +.chart-tools label { cursor: pointer; display: flex; align-items: center; gap: 2px; } | ||
| 182 | +.chart-body { padding: 10px 14px; } | ||
| 183 | +.chart-body svg { width: 100%; height: auto; } | ||
| 184 | +</style> |
src/components/HelloWorld.vue
0 → 100644
| 1 | +<script setup> | ||
| 2 | +defineProps({ | ||
| 3 | + msg: { | ||
| 4 | + type: String, | ||
| 5 | + required: true, | ||
| 6 | + }, | ||
| 7 | +}) | ||
| 8 | +</script> | ||
| 9 | + | ||
| 10 | +<template> | ||
| 11 | + <div class="greetings"> | ||
| 12 | + <h1 class="green">{{ msg }}</h1> | ||
| 13 | + <h3> | ||
| 14 | + You’ve successfully created a project with | ||
| 15 | + <a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> + | ||
| 16 | + <a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. | ||
| 17 | + </h3> | ||
| 18 | + </div> | ||
| 19 | +</template> | ||
| 20 | + | ||
| 21 | +<style scoped> | ||
| 22 | +h1 { | ||
| 23 | + font-weight: 500; | ||
| 24 | + font-size: 2.6rem; | ||
| 25 | + position: relative; | ||
| 26 | + top: -10px; | ||
| 27 | +} | ||
| 28 | + | ||
| 29 | +h3 { | ||
| 30 | + font-size: 1.2rem; | ||
| 31 | +} | ||
| 32 | + | ||
| 33 | +.greetings h1, | ||
| 34 | +.greetings h3 { | ||
| 35 | + text-align: center; | ||
| 36 | +} | ||
| 37 | + | ||
| 38 | +@media (min-width: 1024px) { | ||
| 39 | + .greetings h1, | ||
| 40 | + .greetings h3 { | ||
| 41 | + text-align: left; | ||
| 42 | + } | ||
| 43 | +} | ||
| 44 | +</style> |
src/components/OeeDialog.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <el-dialog | ||
| 3 | + :model-value="visible" | ||
| 4 | + @update:model-value="$emit('update:visible', $event)" | ||
| 5 | + :title="dialogTitle" | ||
| 6 | + width="calc(100vw - 40px)" | ||
| 7 | + :style="{ maxWidth: '1400px' }" | ||
| 8 | + top="3vh" | ||
| 9 | + destroy-on-close | ||
| 10 | + class="oee-dialog" | ||
| 11 | + > | ||
| 12 | + <template #header> | ||
| 13 | + <div class="dialog-header"> | ||
| 14 | + <div class="header-left"> | ||
| 15 | + <span class="title-text">{{ dialogTitle }}</span> | ||
| 16 | + <span class="query-label">查询方式:</span> | ||
| 17 | + <el-radio-group v-model="queryType" size="small"> | ||
| 18 | + <el-radio-button value="day">日查询</el-radio-button> | ||
| 19 | + </el-radio-group> | ||
| 20 | + <el-date-picker v-model="queryDate" type="date" placeholder="选择日期" size="small" | ||
| 21 | + style="width: 160px; margin-left: 8px;" @change="fetchLampData" /> | ||
| 22 | + </div> | ||
| 23 | + </div> | ||
| 24 | + </template> | ||
| 25 | + | ||
| 26 | + <div class="oee-body"> | ||
| 27 | + <!-- OEE时序图 --> | ||
| 28 | + <div class="chart-section"> | ||
| 29 | + <h4 class="section-title">OEE时序</h4> | ||
| 30 | + <div class="timeline-chart" ref="timelineRef" @wheel.prevent="onWheel"> | ||
| 31 | + <canvas ref="canvasRef" :width="canvasW" :height="canvasH" | ||
| 32 | + @mousemove="onCanvasMouseMove" @mouseleave="hoverSeg = null"></canvas> | ||
| 33 | + <!-- 自定义悬浮提示框 --> | ||
| 34 | + <div v-if="hoverSeg" class="hover-tooltip" :style="{ left: tooltipPos.x + 'px', top: tooltipPos.y + 'px' }"> | ||
| 35 | + <div class="tip-row"><span class="tip-label">开始时间:</span>{{ hoverSeg.startTimeText }}</div> | ||
| 36 | + <div class="tip-row"><span class="tip-label">结束时间:</span>{{ hoverSeg.endTimeText }}</div> | ||
| 37 | + <div class="tip-row"><span class="tip-label">状态:</span><span :style="{ color: stateColorMap[hoverSeg.state] }">{{ stateNameMap[hoverSeg.state] || '未知' }}</span></div> | ||
| 38 | + <div class="tip-row"><span class="tip-label">持续时长:</span>{{ formatDuration(hoverSeg.duration) }}</div> | ||
| 39 | + </div> | ||
| 40 | + </div> | ||
| 41 | + </div> | ||
| 42 | + | ||
| 43 | + <!-- 底部两栏 --> | ||
| 44 | + <div class="bottom-grid"> | ||
| 45 | + <!-- 左侧:OEE时序详情表格 --> | ||
| 46 | + <div class="table-panel"> | ||
| 47 | + <div class="panel-header"> | ||
| 48 | + <h4 class="panel-title">OEE时序详情</h4> | ||
| 49 | + <div class="panel-actions"> | ||
| 50 | + <el-select v-model="durationFilter" placeholder="请选择" size="small" style="width: 140px;"> | ||
| 51 | + <el-option v-for="n in 60" :key="n" :label="`>${n}分`" :value="n"/> | ||
| 52 | + </el-select> | ||
| 53 | + </div> | ||
| 54 | + </div> | ||
| 55 | + <el-table :data="filteredTableData" size="small" stripe max-height="400" style="font-size: 12px;"> | ||
| 56 | + <el-table-column prop="startTime" label="开始时间" width="170" /> | ||
| 57 | + <el-table-column prop="status" label="状态" width="80"> | ||
| 58 | + <template #default="{ row }"> | ||
| 59 | + <el-tag :type="statusTagType(row.statusName)" size="small">{{ row.statusName }}</el-tag> | ||
| 60 | + </template> | ||
| 61 | + </el-table-column> | ||
| 62 | + <el-table-column prop="durationText" label="运行时长" sortable width="100" /> | ||
| 63 | + <el-table-column prop="reason" label="原因" /> | ||
| 64 | + <el-table-column prop="operator" label="操作人" width="120" /> | ||
| 65 | + </el-table> | ||
| 66 | + </div> | ||
| 67 | + | ||
| 68 | + <!-- 右侧:双图表(左右布局) --> | ||
| 69 | + <div class="chart-panel"> | ||
| 70 | + <!-- 当日时长分布 — 实心饼图 --> | ||
| 71 | + <div class="sub-chart"> | ||
| 72 | + <h4 class="panel-title">当日时长分布</h4> | ||
| 73 | + <div class="legend-list"> | ||
| 74 | + <div class="legend-item"><span class="dot green"></span>绿灯</div> | ||
| 75 | + <div class="legend-item"><span class="dot red"></span>红灯</div> | ||
| 76 | + <div class="legend-item"><span class="dot yellow"></span>黄灯</div> | ||
| 77 | + </div> | ||
| 78 | + <div class="pie-canvas-wrap"> | ||
| 79 | + <canvas ref="pieCanvasRef" width="320" height="320" class="pie-canvas" | ||
| 80 | + @mousemove="onPieMouseMove" @mouseleave="pieHoverItem = null"></canvas> | ||
| 81 | + <!-- 饼图悬浮提示框 --> | ||
| 82 | + <div v-if="pieHoverItem" class="pie-tooltip" :style="{ left: pieTooltipPos.x + 'px', top: pieTooltipPos.y + 'px' }"> | ||
| 83 | + <div class="pie-tip-title">时长详情</div> | ||
| 84 | + <div class="pie-tip-row"><span class="dot" :style="{ background: pieHoverItem.color }"></span>{{ pieHoverItem.key }}:{{ formatDuration(pieHoverItem.sec) }}</div> | ||
| 85 | + </div> | ||
| 86 | + </div> | ||
| 87 | + </div> | ||
| 88 | + <!-- 异常原因分布 — 环形图 --> | ||
| 89 | + <div class="sub-chart"> | ||
| 90 | + <h4 class="panel-title">异常原因分布</h4> | ||
| 91 | + <div class="legend-list"> | ||
| 92 | + <div class="legend-item"><span class="dot" style="background:#f56c6c"></span>红灯未知原因</div> | ||
| 93 | + <div class="legend-item"><span class="dot" style="background:#f5a623"></span>黄灯未知原因</div> | ||
| 94 | + </div> | ||
| 95 | + <div class="pie-canvas-wrap"> | ||
| 96 | + <canvas ref="reasonCanvasRef" width="320" height="320" class="pie-canvas" | ||
| 97 | + @mousemove="onReasonMouseMove" @mouseleave="reasonHoverItem = null"></canvas> | ||
| 98 | + <!-- 异常原因 hover 提示框 --> | ||
| 99 | + <div v-if="reasonHoverItem" class="pie-tooltip" :style="{ left: reasonTooltipPos.x + 'px', top: reasonTooltipPos.y + 'px' }"> | ||
| 100 | + <div class="pie-tip-title">时长占比</div> | ||
| 101 | + <div v-for="(row, i) in reasonHoverItem.rows" :key="i" class="pie-tip-row">{{ row.key }}总时长 {{ formatDuration(row.sec) }}</div> | ||
| 102 | + <div class="pie-tip-row">时长占比 {{ reasonHoverItem.pct }}%</div> | ||
| 103 | + </div> | ||
| 104 | + </div> | ||
| 105 | + </div> | ||
| 106 | + </div> | ||
| 107 | + </div> | ||
| 108 | + </div> | ||
| 109 | + </el-dialog> | ||
| 110 | +</template> | ||
| 111 | + | ||
| 112 | +<script setup> | ||
| 113 | +import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue' | ||
| 114 | + | ||
| 115 | +const props = defineProps({ | ||
| 116 | + visible: Boolean, | ||
| 117 | + device: Object | ||
| 118 | +}) | ||
| 119 | +const emit = defineEmits(['update:visible']) | ||
| 120 | + | ||
| 121 | +const queryType = ref('day') | ||
| 122 | +const queryDate = ref(new Date()) | ||
| 123 | +const durationFilter = ref(null) | ||
| 124 | + | ||
| 125 | +// 状态映射 | ||
| 126 | +const stateColorMap = { 0: '#909399', 1: '#c0392b', 2: '#e67e22', 3: '#67c23a', 4: '#2463aa' } | ||
| 127 | +const stateNameMap = { '0': '灭灯', '1': '红灯', '2': '黄灯', '3': '绿灯', '4': '蓝灯' } | ||
| 128 | + | ||
| 129 | +// 数据 | ||
| 130 | +const lampData = ref([]) | ||
| 131 | +const stats = reactive({ off: '0秒', red: '0秒', yellow: '0秒', green: '0秒', blue: '0秒' }) | ||
| 132 | + | ||
| 133 | +// 标题 | ||
| 134 | +const dialogTitle = computed(() => { | ||
| 135 | + const name = props.device?.name || props.device?._raw?.deviceName || '设备' | ||
| 136 | + return `${name} OEE时序` | ||
| 137 | +}) | ||
| 138 | + | ||
| 139 | +// 格式化日期 YYYY-MM-DD | ||
| 140 | +function formatDate(d) { | ||
| 141 | + if (!d) return new Date().toISOString().slice(0, 10) | ||
| 142 | + const dt = new Date(d) | ||
| 143 | + const y = dt.getFullYear() | ||
| 144 | + const m = String(dt.getMonth() + 1).padStart(2, '0') | ||
| 145 | + const dd = String(dt.getDate()).padStart(2, '0') | ||
| 146 | + return `${y}-${m}-${dd}` | ||
| 147 | +} | ||
| 148 | + | ||
| 149 | +// 秒数 → X分Y秒 / X时X分X秒 | ||
| 150 | +function formatDuration(seconds) { | ||
| 151 | + if (!seconds || seconds <= 0) return '0秒' | ||
| 152 | + seconds = Number(seconds) | ||
| 153 | + const h = Math.floor(seconds / 3600) | ||
| 154 | + const m = Math.floor((seconds % 3600) / 60) | ||
| 155 | + const s = Math.round(seconds % 60) | ||
| 156 | + if (h > 0) return `${h}时${m}分${s}秒` | ||
| 157 | + if (m > 0) return `${m}分${s}秒` | ||
| 158 | + return `${s}秒` | ||
| 159 | +} | ||
| 160 | + | ||
| 161 | +// 解析时长字符串为秒数(用于筛选) | ||
| 162 | +function parseDurationToSec(str) { | ||
| 163 | + if (!str) return 0 | ||
| 164 | + let total = 0 | ||
| 165 | + const hMatch = str.match(/(\d+)时/) | ||
| 166 | + const mMatch = str.match(/(\d+)分/) | ||
| 167 | + const sMatch = str.match(/(\d+)秒/) | ||
| 168 | + if (hMatch) total += parseInt(hMatch[1]) * 3600 | ||
| 169 | + if (mMatch) total += parseInt(mMatch[1]) * 60 | ||
| 170 | + if (sMatch) total += parseInt(sMatch[1]) | ||
| 171 | + return total | ||
| 172 | +} | ||
| 173 | + | ||
| 174 | +// 表格数据 | ||
| 175 | +const tableData = computed(() => { | ||
| 176 | + return lampData.value.map(item => ({ | ||
| 177 | + startTime: item.startTime, | ||
| 178 | + status: item.lampState, | ||
| 179 | + statusName: stateNameMap[String(item.lampState)] || '未知', | ||
| 180 | + duration: item.duration, | ||
| 181 | + durationText: formatDuration(item.duration), | ||
| 182 | + reason: '无', | ||
| 183 | + operator: '设备上传', | ||
| 184 | + })) | ||
| 185 | +}) | ||
| 186 | + | ||
| 187 | +// 时长筛选后的表格数据 | ||
| 188 | +const filteredTableData = computed(() => { | ||
| 189 | + if (!durationFilter.value) return tableData.value | ||
| 190 | + const threshold = durationFilter.value * 60 // 分 → 秒 | ||
| 191 | + return tableData.value.filter(row => parseDurationToSec(row.durationText) >= threshold) | ||
| 192 | +}) | ||
| 193 | + | ||
| 194 | +// ========== 时序图 Canvas 绘制 ========== | ||
| 195 | +const CANVAS_H = 100 | ||
| 196 | +const timelineRef = ref(null) | ||
| 197 | +const canvasRef = ref(null) | ||
| 198 | +const zoomLevel = ref(1) | ||
| 199 | +const minZoom = 0.001 | ||
| 200 | +const maxZoom = 1 | ||
| 201 | +const hoverSeg = ref(null) | ||
| 202 | +const tooltipPos = reactive({ x: 0, y: 0 }) | ||
| 203 | +const containerW = ref(900) // 容器 CSS 宽度 | ||
| 204 | +const canvasW = ref(900) // canvas 像素宽(=容器宽,1:1映射) | ||
| 205 | +const canvasH = ref(CANVAS_H) // canvas 像素高 | ||
| 206 | +const viewOffsetX = ref(0) // 视口偏移(数据空间) | ||
| 207 | + | ||
| 208 | +// 格式化时间字符串 (用于tooltip) | ||
| 209 | +function formatTimeStr(isoStr) { | ||
| 210 | + if (!isoStr) return '' | ||
| 211 | + const d = new Date(isoStr) | ||
| 212 | + const m = String(d.getMonth() + 1).padStart(2, '0') | ||
| 213 | + const dd = String(d.getDate()).padStart(2, '0') | ||
| 214 | + const h = String(d.getHours()).padStart(2, '0') | ||
| 215 | + const min = String(d.getMinutes()).padStart(2, '0') | ||
| 216 | + const s = String(d.getSeconds()).padStart(2, '0') | ||
| 217 | + return `${m}/${dd} ${h}:${min}:${s}` | ||
| 218 | +} | ||
| 219 | + | ||
| 220 | +// 计算结束时间 | ||
| 221 | +function calcEndTime(startTime, durationSec) { | ||
| 222 | + if (!startTime) return '' | ||
| 223 | + const d = new Date(startTime) | ||
| 224 | + d.setSeconds(d.getSeconds() + (durationSec || 0)) | ||
| 225 | + return formatTimeStr(d.toISOString()) | ||
| 226 | +} | ||
| 227 | + | ||
| 228 | +function onWheel(e) { | ||
| 229 | + const container = timelineRef.value | ||
| 230 | + if (!container) return | ||
| 231 | + const rect = container.getBoundingClientRect() | ||
| 232 | + const mx = e.clientX - rect.left | ||
| 233 | + | ||
| 234 | + const mouseDataX = mx * zoomLevel.value + viewOffsetX.value | ||
| 235 | + // 向上滚动(deltaY < 0)缩小,向下(deltaY > 0)放大 | ||
| 236 | + const delta = e.deltaY < 0 ? 0.8 : 1.25 | ||
| 237 | + const nextZ = Math.max(minZoom, Math.min(maxZoom, zoomLevel.value * delta)) | ||
| 238 | + viewOffsetX.value = mouseDataX - mx * nextZ | ||
| 239 | + zoomLevel.value = nextZ | ||
| 240 | +} | ||
| 241 | + | ||
| 242 | +// 数据空间:24h → plotW 像素(zoom=1时),左侧留 offset 像素放标签 | ||
| 243 | +const rawSegments = computed(() => { | ||
| 244 | + if (!lampData.value.length) return [] | ||
| 245 | + const dayStr = formatDate(queryDate.value) | ||
| 246 | + const [y, m, d] = dayStr.split('-').map(Number) | ||
| 247 | + const dayStartMs = new Date(y, m - 1, d).getTime() | ||
| 248 | + const plotLeft = 55 | ||
| 249 | + const plotW = canvasW.value - plotLeft - 10 | ||
| 250 | + | ||
| 251 | + return lampData.value.map(item => { | ||
| 252 | + const startMs = new Date(item.startTime).getTime() | ||
| 253 | + const durSec = item.duration || 0 | ||
| 254 | + const endMs = startMs + durSec * 1000 | ||
| 255 | + | ||
| 256 | + const startX = ((startMs - dayStartMs) / 86400000) * plotW + plotLeft | ||
| 257 | + const endX = ((endMs - dayStartMs) / 86400000) * plotW + plotLeft | ||
| 258 | + const x = Math.max(plotLeft, startX) | ||
| 259 | + const w = Math.max(1, endX - x) | ||
| 260 | + | ||
| 261 | + return { x, w, | ||
| 262 | + state: String(item.lampState), | ||
| 263 | + startTime: item.startTime, | ||
| 264 | + duration: item.duration, | ||
| 265 | + startTimeText: formatTimeStr(item.startTime), | ||
| 266 | + endTimeText: calcEndTime(item.startTime, item.duration), | ||
| 267 | + } | ||
| 268 | + }) | ||
| 269 | +}) | ||
| 270 | + | ||
| 271 | +// Canvas 绘制 —— 数据坐标 → 屏幕坐标: screenX = (dataX - viewOffsetX) / zoomLevel | ||
| 272 | +function drawTimeline() { | ||
| 273 | + const canvas = canvasRef.value | ||
| 274 | + if (!canvas) return | ||
| 275 | + const ctx = canvas.getContext('2d') | ||
| 276 | + const W = canvasW.value | ||
| 277 | + const H = canvasH.value | ||
| 278 | + const z = zoomLevel.value | ||
| 279 | + const vo = viewOffsetX.value | ||
| 280 | + | ||
| 281 | + // 布局参数(参考目标样式:时刻左上、条带居中、时间轴底部) | ||
| 282 | + const labelLeft = 50 // "时刻" 标签 X 偏移 | ||
| 283 | + const plotLeft = 55 // 数据区起点 X | ||
| 284 | + const plotW = W - plotLeft - 10 // 数据区宽度 | ||
| 285 | + const barY = 24 // 条带顶部 Y | ||
| 286 | + const barH = 46 // 条带高度 | ||
| 287 | + const axisY = barY + barH + 8 // 分隔线 Y | ||
| 288 | + const timeLabelY = H - 4 // 时间标签 Y | ||
| 289 | + | ||
| 290 | + ctx.clearRect(0, 0, W, H) | ||
| 291 | + | ||
| 292 | + // "时刻" 标签 | ||
| 293 | + ctx.fillStyle = '#999' | ||
| 294 | + ctx.font = '12px sans-serif' | ||
| 295 | + ctx.fillText('时刻', labelLeft - vo / z, 16) | ||
| 296 | + | ||
| 297 | + // 时间轴标签 — 整数刻度对齐 | ||
| 298 | + const totalSec = 24 * 3600 | ||
| 299 | + // 视口可见时间跨度(秒) | ||
| 300 | + const visSpanSec = Math.max(60, (W * z / plotW) * totalSec) | ||
| 301 | + // 根据可见跨度选漂亮步长:确保屏幕上约显示 6~10 个标签 | ||
| 302 | + let stepSec = 14400 // 默认 4 小时 | ||
| 303 | + if (visSpanSec <= 120) stepSec = 30 // <2分钟 → 每30秒 | ||
| 304 | + else if (visSpanSec <= 300) stepSec = 60 // <5分钟 → 每分钟 | ||
| 305 | + else if (visSpanSec <= 600) stepSec = 300 // <10分钟 → 每5分钟 | ||
| 306 | + else if (visSpanSec <= 1800) stepSec = 600 // <30分钟 → 每10分钟 | ||
| 307 | + else if (visSpanSec <= 3600) stepSec = 900 // <1小时 → 每15分钟 | ||
| 308 | + else if (visSpanSec <= 7200) stepSec = 1800// <2小时 → 每30分钟 | ||
| 309 | + else if (visSpanSec <= 28800) stepSec = 3600 // <8小时 → 每小时 | ||
| 310 | + else stepSec = 7200 // >=8小时 → 每2小时 | ||
| 311 | + | ||
| 312 | + ctx.font = '11px sans-serif' | ||
| 313 | + ctx.fillStyle = '#666' | ||
| 314 | + | ||
| 315 | + // 视口左边界对应的时间(秒) | ||
| 316 | + const leftSec = ((vo - plotLeft) / plotW) * totalSec | ||
| 317 | + // 对齐到 stepSec 边界(向下取整),找第一个标签 | ||
| 318 | + let firstSec = Math.floor(leftSec / stepSec) * stepSec | ||
| 319 | + if (firstSec < 0) firstSec = 0 | ||
| 320 | + | ||
| 321 | + // 整数循环,只画整数刻度 | ||
| 322 | + for (let s = firstSec; s <= totalSec + stepSec * 2; s += stepSec) { | ||
| 323 | + const dataPx = plotLeft + (s / totalSec) * plotW | ||
| 324 | + const screenPx = (dataPx - vo) / z | ||
| 325 | + if (screenPx < -50 || screenPx > W + 50) continue | ||
| 326 | + | ||
| 327 | + const h = Math.floor(s / 3600) | ||
| 328 | + const mm = Math.floor((s % 3600) / 60) | ||
| 329 | + const ss = s % 60 | ||
| 330 | + | ||
| 331 | + if (stepSec < 60) { | ||
| 332 | + ctx.fillText(`${String(h).padStart(2,'0')}:${String(mm).padStart(2,'0')}:${String(ss).padStart(2,'0')}`, screenPx, timeLabelY) | ||
| 333 | + } else { | ||
| 334 | + ctx.fillText(`${String(h).padStart(2,'0')}:${String(mm).padStart(2,'0')}`, screenPx, timeLabelY) | ||
| 335 | + } | ||
| 336 | + } | ||
| 337 | + | ||
| 338 | + // 分隔线(条带与时间轴之间) | ||
| 339 | + ctx.strokeStyle = '#ddd' | ||
| 340 | + ctx.lineWidth = 1 | ||
| 341 | + ctx.beginPath() | ||
| 342 | + ctx.moveTo((plotLeft - vo) / z, axisY) | ||
| 343 | + ctx.lineTo(((plotLeft + plotW) - vo) / z, axisY) | ||
| 344 | + ctx.stroke() | ||
| 345 | + | ||
| 346 | + // 条带 | ||
| 347 | + const hoveredIdx = hoverSeg.value ? rawSegments.value.indexOf(hoverSeg.value) : -1 | ||
| 348 | + | ||
| 349 | + rawSegments.value.forEach((seg, idx) => { | ||
| 350 | + const sx = (seg.x - vo) / z | ||
| 351 | + const sw = seg.w / z | ||
| 352 | + if (sx + sw < -1 || sx > W + 1) return | ||
| 353 | + | ||
| 354 | + ctx.fillStyle = stateColorMap[seg.state] || '#909399' | ||
| 355 | + ctx.globalAlpha = idx === hoveredIdx ? 0.7 : 1 | ||
| 356 | + | ||
| 357 | + // 纯矩形,无圆角 | ||
| 358 | + ctx.fillRect(sx, barY, sw, barH) | ||
| 359 | + }) | ||
| 360 | + | ||
| 361 | + ctx.globalAlpha = 1 | ||
| 362 | +} | ||
| 363 | + | ||
| 364 | +// 鼠标 hit test | ||
| 365 | +function onCanvasMouseMove(e) { | ||
| 366 | + const container = timelineRef.value | ||
| 367 | + if (!container) return | ||
| 368 | + const rect = container.getBoundingClientRect() | ||
| 369 | + const mx = e.clientX - rect.left | ||
| 370 | + const my = e.clientY - rect.top | ||
| 371 | + const z = zoomLevel.value | ||
| 372 | + const vo = viewOffsetX.value | ||
| 373 | + const dataX = mx * z + vo | ||
| 374 | + | ||
| 375 | + // 与 drawTimeline 中一致的条带位置 | ||
| 376 | + const barY = 24 | ||
| 377 | + const barH = 46 | ||
| 378 | + let found = null | ||
| 379 | + | ||
| 380 | + for (let i = rawSegments.value.length - 1; i >= 0; i--) { | ||
| 381 | + const seg = rawSegments.value[i] | ||
| 382 | + if (dataX >= seg.x && dataX <= seg.x + seg.w && my >= barY && my <= barY + barH) { | ||
| 383 | + found = seg | ||
| 384 | + break | ||
| 385 | + } | ||
| 386 | + } | ||
| 387 | + | ||
| 388 | + hoverSeg.value = found | ||
| 389 | + if (found) { | ||
| 390 | + tooltipPos.x = Math.min(Math.max(mx + 12, 12), rect.width - 190) | ||
| 391 | + tooltipPos.y = Math.max(my - 80, 6) | ||
| 392 | + } | ||
| 393 | +} | ||
| 394 | + | ||
| 395 | +// ========== 监听容器尺寸变化 ========== | ||
| 396 | +function updateCanvasSize() { | ||
| 397 | + if (!timelineRef.value) return | ||
| 398 | + // 减去 padding (左右各 6px ≈ 12px) | ||
| 399 | + containerW.value = timelineRef.value.clientWidth - 12 | ||
| 400 | + canvasW.value = containerW.value | ||
| 401 | + canvasH.value = CANVAS_H | ||
| 402 | +} | ||
| 403 | + | ||
| 404 | +// ========== 饼图 Canvas ========== | ||
| 405 | +const pieCanvasRef = ref(null) | ||
| 406 | +const reasonCanvasRef = ref(null) | ||
| 407 | +const pieHoverItem = ref(null) | ||
| 408 | +const pieTooltipPos = reactive({ x: 0, y: 0 }) | ||
| 409 | +const reasonHoverItem = ref(null) | ||
| 410 | +const reasonTooltipPos = reactive({ x: 0, y: 0 }) | ||
| 411 | +// 存储饼图扇区角度范围用于 hover 检测 | ||
| 412 | +let pieAngleRanges = [] | ||
| 413 | +let reasonAngleRanges = [] | ||
| 414 | + | ||
| 415 | +// 当日时长分布 — 各状态秒数 | ||
| 416 | +const stateSeconds = computed(() => ({ | ||
| 417 | + green: parseDurationToSec(stats.green), | ||
| 418 | + red: parseDurationToSec(stats.red), | ||
| 419 | + yellow: parseDurationToSec(stats.yellow) | ||
| 420 | +})) | ||
| 421 | + | ||
| 422 | +const totalSeconds = computed(() => { | ||
| 423 | + return stateSeconds.value.green + stateSeconds.value.red + stateSeconds.value.yellow || 1 | ||
| 424 | +}) | ||
| 425 | + | ||
| 426 | +// 异常原因分布 — 只用 lampDurationStats 中红灯 + 黄灯的数据 | ||
| 427 | +const reasonStats = computed(() => { | ||
| 428 | + const redSec = parseDurationToSec(stats.red) | ||
| 429 | + const yellowSec = parseDurationToSec(stats.yellow) | ||
| 430 | + const items = [] | ||
| 431 | + if (redSec > 0) items.push({ key: '红灯', sec: redSec, color: '#f56c6c' }) | ||
| 432 | + if (yellowSec > 0) items.push({ key: '黄灯', sec: yellowSec, color: '#f5a623' }) | ||
| 433 | + return items | ||
| 434 | +}) | ||
| 435 | + | ||
| 436 | +function drawPieChart(canvasRefKey, items, options = {}) { | ||
| 437 | + const canvas = canvasRefKey?.value | ||
| 438 | + if (!canvas) return | ||
| 439 | + const ctx = canvas.getContext('2d') | ||
| 440 | + const W = canvas.width, H = canvas.height | ||
| 441 | + const cx = W / 2, cy = H / 2 | ||
| 442 | + const r = Math.min(W, H) / 2 - 8 | ||
| 443 | + const solid = !!options.solid // 实心饼图(无内圆) | ||
| 444 | + const innerR = solid ? 0 : r * 0.55 | ||
| 445 | + | ||
| 446 | + ctx.clearRect(0, 0, W, H) | ||
| 447 | + | ||
| 448 | + if (!items || items.length === 0) { | ||
| 449 | + ctx.beginPath() | ||
| 450 | + ctx.arc(cx, cy, r, 0, Math.PI * 2) | ||
| 451 | + if (!solid) { ctx.arc(cx, cy, innerR, 0, Math.PI * 2, true) } | ||
| 452 | + ctx.fillStyle = '#eee' | ||
| 453 | + ctx.fill() | ||
| 454 | + ctx.fillStyle = '#999' | ||
| 455 | + ctx.font = '12px sans-serif' | ||
| 456 | + ctx.textAlign = 'center' | ||
| 457 | + ctx.textBaseline = 'middle' | ||
| 458 | + ctx.fillText('暂无数据', cx, cy) | ||
| 459 | + // 清空角度范围 | ||
| 460 | + if (canvas === pieCanvasRef.value) pieAngleRanges = [] | ||
| 461 | + if (canvas === reasonCanvasRef.value) reasonAngleRanges = [] | ||
| 462 | + return | ||
| 463 | + } | ||
| 464 | + | ||
| 465 | + const total = items.reduce((s, i) => s + i.sec, 0) || 1 | ||
| 466 | + let startAngle = -Math.PI / 2 | ||
| 467 | + | ||
| 468 | + // 重置对应画布的角度范围 | ||
| 469 | + if (canvas === pieCanvasRef.value) pieAngleRanges = [] | ||
| 470 | + if (canvas === reasonCanvasRef.value) reasonAngleRanges = [] | ||
| 471 | + | ||
| 472 | + items.forEach(item => { | ||
| 473 | + const angle = (item.sec / total) * Math.PI * 2 | ||
| 474 | + const endAngle = startAngle + angle | ||
| 475 | + const midAngle = startAngle + angle / 2 | ||
| 476 | + | ||
| 477 | + // 扇区路径 | ||
| 478 | + ctx.beginPath() | ||
| 479 | + if (solid) { | ||
| 480 | + ctx.moveTo(cx, cy) | ||
| 481 | + ctx.arc(cx, cy, r, startAngle, endAngle) | ||
| 482 | + ctx.closePath() | ||
| 483 | + } else { | ||
| 484 | + ctx.moveTo(cx + innerR * Math.cos(startAngle), cy + innerR * Math.sin(startAngle)) | ||
| 485 | + ctx.arc(cx, cy, r, startAngle, endAngle) | ||
| 486 | + ctx.arc(cx, cy, innerR, endAngle, startAngle, true) | ||
| 487 | + ctx.closePath() | ||
| 488 | + } | ||
| 489 | + ctx.fillStyle = item.color | ||
| 490 | + ctx.fill() | ||
| 491 | + | ||
| 492 | + // 记录角度范围用于 hover 检测 | ||
| 493 | + if (canvas === pieCanvasRef.value) { | ||
| 494 | + pieAngleRanges.push({ ...item, startAngle, endAngle, cx, cy, r, innerR, total }) | ||
| 495 | + } else if (canvas === reasonCanvasRef.value) { | ||
| 496 | + reasonAngleRanges.push({ ...item, startAngle, endAngle, cx, cy, r, innerR, total }) | ||
| 497 | + } | ||
| 498 | + | ||
| 499 | + // 标签 | ||
| 500 | + if (angle > 0.25) { | ||
| 501 | + const labelR = solid ? r * 0.65 : (r + innerR) / 2 | ||
| 502 | + const lx = cx + labelR * Math.cos(midAngle) | ||
| 503 | + const ly = cy + labelR * Math.sin(midAngle) | ||
| 504 | + const pct = (item.sec / total * 100).toFixed(2) | ||
| 505 | + | ||
| 506 | + ctx.fillStyle = '#fff' | ||
| 507 | + // canvas 分辨率 320,字体需放大以匹配显示尺寸 | ||
| 508 | + ctx.font = solid ? 'bold 18px sans-serif' : 'bold 16px sans-serif' | ||
| 509 | + ctx.textAlign = 'center' | ||
| 510 | + ctx.textBaseline = 'middle' | ||
| 511 | + ctx.fillText(`${pct}%`, lx, ly - (solid ? 11 : 8)) | ||
| 512 | + | ||
| 513 | + ctx.font = solid ? '14px sans-serif' : '12px sans-serif' | ||
| 514 | + if (solid) { | ||
| 515 | + const h = Math.floor(item.sec / 3600) | ||
| 516 | + const m = Math.floor((item.sec % 3600) / 60) | ||
| 517 | + let durText = '' | ||
| 518 | + if (h > 0) durText = `${item.key}:${h}.${String(m).padStart(2,'0')}时` | ||
| 519 | + else if (m > 0) durText = `${item.key}:${m}分` | ||
| 520 | + else durText = `${item.key}:${Math.round(item.sec % 60)}秒` | ||
| 521 | + ctx.fillText(durText, lx, ly + 11) | ||
| 522 | + } else { | ||
| 523 | + ctx.fillText(item.key, lx, ly + (solid ? 12 : 10)) | ||
| 524 | + } | ||
| 525 | + } | ||
| 526 | + | ||
| 527 | + startAngle = endAngle | ||
| 528 | + }) | ||
| 529 | +} | ||
| 530 | + | ||
| 531 | +// 饼图 hover 检测 | ||
| 532 | +function onPieMouseMove(e) { | ||
| 533 | + const canvas = pieCanvasRef.value | ||
| 534 | + if (!canvas) return | ||
| 535 | + const rect = canvas.getBoundingClientRect() | ||
| 536 | + const mx = e.clientX - rect.left | ||
| 537 | + const my = e.clientY - rect.top | ||
| 538 | + // 转换为 canvas 内部坐标(考虑 CSS 缩放) | ||
| 539 | + const scaleX = canvas.width / rect.width | ||
| 540 | + const scaleY = canvas.height / rect.height | ||
| 541 | + const px = mx * scaleX | ||
| 542 | + const py = my * scaleY | ||
| 543 | + | ||
| 544 | + let found = null | ||
| 545 | + for (let i = pieAngleRanges.length - 1; i >= 0; i--) { | ||
| 546 | + const seg = pieAngleRanges[i] | ||
| 547 | + const dx = px - seg.cx | ||
| 548 | + const dy = py - seg.cy | ||
| 549 | + const dist = Math.sqrt(dx * dx + dy * dy) | ||
| 550 | + | ||
| 551 | + // 判断是否在扇区内(实心:dist <= r;环形:innerR < dist <= r) | ||
| 552 | + const inRadius = dist <= seg.r && dist >= seg.innerR | ||
| 553 | + if (!inRadius) continue | ||
| 554 | + | ||
| 555 | + // 计算鼠标角度,与扇区角度比较 | ||
| 556 | + let mouseAngle = Math.atan2(dy, dx) | ||
| 557 | + // 归一化角度到 [-PI, PI],然后统一到 startAngle~endAngle 的范围 | ||
| 558 | + // 简化处理:直接比较,考虑跨 -PI 边界的情况 | ||
| 559 | + let a = mouseAngle | ||
| 560 | + let sa = seg.startAngle | ||
| 561 | + let ea = seg.endAngle | ||
| 562 | + | ||
| 563 | + // 处理跨边界情况 | ||
| 564 | + function inRange(a, sa, ea) { | ||
| 565 | + // 标准化为 [0, 2PI) | ||
| 566 | + const norm = v => { let x = v % (Math.PI * 2); if (x < 0) x += Math.PI * 2; return x } | ||
| 567 | + const na = norm(a), nsa = norm(sa), nea = norm(ea) | ||
| 568 | + if (nea >= nsa) return na >= nsa && na <= nea | ||
| 569 | + else return na >= nsa || na <= nea | ||
| 570 | + } | ||
| 571 | + | ||
| 572 | + if (inRange(a, sa, ea)) { | ||
| 573 | + found = seg | ||
| 574 | + break | ||
| 575 | + } | ||
| 576 | + } | ||
| 577 | + | ||
| 578 | + pieHoverItem.value = found | ||
| 579 | + if (found) { | ||
| 580 | + pieTooltipPos.x = Math.min(Math.max(mx + 12, 12), rect.width - 160) | ||
| 581 | + pieTooltipPos.y = Math.max(my - 60, 6) | ||
| 582 | + } | ||
| 583 | +} | ||
| 584 | + | ||
| 585 | +// 异常原因分布 hover 检测 — tooltip 显示"时长占比"格式 | ||
| 586 | +function onReasonMouseMove(e) { | ||
| 587 | + const canvas = reasonCanvasRef.value | ||
| 588 | + if (!canvas) return | ||
| 589 | + const rect = canvas.getBoundingClientRect() | ||
| 590 | + const mx = e.clientX - rect.left | ||
| 591 | + const my = e.clientY - rect.top | ||
| 592 | + const scaleX = canvas.width / rect.width | ||
| 593 | + const scaleY = canvas.height / rect.height | ||
| 594 | + const px = mx * scaleX | ||
| 595 | + const py = my * scaleY | ||
| 596 | + | ||
| 597 | + let found = null | ||
| 598 | + for (let i = reasonAngleRanges.length - 1; i >= 0; i--) { | ||
| 599 | + const seg = reasonAngleRanges[i] | ||
| 600 | + const dx = px - seg.cx | ||
| 601 | + const dy = py - seg.cy | ||
| 602 | + const dist = Math.sqrt(dx * dx + dy * dy) | ||
| 603 | + const inRadius = dist <= seg.r && dist >= seg.innerR | ||
| 604 | + if (!inRadius) continue | ||
| 605 | + | ||
| 606 | + const mouseAngle = Math.atan2(dy, dx) | ||
| 607 | + function inRange(a, sa, ea) { | ||
| 608 | + const norm = v => { let x = v % (Math.PI * 2); if (x < 0) x += Math.PI * 2; return x } | ||
| 609 | + const na = norm(a), nsa = norm(sa), nea = norm(ea) | ||
| 610 | + if (nea >= nsa) return na >= nsa && na <= nea | ||
| 611 | + else return na >= nsa || na <= nea | ||
| 612 | + } | ||
| 613 | + | ||
| 614 | + if (inRange(mouseAngle, seg.startAngle, seg.endAngle)) { | ||
| 615 | + found = seg | ||
| 616 | + break | ||
| 617 | + } | ||
| 618 | + } | ||
| 619 | + | ||
| 620 | + // 构造 tooltip 数据:显示所有项 + 总占比 | ||
| 621 | + if (found || reasonAngleRanges.length > 0) { | ||
| 622 | + const total = reasonAngleRanges.reduce((s, r) => s + r.sec, 0) || 1 | ||
| 623 | + const pct = (found ? found.sec / total : total > 0 ? 1 : 0) * 100 | ||
| 624 | + reasonHoverItem.value = found ? { | ||
| 625 | + rows: [{ key: found.key, sec: found.sec }], | ||
| 626 | + pct: pct.toFixed(2) | ||
| 627 | + } : { | ||
| 628 | + rows: reasonAngleRanges.map(r => ({ key: r.key, sec: r.sec })), | ||
| 629 | + pct: '100.00' | ||
| 630 | + } | ||
| 631 | + if (!found) { | ||
| 632 | + // 悬浮在空白区域显示所有数据 | ||
| 633 | + reasonHoverItem.value = { | ||
| 634 | + rows: reasonAngleRanges.map(r => ({ key: r.key, sec: r.sec })), | ||
| 635 | + pct: '100.00' | ||
| 636 | + } | ||
| 637 | + } | ||
| 638 | + } else { | ||
| 639 | + reasonHoverItem.value = null | ||
| 640 | + } | ||
| 641 | + | ||
| 642 | + if (reasonHoverItem.value) { | ||
| 643 | + reasonTooltipPos.x = Math.min(Math.max(mx + 12, 12), rect.width - 180) | ||
| 644 | + reasonTooltipPos.y = Math.max(my - 70, 6) | ||
| 645 | + } | ||
| 646 | +} | ||
| 647 | + | ||
| 648 | +// 绘制当日时长分布饼图(实心) | ||
| 649 | +function drawDurationPie() { | ||
| 650 | + const { green, red, yellow } = stateSeconds.value | ||
| 651 | + const items = [] | ||
| 652 | + if (green > 0) items.push({ key: '绿灯', sec: green, color: '#67c23a' }) | ||
| 653 | + if (red > 0) items.push({ key: '红灯', sec: red, color: '#f56c6c' }) | ||
| 654 | + if (yellow > 0) items.push({ key: '黄灯', sec: yellow, color: '#f5a623' }) | ||
| 655 | + drawPieChart(pieCanvasRef, items, { solid: true }) | ||
| 656 | +} | ||
| 657 | + | ||
| 658 | +// 绘制异常原因分布饼图(环形) | ||
| 659 | +function drawReasonPie() { | ||
| 660 | + drawPieChart(reasonCanvasRef, reasonStats.value) | ||
| 661 | +} | ||
| 662 | + | ||
| 663 | +// ========== 接口调用 ========== | ||
| 664 | +async function fetchLampData() { | ||
| 665 | + const dtuSn = props.device?._raw?.dtuSn || '' | ||
| 666 | + const date = formatDate(queryDate.value) | ||
| 667 | + if (!dtuSn) return | ||
| 668 | + try { | ||
| 669 | + const res = await fetch(`/api/device/lampData?dtuSn=${dtuSn}&date=${date}`) | ||
| 670 | + const data = await res.json() | ||
| 671 | + // list 中只有一个元素,取其 lampData | ||
| 672 | + const entry = (data.list && data.list[0]) || {} | ||
| 673 | + lampData.value = (entry.lampData || []).sort((a, b) => | ||
| 674 | + new Date(b.startTime) - new Date(a.startTime) | ||
| 675 | + ) | ||
| 676 | + // 统计 | ||
| 677 | + const s = data.lampDurationStats || {} | ||
| 678 | + stats.off = s.off || '0秒' | ||
| 679 | + stats.red = s.red || '0秒' | ||
| 680 | + stats.yellow = s.yellow || '0秒' | ||
| 681 | + stats.green = s.green || '0秒' | ||
| 682 | + stats.blue = s.blue || '0秒' | ||
| 683 | + } catch (err) { | ||
| 684 | + console.error('获取灯数据失败:', err) | ||
| 685 | + } | ||
| 686 | +} | ||
| 687 | + | ||
| 688 | +function statusTagType(status) { | ||
| 689 | + const map = { '绿灯': '', '黄灯': 'warning', '红灯': 'danger', '蓝灯': '', '灭灯': 'info' } | ||
| 690 | + return map[status] || '' | ||
| 691 | +} | ||
| 692 | + | ||
| 693 | +// 弹窗打开时自动请求 | ||
| 694 | +watch(() => props.visible, (val) => { | ||
| 695 | + if (val) { | ||
| 696 | + fetchLampData() | ||
| 697 | + // 等 Vue DOM 更新后获取正确尺寸,再等 dialog 动画结束再次确认 | ||
| 698 | + nextTick(() => { | ||
| 699 | + updateCanvasSize() | ||
| 700 | + setTimeout(updateCanvasSize, 350) | ||
| 701 | + }) | ||
| 702 | + } | ||
| 703 | +}) | ||
| 704 | + | ||
| 705 | +// 数据或视口变化时重绘 | ||
| 706 | +watch([lampData, zoomLevel, viewOffsetX, canvasW], () => { | ||
| 707 | + nextTick(drawTimeline) | ||
| 708 | +}, { deep: true }) | ||
| 709 | + | ||
| 710 | +// 饼图数据变化时重绘 | ||
| 711 | +watch([stats, lampData], () => { | ||
| 712 | + nextTick(() => { | ||
| 713 | + drawDurationPie() | ||
| 714 | + drawReasonPie() | ||
| 715 | + }) | ||
| 716 | +}, { deep: true }) | ||
| 717 | + | ||
| 718 | +onMounted(() => { | ||
| 719 | + updateCanvasSize() | ||
| 720 | + const el = timelineRef.value | ||
| 721 | + if (!el) return | ||
| 722 | + const ro = new ResizeObserver(updateCanvasSize) | ||
| 723 | + ro.observe(el) | ||
| 724 | + // 初始绘制饼图 | ||
| 725 | + nextTick(() => { drawDurationPie(); drawReasonPie() }) | ||
| 726 | +}) | ||
| 727 | +</script> | ||
| 728 | + | ||
| 729 | +<style scoped> | ||
| 730 | +.oee-dialog :deep(.el-dialog) { | ||
| 731 | + max-height: 92vh; | ||
| 732 | + display: flex; | ||
| 733 | + flex-direction: column; | ||
| 734 | +} | ||
| 735 | +.oee-dialog :deep(.el-dialog__header) { | ||
| 736 | + padding: 12px 20px; | ||
| 737 | + border-bottom: 1px solid #e8e8e8; | ||
| 738 | + margin: 0; | ||
| 739 | + flex-shrink: 0; | ||
| 740 | +} | ||
| 741 | +.oee-dialog :deep(.el-dialog__body) { | ||
| 742 | + overflow-y: auto; | ||
| 743 | + flex: 1; | ||
| 744 | +} | ||
| 745 | +.dialog-header { | ||
| 746 | + display: flex; | ||
| 747 | + align-items: center; | ||
| 748 | +} | ||
| 749 | +.header-left { | ||
| 750 | + display: flex; | ||
| 751 | + align-items: center; | ||
| 752 | + gap: 8px; | ||
| 753 | +} | ||
| 754 | +.title-text { | ||
| 755 | + font-size: 15px; | ||
| 756 | + font-weight: bold; | ||
| 757 | + color: #333; | ||
| 758 | +} | ||
| 759 | +.query-label { | ||
| 760 | + font-size: 13px; | ||
| 761 | + color: #666; | ||
| 762 | + white-space: nowrap; | ||
| 763 | +} | ||
| 764 | + | ||
| 765 | +.oee-body { padding: 0; } | ||
| 766 | +.chart-section { | ||
| 767 | + border-bottom: 1px solid #ebeef5; | ||
| 768 | + padding: 12px 16px; | ||
| 769 | +} | ||
| 770 | +.section-title { | ||
| 771 | + font-size: 14px; | ||
| 772 | + font-weight: bold; | ||
| 773 | + color: #333; | ||
| 774 | + margin: 0 0 8px 0; | ||
| 775 | +} | ||
| 776 | +.timeline-chart { | ||
| 777 | + background: #fff; | ||
| 778 | + border: 1px solid #e0e0e0; | ||
| 779 | + border-radius: 4px; | ||
| 780 | + padding: 6px; | ||
| 781 | + overflow: hidden; | ||
| 782 | + position: relative; | ||
| 783 | +} | ||
| 784 | +.timeline-chart canvas { | ||
| 785 | + display: block; | ||
| 786 | +} | ||
| 787 | + | ||
| 788 | +.hover-tooltip { | ||
| 789 | + position: absolute; | ||
| 790 | + background: rgba(32, 40, 52, 0.92); | ||
| 791 | + color: #fff; | ||
| 792 | + font-size: 12px; | ||
| 793 | + padding: 10px 14px; | ||
| 794 | + border-radius: 6px; | ||
| 795 | + pointer-events: none; | ||
| 796 | + z-index: 100; | ||
| 797 | + white-space: nowrap; | ||
| 798 | + box-shadow: 0 4px 16px rgba(0,0,0,0.25); | ||
| 799 | + line-height: 1.7; | ||
| 800 | +} | ||
| 801 | +.tip-row { white-space: nowrap; } | ||
| 802 | +.tip-label { color: #aaa; } | ||
| 803 | + | ||
| 804 | +.bottom-grid { | ||
| 805 | + display: grid; | ||
| 806 | + grid-template-columns: 1fr 1fr; | ||
| 807 | + gap: 12px; | ||
| 808 | +} | ||
| 809 | +.table-panel { | ||
| 810 | + border: 1px solid #ebeef5; | ||
| 811 | + border-radius: 4px; | ||
| 812 | + overflow: hidden; | ||
| 813 | +} | ||
| 814 | +.panel-header { | ||
| 815 | + display: flex; | ||
| 816 | + justify-content: space-between; | ||
| 817 | + align-items: center; | ||
| 818 | + padding: 12px 16px; | ||
| 819 | + border-bottom: 1px solid #ebeef5; | ||
| 820 | + background: #fafafa; | ||
| 821 | +} | ||
| 822 | +.panel-title { | ||
| 823 | + font-size: 14px; | ||
| 824 | + font-weight: bold; | ||
| 825 | + color: #333; | ||
| 826 | + margin: 0; | ||
| 827 | +} | ||
| 828 | +.panel-actions { display: flex; gap: 8px; } | ||
| 829 | + | ||
| 830 | +.chart-panel { | ||
| 831 | + border: 1px solid #ebeef5; | ||
| 832 | + border-radius: 4px; | ||
| 833 | + padding: 16px; | ||
| 834 | + background: #fafafa; | ||
| 835 | + display: flex; | ||
| 836 | + flex-direction: row; | ||
| 837 | + align-items: center; | ||
| 838 | + justify-content: center; | ||
| 839 | + gap: 56px; | ||
| 840 | +} | ||
| 841 | +.sub-chart { | ||
| 842 | + display: flex; | ||
| 843 | + flex-direction: column; | ||
| 844 | + align-items: center; | ||
| 845 | + gap: 8px; | ||
| 846 | + flex: 0 0 auto; | ||
| 847 | +} | ||
| 848 | +.sub-chart .panel-title { | ||
| 849 | + font-size: 13px; | ||
| 850 | + font-weight: bold; | ||
| 851 | + color: #333; | ||
| 852 | + margin: 0; | ||
| 853 | +} | ||
| 854 | +.legend-list { | ||
| 855 | + display: flex; | ||
| 856 | + flex-wrap: wrap; | ||
| 857 | + gap: 12px; | ||
| 858 | + justify-content: center; | ||
| 859 | + font-size: 12px; | ||
| 860 | + color: #666; | ||
| 861 | +} | ||
| 862 | +.legend-item { | ||
| 863 | + display: flex; | ||
| 864 | + align-items: center; | ||
| 865 | + gap: 4px; | ||
| 866 | +} | ||
| 867 | +.dot { | ||
| 868 | + width: 10px; | ||
| 869 | + height: 10px; | ||
| 870 | + border-radius: 50%; | ||
| 871 | +} | ||
| 872 | +.dot.blue { background: #409eff; } | ||
| 873 | +.dot.red { background: #f56c6c; } | ||
| 874 | +.dot.yellow { background: #f5a623; } | ||
| 875 | +.dot.green { background: #67c23a; } | ||
| 876 | +.dot.gray { background: #909399; } | ||
| 877 | + | ||
| 878 | +.pie-canvas-wrap { | ||
| 879 | + position: relative; | ||
| 880 | + display: flex; | ||
| 881 | + align-items: center; | ||
| 882 | + justify-content: center; | ||
| 883 | +} | ||
| 884 | +.pie-canvas { | ||
| 885 | + width: 200px; | ||
| 886 | + height: 200px; | ||
| 887 | +} | ||
| 888 | +.pie-tooltip { | ||
| 889 | + position: absolute; | ||
| 890 | + background: rgba(255, 255, 255, 0.96); | ||
| 891 | + color: #333; | ||
| 892 | + font-size: 12px; | ||
| 893 | + padding: 8px 14px; | ||
| 894 | + border-radius: 6px; | ||
| 895 | + pointer-events: none; | ||
| 896 | + z-index: 100; | ||
| 897 | + box-shadow: 0 2px 12px rgba(0,0,0,0.15); | ||
| 898 | + border: 1px solid #e4e7ed; | ||
| 899 | + white-space: nowrap; | ||
| 900 | +} | ||
| 901 | +.pie-tip-title { | ||
| 902 | + font-weight: bold; | ||
| 903 | + margin-bottom: 4px; | ||
| 904 | + color: #333; | ||
| 905 | + border-bottom: 1px solid #ebeef5; | ||
| 906 | + padding-bottom: 4px; | ||
| 907 | +} | ||
| 908 | +.pie-tip-row { | ||
| 909 | + display: flex; | ||
| 910 | + align-items: center; | ||
| 911 | + gap: 6px; | ||
| 912 | +} | ||
| 913 | +</style> |
src/components/ParamSettingDialog.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <el-dialog | ||
| 3 | + :model-value="visible" | ||
| 4 | + @update:model-value="$emit('update:visible', $event)" | ||
| 5 | + title="" | ||
| 6 | + width="calc(100vw - 40px)" | ||
| 7 | + :style="{ maxWidth: '1400px' }" | ||
| 8 | + top="3vh" | ||
| 9 | + destroy-on-close | ||
| 10 | + class="param-dialog" | ||
| 11 | + > | ||
| 12 | + <template #header> | ||
| 13 | + <div class="dialog-header"> | ||
| 14 | + <span class="title-text">{{ device?.name || '能耗设备1' }}</span> | ||
| 15 | + <div class="header-right"> | ||
| 16 | + <el-icon :size="16" style="cursor:pointer;color:#409eff;"><Refresh /></el-icon> | ||
| 17 | + <el-icon :size="16" style="cursor:pointer;color:#409eff;margin-left:6px;"><Grid /></el-icon> | ||
| 18 | + <el-icon :size="16" style="cursor:pointer;color:#409eff;margin-left:6px;"><List /></el-icon> | ||
| 19 | + <el-icon :size="16" style="cursor:pointer;color:#409eff;margin-left:6px;" @click="$emit('update:visible', false)"><Close /></el-icon> | ||
| 20 | + </div> | ||
| 21 | + </div> | ||
| 22 | + </template> | ||
| 23 | + | ||
| 24 | + <div class="param-body"> | ||
| 25 | + <!-- 相位标签 --> | ||
| 26 | + <div class="phase-tabs"> | ||
| 27 | + <div v-for="p in phases" :key="p" :class="['phase-tab', { active: activePhase === p }]" | ||
| 28 | + @click="activePhase = p">{{ p }}:0</div> | ||
| 29 | + </div> | ||
| 30 | + | ||
| 31 | + <!-- 主内容区:左侧数据表 + 右侧统计 --> | ||
| 32 | + <div class="main-grid"> | ||
| 33 | + <!-- 左侧:电压电流功率温度表格 + 曲线 --> | ||
| 34 | + <div class="left-panel"> | ||
| 35 | + <div class="data-table-area"> | ||
| 36 | + <table class="param-table"> | ||
| 37 | + <thead> | ||
| 38 | + <tr><th></th><th>电压</th><th>电流</th><th>功率</th><th>温度</th></tr> | ||
| 39 | + </thead> | ||
| 40 | + <tbody> | ||
| 41 | + <tr v-for="row in tableRows" :key="row.label"> | ||
| 42 | + <td class="row-label">{{ row.label }}</td> | ||
| 43 | + <td>{{ row.voltage }}</td> | ||
| 44 | + <td>{{ row.current }}</td> | ||
| 45 | + <td>{{ row.power }}</td> | ||
| 46 | + <td>{{ row.temp }}</td> | ||
| 47 | + </tr> | ||
| 48 | + </tbody> | ||
| 49 | + </table> | ||
| 50 | + <!-- 右侧额外信息 --> | ||
| 51 | + <div class="extra-info"> | ||
| 52 | + <div v-for="(info, i) in extraInfos" :key="i" class="ei-item">{{ info }}:0</div> | ||
| 53 | + </div> | ||
| 54 | + </div> | ||
| 55 | + | ||
| 56 | + <!-- 电流均值/峰值曲线 --> | ||
| 57 | + <div class="curve-section"> | ||
| 58 | + <div class="curve-header"> | ||
| 59 | + <span class="curve-title">电流均值/峰值曲线 ▼</span> | ||
| 60 | + <el-date-picker v-model="curveDate" type="date" size="small" placeholder="2026-04-28" /> | ||
| 61 | + </div> | ||
| 62 | + <div class="curve-chart"> | ||
| 63 | + <svg viewBox="0 0 700 180"> | ||
| 64 | + <g font-size="10" fill="#999" text-anchor="end"> | ||
| 65 | + <text x="24" y="18">5</text><text x="24" y="53">4</text><text x="24" y="88">3</text> | ||
| 66 | + <text x="24" y="123">2</text><text x="24" y="158">1</text> | ||
| 67 | + </g> | ||
| 68 | + <line x1="30" y1="154" x2="680" y2="154" stroke="#ddd"/> | ||
| 69 | + <polyline points="36,154 56,154 76,154 ... 660,154" fill="none" stroke="#f5a623" stroke-width="1.2"/> | ||
| 70 | + <g font-size="8" fill="#666" text-anchor="middle"> | ||
| 71 | + <text x="46" y="168">00:00</text><text x="130" y="168">04:00</text> | ||
| 72 | + <text x="220" y="168">08:00</text><text x="310" y="168">12:00</text> | ||
| 73 | + <text x="400" y="168">16:00</text><text x="490" y="168">20:00</text><text x="580" y="168">24:00</text> | ||
| 74 | + </g> | ||
| 75 | + </svg> | ||
| 76 | + </div> | ||
| 77 | + <div class="curve-legend"> | ||
| 78 | + <span class="leg"><i class="dot y"/>电流最大值</span> | ||
| 79 | + <span class="leg"><i class="dot g"/>电流最小值</span> | ||
| 80 | + <span class="leg"><i class="dot r"/>电流瞬时平均值</span> | ||
| 81 | + </div> | ||
| 82 | + </div> | ||
| 83 | + | ||
| 84 | + <!-- 电能 & 相位角 --> | ||
| 85 | + <div class="bottom-row"> | ||
| 86 | + <div class="power-card"> | ||
| 87 | + <div class="pc-title">电能</div> | ||
| 88 | + <div class="pc-list"> | ||
| 89 | + <div v-for="(p, i) in powerItems" :key="i" :class="['pc-item', p.color]"> | ||
| 90 | + <span class="pc-dot"></span> {{ p.label }} | ||
| 91 | + <span class="pc-val">{{ p.val }}</span> | ||
| 92 | + </div> | ||
| 93 | + </div> | ||
| 94 | + </div> | ||
| 95 | + <div class="angle-card"> | ||
| 96 | + <div class="ac-title">相位角</div> | ||
| 97 | + <div class="ac-charts"> | ||
| 98 | + <div class="ac-pie-wrap"> | ||
| 99 | + <svg viewBox="0 0 120 120"><circle cx="60" cy="60" r="48" fill="#e8f4fd"/><text x="60" y="55" text-anchor="middle" font-size="11" fill="#333">电压相位角</text> | ||
| 100 | + <g font-size="10" fill="#666"><text x="45" y="72">A相 0</text><text x="75" y="72">B相 0</text><text x="60" y="86">C相 0</text></g> | ||
| 101 | + <circle cx="35" cy="90" r="3" fill="#e74c3c"/><circle cx="60" cy="93" r="3" fill="#67c23a"/><circle cx="85" cy="90" r="3" fill="#409eff"/> | ||
| 102 | + </svg> | ||
| 103 | + </div> | ||
| 104 | + <div class="ac-pie-wrap"> | ||
| 105 | + <svg viewBox="0 0 120 120"><circle cx="60" cy="60" r="48" fill="#e8f4fd"/><text x="60" y="55" text-anchor="middle" font-size="11" fill="#333">电流相位角</text> | ||
| 106 | + <g font-size="10" fill="#666"><text x="42" y="70">A相 0</text><text x="78" y="70">B相 0</text><text x="60" y="84">C相 0</text></g> | ||
| 107 | + <circle cx="32" cy="92" r="3" fill="#e74c3c"/><circle cx="58" cy="95" r="3" fill="#67c23a"/><circle cx="84" cy="92" r="3" fill="#409eff"/> | ||
| 108 | + </svg> | ||
| 109 | + </div> | ||
| 110 | + </div> | ||
| 111 | + <div class="ac-ref">参考值: A相 120 B相 120 C相 240</div> | ||
| 112 | + </div> | ||
| 113 | + </div> | ||
| 114 | + </div> | ||
| 115 | + | ||
| 116 | + <!-- 右侧:功率统计 --> | ||
| 117 | + <div class="right-panel"> | ||
| 118 | + <div class="rp-title">功率</div> | ||
| 119 | + <div class="power-stats"> | ||
| 120 | + <div v-for="(ps, i) in powerStats" :key="i" :class="['ps-item', 'ps-'+ps.color]"> | ||
| 121 | + <div class="ps-circle"> | ||
| 122 | + <svg viewBox="0 0 50 50"><circle cx="25" cy="25" r="20" fill="none" stroke="#eee" stroke-width="5"/> | ||
| 123 | + <text x="25" y="28" text-anchor="middle" font-size="13" font-weight="bold" fill="#333">{{ ps.val }}</text> | ||
| 124 | + </svg> | ||
| 125 | + </div> | ||
| 126 | + <div class="ps-info"> | ||
| 127 | + <div class="ps-bars"> | ||
| 128 | + <span :class="'ps-bar '+pb.color" v-for="(pb, j) in ps.bars" :key="j"> | ||
| 129 | + {{ pb.label }}{{ pb.val }} | ||
| 130 | + </span> | ||
| 131 | + </div> | ||
| 132 | + <div class="ps-result">视在功率 {{ ps.result }}</div> | ||
| 133 | + </div> | ||
| 134 | + </div> | ||
| 135 | + </div> | ||
| 136 | + | ||
| 137 | + <!-- 温度图表 --> | ||
| 138 | + <div class="temp-card"> | ||
| 139 | + <div class="temp-header">温度 <b>▼</b></div> | ||
| 140 | + <div class="temp-chart"> | ||
| 141 | + <svg viewBox="0 0 280 140"> | ||
| 142 | + <g font-size="9" fill="#999" text-anchor="end"> | ||
| 143 | + <text x="22" y="15">1</text><text x="22" y="43">0.8</text><text x="22" y="71">0.6</text> | ||
| 144 | + <text x="22" y="99">0.4</text><text x="22" y="127">0.2</text> | ||
| 145 | + </g> | ||
| 146 | + <line x1="28" y1="124" x2="268" y2="124" stroke="#ddd"/> | ||
| 147 | + <g font-size="7" fill="#666" text-anchor="middle"> | ||
| 148 | + <text x="38" y="136">温度TA</text><text x="68" y="136">温度TB</text> | ||
| 149 | + <text x="98" y="136">温度TC</text><text x="128" y="136">温度TN</text> | ||
| 150 | + <text x="158" y="136">温度TE</text> | ||
| 151 | + </g> | ||
| 152 | + </svg> | ||
| 153 | + </div> | ||
| 154 | + </div> | ||
| 155 | + </div> | ||
| 156 | + </div> | ||
| 157 | + </div> | ||
| 158 | + </el-dialog> | ||
| 159 | +</template> | ||
| 160 | + | ||
| 161 | +<script setup> | ||
| 162 | +import { ref } from 'vue' | ||
| 163 | +import { Refresh, Grid, List, Close } from '@element-plus/icons-vue' | ||
| 164 | + | ||
| 165 | +defineProps({ visible: Boolean, device: Object }) | ||
| 166 | +defineEmits(['update:visible']) | ||
| 167 | + | ||
| 168 | +const phases = ['UA', 'UB', 'UC', 'UAB', 'UBC', 'UAC', 'UF'] | ||
| 169 | +const activePhase = ref('UA') | ||
| 170 | +const curveDate = ref('') | ||
| 171 | + | ||
| 172 | +const tableRows = [ | ||
| 173 | + { label: '极大值', voltage: 0, current: 0, power: 0, temp: 0 }, | ||
| 174 | + { label: '平均值', voltage: 0, current: 0, power: 0, temp: 0 }, | ||
| 175 | + { label: '极小值', voltage: 0, current: 0, power: 0, temp: 0 } | ||
| 176 | +] | ||
| 177 | +const extraInfos = ['IA', 'IB', 'IC'] | ||
| 178 | +const powerItems = [ | ||
| 179 | + { label: 'A相', val: 0, color: 'o' }, | ||
| 180 | + { label: 'B相', val: 0, color: 'g' }, | ||
| 181 | + { label: 'C相', val: 0, color: 'r' }, | ||
| 182 | + { label: '合相', val: 0, color: 'b' } | ||
| 183 | +] | ||
| 184 | +const powerStats = [ | ||
| 185 | + { | ||
| 186 | + color: 'o', val: 0, | ||
| 187 | + bars: [ | ||
| 188 | + { label: '有功功率', val: 0, color: '' }, | ||
| 189 | + { label: 'A相 ', val: '', color: 'o' }, { label: '无功功率', val: 0, color: '' } | ||
| 190 | + ], | ||
| 191 | + result: 0 | ||
| 192 | + }, | ||
| 193 | + { | ||
| 194 | + color: 'g', val: 0, | ||
| 195 | + bars: [ | ||
| 196 | + { label: '有功功率', val: 0, color: '' }, | ||
| 197 | + { label: 'B相 ', val: '', color: 'g' }, { label: '无功功率', val: 0, color: '' } | ||
| 198 | + ], | ||
| 199 | + result: 0 | ||
| 200 | + }, | ||
| 201 | + { | ||
| 202 | + color: 'r', val: 0, | ||
| 203 | + bars: [ | ||
| 204 | + { label: '有功功率', val: 0, color: '' }, | ||
| 205 | + { label: 'C相 ', val: '', color: 'r' }, { label: '无功功率', val: 0, color: '' } | ||
| 206 | + ], | ||
| 207 | + result: 0 | ||
| 208 | + }, | ||
| 209 | + { | ||
| 210 | + color: 'b', val: 0, | ||
| 211 | + bars: [ | ||
| 212 | + { label: '有功功率', val: 0, color: '' }, | ||
| 213 | + { label: '合相 ', val: '', color: 'b' }, { label: '无功功率', val: 0, color: '' } | ||
| 214 | + ], | ||
| 215 | + result: 0 | ||
| 216 | + } | ||
| 217 | +] | ||
| 218 | +</script> | ||
| 219 | + | ||
| 220 | +<style scoped> | ||
| 221 | +.param-dialog :deep(.el-dialog){max-height:92vh;display:flex;flex-direction:column;} | ||
| 222 | +.param-dialog :deep(.el-dialog__header){padding:10px 20px;border-bottom:1px solid #e8e8e8;margin:0;flex-shrink:0;} | ||
| 223 | +.param-dialog :deep(.el-dialog__body){overflow-y:auto;flex:1;padding:12px;} | ||
| 224 | +.dialog-header{display:flex;align-items:center;justify-content:space-between;} | ||
| 225 | +.title-text{font-size:15px;font-weight:bold;color:#333;} | ||
| 226 | +.header-right{display:flex;align-items:center;} | ||
| 227 | + | ||
| 228 | +.phase-tabs{display:flex;gap:4px;background:#fff;border:1px solid #eee;border-radius:6px 6px 0 0;padding:8px 14px;} | ||
| 229 | +.phase-tab{padding:6px 14px;font-size:12px;cursor:pointer;color:#666;border-radius:4px;} | ||
| 230 | +.phase-tab.active{background:#409eff;color:#fff;font-weight:bold;} | ||
| 231 | + | ||
| 232 | +.main-grid{display:flex;gap:14px;background:#fff;border:1px solid #eee;border-top:none;border-radius:0 0 6px 6px;padding:14px;} | ||
| 233 | + | ||
| 234 | +.left-panel{flex:1;min-width:0;} | ||
| 235 | +.data-table-area{background:#408aff;border-radius:6px;padding:14px;display:flex;gap:16px;margin-bottom:12px;color:#fff;} | ||
| 236 | +.param-table{border-collapse:collapse;font-size:13px;} | ||
| 237 | +.param-table th{padding:6px 16px;text-align:center;font-weight:normal;opacity:0.85;border-bottom:1px solid rgba(255,255,255,0.2);} | ||
| 238 | +.param-table td{padding:6px 16px;text-align:center;} | ||
| 239 | +.row-label{text-align:left!important;font-weight:bold;} | ||
| 240 | +.extra-info{font-size:12px;opacity:0.85;line-height:2;} | ||
| 241 | + | ||
| 242 | +.curve-section{border:1px solid #eee;border-radius:6px;padding:12px;margin-bottom:12px;} | ||
| 243 | +.curve-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;} | ||
| 244 | +.curve-title{font-size:13px;font-weight:bold;color:#333;} | ||
| 245 | +.curve-legend{display:flex;gap:14px;font-size:11px;color:#666;margin-top:6px;} | ||
| 246 | +.curve-chart svg{width:100%;height:auto;} | ||
| 247 | + | ||
| 248 | +.bottom-row{display:flex;gap:12px;} | ||
| 249 | +.power-card,.angle-card{flex:1;border:1px solid #eee;border-radius:6px;padding:12px;} | ||
| 250 | +.pc-title,.ac-title{font-size:13px;font-weight:bold;color:#333;margin-bottom:8px;} | ||
| 251 | +.pc-list{display:flex;flex-direction:column;gap:6px;} | ||
| 252 | +.pc-item{display:flex;align-items:center;gap:6px;font-size:12px;color:#666;} | ||
| 253 | +.pc-dot{width:10px;height:10px;border-radius:2px;} | ||
| 254 | +.pc-dot.o{background:#f56c6c;}.pc-dot.g{background:#67c23a;} | ||
| 255 | +.pc-dot.r{background:#e74c3c;}.pc-dot.b{background:#409eff;} | ||
| 256 | +.pc-val{margin-left:auto;font-weight:bold;color:#333;} | ||
| 257 | + | ||
| 258 | +.ac-charts{display:flex;gap:12px;justify-content:center;} | ||
| 259 | +.ac-pie-wrap svg{width:110px;height:110px;} | ||
| 260 | +.ac-ref{text-align:center;font-size:11px;color:#999;margin-top:6px;} | ||
| 261 | + | ||
| 262 | +.right-panel{width:260px;flex-shrink:0;} | ||
| 263 | +.rp-title{font-size:14px;font-weight:bold;color:#333;margin-bottom:10px;} | ||
| 264 | +.power-stats{display:flex;flex-direction:column;gap:10px;margin-bottom:14px;} | ||
| 265 | +.ps-item{display:flex;align-items:center;gap:8px;} | ||
| 266 | +.ps-circle svg{width:44px;height:44px;flex-shrink:0;} | ||
| 267 | +.ps-info{flex:1;font-size:10px;color:#666;} | ||
| 268 | +.ps-bar{display:inline-block;padding:2px 4px;border-radius:2px;margin-right:4px;} | ||
| 269 | +.ps-bar.o{background:#fff3e0;color:#e65100;}.ps-bar.g{background:#e8f5e9;color:#2e7d32;} | ||
| 270 | +.ps-bar.r{background:#ffebee;color:#c62828;}.ps-bar.b{background:#e3f2fd;color:#1565c0;} | ||
| 271 | +.ps-result{margin-top:2px;} | ||
| 272 | + | ||
| 273 | +.temp-card{border:1px solid #eee;border-radius:6px;padding:12px;} | ||
| 274 | +.temp-header{font-size:13px;font-weight:bold;color:#333;margin-bottom:8px;} | ||
| 275 | +.temp-chart svg{width:100%;height:auto;} | ||
| 276 | + | ||
| 277 | +.leg{display:flex;align-items:center;gap:3px;} | ||
| 278 | +.dot{display:inline-block;width:10px;height:10px;border-radius:2px;} | ||
| 279 | +.dot.y{background:#f5a623;}.dot.g{background:#67c23a;}.dot.r{background:#f56c6c;} | ||
| 280 | +</style> |
src/components/SafetyDialog.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <el-dialog | ||
| 3 | + :model-value="visible" | ||
| 4 | + @update:model-value="$emit('update:visible', $event)" | ||
| 5 | + title="" | ||
| 6 | + width="calc(100vw - 40px)" | ||
| 7 | + :style="{ maxWidth: '1400px' }" | ||
| 8 | + top="3vh" | ||
| 9 | + destroy-on-close | ||
| 10 | + class="safety-dialog" | ||
| 11 | + > | ||
| 12 | + <template #header> | ||
| 13 | + <div class="dialog-header"> | ||
| 14 | + <span class="title-text">{{ device?.name || '能耗设备1' }}</span> | ||
| 15 | + <div class="header-right"> | ||
| 16 | + <el-icon :size="16" style="cursor:pointer;color:#409eff;"><Refresh /></el-icon> | ||
| 17 | + <el-icon :size="16" style="cursor:pointer;color:#409eff;margin-left:6px;"><FullScreen /></el-icon> | ||
| 18 | + <el-icon :size="16" style="cursor:pointer;color:#409eff;margin-left:6px;" @click="$emit('update:visible', false)"><Close /></el-icon> | ||
| 19 | + </div> | ||
| 20 | + </div> | ||
| 21 | + </template> | ||
| 22 | + | ||
| 23 | + <div class="safety-body"> | ||
| 24 | + <!-- 设备运行状态 --> | ||
| 25 | + <div class="panel"> | ||
| 26 | + <div class="panel-title">设备运行状态:</div> | ||
| 27 | + <div class="status-chart-area"> | ||
| 28 | + <div class="status-bar-chart"> | ||
| 29 | + <svg viewBox="0 0 700 100"> | ||
| 30 | + <g font-size="10" fill="#999" text-anchor="middle"> | ||
| 31 | + <text x="50" y="85">04:00</text><text x="180" y="85">08:00</text><text x="310" y="85">12:00</text><text x="440" y="85">16:00</text><text x="570" y="85">20:00</text><text x="650" y="85">25:00</text> | ||
| 32 | + </g> | ||
| 33 | + <rect x="200" y="15" width="90" height="55" rx="3" fill="#999" opacity="0.7"/> | ||
| 34 | + </svg> | ||
| 35 | + </div> | ||
| 36 | + <el-date-picker v-model="runDate" type="date" size="small" placeholder="2026-04-28" /> | ||
| 37 | + </div> | ||
| 38 | + </div> | ||
| 39 | + | ||
| 40 | + <!-- 运行状态能耗产量复合 --> | ||
| 41 | + <div class="panel"> | ||
| 42 | + <div class="panel-title">运行状态能耗产量复合</div> | ||
| 43 | + <div class="composite-row"> | ||
| 44 | + <div class="composite-left"> | ||
| 45 | + <div class="pie-wrap"> | ||
| 46 | + <svg viewBox="0 0 120 120"><circle cx="60" cy="60" r="48" fill="#e8f4fd" stroke="#ddd"/> | ||
| 47 | + <circle cx="60" cy="60" r="38" fill="none" stroke="#409eff" stroke-width="12" | ||
| 48 | + stroke-dasharray="239 239" transform="rotate(-90 60 60)"/> | ||
| 49 | + <text x="60" y="58" text-anchor="middle" font-size="11" fill="#333">正常</text> | ||
| 50 | + <text x="60" y="72" text-anchor="middle" font-size="9" fill="#666">100%</text> | ||
| 51 | + </svg> | ||
| 52 | + </div> | ||
| 53 | + <div class="composite-info"> | ||
| 54 | + <div class="ci-row"><span class="ci-dot b"></span>本月耗电</div> | ||
| 55 | + <div class="ci-row"><span class="ci-dot g"></span>日产量</div> | ||
| 56 | + </div> | ||
| 57 | + </div> | ||
| 58 | + <div class="composite-right"> | ||
| 59 | + <div>待机电能范围:<b>0.00A ≤ I ≤ 0.00A</b></div> | ||
| 60 | + <div>运行电能范围:<b>I ≥ 0.00A</b></div> | ||
| 61 | + </div> | ||
| 62 | + </div> | ||
| 63 | + </div> | ||
| 64 | + | ||
| 65 | + <!-- 运行时长统计 + 健康度/能效统计 --> | ||
| 66 | + <div class="two-col-row"> | ||
| 67 | + <div class="panel flex-1"> | ||
| 68 | + <div class="panel-title">运行时长统计:</div> | ||
| 69 | + <div class="bar-legend"> | ||
| 70 | + <span class="leg"><i class="dot o"/>停机</span> | ||
| 71 | + <span class="leg"><i class="dot g"/>待机</span> | ||
| 72 | + <span class="leg"><i class="dot b"/>运行</span> | ||
| 73 | + <span class="leg"><i class="dot gy"/>离线</span> | ||
| 74 | + </div> | ||
| 75 | + <div class="runtime-bar-chart"> | ||
| 76 | + <svg viewBox="0 0 600 160"> | ||
| 77 | + <g font-size="9" fill="#999" text-anchor="end"> | ||
| 78 | + <text x="24" y="18">24</text><text x="24" y="46">18</text><text x="24" y="74">12</text> | ||
| 79 | + <text x="24" y="102">6</text><text x="24" y="130">0</text> | ||
| 80 | + </g> | ||
| 81 | + <line x1="30" y1="126" x2="580" y2="126" stroke="#ddd"/> | ||
| 82 | + <rect x="36" y="86" width="14" height="40" fill="#999" rx="1"/><text x="43" y="141" text-anchor="middle" font-size="7">03-13</text> | ||
| 83 | + <rect x="54" y="106" width="14" height="20" fill="#999" rx="1"/><text x="61" y="141" text-anchor="middle" font-size="7">03-14</text> | ||
| 84 | + <rect x="72" y="126" width="14" height="0" rx="1"/><text x="79" y="141" text-anchor="middle" font-size="7">03-15</text> | ||
| 85 | + <rect x="90" y="126" width="14" height="0" rx="1"/><text x="97" y="141" text-anchor="middle" font-size="7">03-16</text> | ||
| 86 | + <rect x="108" y="126" width="14" height="0" rx="1"/><text x="115" y="141" text-anchor="middle" font-size="7">03-17</text> | ||
| 87 | + <rect x="126" y="126" width="14" height="0" rx="1"/><text x="133" y="141" text-anchor="middle" font-size="7">03-18</text> | ||
| 88 | + <rect x="144" y="126" width="14" height="0" rx="1"/><text x="151" y="141" text-anchor="middle" font-size="7">03-19</text> | ||
| 89 | + <rect x="162" y="126" width="14" height="0" rx="1"/><text x="169" y="141" text-anchor="middle" font-size="7">03-20</text> | ||
| 90 | + <rect x="180" y="126" width="14" height="0" rx="1"/><text x="187" y="141" text-anchor="middle" font-size="7">03-21</text> | ||
| 91 | + <rect x="198" y="126" width="14" height="0" rx="1"/><text x="205" y="141" text-anchor="middle" font-size="7">03-22</text> | ||
| 92 | + <rect x="216" y="116" width="14" height="10" fill="#999" rx="1"/><text x="223" y="141" text-anchor="middle" font-size="7">03-23</text> | ||
| 93 | + <rect x="234" y="56" width="14" height="70" fill="#999" rx="1"/><text x="241" y="141" text-anchor="middle" font-size="7">03-24</text> | ||
| 94 | + <rect x="252" y="46" width="14" height="80" fill="#999" rx="1"/><text x="259" y="141" text-anchor="middle" font-size="7">03-25</text> | ||
| 95 | + <rect x="270" y="46" width="14" height="80" fill="#999" rx="1"/><text x="277" y="141" text-anchor="middle" font-size="7">03-26</text> | ||
| 96 | + <rect x="288" y="76" width="14" height="50" fill="#999" rx="1"/><text x="295" y="141" text-anchor="middle" font-size="7">03-27</text> | ||
| 97 | + <rect x="306" y="96" width="14" height="30" fill="#999" rx="1"/><text x="313" y="141" text-anchor="middle" font-size="7">03-28</text> | ||
| 98 | + <line x1="330" y1="10" x2="330" y2="130" stroke="#eee" stroke-dasharray="3,3"/> | ||
| 99 | + <text x="380" y="75" text-anchor="middle" font-size="11" fill="#666" opacity="0.6">2026-04-13 — 2026-04-28</text> | ||
| 100 | + </svg> | ||
| 101 | + </div> | ||
| 102 | + </div> | ||
| 103 | + | ||
| 104 | + <div class="right-stats"> | ||
| 105 | + <!-- 健康度 --> | ||
| 106 | + <div class="stat-card"> | ||
| 107 | + <div class="sc-header">健康度:</div> | ||
| 108 | + <div class="sc-content"> | ||
| 109 | + <div class="score-circle"> | ||
| 110 | + <svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="42" fill="none" stroke="#eee" stroke-width="8"/> | ||
| 111 | + <circle cx="50" cy="50" r="42" fill="none" stroke="#e6a23c" stroke-width="8" stroke-dasharray="2 262" transform="rotate(-90 50 50)"/> | ||
| 112 | + <text x="50" y="53" text-anchor="middle" font-size="18" font-weight="bold" fill="#333">0.00</text> | ||
| 113 | + </svg> | ||
| 114 | + </div> | ||
| 115 | + <div class="sc-detail"> | ||
| 116 | + <div class="sd-row"><span class="sd-label">总评分:</span><span class="sd-val">0.00</span></div> | ||
| 117 | + <div class="sd-row"><span class="sd-label">月最大需量</span></div> | ||
| 118 | + <div class="sd-big"><b>0.00Kw</b></div> | ||
| 119 | + </div> | ||
| 120 | + <div class="sc-bars"> | ||
| 121 | + <div v-for="(item, i) in healthBars" :key="i" :class="['hb-item', 'hb-'+item.color]"> | ||
| 122 | + {{ item.pct }}% {{ item.label }} | ||
| 123 | + </div> | ||
| 124 | + </div> | ||
| 125 | + </div> | ||
| 126 | + </div> | ||
| 127 | + | ||
| 128 | + <!-- 能效统计 --> | ||
| 129 | + <div class="stat-card"> | ||
| 130 | + <div class="sc-header">能效统计:</div> | ||
| 131 | + <div class="sc-content"> | ||
| 132 | + <div class="score-circle small"> | ||
| 133 | + <svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="42" fill="none" stroke="#eee" stroke-width="8"/> | ||
| 134 | + <text x="50" y="53" text-anchor="middle" font-size="18" font-weight="bold" fill="#333">0</text> | ||
| 135 | + </svg> | ||
| 136 | + </div> | ||
| 137 | + <div class="ef-bars"> | ||
| 138 | + <div v-for="(item, i) in effBars" :key="i" class="eb-item"> | ||
| 139 | + <div class="eb-bar-wrap"><div :class="['eb-fill', 'fill-' + item.color]" :style="{width:item.val+'%'}"></div></div> | ||
| 140 | + <div class="eb-label">{{ item.val }}<br/>{{ item.label }}</div> | ||
| 141 | + </div> | ||
| 142 | + </div> | ||
| 143 | + <div class="ef-bottom">日能效曲线</div> | ||
| 144 | + </div> | ||
| 145 | + </div> | ||
| 146 | + </div> | ||
| 147 | + </div> | ||
| 148 | + | ||
| 149 | + <!-- 产量分析 + 日投入产出 --> | ||
| 150 | + <div class="two-col-row"> | ||
| 151 | + <div class="panel flex-1"> | ||
| 152 | + <div class="panel-title">产量分析:</div> | ||
| 153 | + <div class="prod-bars"> | ||
| 154 | + <div v-for="(item, i) in prodData" :key="i" class="prod-item"> | ||
| 155 | + <div :class="['pb-dot', item.color]"></div> | ||
| 156 | + <div>{{ item.label }}</div> | ||
| 157 | + <div class="pb-val">{{ item.val }}</div> | ||
| 158 | + </div> | ||
| 159 | + </div> | ||
| 160 | + <div class="prod-line">日产量曲线</div> | ||
| 161 | + </div> | ||
| 162 | + <div class="panel flex-1"> | ||
| 163 | + <div class="panel-title">日投入产出、日生产效率</div> | ||
| 164 | + <div class="io-legend"> | ||
| 165 | + <span class="leg"><i class="dot b"/>收入产出比</span> | ||
| 166 | + <span class="leg"><i class="dot g"/>生产效率</span> | ||
| 167 | + </div> | ||
| 168 | + </div> | ||
| 169 | + </div> | ||
| 170 | + </div> | ||
| 171 | + </el-dialog> | ||
| 172 | +</template> | ||
| 173 | + | ||
| 174 | +<script setup> | ||
| 175 | +import { ref } from 'vue' | ||
| 176 | +import { Refresh, FullScreen, Close } from '@element-plus/icons-vue' | ||
| 177 | + | ||
| 178 | +defineProps({ visible: Boolean, device: Object }) | ||
| 179 | +defineEmits(['update:visible']) | ||
| 180 | + | ||
| 181 | +const runDate = ref('') | ||
| 182 | +const healthBars = [ | ||
| 183 | + { pct: 0.00, label: '自愈率', color: 'orange' }, | ||
| 184 | + { pct: 0.00, label: '电平平衡率', color: 'green' }, | ||
| 185 | + { pct: 0.00, label: '总故障数', color: 'red' }, | ||
| 186 | + { pct: 0.00, label: '电压不平衡', color: 'blue' } | ||
| 187 | +] | ||
| 188 | +const effBars = [ | ||
| 189 | + { val: 0.00, label: '日累计运行时长', color: 'orange' }, | ||
| 190 | + { val: 0.00, label: '日累计能耗', color: 'green' }, | ||
| 191 | + { val: 0.00, label: '月累计运行时长', color: 'red' }, | ||
| 192 | + { val: 0.00, label: '月累计能耗', color: 'blue' } | ||
| 193 | +] | ||
| 194 | +const prodData = [ | ||
| 195 | + { label: '日累计产量', val: '0.00', color: 'o' }, | ||
| 196 | + { label: '月累计产量', val: '0.00', color: 'g' }, | ||
| 197 | + { label: '日投入产出', val: '0.00', color: 'r' }, | ||
| 198 | + { label: '日生产效率', val: '0.00', color: 'b' } | ||
| 199 | +] | ||
| 200 | +</script> | ||
| 201 | + | ||
| 202 | +<style scoped> | ||
| 203 | +.safety-dialog :deep(.el-dialog) { max-height: 92vh; display:flex;flex-direction:column; } | ||
| 204 | +.safety-dialog :deep(.el-dialog__header){padding:10px 20px;border-bottom:1px solid #e8e8e8;margin:0;flex-shrink:0; } | ||
| 205 | +.safety-dialog :deep(.el-dialog__body){overflow-y:auto;flex:1;padding:12px;} | ||
| 206 | +.dialog-header{display:flex;align-items:center;justify-content:space-between;} | ||
| 207 | +.title-text{font-size:15px;font-weight:bold;color:#333;} | ||
| 208 | +.header-right{display:flex;align-items:center;} | ||
| 209 | + | ||
| 210 | +.safety-body .panel{background:#fff;border:1px solid #eee;border-radius:6px;padding:14px;margin-bottom:12px;} | ||
| 211 | +.panel-title{font-size:13px;font-weight:bold;color:#333;margin-bottom:10px;} | ||
| 212 | +.flex-1{flex:1;} | ||
| 213 | +.two-col-row{display:flex;gap:12px;} | ||
| 214 | + | ||
| 215 | +.status-chart-area{display:flex;align-items:center;justify-content:space-between;gap:12px;} | ||
| 216 | +.status-bar-chart{flex:1;} | ||
| 217 | +.status-bar-chart svg{width:100%;height:auto;} | ||
| 218 | + | ||
| 219 | +.composite-row{display:flex;justify-content:space-between;align-items:center;gap:16px;} | ||
| 220 | +.composite-left{display:flex;align-items:center;gap:16px;} | ||
| 221 | +.pie-wrap svg{width:120px;height:120px;} | ||
| 222 | +.composite-info{font-size:12px;color:#666;display:flex;gap:16px;} | ||
| 223 | +.ci-row{display:flex;align-items:center;gap:4px;} | ||
| 224 | +.ci-dot{display:inline-block;width:10px;height:10px;border-radius:2px;} | ||
| 225 | +.ci-dot.b{background:#409eff;}.ci-dot.g{background:#67c23a;} | ||
| 226 | +.composite-right{font-size:12px;color:#666;line-height:2;} | ||
| 227 | + | ||
| 228 | +.bar-legend{display:flex;gap:14px;font-size:11px;color:#666;margin-bottom:6px;} | ||
| 229 | +.leg{display:flex;align-items:center;gap:3px;} | ||
| 230 | +.dot{display:inline-block;width:10px;height:10px;border-radius:2px;} | ||
| 231 | +.dot.o{background:#f56c6c;}.dot.g{background:#67c23a;}.dot.b{background:#409eff;}.dot.gy{background:#909399;} | ||
| 232 | +.runtime-bar-chart{overflow-x:auto;} | ||
| 233 | + | ||
| 234 | +.right-stats{width:280px;display:flex;flex-direction:column;gap:12px;} | ||
| 235 | +.stat-card{background:#fff;border:1px solid #eee;border-radius:6px;padding:12px;} | ||
| 236 | +.sc-header{font-size:13px;font-weight:bold;color:#333;margin-bottom:8px;} | ||
| 237 | +.sc-content{display:flex;align-items:flex-start;gap:10px;} | ||
| 238 | +.score-circle svg{width:90px;height:90px;flex-shrink:0;} | ||
| 239 | +.score-circle.small svg{width:76px;height:76px;} | ||
| 240 | +.sc-detail{flex:1;font-size:11px;color:#666;} | ||
| 241 | +.sd-row{margin-bottom:2px;} | ||
| 242 | +.sd-val{color:#e6a23c;font-weight:bold;font-size:13px;} | ||
| 243 | +.sd-big{margin-top:4px;color:#409eff;font-size:16px;} | ||
| 244 | +.sc-bars,.ef-bars{flex:1;display:flex;flex-direction:column;gap:4px;} | ||
| 245 | +.hb-item{font-size:10px;padding:3px 6px;border-radius:3px;text-align:center;} | ||
| 246 | +.hb-orange{background:#fff3e0;color:#e65100;} | ||
| 247 | +.hb-green{background:#e8f5e9;color:#2e7d32;} | ||
| 248 | +.hb-red{background:#ffebee;color:#c62828;} | ||
| 249 | +.hb-blue{background:#e3f2fd;color:#1565c0;} | ||
| 250 | +.eb-item{display:flex;align-items:center;gap:6px;font-size:10px;} | ||
| 251 | +.eb-bar-wrap{width:50px;height:8px;background:#f0f0f0;border-radius:2px;overflow:hidden;} | ||
| 252 | +.eb-fill{height:100%;border-radius:2px;} | ||
| 253 | +.fill-orange{background:#f56c6c;}.fill-green{background:#67c23a;} | ||
| 254 | +.fill-red{background:#f56c6c;}.fill-blue{background:#409eff;} | ||
| 255 | +.eb-label{color:#666;white-space:nowrap;} | ||
| 256 | +.ef-bottom{text-align:center;font-size:11px;color:#999;margin-top:4px;} | ||
| 257 | + | ||
| 258 | +.prod-bars{display:flex;gap:24px;margin-bottom:8px;} | ||
| 259 | +.pb-dot{width:12px;height:12px;border-radius:2px;margin-right:4px;} | ||
| 260 | +.pb-dot.o{background:#f56c6c;}.pb-dot.g{background:#67c23a;}.pb-dot.r{background:#e74c3c;}.pb-dot.b{background:#409eff;} | ||
| 261 | +.prod-item{display:flex;align-items:center;gap:4px;font-size:12px;color:#666;} | ||
| 262 | +.pb-val{font-weight:bold;color:#333;margin-left:4px;} | ||
| 263 | +.prod-line{text-align:center;font-size:11px;color:#999;} | ||
| 264 | +.io-legend{display:flex;gap:14px;font-size:11px;color:#666;} | ||
| 265 | +</style> |
src/components/SettingDialog.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <el-dialog | ||
| 3 | + :model-value="visible" | ||
| 4 | + @update:model-value="$emit('update:visible', $event)" | ||
| 5 | + title="" | ||
| 6 | + width="calc(100vw - 40px)" | ||
| 7 | + :style="{ maxWidth: '1400px' }" | ||
| 8 | + top="3vh" | ||
| 9 | + destroy-on-close | ||
| 10 | + class="setting-dialog" | ||
| 11 | + > | ||
| 12 | + <template #header> | ||
| 13 | + <div class="dialog-header"> | ||
| 14 | + <div class="left-tabs"> | ||
| 15 | + <span :class="['tab-item', { active: activeTab === 'trigger' }]" @click="activeTab = 'trigger'">触发器设置</span> | ||
| 16 | + <span :class="['tab-item', { active: activeTab === 'history' }]" @click="activeTab = 'history'">原因列表查看</span> | ||
| 17 | + <span :class="['tab-item', { active: activeTab === 'setting' }]" @click="activeTab = 'setting'">设置</span> | ||
| 18 | + </div> | ||
| 19 | + <div class="header-right"> | ||
| 20 | + <span class="device-name">中速纸杯机24号机</span> | ||
| 21 | + </div> | ||
| 22 | + </div> | ||
| 23 | + </template> | ||
| 24 | + | ||
| 25 | + <div class="setting-body"> | ||
| 26 | + <div class="sub-tabs"> | ||
| 27 | + <span | ||
| 28 | + v-for="st in subTabs" :key="st.key" | ||
| 29 | + :class="['sub-tab', { active: activeSub === st.key }]" | ||
| 30 | + @click="activeSub = st.key" | ||
| 31 | + >{{ st.label }}</span> | ||
| 32 | + <el-button size="small" type="primary" plain class="add-trigger-btn">添加触发条件</el-button> | ||
| 33 | + </div> | ||
| 34 | + | ||
| 35 | + <el-table :data="triggerTableData" size="small" stripe border style="width: 100%; font-size: 12px;"> | ||
| 36 | + <el-table-column prop="name" label="触发器名称" min-width="120" fixed> | ||
| 37 | + <template #default="{ row }"> | ||
| 38 | + <span class="link-text">{{ row.name }}</span> | ||
| 39 | + </template> | ||
| 40 | + </el-table-column> | ||
| 41 | + <el-table-column prop="condition" label="触发条件" min-width="100" /> | ||
| 42 | + <el-table-column prop="threshold" label="静默阈值" min-width="100" /> | ||
| 43 | + <el-table-column prop="duration" label="累管时间" min-width="110"> | ||
| 44 | + <template #default="{ row }"> | ||
| 45 | + <span class="link-text">{{ row.duration }}</span> | ||
| 46 | + </template> | ||
| 47 | + </el-table-column> | ||
| 48 | + <el-table-column prop="operator" label="接收人员" min-width="100" /> | ||
| 49 | + <el-table-column prop="alertTime" label="触发时间" min-width="110"> | ||
| 50 | + <template #default="{ row }"> | ||
| 51 | + <span class="link-text">{{ row.alertTime }}</span> | ||
| 52 | + </template> | ||
| 53 | + </el-table-column> | ||
| 54 | + <el-table-column prop="receiveMethod" label="接收方式" min-width="90" /> | ||
| 55 | + <el-table-column prop="part" label="部件" min-width="80" /> | ||
| 56 | + <el-table-column prop="contactGroup" label="联系组" min-width="90" /> | ||
| 57 | + <el-table-column prop="config" label="配置" min-width="200" /> | ||
| 58 | + </el-table> | ||
| 59 | + | ||
| 60 | + <div class="footer-bar"> | ||
| 61 | + <span>共 1 条</span> | ||
| 62 | + <el-pagination layout="prev, pager, next" :total="1" :page-size="10" small /> | ||
| 63 | + <span>前往 <el-input-number :min="1" :max="1" :controls="false" size="small" style="width: 46px;" /> 页</span> | ||
| 64 | + </div> | ||
| 65 | + </div> | ||
| 66 | + </el-dialog> | ||
| 67 | +</template> | ||
| 68 | + | ||
| 69 | +<script setup> | ||
| 70 | +import { ref } from 'vue' | ||
| 71 | + | ||
| 72 | +defineProps({ | ||
| 73 | + visible: Boolean, | ||
| 74 | + device: Object | ||
| 75 | +}) | ||
| 76 | +defineEmits(['update:visible']) | ||
| 77 | + | ||
| 78 | +const activeTab = ref('trigger') | ||
| 79 | +const activeSub = ref('light') | ||
| 80 | + | ||
| 81 | +const subTabs = [ | ||
| 82 | + { key: 'light', label: '绿灯' }, | ||
| 83 | + { key: 'yellow', label: '黄灯' }, | ||
| 84 | + { key: 'offline', label: '离线' }, | ||
| 85 | + { key: 'lightOff', label: '灭灯' }, | ||
| 86 | + { key: 'counter', label: '计数' }, | ||
| 87 | + { key: 'blue', label: '蓝灯' }, | ||
| 88 | + { key: 'reason', label: '原因码' }, | ||
| 89 | + { key: 'safe', label: '安灯' }, | ||
| 90 | +] | ||
| 91 | + | ||
| 92 | +const triggerTableData = [ | ||
| 93 | + { name: '红灯即时', condition: '>6秒', threshold: '0分', duration: '00:00:24:00', operator: '', alertTime: '', receiveMethod: '', part: '', contactGroup: '', config: '启用 / 禁用 / 删除 / 添加联系人 / 编辑联系组' } | ||
| 94 | +] | ||
| 95 | +</script> | ||
| 96 | + | ||
| 97 | +<style scoped> | ||
| 98 | +.setting-dialog :deep(.el-dialog) { | ||
| 99 | + max-height: 92vh; | ||
| 100 | + display: flex; | ||
| 101 | + flex-direction: column; | ||
| 102 | +} | ||
| 103 | +.setting-dialog :deep(.el-dialog__header) { | ||
| 104 | + padding: 0; | ||
| 105 | + margin: 0; | ||
| 106 | + border-bottom: 1px solid #e8e8e8; | ||
| 107 | + flex-shrink: 0; | ||
| 108 | +} | ||
| 109 | +.setting-dialog :deep(.el-dialog__body) { | ||
| 110 | + overflow-y: auto; | ||
| 111 | + flex: 1; | ||
| 112 | +} | ||
| 113 | +.dialog-header { | ||
| 114 | + display: flex; | ||
| 115 | + justify-content: space-between; | ||
| 116 | + align-items: center; | ||
| 117 | + height: 48px; | ||
| 118 | + padding: 0 20px; | ||
| 119 | +} | ||
| 120 | +.left-tabs { | ||
| 121 | + display: flex; | ||
| 122 | + gap: 0; | ||
| 123 | + height: 100%; | ||
| 124 | + align-items: center; | ||
| 125 | +} | ||
| 126 | +.tab-item { | ||
| 127 | + padding: 0 18px; | ||
| 128 | + height: 100%; | ||
| 129 | + display: flex; | ||
| 130 | + align-items: center; | ||
| 131 | + font-size: 13px; | ||
| 132 | + color: #666; | ||
| 133 | + cursor: pointer; | ||
| 134 | + border-bottom: 2px solid transparent; | ||
| 135 | + transition: all 0.2s; | ||
| 136 | +} | ||
| 137 | +.tab-item.active { | ||
| 138 | + color: #409eff; | ||
| 139 | + font-weight: bold; | ||
| 140 | + border-bottom-color: #409eff; | ||
| 141 | +} | ||
| 142 | +.header-right { | ||
| 143 | + display: flex; | ||
| 144 | + align-items: center; | ||
| 145 | + gap: 12px; | ||
| 146 | +} | ||
| 147 | +.device-name { | ||
| 148 | + font-size: 13px; | ||
| 149 | + color: #333; | ||
| 150 | + font-weight: 500; | ||
| 151 | +} | ||
| 152 | + | ||
| 153 | +.setting-body { | ||
| 154 | + padding: 0; | ||
| 155 | +} | ||
| 156 | +.sub-tabs { | ||
| 157 | + display: flex; | ||
| 158 | + align-items: center; | ||
| 159 | + gap: 4px; | ||
| 160 | + padding: 12px 20px; | ||
| 161 | + border-bottom: 1px solid #e8e8e8; | ||
| 162 | + background: #fafafa; | ||
| 163 | +} | ||
| 164 | +.sub-tab { | ||
| 165 | + padding: 6px 18px; | ||
| 166 | + font-size: 13px; | ||
| 167 | + color: #333; | ||
| 168 | + cursor: pointer; | ||
| 169 | + border-radius: 4px; | ||
| 170 | + transition: all 0.2s; | ||
| 171 | +} | ||
| 172 | +.sub-tab:hover { | ||
| 173 | + color: #409eff; | ||
| 174 | +} | ||
| 175 | +.sub-tab.active { | ||
| 176 | + background: #409eff; | ||
| 177 | + color: #fff; | ||
| 178 | + font-weight: bold; | ||
| 179 | +} | ||
| 180 | +.add-trigger-btn { | ||
| 181 | + margin-left: auto; | ||
| 182 | +} | ||
| 183 | + | ||
| 184 | +.link-text { | ||
| 185 | + color: #409eff; | ||
| 186 | + cursor: pointer; | ||
| 187 | +} | ||
| 188 | +.link-text:hover { | ||
| 189 | + text-decoration: underline; | ||
| 190 | +} | ||
| 191 | + | ||
| 192 | +.footer-bar { | ||
| 193 | + display: flex; | ||
| 194 | + align-items: center; | ||
| 195 | + justify-content: space-between; | ||
| 196 | + padding: 12px 20px; | ||
| 197 | + border-top: 1px solid #e8e8e8; | ||
| 198 | + background: #fafafa; | ||
| 199 | + font-size: 12px; | ||
| 200 | + color: #999; | ||
| 201 | + gap: 16px; | ||
| 202 | +} | ||
| 203 | +</style> |
src/components/TheWelcome.vue
0 → 100644
| 1 | +<script setup> | ||
| 2 | +import WelcomeItem from './WelcomeItem.vue' | ||
| 3 | +import DocumentationIcon from './icons/IconDocumentation.vue' | ||
| 4 | +import ToolingIcon from './icons/IconTooling.vue' | ||
| 5 | +import EcosystemIcon from './icons/IconEcosystem.vue' | ||
| 6 | +import CommunityIcon from './icons/IconCommunity.vue' | ||
| 7 | +import SupportIcon from './icons/IconSupport.vue' | ||
| 8 | + | ||
| 9 | +const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md') | ||
| 10 | +</script> | ||
| 11 | + | ||
| 12 | +<template> | ||
| 13 | + <WelcomeItem> | ||
| 14 | + <template #icon> | ||
| 15 | + <DocumentationIcon /> | ||
| 16 | + </template> | ||
| 17 | + <template #heading>Documentation</template> | ||
| 18 | + | ||
| 19 | + Vue’s | ||
| 20 | + <a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a> | ||
| 21 | + provides you with all information you need to get started. | ||
| 22 | + </WelcomeItem> | ||
| 23 | + | ||
| 24 | + <WelcomeItem> | ||
| 25 | + <template #icon> | ||
| 26 | + <ToolingIcon /> | ||
| 27 | + </template> | ||
| 28 | + <template #heading>Tooling</template> | ||
| 29 | + | ||
| 30 | + This project is served and bundled with | ||
| 31 | + <a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The | ||
| 32 | + recommended IDE setup is | ||
| 33 | + <a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> | ||
| 34 | + + | ||
| 35 | + <a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener" | ||
| 36 | + >Vue - Official</a | ||
| 37 | + >. If you need to test your components and web pages, check out | ||
| 38 | + <a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a> | ||
| 39 | + and | ||
| 40 | + <a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> | ||
| 41 | + / | ||
| 42 | + <a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>. | ||
| 43 | + | ||
| 44 | + <br /> | ||
| 45 | + | ||
| 46 | + More instructions are available in | ||
| 47 | + <a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a | ||
| 48 | + >. | ||
| 49 | + </WelcomeItem> | ||
| 50 | + | ||
| 51 | + <WelcomeItem> | ||
| 52 | + <template #icon> | ||
| 53 | + <EcosystemIcon /> | ||
| 54 | + </template> | ||
| 55 | + <template #heading>Ecosystem</template> | ||
| 56 | + | ||
| 57 | + Get official tools and libraries for your project: | ||
| 58 | + <a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>, | ||
| 59 | + <a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>, | ||
| 60 | + <a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and | ||
| 61 | + <a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If | ||
| 62 | + you need more resources, we suggest paying | ||
| 63 | + <a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a> | ||
| 64 | + a visit. | ||
| 65 | + </WelcomeItem> | ||
| 66 | + | ||
| 67 | + <WelcomeItem> | ||
| 68 | + <template #icon> | ||
| 69 | + <CommunityIcon /> | ||
| 70 | + </template> | ||
| 71 | + <template #heading>Community</template> | ||
| 72 | + | ||
| 73 | + Got stuck? Ask your question on | ||
| 74 | + <a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a> | ||
| 75 | + (our official Discord server), or | ||
| 76 | + <a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener" | ||
| 77 | + >StackOverflow</a | ||
| 78 | + >. You should also follow the official | ||
| 79 | + <a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a> | ||
| 80 | + Bluesky account or the | ||
| 81 | + <a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a> | ||
| 82 | + X account for latest news in the Vue world. | ||
| 83 | + </WelcomeItem> | ||
| 84 | + | ||
| 85 | + <WelcomeItem> | ||
| 86 | + <template #icon> | ||
| 87 | + <SupportIcon /> | ||
| 88 | + </template> | ||
| 89 | + <template #heading>Support Vue</template> | ||
| 90 | + | ||
| 91 | + As an independent project, Vue relies on community backing for its sustainability. You can help | ||
| 92 | + us by | ||
| 93 | + <a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>. | ||
| 94 | + </WelcomeItem> | ||
| 95 | +</template> |
src/components/UtilizationDialog.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <el-dialog | ||
| 3 | + :model-value="visible" | ||
| 4 | + @update:model-value="$emit('update:visible', $event)" | ||
| 5 | + title="" | ||
| 6 | + width="calc(100vw - 40px)" | ||
| 7 | + :style="{ maxWidth: '1400px' }" | ||
| 8 | + top="3vh" | ||
| 9 | + destroy-on-close | ||
| 10 | + class="util-dialog" | ||
| 11 | + > | ||
| 12 | + <template #header> | ||
| 13 | + <div class="dialog-header"> | ||
| 14 | + <span class="title-text">{{ deviceName }} 稼动率查询方式:</span> | ||
| 15 | + <div class="header-center"> | ||
| 16 | + <el-radio-group v-model="queryMode" size="small"> | ||
| 17 | + <el-radio-button value="day">日查询</el-radio-button> | ||
| 18 | + <el-radio-button value="week">周查询</el-radio-button> | ||
| 19 | + <el-radio-button value="month">月查询</el-radio-button> | ||
| 20 | + </el-radio-group> | ||
| 21 | + <span class="date-wrap" v-if="queryMode === 'day'"> | ||
| 22 | + <el-date-picker v-model="queryDateRange" type="daterange" start-placeholder="开始" end-placeholder="结束" | ||
| 23 | + size="small" value-format="YYYY-MM-DD" @change="fetchOeeData" /> | ||
| 24 | + </span> | ||
| 25 | + </div> | ||
| 26 | + <div class="header-right"></div> | ||
| 27 | + </div> | ||
| 28 | + </template> | ||
| 29 | + | ||
| 30 | + <div class="util-body"> | ||
| 31 | + <!-- 上排:颜色时长明细 + 颜色时长总计 --> | ||
| 32 | + <div class="top-row"> | ||
| 33 | + <div class="chart-card"> | ||
| 34 | + <div class="card-title-row"> | ||
| 35 | + <h4>颜色时长明细</h4> | ||
| 36 | + <span class="card-subtitle">Duration detail</span> | ||
| 37 | + </div> | ||
| 38 | + <div class="legend-inline"> | ||
| 39 | + <span class="leg"><i class="dot green"></i>绿灯</span> | ||
| 40 | + <span class="leg"><i class="dot yellow"></i>黄灯</span> | ||
| 41 | + <span class="leg"><i class="dot red"></i>红灯</span> | ||
| 42 | + <span class="leg"><i class="dot gray"></i>离线</span> | ||
| 43 | + </div> | ||
| 44 | + <div class="canvas-wrap" @mousemove="onBarMove($event, 'dur')" @mouseleave="onBarLeave('dur')"> | ||
| 45 | + <canvas ref="durCanvas" :width="durCanvasW" :height="260"></canvas> | ||
| 46 | + <div v-if="barHover.dur.show" class="bar-tooltip" :style="{ left: barHover.dur.x + 'px', top: barHover.dur.y + 'px' }"> | ||
| 47 | + <div class="tt-title">{{ barHover.dur.label }}</div> | ||
| 48 | + <template v-for="(row, i) in barHover.dur.rows" :key="i"> | ||
| 49 | + <div class="tt-row" v-if="row.v > 0"><i class="tt-dot" :style="{ background: row.c }"></i>{{ row.n }}: {{ row.t }}</div> | ||
| 50 | + </template> | ||
| 51 | + </div> | ||
| 52 | + </div> | ||
| 53 | + </div> | ||
| 54 | + | ||
| 55 | + <div class="pie-card"> | ||
| 56 | + <div class="card-title-row"> | ||
| 57 | + <h4>颜色时长总计</h4> | ||
| 58 | + <span class="card-subtitle">Duration statistics</span> | ||
| 59 | + </div> | ||
| 60 | + <div class="pie-main"> | ||
| 61 | + <div class="canvas-wrap pie-canvas-wrap" @mousemove="onPieMove($event, 'dur')" @mouseleave="onPieLeave('dur')"> | ||
| 62 | + <canvas ref="durPieCanvas" width="200" height="200"></canvas> | ||
| 63 | + <div v-if="pieHover.dur.show && pieHover.dur.seg" class="pie-tooltip" :style="{ left: pieHover.dur.x + 'px', top: pieHover.dur.y + 'px' }"> | ||
| 64 | + <strong>{{ pieHover.dur.seg.name }}:</strong> {{ formatDur(pieHover.dur.seg.val) }} | ||
| 65 | + ({{ pieHover.dur.seg.pct }}%) | ||
| 66 | + </div> | ||
| 67 | + </div> | ||
| 68 | + <div class="pie-legend"> | ||
| 69 | + <div class="pleg"><span class="pdot green"></span>绿灯</div> | ||
| 70 | + <div class="pleg"><span class="pdot yellow"></span>黄灯</div> | ||
| 71 | + <div class="pleg"><span class="pdot red"></span>红灯</div> | ||
| 72 | + <div class="pleg"><span class="pdot gray"></span>离线</div> | ||
| 73 | + </div> | ||
| 74 | + </div> | ||
| 75 | + </div> | ||
| 76 | + </div> | ||
| 77 | + | ||
| 78 | + <!-- 下排:颜色次数明细 + 颜色次数总计 --> | ||
| 79 | + <div class="bottom-row"> | ||
| 80 | + <div class="chart-card"> | ||
| 81 | + <div class="card-title-row"> | ||
| 82 | + <h4>颜色次数明细</h4> | ||
| 83 | + <span class="card-subtitle">Frequency detail</span> | ||
| 84 | + </div> | ||
| 85 | + <div class="legend-inline"> | ||
| 86 | + <span class="leg"><i class="dot green"></i>绿灯</span> | ||
| 87 | + <span class="leg"><i class="dot yellow"></i>黄灯</span> | ||
| 88 | + <span class="leg"><i class="dot red"></i>红灯</span> | ||
| 89 | + <span class="leg"><i class="dot gray"></i>离线</span> | ||
| 90 | + </div> | ||
| 91 | + <div class="canvas-wrap" @mousemove="onBarMove($event, 'freq')" @mouseleave="onBarLeave('freq')"> | ||
| 92 | + <canvas ref="freqCanvas" :width="freqCanvasW" :height="280"></canvas> | ||
| 93 | + <div v-if="barHover.freq.show" class="bar-tooltip" :style="{ left: barHover.freq.x + 'px', top: barHover.freq.y + 'px' }"> | ||
| 94 | + <div class="tt-title">{{ barHover.freq.label }}</div> | ||
| 95 | + <template v-for="(row, i) in barHover.freq.rows" :key="i"> | ||
| 96 | + <div class="tt-row" v-if="row.v > 0"><i class="tt-dot" :style="{ background: row.c }"></i>{{ row.n }}: {{ row.v }}次</div> | ||
| 97 | + </template> | ||
| 98 | + </div> | ||
| 99 | + </div> | ||
| 100 | + </div> | ||
| 101 | + | ||
| 102 | + <div class="pie-card"> | ||
| 103 | + <div class="card-title-row"> | ||
| 104 | + <h4>颜色次数总计</h4> | ||
| 105 | + <span class="card-subtitle">Frequency statistics</span> | ||
| 106 | + </div> | ||
| 107 | + <div class="pie-main"> | ||
| 108 | + <div class="canvas-wrap pie-canvas-wrap" @mousemove="onPieMove($event, 'freq')" @mouseleave="onPieLeave('freq')"> | ||
| 109 | + <canvas ref="freqPieCanvas" width="200" height="200"></canvas> | ||
| 110 | + <div v-if="pieHover.freq.show && pieHover.freq.seg" class="pie-tooltip" :style="{ left: pieHover.freq.x + 'px', top: pieHover.freq.y + 'px' }"> | ||
| 111 | + <strong>{{ pieHover.freq.seg.name }}:</strong> {{ pieHover.freq.seg.val }}次 | ||
| 112 | + ({{ pieHover.freq.seg.pct }}%) | ||
| 113 | + </div> | ||
| 114 | + </div> | ||
| 115 | + <div class="pie-legend"> | ||
| 116 | + <div class="pleg"><span class="pdot green"></span>绿灯</div> | ||
| 117 | + <div class="pleg"><span class="pdot yellow"></span>黄灯</div> | ||
| 118 | + <div class="pleg"><span class="pdot red"></span>红灯</div> | ||
| 119 | + <div class="pleg"><span class="pdot gray"></span>离线</div> | ||
| 120 | + </div> | ||
| 121 | + </div> | ||
| 122 | + </div> | ||
| 123 | + </div> | ||
| 124 | + </div> | ||
| 125 | + </el-dialog> | ||
| 126 | +</template> | ||
| 127 | + | ||
| 128 | +<script setup> | ||
| 129 | +import { ref, reactive, computed, watch, onMounted, nextTick, getCurrentInstance } from 'vue' | ||
| 130 | + | ||
| 131 | +const props = defineProps({ | ||
| 132 | + visible: Boolean, | ||
| 133 | + device: Object | ||
| 134 | +}) | ||
| 135 | +defineEmits(['update:visible']) | ||
| 136 | + | ||
| 137 | +const queryMode = ref('day') | ||
| 138 | +const now = new Date() | ||
| 139 | +const weekAgo = new Date(now) | ||
| 140 | +weekAgo.setDate(weekAgo.getDate() - 6) | ||
| 141 | +const queryDateRange = ref([formatDate(weekAgo), formatDate(now)]) | ||
| 142 | +const oeeData = ref(null) | ||
| 143 | + | ||
| 144 | +// 布局常量 | ||
| 145 | +const padL = 50 | ||
| 146 | +const padB = 24 | ||
| 147 | +const COLORS = { green: '#67c23a', yellow: '#e6a23c', red: '#f56c6c', off: '#909399' } | ||
| 148 | +const COLOR_NAMES = { green: '绿灯', yellow: '黄灯', red: '红灯', off: '离线' } | ||
| 149 | + | ||
| 150 | +// Canvas refs | ||
| 151 | +const durCanvas = ref(null) | ||
| 152 | +const freqCanvas = ref(null) | ||
| 153 | +const durPieCanvas = ref(null) | ||
| 154 | +const freqPieCanvas = ref(null) | ||
| 155 | + | ||
| 156 | +// ========== 工具函数 ========== | ||
| 157 | +function formatDate(d) { | ||
| 158 | + if (!d) return '' | ||
| 159 | + const dt = new Date(d) | ||
| 160 | + return `${dt.getFullYear()}-${String(dt.getMonth()+1).padStart(2,'0')}-${String(dt.getDate()).padStart(2,'0')}` | ||
| 161 | +} | ||
| 162 | +function formatDateShort(s) { | ||
| 163 | + if (!s) return '' | ||
| 164 | + return s.slice(5) | ||
| 165 | +} | ||
| 166 | +function formatSec(s) { | ||
| 167 | + if (s <= 0) return '0秒' | ||
| 168 | + const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = Math.floor(s % 60) | ||
| 169 | + if (h > 0) return `${h}时${m}分${sec}秒` | ||
| 170 | + if (m > 0) return `${m}分${sec}秒` | ||
| 171 | + return `${sec}秒` | ||
| 172 | +} | ||
| 173 | +function formatDur(v) { | ||
| 174 | + return typeof v === 'number' ? formatSec(v) : (v || '0') | ||
| 175 | +} | ||
| 176 | + | ||
| 177 | +// ========== computed 数据 ========== | ||
| 178 | +const summaryDur = computed(() => oeeData.value?.summary || {}) | ||
| 179 | +const summaryFreq = computed(() => oeeData.value?.summary || {}) | ||
| 180 | +const displayList = computed(() => oeeData.value?.list || []) | ||
| 181 | + | ||
| 182 | +// 设备名称 | ||
| 183 | +const deviceName = computed(() => props.device?._raw?.name || props.device?.name || '设备') | ||
| 184 | + | ||
| 185 | +// 时长最大值(每根柱子堆叠总和的最大值) | ||
| 186 | +const maxDurSec = computed(() => { | ||
| 187 | + const list = displayList.value | ||
| 188 | + if (!list.length) return 86400 | ||
| 189 | + let max = 1 | ||
| 190 | + list.forEach(item => { | ||
| 191 | + const total = (item.off?.seconds || 0) + (item.red?.seconds || 0) + | ||
| 192 | + (item.yellow?.seconds || 0) + (item.green?.seconds || 0) | ||
| 193 | + max = Math.max(max, total) | ||
| 194 | + }) | ||
| 195 | + return max | ||
| 196 | +}) | ||
| 197 | + | ||
| 198 | +// 次数最大值(每根柱子堆叠总和的最大值) | ||
| 199 | +const maxFreqCount = computed(() => { | ||
| 200 | + const list = displayList.value | ||
| 201 | + if (!list.length) return 100 | ||
| 202 | + let max = 1 | ||
| 203 | + list.forEach(item => { | ||
| 204 | + const total = (item.off?.count || 0) + (item.red?.count || 0) + | ||
| 205 | + (item.yellow?.count || 0) + (item.green?.count || 0) | ||
| 206 | + max = Math.max(max, total) | ||
| 207 | + }) | ||
| 208 | + return max | ||
| 209 | +}) | ||
| 210 | + | ||
| 211 | +// Canvas 宽度 | ||
| 212 | +const durCanvasW = computed(() => { | ||
| 213 | + const n = Math.max(displayList.value.length, 1) | ||
| 214 | + return Math.max(400, padL + n * 80 + 30) | ||
| 215 | +}) | ||
| 216 | +const freqCanvasW = computed(() => { | ||
| 217 | + const n = Math.max(displayList.value.length, 1) | ||
| 218 | + return Math.max(400, padL + n * 80 + 30) | ||
| 219 | +}) | ||
| 220 | + | ||
| 221 | +// Y轴刻度 — 时长(0在下,最大值在上) | ||
| 222 | +const durYTicks = computed(() => { | ||
| 223 | + const ticks = [] | ||
| 224 | + for (let i = 0; i <= 5; i++) { | ||
| 225 | + const sec = maxDurSec.value * i / 5 | ||
| 226 | + const h = Math.floor(sec / 3600) | ||
| 227 | + const m = Math.floor((sec % 3600) / 60) | ||
| 228 | + ticks.push(h > 0 ? `${h}时` : `${m}分`) | ||
| 229 | + } | ||
| 230 | + return ticks | ||
| 231 | +}) | ||
| 232 | +// Y轴刻度 — 次数(0在下,最大值在上) | ||
| 233 | +const freqYTicks = computed(() => { | ||
| 234 | + const ticks = [] | ||
| 235 | + const step = Math.ceil(maxFreqCount.value / 6 / 10) * 10 || 10 | ||
| 236 | + for (let i = 0; i <= 6; i++) ticks.push(i * step) | ||
| 237 | + return ticks | ||
| 238 | +}) | ||
| 239 | + | ||
| 240 | +// 饼图数据 — 时长 | ||
| 241 | +const durPieSegs = computed(() => { | ||
| 242 | + const s = summaryDur.value | ||
| 243 | + const items = [ | ||
| 244 | + { key: 'green', name: '绿灯', val: s.green?.seconds || 0, color: COLORS.green }, | ||
| 245 | + { key: 'yellow', name: '黄灯', val: s.yellow?.seconds || 0, color: COLORS.yellow }, | ||
| 246 | + { key: 'red', name: '红灯', val: s.red?.seconds || 0, color: COLORS.red }, | ||
| 247 | + { key: 'off', name: '离线', val: s.off?.seconds || 0, color: COLORS.off }, | ||
| 248 | + ].filter(x => x.val > 0).sort((a, b) => b.val - a.val) | ||
| 249 | + const total = items.reduce((sum, x) => sum + x.val, 0) || 1 | ||
| 250 | + let angle = -Math.PI / 2 | ||
| 251 | + return items.map(item => { | ||
| 252 | + const sweep = (item.val / total) * Math.PI * 2 | ||
| 253 | + const seg = { ...item, startAngle: angle, endAngle: angle + sweep, pct: ((item.val / total) * 100).toFixed(2) } | ||
| 254 | + angle += sweep | ||
| 255 | + return seg | ||
| 256 | + }) | ||
| 257 | +}) | ||
| 258 | + | ||
| 259 | +// 饼图数据 — 次数 | ||
| 260 | +const freqPieSegs = computed(() => { | ||
| 261 | + const s = summaryFreq.value | ||
| 262 | + const items = [ | ||
| 263 | + { key: 'green', name: '绿灯', val: s.green?.count || 0, color: COLORS.green }, | ||
| 264 | + { key: 'yellow', name: '黄灯', val: s.yellow?.count || 0, color: COLORS.yellow }, | ||
| 265 | + { key: 'red', name: '红灯', val: s.red?.count || 0, color: COLORS.red }, | ||
| 266 | + { key: 'off', name: '离线', val: s.off?.count || 0, color: COLORS.off }, | ||
| 267 | + ].filter(x => x.val > 0).sort((a, b) => b.val - a.val) | ||
| 268 | + const total = items.reduce((sum, x) => sum + x.val, 0) || 1 | ||
| 269 | + let angle = -Math.PI / 2 | ||
| 270 | + return items.map(item => { | ||
| 271 | + const sweep = (item.val / total) * Math.PI * 2 | ||
| 272 | + const seg = { ...item, startAngle: angle, endAngle: angle + sweep, pct: ((item.val / total) * 100).toFixed(2) } | ||
| 273 | + angle += sweep | ||
| 274 | + return seg | ||
| 275 | + }) | ||
| 276 | +}) | ||
| 277 | + | ||
| 278 | +// ========== hover 状态 ========== | ||
| 279 | +const barHover = reactive({ | ||
| 280 | + dur: { show: false, x: 0, y: 0, label: '', rows: [] }, | ||
| 281 | + freq: { show: false, x: 0, y: 0, label: '', rows: [] }, | ||
| 282 | +}) | ||
| 283 | +const pieHover = reactive({ | ||
| 284 | + dur: { show: false, x: 0, y: 0, seg: null }, | ||
| 285 | + freq: { show: false, x: 0, y: 0, seg: null }, | ||
| 286 | +}) | ||
| 287 | + | ||
| 288 | +// 存储柱子位置用于 hit test(每次重绘后更新) | ||
| 289 | +let durBarsMeta = [] | ||
| 290 | +let freqBarsMeta = [] | ||
| 291 | + | ||
| 292 | +// ========== 绘制函数 ========== | ||
| 293 | +const dpr = window.devicePixelRatio || 1 | ||
| 294 | +function setupCanvas(cvs, cssW, cssH) { | ||
| 295 | + cvs.width = Math.round(cssW * dpr) | ||
| 296 | + cvs.height = Math.round(cssH * dpr) | ||
| 297 | + cvs.style.width = cssW + 'px' | ||
| 298 | + cvs.style.height = cssH + 'px' | ||
| 299 | + const ctx = cvs.getContext('2d') | ||
| 300 | + ctx.setTransform(dpr, 0, 0, dpr, 0, 0) | ||
| 301 | + return { ctx, W: cssW, H: cssH } | ||
| 302 | +} | ||
| 303 | + | ||
| 304 | +function drawDurBar() { | ||
| 305 | + const cvs = durCanvas.value | ||
| 306 | + if (!cvs) return | ||
| 307 | + const wrap = cvs.parentElement | ||
| 308 | + const cssW = wrap ? wrap.clientWidth : durCanvasW.value | ||
| 309 | + const cssH = 260 | ||
| 310 | + const { ctx, W, H } = setupCanvas(cvs, cssW, cssH) | ||
| 311 | + ctx.clearRect(0, 0, W, H) | ||
| 312 | + const plotTop = 20, plotBottom = H - padB | ||
| 313 | + const plotH = plotBottom - plotTop | ||
| 314 | + const n = displayList.value.length | ||
| 315 | + const groupW = n ? (W - padL - 10) / n : 1 | ||
| 316 | + const ticks = durYTicks.value | ||
| 317 | + const maxSec = maxDurSec.value | ||
| 318 | + | ||
| 319 | + // Y轴刻度(最大值在上,0在下) | ||
| 320 | + ctx.font = '10px sans-serif' | ||
| 321 | + ctx.fillStyle = '#999' | ||
| 322 | + ctx.textAlign = 'right' | ||
| 323 | + for (let i = 0; i < ticks.length; i++) { | ||
| 324 | + const y = plotBottom - (i / (ticks.length - 1)) * plotH + 3 | ||
| 325 | + ctx.fillText(ticks[i], 32, y) | ||
| 326 | + } | ||
| 327 | + | ||
| 328 | + // X轴标签 | ||
| 329 | + ctx.font = '9px sans-serif' | ||
| 330 | + ctx.fillStyle = '#666' | ||
| 331 | + ctx.textAlign = 'center' | ||
| 332 | + displayList.value.forEach((item, i) => { | ||
| 333 | + ctx.fillText(item.label || formatDateShort(item.startDate), padL + (i + 0.5) * groupW, H - 4) | ||
| 334 | + }) | ||
| 335 | + | ||
| 336 | + // 网格线 | ||
| 337 | + ctx.strokeStyle = '#f0f0f0' | ||
| 338 | + ctx.lineWidth = 1 | ||
| 339 | + for (let i = 0; i < ticks.length; i++) { | ||
| 340 | + const y = plotBottom - (i / (ticks.length - 1)) * plotH | ||
| 341 | + ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(W - 10, y); ctx.stroke() | ||
| 342 | + } | ||
| 343 | + | ||
| 344 | + // 绘制柱子并记录位置(从底部向上堆叠:离线底 → 绿灯顶) | ||
| 345 | + durBarsMeta = [] | ||
| 346 | + displayList.value.forEach((item, idx) => { | ||
| 347 | + const bx = padL + idx * groupW + (groupW - Math.min(groupW - 8, 50)) / 2 | ||
| 348 | + const bw = Math.min(groupW - 8, 50) | ||
| 349 | + const stack = [ | ||
| 350 | + { k: 'off', sec: item.off?.seconds || 0 }, | ||
| 351 | + { k: 'red', sec: item.red?.seconds || 0 }, | ||
| 352 | + { k: 'yellow', sec: item.yellow?.seconds || 0 }, | ||
| 353 | + { k: 'green', sec: item.green?.seconds || 0 }, | ||
| 354 | + ] | ||
| 355 | + const scale = plotH / maxSec | ||
| 356 | + let curY = plotBottom | ||
| 357 | + const barRect = { x: bx, w: bw, idx, label: item.label || formatDateShort(item.startDate), colors: [] } | ||
| 358 | + stack.forEach(seg => { | ||
| 359 | + if (seg.sec <= 0) return | ||
| 360 | + const h = seg.sec * scale | ||
| 361 | + curY -= h | ||
| 362 | + ctx.fillStyle = COLORS[seg.k] | ||
| 363 | + ctx.fillRect(bx, curY, bw, h) | ||
| 364 | + barRect.colors.push({ c: COLORS[seg.k], n: COLOR_NAMES[seg.k], t: formatSec(seg.sec), v: seg.sec, rect: { x: bx, y: curY, w: bw, h } }) | ||
| 365 | + }) | ||
| 366 | + durBarsMeta.push(barRect) | ||
| 367 | + }) | ||
| 368 | +} | ||
| 369 | + | ||
| 370 | +function drawFreqBar() { | ||
| 371 | + const cvs = freqCanvas.value | ||
| 372 | + if (!cvs) return | ||
| 373 | + const wrap = cvs.parentElement | ||
| 374 | + const cssW = wrap ? wrap.clientWidth : freqCanvasW.value | ||
| 375 | + const cssH = 280 | ||
| 376 | + const { ctx, W, H } = setupCanvas(cvs, cssW, cssH) | ||
| 377 | + ctx.clearRect(0, 0, W, H) | ||
| 378 | + const plotTop = 28, plotBottom = H - padB | ||
| 379 | + const plotH = plotBottom - plotTop | ||
| 380 | + const n = displayList.value.length | ||
| 381 | + const groupW = n ? (W - padL - 10) / n : 1 | ||
| 382 | + const ticks = freqYTicks.value | ||
| 383 | + const maxCnt = freqYTicks.value[ticks.length - 1] || maxFreqCount.value || 1 | ||
| 384 | + | ||
| 385 | + // Y轴(最大值在上,0在下) | ||
| 386 | + ctx.font = '10px sans-serif'; ctx.fillStyle = '#999'; ctx.textAlign = 'right' | ||
| 387 | + for (let i = 0; i < ticks.length; i++) { | ||
| 388 | + const y = plotBottom - (i / (ticks.length - 1)) * plotH + 3 | ||
| 389 | + ctx.fillText(ticks[i] + '次', 32, y) | ||
| 390 | + } | ||
| 391 | + | ||
| 392 | + // X轴 | ||
| 393 | + ctx.font = '9px sans-serif'; ctx.fillStyle = '#666'; ctx.textAlign = 'center' | ||
| 394 | + displayList.value.forEach((item, i) => { | ||
| 395 | + ctx.fillText(item.label || formatDateShort(item.startDate), padL + (i + 0.5) * groupW, H - 4) | ||
| 396 | + }) | ||
| 397 | + | ||
| 398 | + // 网格 | ||
| 399 | + ctx.strokeStyle = '#f0f0f0'; ctx.lineWidth = 1 | ||
| 400 | + for (let i = 0; i < ticks.length; i++) { | ||
| 401 | + const y = plotBottom - (i / (ticks.length - 1)) * plotH | ||
| 402 | + ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(W - 10, y); ctx.stroke() | ||
| 403 | + } | ||
| 404 | + | ||
| 405 | + // 柱子(从底部向上堆叠:离线底 → 绿灯顶) | ||
| 406 | + freqBarsMeta = [] | ||
| 407 | + displayList.value.forEach((item, idx) => { | ||
| 408 | + const bx = padL + idx * groupW + (groupW - Math.min(groupW - 8, 50)) / 2 | ||
| 409 | + const bw = Math.min(groupW - 8, 50) | ||
| 410 | + const scale = plotH / maxCnt | ||
| 411 | + let curY = plotBottom | ||
| 412 | + const barRect = { x: bx, w: bw, idx, label: item.label || formatDateShort(item.startDate), colors: [] } | ||
| 413 | + const stack = [ | ||
| 414 | + { k: 'off', cnt: item.off?.count || 0 }, | ||
| 415 | + { k: 'red', cnt: item.red?.count || 0 }, | ||
| 416 | + { k: 'yellow', cnt: item.yellow?.count || 0 }, | ||
| 417 | + { k: 'green', cnt: item.green?.count || 0 }, | ||
| 418 | + ] | ||
| 419 | + stack.forEach(seg => { | ||
| 420 | + if (seg.cnt <= 0) return | ||
| 421 | + const h = seg.cnt * scale | ||
| 422 | + curY -= h | ||
| 423 | + ctx.fillStyle = COLORS[seg.k] | ||
| 424 | + ctx.fillRect(bx, curY, bw, h) | ||
| 425 | + barRect.colors.push({ c: COLORS[seg.k], n: COLOR_NAMES[seg.k], t: '', v: seg.cnt, rect: { x: bx, y: curY, w: bw, h } }) | ||
| 426 | + }) | ||
| 427 | + freqBarsMeta.push(barRect) | ||
| 428 | + }) | ||
| 429 | +} | ||
| 430 | + | ||
| 431 | +let durPieHighlightIdx = -1 | ||
| 432 | +let freqPieHighlightIdx = -1 | ||
| 433 | + | ||
| 434 | +function drawDurPie(highlightIdx = -1) { | ||
| 435 | + const cvs = durPieCanvas.value | ||
| 436 | + if (!cvs) return | ||
| 437 | + const { ctx } = setupCanvas(cvs, 180, 180) | ||
| 438 | + ctx.clearRect(0, 0, 180, 180) | ||
| 439 | + const cx = 90, cy = 90, R = 68 | ||
| 440 | + const segs = durPieSegs.value | ||
| 441 | + | ||
| 442 | + segs.forEach((seg, i) => { | ||
| 443 | + const isHL = i === highlightIdx | ||
| 444 | + const r = isHL ? R + 5 : R | ||
| 445 | + ctx.beginPath() | ||
| 446 | + ctx.moveTo(cx, cy) | ||
| 447 | + ctx.arc(cx, cy, r, seg.startAngle, seg.endAngle) | ||
| 448 | + ctx.closePath() | ||
| 449 | + ctx.fillStyle = isHL ? lightenColor(seg.color, 20) : seg.color | ||
| 450 | + ctx.fill() | ||
| 451 | + }) | ||
| 452 | + | ||
| 453 | + // 每个扇区中间显示白色文字(百分比 + 名称) | ||
| 454 | + ctx.fillStyle = '#fff' | ||
| 455 | + ctx.textAlign = 'center' | ||
| 456 | + ctx.textBaseline = 'middle' | ||
| 457 | + segs.forEach((seg) => { | ||
| 458 | + const midAngle = (seg.startAngle + seg.endAngle) / 2 | ||
| 459 | + // 文字位置在扇区半径的约60%处 | ||
| 460 | + const tx = cx + Math.cos(midAngle) * R * 0.58 | ||
| 461 | + const ty = cy + Math.sin(midAngle) * R * 0.58 | ||
| 462 | + ctx.font = 'bold 12px sans-serif' | ||
| 463 | + ctx.fillText(seg.pct + '%', tx, ty - 6) | ||
| 464 | + ctx.font = '9px sans-serif' | ||
| 465 | + ctx.fillText(seg.name, tx, ty + 7) | ||
| 466 | + }) | ||
| 467 | +} | ||
| 468 | + | ||
| 469 | +function drawFreqPie(highlightIdx = -1) { | ||
| 470 | + const cvs = freqPieCanvas.value | ||
| 471 | + if (!cvs) return | ||
| 472 | + const { ctx } = setupCanvas(cvs, 180, 180) | ||
| 473 | + ctx.clearRect(0, 0, 180, 180) | ||
| 474 | + const cx = 90, cy = 90, R = 68 | ||
| 475 | + const segs = freqPieSegs.value | ||
| 476 | + | ||
| 477 | + segs.forEach((seg, i) => { | ||
| 478 | + const isHL = i === highlightIdx | ||
| 479 | + const r = isHL ? R + 5 : R | ||
| 480 | + ctx.beginPath() | ||
| 481 | + ctx.moveTo(cx, cy) | ||
| 482 | + ctx.arc(cx, cy, r, seg.startAngle, seg.endAngle) | ||
| 483 | + ctx.closePath() | ||
| 484 | + ctx.fillStyle = isHL ? lightenColor(seg.color, 20) : seg.color | ||
| 485 | + ctx.fill() | ||
| 486 | + }) | ||
| 487 | + | ||
| 488 | + // 每个扇区中间显示白色文字(百分比 + 名称) | ||
| 489 | + ctx.fillStyle = '#fff' | ||
| 490 | + ctx.textAlign = 'center' | ||
| 491 | + ctx.textBaseline = 'middle' | ||
| 492 | + segs.forEach((seg) => { | ||
| 493 | + const midAngle = (seg.startAngle + seg.endAngle) / 2 | ||
| 494 | + const tx = cx + Math.cos(midAngle) * R * 0.58 | ||
| 495 | + const ty = cy + Math.sin(midAngle) * R * 0.58 | ||
| 496 | + ctx.font = 'bold 12px sans-serif' | ||
| 497 | + ctx.fillText(seg.pct + '%', tx, ty - 6) | ||
| 498 | + ctx.font = '9px sans-serif' | ||
| 499 | + ctx.fillText(seg.name, tx, ty + 7) | ||
| 500 | + }) | ||
| 501 | +} | ||
| 502 | + | ||
| 503 | +function lightenColor(hex, amt) { | ||
| 504 | + let c = hex.replace('#','') | ||
| 505 | + if(c.length===3) c=c[0]+c[0]+c[1]+c[1]+c[2]+c[2] | ||
| 506 | + const num = parseInt(c,16) | ||
| 507 | + const r = Math.min(255,(num>>16)+amt), g = Math.min(255,((num>>8)&0xff)+amt), b = Math.min(255,(num&0xff)+amt) | ||
| 508 | + return `rgb(${r},${g},${b})` | ||
| 509 | +} | ||
| 510 | + | ||
| 511 | +// ========== hover 事件处理 ========== | ||
| 512 | +function getCanvasPos(e, canvasEl) { | ||
| 513 | + const rect = canvasEl.getBoundingClientRect() | ||
| 514 | + const scaleX = canvasEl.width / rect.width | ||
| 515 | + const scaleY = canvasEl.height / rect.height | ||
| 516 | + return { x: (e.clientX - rect.left) * scaleX, y: (e.clientY - rect.top) * scaleY } | ||
| 517 | +} | ||
| 518 | + | ||
| 519 | +function onBarMove(e, type) { | ||
| 520 | + const cvs = type === 'dur' ? durCanvas.value : freqCanvas.value | ||
| 521 | + if (!cvs) return | ||
| 522 | + const pos = getCanvasPos(e, cvs) | ||
| 523 | + const metaArr = type === 'dur' ? durBarsMeta : freqBarsMeta | ||
| 524 | + const state = barHover[type] | ||
| 525 | + | ||
| 526 | + for (const bar of metaArr) { | ||
| 527 | + if (pos.x >= bar.x && pos.x <= bar.x + bar.w) { | ||
| 528 | + state.show = true | ||
| 529 | + state.x = e.offsetX + 10 | ||
| 530 | + state.y = e.offsetY - 10 | ||
| 531 | + state.label = bar.label | ||
| 532 | + state.rows = bar.colors.map(c => ({ n: c.n, c: c.c, t: c.t, v: c.v })) | ||
| 533 | + return | ||
| 534 | + } | ||
| 535 | + } | ||
| 536 | + state.show = false | ||
| 537 | +} | ||
| 538 | + | ||
| 539 | +function onBarLeave(type) { | ||
| 540 | + barHover[type].show = false | ||
| 541 | +} | ||
| 542 | + | ||
| 543 | +function onPieMove(e, type) { | ||
| 544 | + const cvs = type === 'dur' ? durPieCanvas.value : freqPieCanvas.value | ||
| 545 | + if (!cvs) return | ||
| 546 | + const pos = getCanvasPos(e, cvs) | ||
| 547 | + const cx = 90, cy = 90 | ||
| 548 | + const dx = pos.x - cx, dy = pos.y - cy | ||
| 549 | + const dist = Math.sqrt(dx * dx + dy * dy) | ||
| 550 | + const segs = type === 'dur' ? durPieSegs.value : freqPieSegs.value | ||
| 551 | + const state = pieHover[type] | ||
| 552 | + | ||
| 553 | + if (dist > 0 && dist <= 76) { | ||
| 554 | + let angle = Math.atan2(dy, dx) | ||
| 555 | + if (angle < -Math.PI / 2) angle += Math.PI * 2 | ||
| 556 | + for (let i = 0; i < segs.length; i++) { | ||
| 557 | + let sa = segs[i].startAngle, ea = segs[i].endAngle | ||
| 558 | + if (sa < -Math.PI / 2) sa += Math.PI * 2 | ||
| 559 | + if (ea < -Math.PI / 2) ea += Math.PI * 2 | ||
| 560 | + if (angle >= sa && angle < ea) { | ||
| 561 | + state.show = true | ||
| 562 | + state.x = e.offsetX + 10 | ||
| 563 | + state.y = e.offsetY - 10 | ||
| 564 | + state.seg = segs[i] | ||
| 565 | + if (type === 'dur') { durPieHighlightIdx = i; drawDurPie(i) } | ||
| 566 | + else { freqPieHighlightIdx = i; drawFreqPie(i) } | ||
| 567 | + return | ||
| 568 | + } | ||
| 569 | + } | ||
| 570 | + } | ||
| 571 | + if (state.show) { | ||
| 572 | + state.show = false | ||
| 573 | + if (type === 'dur') { durPieHighlightIdx = -1; drawDurPie(-1) } | ||
| 574 | + else { freqPieHighlightIdx = -1; drawFreqPie(-1) } | ||
| 575 | + } | ||
| 576 | +} | ||
| 577 | + | ||
| 578 | +function onPieLeave(type) { | ||
| 579 | + pieHover[type].show = false | ||
| 580 | + if (type === 'dur') { durPieHighlightIdx = -1; drawDurPie(-1) } | ||
| 581 | + else { freqPieHighlightIdx = -1; drawFreqPie(-1) } | ||
| 582 | +} | ||
| 583 | + | ||
| 584 | +// ========== 重绘全部 ========== | ||
| 585 | +function redrawAll() { | ||
| 586 | + nextTick(() => { | ||
| 587 | + drawDurBar() | ||
| 588 | + drawFreqBar() | ||
| 589 | + drawDurPie(-1) | ||
| 590 | + drawFreqPie(-1) | ||
| 591 | + }) | ||
| 592 | +} | ||
| 593 | + | ||
| 594 | +// ========== 接口调用 ========== | ||
| 595 | +async function fetchOeeData() { | ||
| 596 | + const dtuSn = props.device?._raw?.dtuSn || '' | ||
| 597 | + if (!dtuSn) return | ||
| 598 | + let params = `dtuSn=${dtuSn}&type=${queryMode.value}` | ||
| 599 | + if (queryMode.value === 'day') { | ||
| 600 | + const [start, end] = queryDateRange.value || [] | ||
| 601 | + if (!start || !end) return | ||
| 602 | + params += `&startDate=${start}&endDate=${end}` | ||
| 603 | + } | ||
| 604 | + try { | ||
| 605 | + const res = await fetch(`/api/device/oeeStats?${params}`) | ||
| 606 | + oeeData.value = await res.json() | ||
| 607 | + } catch (err) { | ||
| 608 | + console.error('获取稼动率数据失败:', err) | ||
| 609 | + } | ||
| 610 | +} | ||
| 611 | + | ||
| 612 | +watch(queryMode, () => fetchOeeData()) | ||
| 613 | +watch(oeeData, () => redrawAll()) | ||
| 614 | +watch([durCanvasW, freqCanvasW], () => redrawAll()) | ||
| 615 | + | ||
| 616 | +watch(() => props.visible, (val) => { | ||
| 617 | + if (val) { fetchOeeData() } | ||
| 618 | +}) | ||
| 619 | + | ||
| 620 | +onMounted(() => { | ||
| 621 | + if (props.visible) fetchOeeData() | ||
| 622 | +}) | ||
| 623 | +</script> | ||
| 624 | + | ||
| 625 | +<style scoped> | ||
| 626 | +.util-dialog :deep(.el-dialog) { | ||
| 627 | + max-height: 92vh; | ||
| 628 | + display: flex; | ||
| 629 | + flex-direction: column; | ||
| 630 | +} | ||
| 631 | +.util-dialog :deep(.el-dialog__header) { | ||
| 632 | + padding: 10px 20px; | ||
| 633 | + border-bottom: 1px solid #e8e8e8; | ||
| 634 | + margin: 0; | ||
| 635 | + flex-shrink: 0; | ||
| 636 | +} | ||
| 637 | +.util-dialog :deep(.el-dialog__body) { | ||
| 638 | + overflow-y: auto; | ||
| 639 | + flex: 1; | ||
| 640 | +} | ||
| 641 | +.dialog-header { | ||
| 642 | + display: flex; | ||
| 643 | + align-items: center; | ||
| 644 | + gap: 12px; | ||
| 645 | + flex-wrap: wrap; | ||
| 646 | +} | ||
| 647 | +.title-text { | ||
| 648 | + font-size: 14px; | ||
| 649 | + font-weight: bold; | ||
| 650 | + color: #333; | ||
| 651 | + white-space: nowrap; | ||
| 652 | +} | ||
| 653 | +.header-center { | ||
| 654 | + display: flex; | ||
| 655 | + align-items: center; | ||
| 656 | + flex: 1; | ||
| 657 | +} | ||
| 658 | +.header-right { | ||
| 659 | + display: flex; | ||
| 660 | + align-items: center; | ||
| 661 | + gap: 8px; | ||
| 662 | + white-space: nowrap; | ||
| 663 | +} | ||
| 664 | + | ||
| 665 | +.util-body { padding: 0; } | ||
| 666 | +.top-row, .bottom-row { | ||
| 667 | + display: grid; | ||
| 668 | + grid-template-columns: minmax(0, 1fr) minmax(240px, 280px); | ||
| 669 | + gap: 12px; | ||
| 670 | + padding: 12px 16px; | ||
| 671 | +} | ||
| 672 | +.bottom-row { border-top: 1px solid #e8e8e8; } | ||
| 673 | + | ||
| 674 | +.chart-card, .pie-card { | ||
| 675 | + border: 1px solid #ebeef5; | ||
| 676 | + border-radius: 6px; | ||
| 677 | + padding: 16px; | ||
| 678 | + background: #fff; | ||
| 679 | +} | ||
| 680 | +.card-title-row { | ||
| 681 | + display: flex; | ||
| 682 | + align-items: baseline; | ||
| 683 | + gap: 8px; | ||
| 684 | + margin-bottom: 12px; | ||
| 685 | +} | ||
| 686 | +.card-title-row h4 { | ||
| 687 | + font-size: 14px; | ||
| 688 | + font-weight: bold; | ||
| 689 | + color: #333; | ||
| 690 | + margin: 0; | ||
| 691 | +} | ||
| 692 | +.card-subtitle { font-size: 11px; color: #aaa; } | ||
| 693 | + | ||
| 694 | +.legend-inline { | ||
| 695 | + display: flex; | ||
| 696 | + gap: 16px; | ||
| 697 | + font-size: 11px; | ||
| 698 | + color: #666; | ||
| 699 | + margin-bottom: 8px; | ||
| 700 | +} | ||
| 701 | +.leg { display: flex; align-items: center; gap: 4px; } | ||
| 702 | +.dot { width: 9px; height: 9px; border-radius: 2px; display: inline-block; } | ||
| 703 | +.dot.green { background: #67c23a; } | ||
| 704 | +.dot.yellow { background: #e6a23c; } | ||
| 705 | +.dot.red { background: #f56c6c; } | ||
| 706 | +.dot.gray { background: #909399; } | ||
| 707 | + | ||
| 708 | +.canvas-wrap { | ||
| 709 | + position: relative; | ||
| 710 | + width: 100%; | ||
| 711 | +} | ||
| 712 | +.canvas-wrap canvas { | ||
| 713 | + width: 100%; | ||
| 714 | + height: auto; | ||
| 715 | + display: block; | ||
| 716 | +} | ||
| 717 | + | ||
| 718 | +/* 柱状图 tooltip */ | ||
| 719 | +.bar-tooltip { | ||
| 720 | + position: absolute; | ||
| 721 | + z-index: 10; | ||
| 722 | + background: rgba(0,0,0,.75); | ||
| 723 | + color: #fff; | ||
| 724 | + padding: 8px 10px; | ||
| 725 | + border-radius: 4px; | ||
| 726 | + font-size: 11px; | ||
| 727 | + line-height: 1.7; | ||
| 728 | + pointer-events: none; | ||
| 729 | + white-space: nowrap; | ||
| 730 | + transform: translate(8px, -100%); | ||
| 731 | +} | ||
| 732 | +.tt-title { | ||
| 733 | + font-weight: bold; | ||
| 734 | + margin-bottom: 2px; | ||
| 735 | + border-bottom: 1px solid rgba(255,255,255,.25); | ||
| 736 | + padding-bottom: 2px; | ||
| 737 | +} | ||
| 738 | +.tt-row { display: flex; align-items: center; gap: 4px; } | ||
| 739 | +.tt-dot { width: 8px; height: 8px; border-radius: 2px; display: inline-block; flex-shrink: 0; } | ||
| 740 | + | ||
| 741 | +/* 环形图区域 */ | ||
| 742 | +.pie-main { | ||
| 743 | + display: flex; | ||
| 744 | + flex-direction: column; | ||
| 745 | + align-items: center; | ||
| 746 | + gap: 12px; | ||
| 747 | +} | ||
| 748 | +.pie-canvas-wrap { width: 180px; height: 180px; } | ||
| 749 | +.pie-canvas-wrap canvas { width: 180px; height: 180px; } | ||
| 750 | + | ||
| 751 | +/* 环形图 tooltip */ | ||
| 752 | +.pie-tooltip { | ||
| 753 | + position: absolute; | ||
| 754 | + z-index: 10; | ||
| 755 | + background: rgba(0,0,0,.75); | ||
| 756 | + color: #fff; | ||
| 757 | + padding: 6px 10px; | ||
| 758 | + border-radius: 4px; | ||
| 759 | + font-size: 11px; | ||
| 760 | + pointer-events: none; | ||
| 761 | + white-space: nowrap; | ||
| 762 | + transform: translate(8px, -100%); | ||
| 763 | +} | ||
| 764 | + | ||
| 765 | +.pie-legend { | ||
| 766 | + display: flex; | ||
| 767 | + gap: 14px; | ||
| 768 | + font-size: 11px; | ||
| 769 | + color: #666; | ||
| 770 | +} | ||
| 771 | +.pleg { display: flex; align-items: center; gap: 4px; } | ||
| 772 | +.pdot { width: 10px; height: 10px; border-radius: 2px; } | ||
| 773 | +.pdot.green { background: #67c23a; } | ||
| 774 | +.pdot.yellow { background: #e6a23c; } | ||
| 775 | +.pdot.red { background: #f56c6c; } | ||
| 776 | +.pdot.gray { background: #909399; } | ||
| 777 | +.date-wrap { | ||
| 778 | + display: inline-block; | ||
| 779 | + width: 360px; | ||
| 780 | + margin-left: 8px; | ||
| 781 | + overflow: hidden; | ||
| 782 | + vertical-align: middle; | ||
| 783 | +} | ||
| 784 | +</style> |
src/components/WarningSettingDialog.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <el-dialog | ||
| 3 | + :model-value="visible" | ||
| 4 | + @update:model-value="$emit('update:visible', $event)" | ||
| 5 | + title="" | ||
| 6 | + width="calc(100vw - 40px)" | ||
| 7 | + :style="{ maxWidth: '1400px' }" | ||
| 8 | + top="3vh" | ||
| 9 | + destroy-on-close | ||
| 10 | + class="warn-dialog" | ||
| 11 | + > | ||
| 12 | + <template #header> | ||
| 13 | + <div class="dialog-header"> | ||
| 14 | + <div class="header-left"> | ||
| 15 | + <span :class="['htab', { active: activeTab === 'trigger' }]" @click="activeTab = 'trigger'">触发器设置</span> | ||
| 16 | + <span :class="['htab', { active: activeTab === 'config' }]" @click="activeTab = 'config'">配置</span> | ||
| 17 | + </div> | ||
| 18 | + <div class="header-right"> | ||
| 19 | + <span class="device-select">{{ device?.name || '能耗设备2' }} <el-icon><ArrowDown /></el-icon> 返回</span> | ||
| 20 | + </div> | ||
| 21 | + </div> | ||
| 22 | + </template> | ||
| 23 | + | ||
| 24 | + <div class="warn-body"> | ||
| 25 | + <!-- 触发器Tab --> | ||
| 26 | + <div v-if="activeTab === 'trigger'" class="tab-content"> | ||
| 27 | + <div class="toolbar"> | ||
| 28 | + <div v-for="t in subTabs" :key="t" :class="['sub-tab', { active: activeSub === t }]" | ||
| 29 | + @click="activeSub = t">{{ t }}</div> | ||
| 30 | + <div style="flex:1"></div> | ||
| 31 | + <el-input v-model="searchKey" placeholder="输入人/机料号" size="small" style="width:180px"> | ||
| 32 | + <template #prefix><el-icon><Search /></el-icon></template> | ||
| 33 | + </el-input> | ||
| 34 | + </div> | ||
| 35 | + | ||
| 36 | + <div class="table-wrap"> | ||
| 37 | + <el-table :data="warningList" size="small" stripe border style="width:100%;font-size:12px;" empty-text="暂无数据"> | ||
| 38 | + <el-table-column prop="name" label="触发器名称" min-width="140"> | ||
| 39 | + <template #default="{ row }"><span class="link-text">{{ row.name }}</span></template> | ||
| 40 | + </el-table-column> | ||
| 41 | + <el-table-column prop="condition" label="触发条件" min-width="130" /> | ||
| 42 | + <el-table-column prop="threshold" label="值域间隔" width="90"> | ||
| 43 | + <template #default="{ row }"> | ||
| 44 | + {{ row.threshold }} <el-icon :size="12" color="#409eff" style="cursor:pointer;"><EditPen /></el-icon> | ||
| 45 | + </template> | ||
| 46 | + </el-table-column> | ||
| 47 | + <el-table-column prop="duration" label="预警时间" width="110"> | ||
| 48 | + <template #default="{ row }"> | ||
| 49 | + <el-icon :size="12" color="#67c23a" style="margin-right:2px;"><Clock /></el-icon>{{ row.duration }} | ||
| 50 | + </template> | ||
| 51 | + </el-table-column> | ||
| 52 | + <el-table-column prop="receiver" label="接收人岗" width="80"> | ||
| 53 | + <template #default="{ row }"> | ||
| 54 | + <el-icon :size="12" color="#409eff" style="margin-right:2px;"><User /></el-icon>{{ row.receiver }} | ||
| 55 | + </template> | ||
| 56 | + </el-table-column> | ||
| 57 | + <el-table-column prop="sendTime" label="推送时间" width="110" /> | ||
| 58 | + <el-table-column prop="method" label="接收方式" width="90" /> | ||
| 59 | + <el-table-column prop="action" label="操作" width="70" align="center"> | ||
| 60 | + <template #default><el-icon :size="14" color="#409eff" style="cursor:pointer;"><Edit /></el-icon></template> | ||
| 61 | + </el-table-column> | ||
| 62 | + <el-table-column prop="group" label="联系组" width="110" /> | ||
| 63 | + <el-table-column prop="settings" label="设置" min-width="200"> | ||
| 64 | + <template #default="{ row }"> | ||
| 65 | + <span v-for="(s, i) in row.settings" :key="i" :class="'tag-'+s.color">{{ s.text }}</span> | ||
| 66 | + </template> | ||
| 67 | + </el-table-column> | ||
| 68 | + </el-table> | ||
| 69 | + | ||
| 70 | + <div class="pagination-footer"> | ||
| 71 | + <span>共 {{ warningList.length }} 条</span> | ||
| 72 | + <el-pagination :current-page="1" :page-size="10" layout="prev, pager, next" :total="warningList.length" small /> | ||
| 73 | + </div> | ||
| 74 | + </div> | ||
| 75 | + </div> | ||
| 76 | + | ||
| 77 | + <!-- 配置Tab --> | ||
| 78 | + <div v-else class="tab-content config-view"> | ||
| 79 | + <p style="color:#999;padding:20px;text-align:center;">配置内容</p> | ||
| 80 | + </div> | ||
| 81 | + </div> | ||
| 82 | + </el-dialog> | ||
| 83 | +</template> | ||
| 84 | + | ||
| 85 | +<script setup> | ||
| 86 | +import { ref } from 'vue' | ||
| 87 | +import { Search, ArrowDown, EditPen, Clock, User, Edit } from '@element-plus/icons-vue' | ||
| 88 | + | ||
| 89 | +defineProps({ visible: Boolean, device: Object }) | ||
| 90 | +defineEmits(['update:visible']) | ||
| 91 | + | ||
| 92 | +const activeTab = ref('trigger') | ||
| 93 | +const activeSub = ref('全部') | ||
| 94 | +const searchKey = ref('') | ||
| 95 | +const subTabs = ['全部', '相电压', '线电压', '电流', '温度', '有功功率', '无功功率', '电能', '功率因数'] | ||
| 96 | + | ||
| 97 | +const warningList = ref([ | ||
| 98 | + { | ||
| 99 | + name: 'C相电压区间报警', | ||
| 100 | + condition: '>C相电压< 20A', | ||
| 101 | + threshold: '0分', | ||
| 102 | + duration: '00:00-24:00', | ||
| 103 | + receiver: '', | ||
| 104 | + sendTime: '', | ||
| 105 | + method: '', | ||
| 106 | + group: '', | ||
| 107 | + settings: [ | ||
| 108 | + { text: '启用', color: 'blue' }, { text: '禁用', color: 'gray' }, | ||
| 109 | + { text: '解除', color: 'red' }, { text: '添加联系人', color: '' }, | ||
| 110 | + { text: '编辑联系组', color: '' } | ||
| 111 | + ] | ||
| 112 | + }, | ||
| 113 | + { | ||
| 114 | + name: 'B相电压区间报警', | ||
| 115 | + condition: '>B相电压< 20A', | ||
| 116 | + threshold: '0分', | ||
| 117 | + duration: '00:00-24:00', | ||
| 118 | + receiver: '', | ||
| 119 | + sendTime: '', | ||
| 120 | + method: '', | ||
| 121 | + group: '', | ||
| 122 | + settings: [ | ||
| 123 | + { text: '启用', color: 'blue' }, { text: '禁用', color: 'gray' }, | ||
| 124 | + { text: '', color: '' }, | ||
| 125 | + { text: '', color: '' }, | ||
| 126 | + { text: '', color: '' } | ||
| 127 | + ] | ||
| 128 | + }, | ||
| 129 | + { | ||
| 130 | + name: 'A相电压区间报警', | ||
| 131 | + condition: '>A相电压< 20A', | ||
| 132 | + threshold: '0分', | ||
| 133 | + duration: '00:00-24:00', | ||
| 134 | + receiver: '', | ||
| 135 | + sendTime: '', | ||
| 136 | + method: '', | ||
| 137 | + group: '', | ||
| 138 | + settings: [ | ||
| 139 | + { text: '启用', color: 'blue' }, { text: '禁用', color: 'gray' }, | ||
| 140 | + { text: '解除', color: 'red' }, { text: '添加联系人', color: '' }, | ||
| 141 | + { text: '编辑联系组', color: '' } | ||
| 142 | + ] | ||
| 143 | + }, | ||
| 144 | + { | ||
| 145 | + name: '总各功功率高', | ||
| 146 | + condition: '>20kW', | ||
| 147 | + threshold: '0分', | ||
| 148 | + duration: '00:00-24:00', | ||
| 149 | + receiver: '', | ||
| 150 | + sendTime: '', | ||
| 151 | + method: '', | ||
| 152 | + group: '', | ||
| 153 | + settings: [ | ||
| 154 | + { text: '启用', color: 'blue' }, { text: '禁用', color: 'gray' }, | ||
| 155 | + { text: '解除', color: 'red' }, { text: '添加联系人', color: '' }, | ||
| 156 | + { text: '编辑联系组', color: '' } | ||
| 157 | + ] | ||
| 158 | + }, | ||
| 159 | + { | ||
| 160 | + name: 'A相电电流低', | ||
| 161 | + condition: '<5A', | ||
| 162 | + threshold: '0分', | ||
| 163 | + duration: '00:00-24:00', | ||
| 164 | + receiver: '', | ||
| 165 | + sendTime: '', | ||
| 166 | + method: '', | ||
| 167 | + group: '', | ||
| 168 | + settings: [ | ||
| 169 | + { text: '启用', color: 'blue' }, { text: '禁用', color: 'gray' }, | ||
| 170 | + { text: '解除', color: 'red' }, { text: '添加联系人', color: '' }, | ||
| 171 | + { text: '编辑联系组', color: '' } | ||
| 172 | + ] | ||
| 173 | + } | ||
| 174 | +]) | ||
| 175 | +</script> | ||
| 176 | + | ||
| 177 | +<style scoped> | ||
| 178 | +.warn-dialog :deep(.el-dialog){max-height:92vh;display:flex;flex-direction:column;} | ||
| 179 | +.warn-dialog :deep(.el-dialog__header){padding:8px 16px;border-bottom:1px solid #e8e8e8;margin:0;flex-shrink:0;} | ||
| 180 | +.warn-dialog :deep(.el-dialog__body){overflow-y:auto;flex:1;padding:0;display:flex;flex-direction:column;} | ||
| 181 | +.dialog-header{display:flex;align-items:center;justify-content:space-between;} | ||
| 182 | +.header-left{display:flex;gap:4px;} | ||
| 183 | +.htab{padding:6px 18px;font-size:13px;cursor:pointer;color:#666;border-bottom:2px solid transparent;} | ||
| 184 | +.htab.active{color:#409eff;font-weight:bold;border-bottom-color:#409eff;} | ||
| 185 | +.header-right{font-size:13px;color:#409eff;cursor:pointer;display:flex;align-items:center;gap:4px;} | ||
| 186 | +.device-select{} | ||
| 187 | + | ||
| 188 | +.warn-body{display:flex;flex-direction:column;flex:1;overflow:hidden;} | ||
| 189 | +.tab-content{display:flex;flex-direction:column;flex:1;} | ||
| 190 | + | ||
| 191 | +.toolbar{background:#fff;padding:10px 16px;display:flex;align-items:center;gap:6px;border-bottom:1px solid #eee;} | ||
| 192 | +.sub-tab{padding:5px 14px;font-size:12px;cursor:pointer;color:#666;border-radius:3px;} | ||
| 193 | +.sub-tab.active{background:#409eff;color:#fff;font-weight:bold;} | ||
| 194 | + | ||
| 195 | +.table-wrap{flex:1;background:#fff;margin:12px 16px;border-radius:6px;overflow:hidden;} | ||
| 196 | +.link-text{color:#409eff;cursor:pointer;} | ||
| 197 | +.tag-blue{background:#ecf5ff;color:#409eff;padding:1px 6px;border-radius:2px;font-size:11px;margin:1px 2px;} | ||
| 198 | +.tag-gray{background:#f4f4f5;color:#909399;padding:1px 6px;border-radius:2px;font-size:11px;margin:1px 2px;} | ||
| 199 | +.tag-red{background:#fef0f0;color:#f56c6c;padding:1px 6px;border-radius:2px;font-size:11px;margin:1px 2px;} | ||
| 200 | + | ||
| 201 | +.pagination-footer{padding:10px 16px;display:flex;justify-content:space-between;align-items:center;border-top:1px solid #f0f0f0;font-size:12px;color:#999;} | ||
| 202 | +.config-view{background:#fff;margin:12px 16px;border-radius:6px;} | ||
| 203 | +</style> |
src/components/WelcomeItem.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="item"> | ||
| 3 | + <i> | ||
| 4 | + <slot name="icon"></slot> | ||
| 5 | + </i> | ||
| 6 | + <div class="details"> | ||
| 7 | + <h3> | ||
| 8 | + <slot name="heading"></slot> | ||
| 9 | + </h3> | ||
| 10 | + <slot></slot> | ||
| 11 | + </div> | ||
| 12 | + </div> | ||
| 13 | +</template> | ||
| 14 | + | ||
| 15 | +<style scoped> | ||
| 16 | +.item { | ||
| 17 | + margin-top: 2rem; | ||
| 18 | + display: flex; | ||
| 19 | + position: relative; | ||
| 20 | +} | ||
| 21 | + | ||
| 22 | +.details { | ||
| 23 | + flex: 1; | ||
| 24 | + margin-left: 1rem; | ||
| 25 | +} | ||
| 26 | + | ||
| 27 | +i { | ||
| 28 | + display: flex; | ||
| 29 | + place-items: center; | ||
| 30 | + place-content: center; | ||
| 31 | + width: 32px; | ||
| 32 | + height: 32px; | ||
| 33 | + | ||
| 34 | + color: var(--color-text); | ||
| 35 | +} | ||
| 36 | + | ||
| 37 | +h3 { | ||
| 38 | + font-size: 1.2rem; | ||
| 39 | + font-weight: 500; | ||
| 40 | + margin-bottom: 0.4rem; | ||
| 41 | + color: var(--color-heading); | ||
| 42 | +} | ||
| 43 | + | ||
| 44 | +@media (min-width: 1024px) { | ||
| 45 | + .item { | ||
| 46 | + margin-top: 0; | ||
| 47 | + padding: 0.4rem 0 1rem calc(var(--section-gap) / 2); | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + i { | ||
| 51 | + top: calc(50% - 25px); | ||
| 52 | + left: -26px; | ||
| 53 | + position: absolute; | ||
| 54 | + border: 1px solid var(--color-border); | ||
| 55 | + background: var(--color-background); | ||
| 56 | + border-radius: 8px; | ||
| 57 | + width: 50px; | ||
| 58 | + height: 50px; | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + .item:before { | ||
| 62 | + content: ' '; | ||
| 63 | + border-left: 1px solid var(--color-border); | ||
| 64 | + position: absolute; | ||
| 65 | + left: 0; | ||
| 66 | + bottom: calc(50% + 25px); | ||
| 67 | + height: calc(50% - 25px); | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + .item:after { | ||
| 71 | + content: ' '; | ||
| 72 | + border-left: 1px solid var(--color-border); | ||
| 73 | + position: absolute; | ||
| 74 | + left: 0; | ||
| 75 | + top: calc(50% + 25px); | ||
| 76 | + height: calc(50% - 25px); | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + .item:first-of-type:before { | ||
| 80 | + display: none; | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + .item:last-of-type:after { | ||
| 84 | + display: none; | ||
| 85 | + } | ||
| 86 | +} | ||
| 87 | +</style> |
src/components/icons/IconCommunity.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor"> | ||
| 3 | + <path | ||
| 4 | + d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z" | ||
| 5 | + /> | ||
| 6 | + </svg> | ||
| 7 | +</template> |
src/components/icons/IconDocumentation.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor"> | ||
| 3 | + <path | ||
| 4 | + d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z" | ||
| 5 | + /> | ||
| 6 | + </svg> | ||
| 7 | +</template> |
src/components/icons/IconEcosystem.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor"> | ||
| 3 | + <path | ||
| 4 | + d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z" | ||
| 5 | + /> | ||
| 6 | + </svg> | ||
| 7 | +</template> |
src/components/icons/IconSupport.vue
0 → 100644
src/components/icons/IconTooling.vue
0 → 100644
| 1 | +<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license--> | ||
| 2 | +<template> | ||
| 3 | + <svg | ||
| 4 | + xmlns="http://www.w3.org/2000/svg" | ||
| 5 | + xmlns:xlink="http://www.w3.org/1999/xlink" | ||
| 6 | + aria-hidden="true" | ||
| 7 | + role="img" | ||
| 8 | + class="iconify iconify--mdi" | ||
| 9 | + width="24" | ||
| 10 | + height="24" | ||
| 11 | + preserveAspectRatio="xMidYMid meet" | ||
| 12 | + viewBox="0 0 24 24" | ||
| 13 | + > | ||
| 14 | + <path | ||
| 15 | + d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z" | ||
| 16 | + fill="currentColor" | ||
| 17 | + ></path> | ||
| 18 | + </svg> | ||
| 19 | +</template> |
src/main.js
0 → 100644
| 1 | +import { createApp } from 'vue' | ||
| 2 | +import ElementPlus from 'element-plus' | ||
| 3 | +import 'element-plus/dist/index.css' | ||
| 4 | +import * as ElementPlusIconsVue from '@element-plus/icons-vue' | ||
| 5 | +import zhCn from 'element-plus/es/locale/lang/zh-cn' | ||
| 6 | +import router from './router' | ||
| 7 | +import App from './App.vue' | ||
| 8 | +import './assets/main.css' | ||
| 9 | + | ||
| 10 | +const app = createApp(App) | ||
| 11 | + | ||
| 12 | +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { | ||
| 13 | + app.component(key, component) | ||
| 14 | +} | ||
| 15 | + | ||
| 16 | +app.use(ElementPlus, { locale: zhCn }) | ||
| 17 | +app.use(router) | ||
| 18 | +app.mount('#app') |
src/router/index.js
0 → 100644
| 1 | +import { createRouter, createWebHistory } from 'vue-router' | ||
| 2 | + | ||
| 3 | +const routes = [ | ||
| 4 | + { | ||
| 5 | + path: '/', | ||
| 6 | + redirect: '/smart-light' | ||
| 7 | + }, | ||
| 8 | + { | ||
| 9 | + path: '/smart-light', | ||
| 10 | + name: 'SmartLight', | ||
| 11 | + component: () => import('../views/SmartLight.vue'), | ||
| 12 | + meta: { title: '动态监控' } | ||
| 13 | + }, | ||
| 14 | + { | ||
| 15 | + path: '/energy', | ||
| 16 | + name: 'Energy', | ||
| 17 | + component: () => import('../views/Energy.vue'), | ||
| 18 | + meta: { title: '能耗' } | ||
| 19 | + } | ||
| 20 | +] | ||
| 21 | + | ||
| 22 | +const router = createRouter({ | ||
| 23 | + history: createWebHistory(), | ||
| 24 | + routes | ||
| 25 | +}) | ||
| 26 | + | ||
| 27 | +export default router |
src/views/Energy.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="energy-page"> | ||
| 3 | + <!-- 第一行:状态Tab (实时状态、时序状态、稼动率、能耗效率) + 搜索 --> | ||
| 4 | + <div class="top-toolbar"> | ||
| 5 | + <div class="status-tabs"> | ||
| 6 | + <div | ||
| 7 | + v-for="tab in statusTabs" | ||
| 8 | + :key="tab.key" | ||
| 9 | + :class="['status-tab', { active: currentStatus === tab.key }]" | ||
| 10 | + @click="currentStatus = tab.key" | ||
| 11 | + > | ||
| 12 | + {{ tab.label }} | ||
| 13 | + </div> | ||
| 14 | + </div> | ||
| 15 | + </div> | ||
| 16 | + | ||
| 17 | + <!-- 筛选栏(仅实时状态显示) --> | ||
| 18 | + <div v-if="currentStatus === 'realtime'" class="filter-bar"> | ||
| 19 | + <span class="filter-label">请输入设备:</span> | ||
| 20 | + <div class="filter-tags"> | ||
| 21 | + <span class="tag-item black"><i></i>全量2台</span> | ||
| 22 | + <span class="tag-item red"><i></i>停机:0台</span> | ||
| 23 | + <span class="tag-item green"><i></i>待机:0台</span> | ||
| 24 | + <span class="tag-item blue"><i></i>运行:0台</span> | ||
| 25 | + <span class="tag-item gray"><i></i>离线:2台</span> | ||
| 26 | + </div> | ||
| 27 | + </div> | ||
| 28 | + | ||
| 29 | + <!-- ========== 实时状态:设备卡片 ========== --> | ||
| 30 | + <div v-if="currentStatus === 'realtime'" class="tab-content"> | ||
| 31 | + <!-- 设备卡片网格 --> | ||
| 32 | + <div class="device-grid" :class="{ 'grid-full': totalDevices > PAGE_SIZE || (pagedDevices.length >= PAGE_SIZE && totalDevices > PAGE_SIZE), 'grid-normal': pagedDevices.length <= 6 }"> | ||
| 33 | + <div v-for="device in pagedDevices" :key="device.id" class="energy-card"> | ||
| 34 | + <div class="card-header"> | ||
| 35 | + <span class="device-name">{{ device.name }}</span> | ||
| 36 | + <el-icon class="menu-icon"><Menu /></el-icon> | ||
| 37 | + </div> | ||
| 38 | + | ||
| 39 | + <div class="card-body"> | ||
| 40 | + <div class="energy-icon"> | ||
| 41 | + <svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"> | ||
| 42 | + <path d="M32 8 L38 28 L48 20 L42 38 L58 38 L38 52 L44 62 L32 54 L20 62 L26 52 L6 38 L22 38 L16 20 L26 28 Z" | ||
| 43 | + fill="#f5a623" stroke="#d48806" stroke-width="2"/> | ||
| 44 | + </svg> | ||
| 45 | + </div> | ||
| 46 | + | ||
| 47 | + <div class="info-list"> | ||
| 48 | + <div class="info-item">电压: <span>{{ device.voltage }}V</span></div> | ||
| 49 | + <div class="info-item">电流: <span>{{ device.current }}A</span></div> | ||
| 50 | + <div class="info-item">昨日能耗: <span class="value-highlight">{{ device.yesterdayEnergy }}</span></div> | ||
| 51 | + </div> | ||
| 52 | + </div> | ||
| 53 | + | ||
| 54 | + <div class="card-footer"> | ||
| 55 | + <button class="action-btn primary" @click="openDetail('report', device)"> | ||
| 56 | + <el-icon><Document /></el-icon>能耗报表 | ||
| 57 | + </button> | ||
| 58 | + <button class="action-btn danger" @click="openDetail('safety', device)"> | ||
| 59 | + <el-icon><Lock /></el-icon>用电安全 | ||
| 60 | + </button> | ||
| 61 | + <button class="action-btn warning" @click="openDetail('param', device)"> | ||
| 62 | + <el-icon><Setting /></el-icon>参数设置 | ||
| 63 | + </button> | ||
| 64 | + <button v-if="false" class="action-btn info" @click="openDetail('warning', device)"> | ||
| 65 | + <el-icon><Warning /></el-icon>预警设置 | ||
| 66 | + </button> | ||
| 67 | + </div> | ||
| 68 | + </div> | ||
| 69 | + </div> | ||
| 70 | + | ||
| 71 | + <!-- 分页 --> | ||
| 72 | + <div v-if="totalDevices > 0" class="pagination-wrapper"> | ||
| 73 | + <!-- 分页控件 --> | ||
| 74 | + <div class="pagination-controls"> | ||
| 75 | + <!-- 每页条数选择 --> | ||
| 76 | + <select class="page-size-select" :value="PAGE_SIZE"> | ||
| 77 | + <option value="12">12 条/页</option> | ||
| 78 | + </select> | ||
| 79 | + | ||
| 80 | + <!-- 导航按钮组 --> | ||
| 81 | + <button class="page-btn" :disabled="currentPage === 1" @click="currentPage = 1">«</button> | ||
| 82 | + <button class="page-btn" :disabled="currentPage === 1" @click="currentPage--"><</button> | ||
| 83 | + <template v-for="(p, i) in visiblePages" :key="i"> | ||
| 84 | + <button v-if="typeof p === 'number'" :class="['page-btn', { active: currentPage === p }]" @click="currentPage = p">{{ p }}</button> | ||
| 85 | + <span v-else class="page-dots">{{ p }}</span> | ||
| 86 | + </template> | ||
| 87 | + <button class="page-btn" :disabled="currentPage === totalPages" @click="currentPage++">></button> | ||
| 88 | + <button class="page-btn" :disabled="currentPage === totalPages" @click="currentPage = totalPages">»</button> | ||
| 89 | + </div> | ||
| 90 | + </div> | ||
| 91 | + </div><!-- /tab-content realtime --> | ||
| 92 | + | ||
| 93 | + <!-- ========== 时序状态:时间轴甘特图 ========== --> | ||
| 94 | + <div v-else-if="currentStatus === 'timeseries'" class="tab-content timeseries-view"> | ||
| 95 | + <div class="ts-toolbar"> | ||
| 96 | + <span class="ts-label">查询方式:</span> | ||
| 97 | + <el-radio-group v-model="tsQueryMode" size="small"> | ||
| 98 | + <el-radio-button value="day">日查询</el-radio-button> | ||
| 99 | + </el-radio-group> | ||
| 100 | + <el-date-picker v-model="tsDateRange" type="daterange" size="small" range-separator="-" | ||
| 101 | + start-placeholder="" end-placeholder="" style="width: 260px; margin-left: 8px;" /> | ||
| 102 | + <el-button type="primary" size="small">查询</el-button> | ||
| 103 | + </div> | ||
| 104 | + | ||
| 105 | + <div class="ts-table-wrap"> | ||
| 106 | + <div class="ts-header-row"> | ||
| 107 | + <div class="ts-col-name">设备名称</div> | ||
| 108 | + <div class="ts-col-name ts-sub-col">稼动率</div> | ||
| 109 | + <div class="ts-col-name ts-sub-col2">用电量</div> | ||
| 110 | + <div class="ts-timeline-area"> | ||
| 111 | + <div style="display:flex;justify-content:space-between;padding-right:16px;"> | ||
| 112 | + <span style="font-size:12px;color:#333;font-weight:bold;">{{ tsHeaderDate }}</span> | ||
| 113 | + <span style="font-size:12px;color:#333;font-weight:bold;">202</span> | ||
| 114 | + </div> | ||
| 115 | + <svg viewBox="0 0 1200 1" preserveAspectRatio="none" style="width:100%;height:30px;"> | ||
| 116 | + <g font-size="11" fill="#666" text-anchor="middle"> | ||
| 117 | + <text x="50" y="-5">10:15</text><text x="150" y="-5">10:30</text> | ||
| 118 | + <text x="250" y="-5">10:45</text> | ||
| 119 | + <text x="350" y="-5">11:00</text> | ||
| 120 | + <text x="450" y="-5">11:15</text> | ||
| 121 | + <text x="550" y="-5">11:30</text> | ||
| 122 | + <text x="650" y="-5">11:45</text><text x="750" y="-5">12:00</text> | ||
| 123 | + <text x="850" y="-5">12:15</text> | ||
| 124 | + <text x="950" y="-5">12:30</text><text x="1050" y="-5">12:</text><text x="1150" y="-5"></text> | ||
| 125 | + </g> | ||
| 126 | + </svg> | ||
| 127 | + </div> | ||
| 128 | + </div> | ||
| 129 | + | ||
| 130 | + <div v-for="(dev, idx) in energyTimeSeriesData" :key="idx" | ||
| 131 | + :class="['ts-row', { 'row-gray': dev.rate === 0 }]"> | ||
| 132 | + <div class="ts-cell-name"> | ||
| 133 | + <span class="ts-link">{{ dev.name }}</span> | ||
| 134 | + </div> | ||
| 135 | + <div class="ts-cell-rate">{{ dev.rate }}%</div> | ||
| 136 | + <div class="ts-cell-rate">{{ dev.power }}</div> | ||
| 137 | + <div class="ts-cell-bars"> | ||
| 138 | + <div class="bar-track"> | ||
| 139 | + <template v-for="(seg, si) in dev.segments" :key="si"> | ||
| 140 | + <div class="bar-seg" :class="'seg-' + seg.color" | ||
| 141 | + :style="{ left: seg.left + '%', width: seg.width + '%' }"></div> | ||
| 142 | + </template> | ||
| 143 | + </div> | ||
| 144 | + </div> | ||
| 145 | + </div> | ||
| 146 | + </div> | ||
| 147 | + | ||
| 148 | + <div class="pagination-wrapper"> | ||
| 149 | + <span>共 {{ energyTimeSeriesData.length }} 条</span> | ||
| 150 | + <el-pagination :current-page="1" :page-size="20" layout="prev, pager, next, total, jumper" :total="energyTimeSeriesData.length" small /> | ||
| 151 | + </div> | ||
| 152 | + </div> | ||
| 153 | + | ||
| 154 | + <!-- ========== 稼动率:多图表视图 ========== --> | ||
| 155 | + <div v-else-if="currentStatus === 'utilization'" class="tab-content util-view"> | ||
| 156 | + <div class="util-toolbar"> | ||
| 157 | + <span class="util-label">查询方式:</span> | ||
| 158 | + <el-radio-group v-model="utilQueryMode" size="small"> | ||
| 159 | + <el-radio-button value="day">日查询</el-radio-button> | ||
| 160 | + <el-radio-button value="week">周查询</el-radio-button> | ||
| 161 | + <el-radio-button value="month">月查询</el-radio-button> | ||
| 162 | + </el-radio-group> | ||
| 163 | + <el-date-picker v-model="utilDate" type="date" placeholder="2026-04-28" size="small" style="width:160px;margin-left:8px;" /> | ||
| 164 | + <div style="flex:1"></div> | ||
| 165 | + <el-button type="primary" size="small">查询</el-button> | ||
| 166 | + </div> | ||
| 167 | + | ||
| 168 | + <div class="util-top-charts"> | ||
| 169 | + <div class="pie-card"> | ||
| 170 | + <div class="pie-title">总稼动率:</div> | ||
| 171 | + <div class="pie-chart-svg"> | ||
| 172 | + <svg viewBox="0 0 200 180"><circle cx="90" cy="90" r="70" fill="none" stroke="#ddd" stroke-width="35"/></svg> | ||
| 173 | + <div class="pie-empty-text">暂无数据</div> | ||
| 174 | + </div> | ||
| 175 | + </div> | ||
| 176 | + <div class="pie-card"> | ||
| 177 | + <div class="pie-title">当前机台运行状态:</div> | ||
| 178 | + <div class="pie-chart-svg"> | ||
| 179 | + <svg viewBox="0 0 200 180"><circle cx="90" cy="90" r="70" fill="none" stroke="#909399" stroke-width="35" stroke-dasharray="440 440" transform="rotate(-90 90 90)"/> | ||
| 180 | + <text x="130" y="85" text-anchor="middle" font-size="12" fill="#333"><tspan>x</tspan> 离线</text> | ||
| 181 | + </svg> | ||
| 182 | + <div class="pie-legend center-leg"> | ||
| 183 | + <span class="leg-item"><i class="dot g"></i>绿灯</span> | ||
| 184 | + <span class="leg-item"><i class="dot r"></i>红灯</span> | ||
| 185 | + <span class="leg-item"><i class="dot gy"></i>离线</span> | ||
| 186 | + </div> | ||
| 187 | + </div> | ||
| 188 | + </div> | ||
| 189 | + <div class="bar-card"> | ||
| 190 | + <div class="pie-title">异常机台排名:</div> | ||
| 191 | + <div class="abnormal-list"></div> | ||
| 192 | + <div class="abn-legend" style="margin-top:auto;"><i class="dot y"></i>待机 <i class="dot r"></i>停机</div> | ||
| 193 | + </div> | ||
| 194 | + </div> | ||
| 195 | + | ||
| 196 | + <div class="util-bottom-chart"> | ||
| 197 | + <div class="stack-bar-toolbar"> | ||
| 198 | + <span>排序:</span> | ||
| 199 | + <el-radio-group v-model="sortMode" size="small"> | ||
| 200 | + <el-radio-button value="duration">绿灯时长</el-radio-button> | ||
| 201 | + <el-radio-button value="rate" checked>稼动率</el-radio-button> | ||
| 202 | + </el-radio-group> | ||
| 203 | + </div> | ||
| 204 | + <div class="stack-bar-legend"> | ||
| 205 | + <span class="leg-item"><i class="dot g"></i>运行</span> | ||
| 206 | + <span class="leg-item"><i class="dot y"></i>待机</span> | ||
| 207 | + <span class="leg-item"><i class="dot r"></i>停机</span> | ||
| 208 | + <span class="leg-item"><i class="dot gy"></i>离线</span> | ||
| 209 | + </div> | ||
| 210 | + <div class="stack-bar-chart"> | ||
| 211 | + <svg viewBox="0 0 1400 280"> | ||
| 212 | + <g font-size="10" fill="#999" text-anchor="end"> | ||
| 213 | + <text x="28" y="18">3时</text><text x="28" y="73">3时</text> | ||
| 214 | + <text x="28" y="128">2时</text><text x="28" y="183">1时</text><text x="28" y="238">0时</text> | ||
| 215 | + </g> | ||
| 216 | + <line x1="36" y1="240" x2="1380" y2="240" stroke="#ddd" stroke-width="1"/> | ||
| 217 | + <template v-for="(col, ci) in energyStackBarData" :key="ci"> | ||
| 218 | + <rect :x="200+ci*80" :y="240-col.g*60" width="40" :height="col.g*60" fill="#67c23a" rx="1"/> | ||
| 219 | + <rect :x="200+ci*80" :y="240-(col.g+col.y)*60" width="40" :height="col.y*60" fill="#e6a23c" rx="1"/> | ||
| 220 | + <rect :x="200+ci*80" :y="240-(col.g+col.y+col.r)*60" width="40" :height="col.r*60" fill="#f56c6c" rx="1"/> | ||
| 221 | + <rect :x="200+ci*80" :y="240-(col.g+col.y+col.r+col.gy)*60" width="40" :height="col.gy*60" fill="#909399" rx="1"/> | ||
| 222 | + <text :x="220+ci*80" y="258" text-anchor="middle" font-size="9" fill="#666">{{ col.name }}</text> | ||
| 223 | + </template> | ||
| 224 | + </svg> | ||
| 225 | + </div> | ||
| 226 | + </div> | ||
| 227 | + </div> | ||
| 228 | + | ||
| 229 | + <!-- ========== 能耗效率:折线图 ========== --> | ||
| 230 | + <div v-else-if="currentStatus === 'efficiency'" class="tab-content eff-view"> | ||
| 231 | + <div class="eff-toolbar"> | ||
| 232 | + <span class="eff-label">查询方式:</span> | ||
| 233 | + <el-radio-group v-model="effQueryMode" size="small"> | ||
| 234 | + <el-radio-button value="day">日查询</el-radio-button> | ||
| 235 | + <el-radio-button value="week">周查询</el-radio-button> | ||
| 236 | + <el-radio-button value="month">月查询</el-radio-button> | ||
| 237 | + </el-radio-group> | ||
| 238 | + <el-date-picker v-model="effDate" type="date" placeholder="2026-04-28" size="small" style="width:160px;margin-left:8px;" /> | ||
| 239 | + <el-select v-model="effDeviceFilter" size="small" style="width:140px;margin-left:8px;"> | ||
| 240 | + <el-option label="磨粉设备1 +1" value="dev1" /> | ||
| 241 | + </el-select> | ||
| 242 | + <div style="flex:1"></div> | ||
| 243 | + <el-button size="small" circle><el-icon><Histogram /></el-icon></el-button> | ||
| 244 | + <el-button size="small" circle><el-icon><Document /></el-icon></el-button> | ||
| 245 | + </div> | ||
| 246 | + <div class="eff-legend"> | ||
| 247 | + <span class="leg-line" style="--lc:#5470c6;"><i></i>磨粉设备1</span> | ||
| 248 | + <span class="leg-line" style="--lc:#91cc75;"><i></i>磨粉设备2</span> | ||
| 249 | + </div> | ||
| 250 | + <div class="eff-chart"> | ||
| 251 | + <svg viewBox="0 0 1400 400"> | ||
| 252 | + <!-- Y轴刻度 --> | ||
| 253 | + <g font-size="11" fill="#999" text-anchor="end"> | ||
| 254 | + <text x="35" y="24">1</text><text x="35" y="96">0.8</text> | ||
| 255 | + <text x="35" y="168">0.6</text><text x="35" y="240">0.4</text> | ||
| 256 | + <text x="35" y="312">0.2</text><text x="35" y="380">0</text> | ||
| 257 | + </g> | ||
| 258 | + <!-- 网格线 --> | ||
| 259 | + <g stroke="#eee" stroke-width="1"> | ||
| 260 | + <line x1="46" y1="20" x2="1370" y2="20"/><line x1="46" y1="92" x2="1370" y2="92"/> | ||
| 261 | + <line x1="46" y1="164" x2="1370" y2="164"/><line x1="46" y1="236" x2="1370" y2="236"/> | ||
| 262 | + <line x1="46" y1="308" x2="1370" y2="308"/><line x1="46" y1="380" x2="1370" y2="380"/> | ||
| 263 | + </g> | ||
| 264 | + <!-- X轴标签 --> | ||
| 265 | + <g font-size="10" fill="#666" text-anchor="middle"> | ||
| 266 | + <template v-for="i in 24" :key="i"> | ||
| 267 | + <text :x="46+(i-1)*55" y="398">{{ i }}</text> | ||
| 268 | + </template> | ||
| 269 | + </g> | ||
| 270 | + <!-- 折线1 --> | ||
| 271 | + <polyline :points="effLine1Points" fill="none" stroke="#5470c6" stroke-width="2"/> | ||
| 272 | + <!-- 折线2 --> | ||
| 273 | + <polyline :points="effLine2Points" fill="none" stroke="#91cc75" stroke-width="2"/> | ||
| 274 | + <!-- X轴线 --> | ||
| 275 | + <line x1="46" y1="380" x2="1370" y2="380" stroke="#ccc" stroke-width="1.5"/> | ||
| 276 | + </svg> | ||
| 277 | + </div> | ||
| 278 | + </div> | ||
| 279 | + | ||
| 280 | + <!-- 能耗报表弹窗 --> | ||
| 281 | + <EnergyReportDialog | ||
| 282 | + v-model:visible="dialogVisible.report" | ||
| 283 | + :device="currentDevice" | ||
| 284 | + /> | ||
| 285 | + <!-- 用电安全弹窗 --> | ||
| 286 | + <SafetyDialog | ||
| 287 | + v-model:visible="dialogVisible.safety" | ||
| 288 | + :device="currentDevice" | ||
| 289 | + /> | ||
| 290 | + <!-- 参数设置弹窗 --> | ||
| 291 | + <ParamSettingDialog | ||
| 292 | + v-model:visible="dialogVisible.param" | ||
| 293 | + :device="currentDevice" | ||
| 294 | + /> | ||
| 295 | + <!-- 预警设置弹窗 --> | ||
| 296 | + <WarningSettingDialog | ||
| 297 | + v-model:visible="dialogVisible.warning" | ||
| 298 | + :device="currentDevice" | ||
| 299 | + /> | ||
| 300 | + </div> | ||
| 301 | +</template> | ||
| 302 | + | ||
| 303 | +<script setup> | ||
| 304 | +import { ref, reactive, computed } from 'vue' | ||
| 305 | +import { Search, Menu, Document, Lock, Setting, Warning, Histogram } from '@element-plus/icons-vue' | ||
| 306 | +import EnergyReportDialog from '../components/EnergyReportDialog.vue' | ||
| 307 | +import SafetyDialog from '../components/SafetyDialog.vue' | ||
| 308 | +import ParamSettingDialog from '../components/ParamSettingDialog.vue' | ||
| 309 | +import WarningSettingDialog from '../components/WarningSettingDialog.vue' | ||
| 310 | + | ||
| 311 | +const selectedFactory = ref('新建') | ||
| 312 | +const searchKeyword = ref('') | ||
| 313 | +const currentStatus = ref('realtime') | ||
| 314 | + | ||
| 315 | +// 能耗页面4个Tab(第4个是能耗效率,与智能灯不同) | ||
| 316 | +const statusTabs = [ | ||
| 317 | + { key: 'realtime', label: '实时状态' }, | ||
| 318 | + { key: 'timeseries', label: '时序状态' }, | ||
| 319 | + { key: 'utilization', label: '稼动率' }, | ||
| 320 | + { key: 'efficiency', label: '能耗效率' } | ||
| 321 | +] | ||
| 322 | + | ||
| 323 | +const deviceList = ref([ | ||
| 324 | + { id: 1, name: '磨粉设备1', voltage: 0, current: 0, yesterdayEnergy: 0 }, | ||
| 325 | + { id: 2, name: '磨粉设备2', voltage: 0, current: 0, yesterdayEnergy: 0 } | ||
| 326 | +]) | ||
| 327 | + | ||
| 328 | +const PAGE_SIZE = 12 | ||
| 329 | +const currentPage = ref(1) | ||
| 330 | +const totalDevices = computed(() => deviceList.value.length) | ||
| 331 | +const totalPages = computed(() => Math.ceil(totalDevices.value / PAGE_SIZE) || 1) | ||
| 332 | +const visiblePages = computed(() => { | ||
| 333 | + const pages = [] | ||
| 334 | + const maxVisible = 5 | ||
| 335 | + const cp = currentPage.value | ||
| 336 | + const tp = totalPages.value | ||
| 337 | + let start = Math.max(1, cp - Math.floor(maxVisible / 2)) | ||
| 338 | + let end = Math.min(tp, start + maxVisible - 1) | ||
| 339 | + if (end - start + 1 < maxVisible) start = Math.max(1, end - maxVisible + 1) | ||
| 340 | + if (start > 1) { pages.push(1); if (start > 2) pages.push('...') } | ||
| 341 | + for (let i = start; i <= end; i++) pages.push(i) | ||
| 342 | + if (end < tp) { if (end < tp - 1) pages.push('...'); pages.push(tp) } | ||
| 343 | + return pages | ||
| 344 | +}) | ||
| 345 | +const pagedDevices = computed(() => { | ||
| 346 | + const start = (currentPage.value - 1) * PAGE_SIZE | ||
| 347 | + return deviceList.value.slice(start, start + PAGE_SIZE) | ||
| 348 | +}) | ||
| 349 | + | ||
| 350 | +const dialogVisible = reactive({ | ||
| 351 | + report: false, | ||
| 352 | + safety: false, | ||
| 353 | + param: false, | ||
| 354 | + warning: false | ||
| 355 | +}) | ||
| 356 | +const currentDevice = ref(null) | ||
| 357 | + | ||
| 358 | +function openDetail(type, device) { | ||
| 359 | + currentDevice.value = device | ||
| 360 | + dialogVisible[type] = true | ||
| 361 | +} | ||
| 362 | + | ||
| 363 | +// ========== 时序状态数据 ========== | ||
| 364 | +const tsQueryMode = ref('day') | ||
| 365 | +const tsDateRange = ref(null) | ||
| 366 | +const tsHeaderDate = ref('2026-04-28') | ||
| 367 | +const energyTimeSeriesData = ref([ | ||
| 368 | + { name: '能耗设备1', rate: 0, power: 0, segments: [{color:'gy',left:0,width:100}] }, | ||
| 369 | + { name: '能耗设备2', rate: 0, power: 0, segments: [{color:'gy',left:0,width:100}] } | ||
| 370 | +]) | ||
| 371 | + | ||
| 372 | +// ========== 稼动率数据 ========== | ||
| 373 | +const utilQueryMode = ref('day') | ||
| 374 | +const utilDate = ref('2026-04-28') | ||
| 375 | +const sortMode = ref('rate') | ||
| 376 | +const energyStackBarData = computed(() => [ | ||
| 377 | + { name: '磨粉设备1', g: 3.5, y: 0, r: 0, gy: 1 }, | ||
| 378 | + { name: '磨粉设备2', g: 3.5, y: 0, r: 0, gy: 1 } | ||
| 379 | +]) | ||
| 380 | + | ||
| 381 | +// ========== 能耗效率数据 ========== | ||
| 382 | +const effQueryMode = ref('day') | ||
| 383 | +const effDate = ref('2026-04-28') | ||
| 384 | +const effDeviceFilter = ref('dev1') | ||
| 385 | +const effLine1Points = computed(() => { | ||
| 386 | + const pts = [] | ||
| 387 | + for (let i = 0; i < 24; i++) { | ||
| 388 | + pts.push(`${46 + i * 55},${380 - 0}`) | ||
| 389 | + } | ||
| 390 | + return pts.join(' ') | ||
| 391 | +}) | ||
| 392 | +const effLine2Points = computed(() => { | ||
| 393 | + const pts = [] | ||
| 394 | + for (let i = 0; i < 24; i++) { | ||
| 395 | + pts.push(`${46 + i * 55},${380 - 0}`) | ||
| 396 | + } | ||
| 397 | + return pts.join(' ') | ||
| 398 | +}) | ||
| 399 | +</script> | ||
| 400 | + | ||
| 401 | +<style scoped> | ||
| 402 | +.energy-page { | ||
| 403 | + min-height: 100%; | ||
| 404 | + height: calc(100vh - 0px); | ||
| 405 | + display: flex; | ||
| 406 | + flex-direction: column; | ||
| 407 | + background-color: #f0f2f5; | ||
| 408 | +} | ||
| 409 | +.device-grid { | ||
| 410 | + flex: 1; | ||
| 411 | + padding: 16px 20px; | ||
| 412 | + display: grid; | ||
| 413 | + grid-template-columns: repeat(6, 1fr); | ||
| 414 | + gap: 16px; | ||
| 415 | + align-content: start; | ||
| 416 | +} | ||
| 417 | +.device-grid.grid-full { | ||
| 418 | + align-content: stretch; | ||
| 419 | +} | ||
| 420 | +.device-grid.grid-normal { | ||
| 421 | + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | ||
| 422 | +} | ||
| 423 | +.top-toolbar { | ||
| 424 | + background: #fff; | ||
| 425 | + padding: 0 20px; | ||
| 426 | + display: flex; | ||
| 427 | + align-items: center; | ||
| 428 | + justify-content: space-between; | ||
| 429 | + border-bottom: 1px solid #e8e8e8; | ||
| 430 | +} | ||
| 431 | +.status-tabs { | ||
| 432 | + display: flex; | ||
| 433 | + gap: 4px; | ||
| 434 | +} | ||
| 435 | +.status-tab { | ||
| 436 | + padding: 14px 18px; | ||
| 437 | + cursor: pointer; | ||
| 438 | + font-size: 13px; | ||
| 439 | + color: #666; | ||
| 440 | + position: relative; | ||
| 441 | + transition: all 0.2s; | ||
| 442 | +} | ||
| 443 | +.status-tab:hover { | ||
| 444 | + color: #409eff; | ||
| 445 | +} | ||
| 446 | +.status-tab.active { | ||
| 447 | + color: #409eff; | ||
| 448 | + font-weight: bold; | ||
| 449 | +} | ||
| 450 | +.status-tab.active::after { | ||
| 451 | + content: ''; | ||
| 452 | + position: absolute; | ||
| 453 | + bottom: 0; | ||
| 454 | + left: 50%; | ||
| 455 | + transform: translateX(-50%); | ||
| 456 | + width: 60%; | ||
| 457 | + height: 2px; | ||
| 458 | + background: #409eff; | ||
| 459 | +} | ||
| 460 | +.toolbar-right { | ||
| 461 | + display: flex; | ||
| 462 | + align-items: center; | ||
| 463 | + gap: 8px; | ||
| 464 | +} | ||
| 465 | +.filter-bar { | ||
| 466 | + background: #fff; | ||
| 467 | + padding: 10px 20px; | ||
| 468 | + display: flex; | ||
| 469 | + align-items: center; | ||
| 470 | + justify-content: space-between; | ||
| 471 | + border-bottom: 1px solid #e8e8e8; | ||
| 472 | +} | ||
| 473 | +.filter-label { | ||
| 474 | + font-size: 13px; | ||
| 475 | + color: #999; | ||
| 476 | +} | ||
| 477 | +.filter-tags { | ||
| 478 | + display: flex; | ||
| 479 | + gap: 16px; | ||
| 480 | +} | ||
| 481 | +.tag-item { | ||
| 482 | + font-size: 12px; | ||
| 483 | + display: flex; | ||
| 484 | + align-items: center; | ||
| 485 | + gap: 4px; | ||
| 486 | +} | ||
| 487 | +.tag-item i { | ||
| 488 | + width: 10px; | ||
| 489 | + height: 10px; | ||
| 490 | + display: inline-block; | ||
| 491 | + border-radius: 2px; | ||
| 492 | +} | ||
| 493 | +.tag-item.black i { background: #333; } | ||
| 494 | +.tag-item.red i { background: #f56c6c; } | ||
| 495 | +.tag-item.green i { background: #67c23a; } | ||
| 496 | +.tag-item.blue i { background: #409eff; } | ||
| 497 | +.tag-item.gray i { background: #909399; } | ||
| 498 | + | ||
| 499 | +.energy-card { | ||
| 500 | + height: 340px; | ||
| 501 | + background: linear-gradient(145deg, #3d3a4d 0%, #2d2a3a 100%); | ||
| 502 | + border-radius: 12px; | ||
| 503 | + overflow: hidden; | ||
| 504 | + box-shadow: 0 4px 16px rgba(0,0,0,0.2); | ||
| 505 | + transition: transform 0.2s; | ||
| 506 | + display: flex; | ||
| 507 | + flex-direction: column; | ||
| 508 | +} | ||
| 509 | +.energy-card:hover { | ||
| 510 | + transform: translateY(-2px); | ||
| 511 | + box-shadow: 0 6px 20px rgba(0,0,0,0.25); | ||
| 512 | +} | ||
| 513 | +.card-header { | ||
| 514 | + color: #fff; | ||
| 515 | + padding: 12px 16px; | ||
| 516 | + display: flex; | ||
| 517 | + justify-content: space-between; | ||
| 518 | + align-items: center; | ||
| 519 | + font-size: 14px; | ||
| 520 | + font-weight: bold; | ||
| 521 | + flex-shrink: 0; | ||
| 522 | +} | ||
| 523 | +.menu-icon { | ||
| 524 | + cursor: pointer; | ||
| 525 | + color: #aaa; | ||
| 526 | +} | ||
| 527 | +.card-body { | ||
| 528 | + padding: 14px 16px 10px; | ||
| 529 | + text-align: center; | ||
| 530 | + flex: 1; | ||
| 531 | +} | ||
| 532 | +.energy-icon { | ||
| 533 | + width: 64px; | ||
| 534 | + height: 64px; | ||
| 535 | + margin: 0 auto 12px; | ||
| 536 | +} | ||
| 537 | +.energy-icon svg { | ||
| 538 | + width: 100%; | ||
| 539 | + height: 100%; | ||
| 540 | + filter: drop-shadow(0 0 14px rgba(245,166,35,0.4)); | ||
| 541 | +} | ||
| 542 | +.info-list { | ||
| 543 | + text-align: left; | ||
| 544 | + color: #ccc; | ||
| 545 | + font-size: 13px; | ||
| 546 | + line-height: 2; | ||
| 547 | +} | ||
| 548 | +.info-item span { | ||
| 549 | + color: #fff; | ||
| 550 | + font-weight: 500; | ||
| 551 | +} | ||
| 552 | +.value-highlight { | ||
| 553 | + color: #f5a623 !important; | ||
| 554 | + font-weight: bold; | ||
| 555 | +} | ||
| 556 | +.card-footer { | ||
| 557 | + padding: 10px 14px 12px; | ||
| 558 | + display: grid; | ||
| 559 | + grid-template-columns: 1fr 1fr; | ||
| 560 | + gap: 8px; | ||
| 561 | + border-top: 1px solid rgba(255,255,255,0.08); | ||
| 562 | + flex-shrink: 0; | ||
| 563 | +} | ||
| 564 | +.action-btn { | ||
| 565 | + display: flex; | ||
| 566 | + align-items: center; | ||
| 567 | + justify-content: center; | ||
| 568 | + gap: 4px; | ||
| 569 | + padding: 7px 8px; | ||
| 570 | + border: none; | ||
| 571 | + border-radius: 6px; | ||
| 572 | + font-size: 13px; | ||
| 573 | + cursor: pointer; | ||
| 574 | + transition: all 0.2s; | ||
| 575 | + color: #fff; | ||
| 576 | +} | ||
| 577 | +.action-btn:hover { | ||
| 578 | + opacity: 0.85; | ||
| 579 | + transform: scale(1.02); | ||
| 580 | +} | ||
| 581 | +.action-btn.primary { | ||
| 582 | + background: rgba(64,158,255,0.25); | ||
| 583 | + color: #409eff; | ||
| 584 | + border: 1px solid rgba(64,158,255,0.3); | ||
| 585 | +} | ||
| 586 | +.action-btn.danger { | ||
| 587 | + background: rgba(245,108,108,0.25); | ||
| 588 | + color: #f56c6c; | ||
| 589 | + border: 1px solid rgba(245,108,108,0.3); | ||
| 590 | +} | ||
| 591 | +.action-btn.warning { | ||
| 592 | + background: rgba(230,162,60,0.25); | ||
| 593 | + color: #e6a23c; | ||
| 594 | + border: 1px solid rgba(230,162,60,0.3); | ||
| 595 | +} | ||
| 596 | +.action-btn.info { | ||
| 597 | + background: rgba(144,147,153,0.25); | ||
| 598 | + color: #909399; | ||
| 599 | + border: 1px solid rgba(144,147,153,0.3); | ||
| 600 | +} | ||
| 601 | +/* ========== 自定义分页 ========== */ | ||
| 602 | +.pagination-wrapper { | ||
| 603 | + display: flex; | ||
| 604 | + align-items: center; | ||
| 605 | + justify-content: flex-end; | ||
| 606 | + padding: 14px 20px; | ||
| 607 | + border-top: 1px solid #e8e8e8; | ||
| 608 | +} | ||
| 609 | +.pagination-info { font-size: 13px; color: #666; } | ||
| 610 | +.pagination-info strong { color: #333; } | ||
| 611 | + | ||
| 612 | +.pagination-controls { | ||
| 613 | + display: flex; | ||
| 614 | + align-items: center; | ||
| 615 | + gap: 4px; | ||
| 616 | +} | ||
| 617 | +.page-size-select { | ||
| 618 | + height: 30px; | ||
| 619 | + padding: 2px 8px; | ||
| 620 | + border: 1px solid #dcdfe6; | ||
| 621 | + border-radius: 4px; | ||
| 622 | + background: #fff; | ||
| 623 | + font-size: 13px; | ||
| 624 | + color: #606266; | ||
| 625 | + outline: none; | ||
| 626 | + cursor: pointer; | ||
| 627 | +} | ||
| 628 | +.page-btn { | ||
| 629 | + display: inline-flex; | ||
| 630 | + align-items: center; | ||
| 631 | + justify-content: center; | ||
| 632 | + width: 32px; | ||
| 633 | + height: 32px; | ||
| 634 | + border: 1px solid #dcdfe6; | ||
| 635 | + border-radius: 4px; | ||
| 636 | + background: #fff; | ||
| 637 | + color: #606266; | ||
| 638 | + font-size: 13px; | ||
| 639 | + cursor: pointer; | ||
| 640 | + transition: all 0.15s; | ||
| 641 | +} | ||
| 642 | +.page-btn:hover:not(:disabled) { | ||
| 643 | + color: #409eff; | ||
| 644 | + border-color: #409eff; | ||
| 645 | +} | ||
| 646 | +.page-btn.active { | ||
| 647 | + background-color: #409eff; | ||
| 648 | + border-color: #409eff; | ||
| 649 | + color: #fff; | ||
| 650 | +} | ||
| 651 | +.page-btn:disabled { | ||
| 652 | + opacity: 0.45; | ||
| 653 | + cursor: not-allowed; | ||
| 654 | +} | ||
| 655 | +.page-dots { | ||
| 656 | + display: inline-flex; | ||
| 657 | + align-items: center; | ||
| 658 | + justify-content: center; | ||
| 659 | + width: 24px; | ||
| 660 | + color: #999; | ||
| 661 | + font-size: 13px; | ||
| 662 | +} | ||
| 663 | + | ||
| 664 | +/* ========== Tab内容区通用 ========== */ | ||
| 665 | +.tab-content { | ||
| 666 | + flex: 1; | ||
| 667 | + display: flex; | ||
| 668 | + flex-direction: column; | ||
| 669 | + overflow: hidden; | ||
| 670 | +} | ||
| 671 | + | ||
| 672 | +/* ========== 时序状态 ========== */ | ||
| 673 | +.timeseries-view { | ||
| 674 | + background: #f5f7fa; | ||
| 675 | +} | ||
| 676 | +.ts-toolbar { | ||
| 677 | + background: #fff; | ||
| 678 | + padding: 10px 20px; | ||
| 679 | + display: flex; | ||
| 680 | + align-items: center; | ||
| 681 | + gap: 10px; | ||
| 682 | + border-bottom: 1px solid #e8e8e8; | ||
| 683 | +} | ||
| 684 | +.ts-label { | ||
| 685 | + font-size: 13px; color: #666; font-weight: bold; | ||
| 686 | +} | ||
| 687 | +.ts-table-wrap { | ||
| 688 | + flex: 1; | ||
| 689 | + overflow: auto; | ||
| 690 | + background: #fff; | ||
| 691 | + margin: 12px 20px; | ||
| 692 | + border: 1px solid #e8e8e8; | ||
| 693 | + border-radius: 4px; | ||
| 694 | +} | ||
| 695 | +.ts-header-row { | ||
| 696 | + display: flex; | ||
| 697 | + align-items: flex-end; | ||
| 698 | + position: sticky; | ||
| 699 | + top: 0; | ||
| 700 | + background: #fafafa; | ||
| 701 | + border-bottom: 2px solid #e0e0e0; | ||
| 702 | + z-index: 2; | ||
| 703 | +} | ||
| 704 | +.ts-col-name { | ||
| 705 | + width: 160px; | ||
| 706 | + padding: 8px 12px; | ||
| 707 | + font-size: 13px; | ||
| 708 | + font-weight: bold; | ||
| 709 | + color: #333; | ||
| 710 | + flex-shrink: 0; | ||
| 711 | + text-align: center; | ||
| 712 | +} | ||
| 713 | +.ts-sub-col { width: 70px; } | ||
| 714 | +.ts-sub-col2 { width: 70px; } | ||
| 715 | +.ts-timeline-area { | ||
| 716 | + flex: 1; | ||
| 717 | + min-width: 800px; | ||
| 718 | +} | ||
| 719 | +.ts-row { | ||
| 720 | + display: flex; | ||
| 721 | + align-items: center; | ||
| 722 | + border-bottom: 1px solid #f0f0f0; | ||
| 723 | + min-height: 36px; | ||
| 724 | +} | ||
| 725 | +.ts-row.row-gray .ts-cell-name { background: #f5f5f5; } | ||
| 726 | +.ts-cell-name { | ||
| 727 | + width: 160px; | ||
| 728 | + padding: 6px 12px; | ||
| 729 | + flex-shrink: 0; | ||
| 730 | + font-size: 12px; | ||
| 731 | +} | ||
| 732 | +.ts-link { color: #409eff; cursor: pointer; } | ||
| 733 | +.ts-link:hover { text-decoration: underline; } | ||
| 734 | +.ts-cell-rate { | ||
| 735 | + width: 70px; | ||
| 736 | + padding: 6px 4px; | ||
| 737 | + text-align: center; | ||
| 738 | + font-size: 12px; | ||
| 739 | + font-weight: bold; | ||
| 740 | + color: #333; | ||
| 741 | + flex-shrink: 0; | ||
| 742 | +} | ||
| 743 | +.ts-row.row-gray .ts-cell-rate { background: #f0f0f0; } | ||
| 744 | +.ts-cell-bars { | ||
| 745 | + flex: 1; | ||
| 746 | + min-width: 800px; | ||
| 747 | + padding: 4px 8px; | ||
| 748 | +} | ||
| 749 | +.bar-track { | ||
| 750 | + height: 22px; | ||
| 751 | + background: #f5f5f5; | ||
| 752 | + border-radius: 3px; | ||
| 753 | + position: relative; | ||
| 754 | + overflow: hidden; | ||
| 755 | +} | ||
| 756 | +.bar-seg { | ||
| 757 | + position: absolute; | ||
| 758 | + top: 0; | ||
| 759 | + height: 100%; | ||
| 760 | + border-radius: 0 2px 2px 0; | ||
| 761 | +} | ||
| 762 | +.seg-g { background: #67c23a; } | ||
| 763 | +.seg-y { background: #e6a23c; } | ||
| 764 | +.seg-r { background: #f56c6c; } | ||
| 765 | +.seg-gy { background: #909399; } | ||
| 766 | + | ||
| 767 | +/* ========== 稼动率 ========== */ | ||
| 768 | +.util-view { | ||
| 769 | + background: #f5f7fa; | ||
| 770 | + overflow-y: auto; | ||
| 771 | +} | ||
| 772 | +.util-toolbar { | ||
| 773 | + background: #fff; | ||
| 774 | + padding: 10px 20px; | ||
| 775 | + display: flex; | ||
| 776 | + align-items: center; | ||
| 777 | + gap: 10px; | ||
| 778 | + border-bottom: 1px solid #e8e8e8; | ||
| 779 | +} | ||
| 780 | +.util-label { | ||
| 781 | + font-size: 13px; color: #666; font-weight: bold; | ||
| 782 | +} | ||
| 783 | +.util-top-charts { | ||
| 784 | + display: grid; | ||
| 785 | + grid-template-columns: repeat(4, 1fr); | ||
| 786 | + gap: 14px; | ||
| 787 | + padding: 14px 20px; | ||
| 788 | +} | ||
| 789 | +.pie-card, .bar-card { | ||
| 790 | + background: #fff; | ||
| 791 | + border-radius: 6px; | ||
| 792 | + box-shadow: 0 1px 4px rgba(0,0,0,0.06); | ||
| 793 | + padding: 14px; | ||
| 794 | + display: flex; | ||
| 795 | + flex-direction: column; | ||
| 796 | +} | ||
| 797 | +.pie-title { | ||
| 798 | + font-size: 13px; | ||
| 799 | + font-weight: bold; | ||
| 800 | + color: #333; | ||
| 801 | + margin-bottom: 10px; | ||
| 802 | +} | ||
| 803 | +.pie-chart-svg { | ||
| 804 | + flex: 1; | ||
| 805 | + display: flex; | ||
| 806 | + align-items: center; | ||
| 807 | + justify-content: center; | ||
| 808 | + min-height: 180px; | ||
| 809 | + position: relative; | ||
| 810 | +} | ||
| 811 | +.pie-chart-svg svg { max-width: 200px; max-height: 180px; } | ||
| 812 | +.pie-empty-text { | ||
| 813 | + position: absolute; | ||
| 814 | + top: 50%; left: 50%; | ||
| 815 | + transform: translate(-50%,-50%); | ||
| 816 | + font-size: 14px; color: #ccc; | ||
| 817 | +} | ||
| 818 | +.pie-legend { | ||
| 819 | + margin-top: 8px; | ||
| 820 | + display: flex; | ||
| 821 | + gap: 10px; | ||
| 822 | + font-size: 11px; | ||
| 823 | + color: #666; | ||
| 824 | + line-height: 1.5; | ||
| 825 | +} | ||
| 826 | +.center-leg { justify-content: center; } | ||
| 827 | +.leg-item { display: inline-flex; align-items: center; gap: 3px; } | ||
| 828 | +.dot { display: inline-block; width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; } | ||
| 829 | +.dot.g { background: #67c23a; } | ||
| 830 | +.dot.y { background: #e6a23c; } | ||
| 831 | +.dot.r { background: #f56c6c; } | ||
| 832 | +.dot.gy { background: #909399; } | ||
| 833 | + | ||
| 834 | +.abnormal-list { flex: 1; overflow-y: auto; } | ||
| 835 | +.abn-footer { | ||
| 836 | + margin-top: 6px; | ||
| 837 | + font-size: 10px; | ||
| 838 | + color: #bbb; | ||
| 839 | + text-align: right; | ||
| 840 | +} | ||
| 841 | +.abn-legend { | ||
| 842 | + margin-top: 4px; | ||
| 843 | + font-size: 11px; | ||
| 844 | + color: #999; | ||
| 845 | + display: flex; | ||
| 846 | + gap: 10px; | ||
| 847 | + justify-content: flex-end; | ||
| 848 | +} | ||
| 849 | + | ||
| 850 | +.util-bottom-chart { | ||
| 851 | + margin: 0 20px 14px; | ||
| 852 | + background: #fff; | ||
| 853 | + border-radius: 6px; | ||
| 854 | + box-shadow: 0 1px 4px rgba(0,0,0,0.06); | ||
| 855 | + padding: 14px; | ||
| 856 | +} | ||
| 857 | +.stack-bar-toolbar { | ||
| 858 | + display: flex; | ||
| 859 | + align-items: center; | ||
| 860 | + gap: 10px; | ||
| 861 | + margin-bottom: 10px; | ||
| 862 | + font-size: 13px; | ||
| 863 | + color: #666; | ||
| 864 | +} | ||
| 865 | +.stack-bar-legend { | ||
| 866 | + display: flex; | ||
| 867 | + gap: 18px; | ||
| 868 | + margin-bottom: 8px; | ||
| 869 | + font-size: 12px; | ||
| 870 | + color: #666; | ||
| 871 | +} | ||
| 872 | +.stack-bar-chart { | ||
| 873 | + overflow-x: auto; | ||
| 874 | +} | ||
| 875 | +.stack-bar-chart svg { min-width: 100%; } | ||
| 876 | + | ||
| 877 | +/* ========== 能耗效率 ========== */ | ||
| 878 | +.eff-view { | ||
| 879 | + background: #f5f7fa; | ||
| 880 | + overflow-y: auto; | ||
| 881 | +} | ||
| 882 | +.eff-toolbar { | ||
| 883 | + background: #fff; | ||
| 884 | + padding: 10px 20px; | ||
| 885 | + display: flex; | ||
| 886 | + align-items: center; | ||
| 887 | + gap: 10px; | ||
| 888 | + border-bottom: 1px solid #e8e8e8; | ||
| 889 | +} | ||
| 890 | +.eff-label { | ||
| 891 | + font-size: 13px; color: #666; font-weight: bold; | ||
| 892 | +} | ||
| 893 | +.eff-legend { | ||
| 894 | + padding: 10px 24px; | ||
| 895 | + font-size: 13px; | ||
| 896 | + color: #666; | ||
| 897 | + display: flex; | ||
| 898 | + align-items: center; | ||
| 899 | + gap: 20px; | ||
| 900 | +} | ||
| 901 | +.leg-line { | ||
| 902 | + display: inline-flex; | ||
| 903 | + align-items: center; | ||
| 904 | + gap: 5px; | ||
| 905 | +} | ||
| 906 | +.leg-line i { | ||
| 907 | + display: inline-block; | ||
| 908 | + width: 16px; | ||
| 909 | + height: 3px; | ||
| 910 | + border-radius: 2px; | ||
| 911 | + background: var(--lc); | ||
| 912 | +} | ||
| 913 | +.eff-chart { | ||
| 914 | + margin: 0 20px 20px; | ||
| 915 | + background: #fff; | ||
| 916 | + border-radius: 6px; | ||
| 917 | + box-shadow: 0 1px 4px rgba(0,0,0,0.06); | ||
| 918 | + padding: 14px; | ||
| 919 | + overflow-x: auto; | ||
| 920 | +} | ||
| 921 | +.eff-chart svg { min-width: 1200px; } | ||
| 922 | +</style> |
src/views/SmartLight.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="smart-light-page"> | ||
| 3 | + <!-- 第一行:状态Tab + 搜索 --> | ||
| 4 | + <div class="top-toolbar"> | ||
| 5 | + <div class="status-tabs"> | ||
| 6 | + <div | ||
| 7 | + v-for="tab in statusTabs" | ||
| 8 | + :key="tab.key" | ||
| 9 | + :class="['status-tab', { active: currentStatus === tab.key }]" | ||
| 10 | + @click="currentStatus = tab.key" | ||
| 11 | + > | ||
| 12 | + {{ tab.label }} | ||
| 13 | + </div> | ||
| 14 | + </div> | ||
| 15 | + </div> | ||
| 16 | + | ||
| 17 | + <!-- 筛选栏(仅实时状态显示) --> | ||
| 18 | + <div v-if="currentStatus === 'realtime'" class="filter-bar"> | ||
| 19 | + <el-input | ||
| 20 | + v-model="searchKeyword" | ||
| 21 | + placeholder="输入设备名称搜索" | ||
| 22 | + clearable | ||
| 23 | + size="default" | ||
| 24 | + style="width: 220px; margin-right: 16px;" | ||
| 25 | + @keyup.enter="doSearch" | ||
| 26 | + @clear="doSearch" | ||
| 27 | + /> | ||
| 28 | + <div class="filter-tags"> | ||
| 29 | + <span :class="['tag-item', 'black', { active: !lampStateFilter }]" @click="filterByLampState('')"><i></i>全部{{ totalCounts.all }}台</span> | ||
| 30 | + <span :class="['tag-item', 'red', { active: lampStateFilter === '1' }]" @click="filterByLampState('1')"><i></i>红:{{ totalCounts.red }}台</span> | ||
| 31 | + <span :class="['tag-item', 'yellow', { active: lampStateFilter === '2' }]" @click="filterByLampState('2')"><i></i>黄:{{ totalCounts.yellow }}台</span> | ||
| 32 | + <span :class="['tag-item', 'green', { active: lampStateFilter === '3' }]" @click="filterByLampState('3')"><i></i>绿:{{ totalCounts.green }}台</span> | ||
| 33 | + <span :class="['tag-item', 'blue', { active: lampStateFilter === '4' }]" @click="filterByLampState('4')"><i></i>蓝:{{ totalCounts.blue }}台</span> | ||
| 34 | + <span :class="['tag-item', 'gray', { active: lampStateFilter === '0' }]" @click="filterByLampState('0')"><i></i>灭灯:{{ totalCounts.gray }}台</span> | ||
| 35 | + </div> | ||
| 36 | + </div> | ||
| 37 | + | ||
| 38 | + <!-- ========== 实时状态:设备卡片 ========== --> | ||
| 39 | + <div v-if="currentStatus === 'realtime'" class="tab-content"> | ||
| 40 | + <!-- 设备卡片网格 --> | ||
| 41 | + <div class="device-grid" :class="{ 'grid-full': totalDevices > PAGE_SIZE || (pagedDevices.length >= PAGE_SIZE && totalDevices > PAGE_SIZE), 'grid-normal': pagedDevices.length <= 6 }"> | ||
| 42 | + <div v-for="device in pagedDevices" :key="device.id" :class="['device-card', device.status]"> | ||
| 43 | + <!-- 标题栏 --> | ||
| 44 | + <div class="card-header"> | ||
| 45 | + <span class="device-name">{{ device.name }}</span> | ||
| 46 | + <el-icon class="menu-icon"><Menu /></el-icon> | ||
| 47 | + </div> | ||
| 48 | + | ||
| 49 | + <!-- 内容区:左侧状态灯 + 右侧信息 --> | ||
| 50 | + <div class="card-body"> | ||
| 51 | + <div class="status-bar"> | ||
| 52 | + <div :class="['bar-block', 'block-red', { active: device.status === 'red' }]"></div> | ||
| 53 | + <div :class="['bar-block', 'block-yellow', { active: device.status === 'yellow' }]"></div> | ||
| 54 | + <div :class="['bar-block', 'block-green', { active: device.status === 'green' }]"></div> | ||
| 55 | + <div :class="['bar-block', 'block-blue', { active: device.status === 'blue' }]"></div> | ||
| 56 | + </div> | ||
| 57 | + | ||
| 58 | + <div class="card-content"> | ||
| 59 | + <div class="info-row">稼动率: <span class="value-highlight">{{ device.utilization }}%</span></div> | ||
| 60 | + <div class="info-row">{{ getLampLabel(device.status) }}: <span>{{ device.status === 'gray' ? '0分' : device.lightTime }}</span></div> | ||
| 61 | + | ||
| 62 | + <div class="digital-display"> | ||
| 63 | + <span v-for="(digit, idx) in device.displayValue" :key="idx" class="digit">{{ digit }}</span> | ||
| 64 | + </div> | ||
| 65 | + </div> | ||
| 66 | + </div> | ||
| 67 | + | ||
| 68 | + <!-- 底部按钮 --> | ||
| 69 | + <div class="card-footer"> | ||
| 70 | + <button class="action-btn" @click="openDetail('oee', device)"> | ||
| 71 | + <el-icon><DataLine /></el-icon>OEE时序 | ||
| 72 | + </button> | ||
| 73 | + <button class="action-btn" @click="openDetail('utilization', device)"> | ||
| 74 | + <el-icon><Warning /></el-icon>稼动率 | ||
| 75 | + </button> | ||
| 76 | + <button v-if="false" class="action-btn" @click="openDetail('setting', device)"> | ||
| 77 | + <el-icon><Setting /></el-icon>设置 | ||
| 78 | + </button> | ||
| 79 | + <button v-if="false" class="action-btn" @click="openDetail('count', device)"> | ||
| 80 | + <el-icon><Document /></el-icon>计数明细 | ||
| 81 | + </button> | ||
| 82 | + </div> | ||
| 83 | + </div> | ||
| 84 | + </div> | ||
| 85 | + | ||
| 86 | + <!-- 分页 --> | ||
| 87 | + <div v-if="totalDevices > 0" class="pagination-wrapper"> | ||
| 88 | + <!-- 分页控件 --> | ||
| 89 | + <div class="pagination-controls"> | ||
| 90 | + <!-- 每页条数选择 --> | ||
| 91 | + <select class="page-size-select" :value="PAGE_SIZE"> | ||
| 92 | + <option value="12">12 条/页</option> | ||
| 93 | + </select> | ||
| 94 | + | ||
| 95 | + <!-- 导航按钮组 --> | ||
| 96 | + <button class="page-btn" :disabled="currentPage === 1" @click="currentPage = 1">«</button> | ||
| 97 | + <button class="page-btn" :disabled="currentPage === 1" @click="currentPage--"><</button> | ||
| 98 | + <template v-for="(p, i) in visiblePages" :key="i"> | ||
| 99 | + <button v-if="typeof p === 'number'" :class="['page-btn', { active: currentPage === p }]" @click="currentPage = p">{{ p }}</button> | ||
| 100 | + <span v-else class="page-dots">{{ p }}</span> | ||
| 101 | + </template> | ||
| 102 | + <button class="page-btn" :disabled="currentPage === totalPages" @click="currentPage++">></button> | ||
| 103 | + <button class="page-btn" :disabled="currentPage === totalPages" @click="currentPage = totalPages">»</button> | ||
| 104 | + </div> | ||
| 105 | + </div> | ||
| 106 | + </div><!-- /tab-content realtime --> | ||
| 107 | + | ||
| 108 | + <!-- ========== 时序状态:时间轴甘特图 ========== --> | ||
| 109 | + <div v-else-if="currentStatus === 'timeseries'" class="tab-content timeseries-view"> | ||
| 110 | + <div class="ts-toolbar"> | ||
| 111 | + <span class="ts-label">查询方式:</span> | ||
| 112 | + <el-radio-group v-model="tsQueryMode" size="small"> | ||
| 113 | + <el-radio-button value="day">日查询</el-radio-button> | ||
| 114 | + </el-radio-group> | ||
| 115 | + <el-date-picker | ||
| 116 | + v-model="tsDate" | ||
| 117 | + type="date" | ||
| 118 | + size="small" | ||
| 119 | + placeholder="选择日期" | ||
| 120 | + value-format="YYYY-MM-DD" | ||
| 121 | + :disabled-date="disabledDate" | ||
| 122 | + style="width: 160px; margin-left: 8px;" | ||
| 123 | + /> | ||
| 124 | + <el-button type="primary" size="small" :loading="tsLoading" @click="fetchTimeSeriesData">查询</el-button> | ||
| 125 | + </div> | ||
| 126 | + | ||
| 127 | + <!-- Canvas 甘特图区域 --> | ||
| 128 | + <div class="ts-table-wrap" ref="ganttContainerRef" @wheel.prevent="onGanttWheel" @mousemove="onGanttMouseMove" @mouseleave="onGanttMouseLeave"> | ||
| 129 | + <canvas ref="ganttCanvasRef" class="gantt-canvas"></canvas> | ||
| 130 | + <!-- Hover Tooltip --> | ||
| 131 | + <div v-if="ganttHoveredSeg" class="gantt-tooltip" :style="{ left: ganttTooltipPos.x + 'px', top: ganttTooltipPos.y + 'px' }"> | ||
| 132 | + <div class="gtt-row"><span class="gtt-dot" :style="{ background: getLampColor(ganttHoveredSeg.lampState) }"></span>{{ getLampLabelName(ganttHoveredSeg.lampState) }}: {{ formatDuration(ganttHoveredSeg.duration) }}</div> | ||
| 133 | + <div class="gtt-sub">{{ formatTimeRange(ganttHoveredSeg.startTime, ganttHoveredSeg.endTime) }}</div> | ||
| 134 | + </div> | ||
| 135 | + </div> | ||
| 136 | + | ||
| 137 | + <!-- 分页 --> | ||
| 138 | + <div class="pagination-wrapper"> | ||
| 139 | + <span>共 {{ tsTotal }} 条</span> | ||
| 140 | + <div class="pagination-controls" style="margin-left: auto;"> | ||
| 141 | + <button class="page-btn" :disabled="tsPageNo === 1" @click="tsPageNo = 1; fetchTimeSeriesData()">«</button> | ||
| 142 | + <button class="page-btn" :disabled="tsPageNo === 1" @click="tsPageNo--; fetchTimeSeriesData()"><</button> | ||
| 143 | + <template v-for="(p, i) in tsVisiblePages" :key="i"> | ||
| 144 | + <button v-if="typeof p === 'number'" :class="['page-btn', { active: tsPageNo === p }]" @click="tsPageNo = p; fetchTimeSeriesData()">{{ p }}</button> | ||
| 145 | + <span v-else class="page-dots">{{ p }}</span> | ||
| 146 | + </template> | ||
| 147 | + <button class="page-btn" :disabled="tsPageNo >= tsTotalPages" @click="tsPageNo++; fetchTimeSeriesData()">></button> | ||
| 148 | + <button class="page-btn" :disabled="tsPageNo >= tsTotalPages" @click="tsPageNo = tsTotalPages; fetchTimeSeriesData()">»</button> | ||
| 149 | + </div> | ||
| 150 | + </div> | ||
| 151 | + </div> | ||
| 152 | + | ||
| 153 | + <!-- ========== 稼动率:多图表视图(Canvas) ========== --> | ||
| 154 | + <div v-else-if="currentStatus === 'utilization'" class="tab-content util-view" style="position:relative;"> | ||
| 155 | + <div class="util-toolbar"> | ||
| 156 | + <span class="util-label">查询方式:</span> | ||
| 157 | + <el-radio-group v-model="utilQueryMode" size="small"> | ||
| 158 | + <el-radio-button value="day">日查询</el-radio-button> | ||
| 159 | + <el-radio-button value="week">周查询</el-radio-button> | ||
| 160 | + <el-radio-button value="month">月查询</el-radio-button> | ||
| 161 | + </el-radio-group> | ||
| 162 | + <el-date-picker | ||
| 163 | + v-if="utilQueryMode === 'day'" | ||
| 164 | + v-model="utilDate" | ||
| 165 | + type="date" | ||
| 166 | + placeholder="选择日期" | ||
| 167 | + size="small" | ||
| 168 | + value-format="YYYY-MM-DD" | ||
| 169 | + style="width:160px;margin-left:8px;" | ||
| 170 | + /> | ||
| 171 | + <el-date-picker | ||
| 172 | + v-else-if="utilQueryMode === 'week'" | ||
| 173 | + v-model="utilWeekDate" | ||
| 174 | + type="date" | ||
| 175 | + :format="utilWeekDisplayFormat" | ||
| 176 | + placeholder="选择周" | ||
| 177 | + size="small" | ||
| 178 | + value-format="YYYY-MM-DD" | ||
| 179 | + :disabled-date="disableNonMonday" | ||
| 180 | + style="width:160px;margin-left:8px;" | ||
| 181 | + /> | ||
| 182 | + <el-date-picker | ||
| 183 | + v-else-if="utilQueryMode === 'month'" | ||
| 184 | + v-model="utilMonthDate" | ||
| 185 | + type="month" | ||
| 186 | + placeholder="选择月份" | ||
| 187 | + size="small" | ||
| 188 | + value-format="YYYY-MM" | ||
| 189 | + style="width:140px;margin-left:8px;" | ||
| 190 | + /> | ||
| 191 | + <el-button type="primary" size="small" :loading="utilLoading" @click="fetchUtilData">查询</el-button> | ||
| 192 | + </div> | ||
| 193 | + | ||
| 194 | + <!-- 上排:3个饼图 + 异常机台排名 --> | ||
| 195 | + <div class="util-top-charts"> | ||
| 196 | + <!-- 总时长 饼图 --> | ||
| 197 | + <div class="pie-card"> | ||
| 198 | + <div class="pie-title">总时长:</div> | ||
| 199 | + <canvas ref="utilPieTotalRef" class="util-canvas-pie" | ||
| 200 | + @mousemove="onUtilPieMove('total', $event)" @mouseleave="onUtilPieLeave('total')"></canvas> | ||
| 201 | + <div v-if="utilLoading" class="util-loading-overlay"><div class="util-spinner"></div></div> | ||
| 202 | + <div class="pie-legend util-total-leg"> | ||
| 203 | + <template v-for="(seg,i) in utilTotalSegments" :key="i"> | ||
| 204 | + <span class="leg-item"> | ||
| 205 | + <i class="dot" :style="{background: seg.color }"></i>{{ seg.label }}<br/> | ||
| 206 | + </span> | ||
| 207 | + </template> | ||
| 208 | + </div> | ||
| 209 | + </div> | ||
| 210 | + <!-- 稼动率 饼图 --> | ||
| 211 | + <div class="pie-card"> | ||
| 212 | + <div class="pie-title">稼动率:</div> | ||
| 213 | + <canvas ref="utilPieRateRef" class="util-canvas-pie" | ||
| 214 | + @mousemove="onUtilPieMove('rate', $event)" @mouseleave="onUtilPieLeave('rate')"></canvas> | ||
| 215 | + <div v-if="utilLoading" class="util-loading-overlay"><div class="util-spinner"></div></div> | ||
| 216 | + <div class="pie-legend center-leg"> | ||
| 217 | + <span class="leg-item"><i class="dot g"></i>绿灯</span> | ||
| 218 | + <span class="leg-item"><i class="dot y"></i>黄灯</span> | ||
| 219 | + <span class="leg-item"><i class="dot r"></i>红灯</span> | ||
| 220 | + </div> | ||
| 221 | + </div> | ||
| 222 | + <!-- 当前机台运行状态 饼图 --> | ||
| 223 | + <div class="pie-card"> | ||
| 224 | + <div class="pie-title">当前机台运行状态:</div> | ||
| 225 | + <canvas ref="utilPieStatusRef" class="util-canvas-pie" | ||
| 226 | + @mousemove="onUtilPieMove('status', $event)" @mouseleave="onUtilPieLeave('status')"></canvas> | ||
| 227 | + <div v-if="utilLoading" class="util-loading-overlay"><div class="util-spinner"></div></div> | ||
| 228 | + <div class="pie-legend center-leg"> | ||
| 229 | + <span class="leg-item"><i class="dot g"></i>绿灯</span> | ||
| 230 | + <span class="leg-item"><i class="dot y"></i>黄灯</span> | ||
| 231 | + <span class="leg-item"><i class="dot r"></i>红灯</span> | ||
| 232 | + <span class="leg-item"><i class="dot gy"></i>灭灯</span> | ||
| 233 | + </div> | ||
| 234 | + </div> | ||
| 235 | + <!-- 异常机台排名 水平柱状图 --> | ||
| 236 | + <div class="bar-card"> | ||
| 237 | + <div class="pie-title">异常机台排名:</div> | ||
| 238 | + <canvas ref="utilAbnormalRef" class="util-canvas-abnormal" | ||
| 239 | + @mousemove="onUtilAbnormalMove" @mouseleave="onUtilAbnormalLeave"></canvas> | ||
| 240 | + <div v-if="utilLoading" class="util-loading-overlay"><div class="util-spinner"></div></div> | ||
| 241 | + <div class="abn-legend"><i class="dot y"></i>黄灯 <i class="dot r"></i>红灯</div> | ||
| 242 | + </div> | ||
| 243 | + </div> | ||
| 244 | + | ||
| 245 | + <!-- 饼图统一Tooltip --> | ||
| 246 | + <div v-if="utilPieTip.show" class="util-tooltip" :style="{ left: utilPieTip.x+'px', top: utilPieTip.y+'px' }"> | ||
| 247 | + <div class="utip-title">{{ utilPieTip.title }}</div> | ||
| 248 | + <template v-for="(line,i) in utilPieTip.lines" :key="i"> | ||
| 249 | + <div class="utip-line"><i :style="{background: line.color }"></i>{{ line.label }} {{ line.value }}</div> | ||
| 250 | + </template> | ||
| 251 | + </div> | ||
| 252 | + | ||
| 253 | + <!-- 异常机台Tooltip --> | ||
| 254 | + <div v-if="utilAbnTip.show" class="util-tooltip utip-wide" :style="{ left: utilAbnTip.x+'px', top: utilAbnTip.y+'px' }"> | ||
| 255 | + <div class="utip-title">{{ utilAbnTip.name }}</div> | ||
| 256 | + <div class="utip-line"><i class="dot y"></i>黄灯 {{ utilAbnTip.yellowDur }}({{ utilAbnTip.yellowPct }})</div> | ||
| 257 | + <div class="utip-line"><i class="dot r"></i>红灯 {{ utilAbnTip.redDur }}({{ utilAbnTip.redPct }})</div> | ||
| 258 | + </div> | ||
| 259 | + | ||
| 260 | + <!-- 下排堆叠柱状图 --> | ||
| 261 | + <div class="util-bottom-chart"> | ||
| 262 | + <div class="stack-bar-toolbar"> | ||
| 263 | + <span>排序:</span> | ||
| 264 | + <el-radio-group v-model="sortMode" size="small" @change="drawUtilStackBar"> | ||
| 265 | + <el-radio-button value="duration">绿灯时长</el-radio-button> | ||
| 266 | + <el-radio-button value="rate">稼动率</el-radio-button> | ||
| 267 | + </el-radio-group> | ||
| 268 | + </div> | ||
| 269 | + <div class="stack-bar-legend"> | ||
| 270 | + <span class="leg-item"><i class="dot g"></i>绿灯</span> | ||
| 271 | + <span class="leg-item"><i class="dot y"></i>黄灯</span> | ||
| 272 | + <span class="leg-item"><i class="dot r"></i>红灯</span> | ||
| 273 | + <span class="leg-item"><i class="dot gy"></i>灭灯</span> | ||
| 274 | + </div> | ||
| 275 | + <div class="stack-bar-canvas-wrap" style="position:relative;"> | ||
| 276 | + <canvas ref="utilStackRef" class="util-stack-canvas" | ||
| 277 | + @mousemove="onUtilStackMove" @mouseleave="onUtilStackLeave"></canvas> | ||
| 278 | + <div v-if="utilLoading" class="util-loading-overlay"><div class="util-spinner"></div></div> | ||
| 279 | + </div> | ||
| 280 | + </div> | ||
| 281 | + | ||
| 282 | + <!-- 堆叠柱状图Tooltip --> | ||
| 283 | + <div v-if="utilStackTip.show" class="util-tooltip utip-wide" :style="{ left: utilStackTip.x+'px', top: utilStackTip.y+'px' }"> | ||
| 284 | + <div class="utip-title">{{ utilStackTip.name }}</div> | ||
| 285 | + <div class="utip-line" v-for="(l,i) in utilStackTip.lines" :key="i"><i :style="{background:l.color}"></i>{{ l.label }} {{ l.dur }}({{ l.pct }})</div> | ||
| 286 | + </div> | ||
| 287 | + </div> | ||
| 288 | + | ||
| 289 | + <!-- ========== 开机率:柱状图 ========== --> | ||
| 290 | + <div v-else-if="currentStatus === 'startup'" class="tab-content startup-view"> | ||
| 291 | + <div class="startup-toolbar"> | ||
| 292 | + <span class="startup-label">查询方式:</span> | ||
| 293 | + <el-radio-group v-model="startupQueryMode" size="small"> | ||
| 294 | + <el-radio-button value="day">日查询</el-radio-button> | ||
| 295 | + <el-radio-button value="week">周查询</el-radio-button> | ||
| 296 | + <el-radio-button value="month">月查询</el-radio-button> | ||
| 297 | + </el-radio-group> | ||
| 298 | + <el-date-picker | ||
| 299 | + v-if="startupQueryMode === 'day'" | ||
| 300 | + v-model="startupDate" | ||
| 301 | + type="date" | ||
| 302 | + placeholder="选择日期" | ||
| 303 | + size="small" | ||
| 304 | + value-format="YYYY-MM-DD" | ||
| 305 | + style="width:160px;margin-left:8px;" | ||
| 306 | + /> | ||
| 307 | + <el-date-picker | ||
| 308 | + v-else-if="startupQueryMode === 'week'" | ||
| 309 | + v-model="startupWeekDate" | ||
| 310 | + type="date" | ||
| 311 | + :format="startupWeekDisplayFormat" | ||
| 312 | + placeholder="选择周" | ||
| 313 | + size="small" | ||
| 314 | + value-format="YYYY-MM-DD" | ||
| 315 | + :disabled-date="disableNonMonday" | ||
| 316 | + style="width:160px;margin-left:8px;" | ||
| 317 | + /> | ||
| 318 | + <el-date-picker | ||
| 319 | + v-else-if="startupQueryMode === 'month'" | ||
| 320 | + v-model="startupMonthDate" | ||
| 321 | + type="month" | ||
| 322 | + placeholder="选择月份" | ||
| 323 | + size="small" | ||
| 324 | + value-format="YYYY-MM" | ||
| 325 | + style="width:140px;margin-left:8px;" | ||
| 326 | + /> | ||
| 327 | + <el-button type="primary" size="small" :loading="startupLoading" @click="fetchStartupData">查询</el-button> | ||
| 328 | + </div> | ||
| 329 | + | ||
| 330 | + <div class="startup-legend"> | ||
| 331 | + <i class="dot g" style="display:inline-block;width:14px;height:14px;border-radius:3px;background:#67c23a;"></i> | ||
| 332 | + 开机率 | ||
| 333 | + </div> | ||
| 334 | + <div class="startup-chart" style="position:relative;"> | ||
| 335 | + <canvas ref="startupCanvasRef" class="startup-canvas" | ||
| 336 | + @mousemove="onStartupMove" @mouseleave="onStartupLeave"></canvas> | ||
| 337 | + <div v-if="startupLoading" class="util-loading-overlay"><div class="util-spinner"></div></div> | ||
| 338 | + </div> | ||
| 339 | + | ||
| 340 | + <!-- Tooltip --> | ||
| 341 | + <div v-if="startupTip.show" class="util-tooltip utip-wide" :style="{ left: startupTip.x+'px', top: startupTip.y+'px' }"> | ||
| 342 | + <div class="utip-title">{{ startupTip.name }}</div> | ||
| 343 | + <div class="utip-line"><i style="background:#67c23a"></i>开机率 {{ startupTip.bootRate }}</div> | ||
| 344 | + <div class="utip-line"><i style="background:#91cc75"></i>开机时长 {{ startupTip.onDuration }}</div> | ||
| 345 | + <div class="utip-line"><i style="background:#909399"></i>关机时长 {{ startupTip.offDuration }}</div> | ||
| 346 | + <div class="utip-line">总时长 {{ startupTip.totalDuration }}</div> | ||
| 347 | + </div> | ||
| 348 | + </div> | ||
| 349 | + | ||
| 350 | + <!-- OEE时序弹窗 --> | ||
| 351 | + <OeeDialog | ||
| 352 | + v-model:visible="dialogVisible.oee" | ||
| 353 | + :device="currentDevice" | ||
| 354 | + /> | ||
| 355 | + | ||
| 356 | + <!-- 设置弹窗 --> | ||
| 357 | + <SettingDialog | ||
| 358 | + v-model:visible="dialogVisible.setting" | ||
| 359 | + :device="currentDevice" | ||
| 360 | + /> | ||
| 361 | + | ||
| 362 | + <!-- 稼动率弹窗 --> | ||
| 363 | + <UtilizationDialog | ||
| 364 | + v-model:visible="dialogVisible.utilization" | ||
| 365 | + :device="currentDevice" | ||
| 366 | + /> | ||
| 367 | + | ||
| 368 | + <!-- 计数明细弹窗 --> | ||
| 369 | + <CountDialog | ||
| 370 | + v-model:visible="dialogVisible.count" | ||
| 371 | + :device="currentDevice" | ||
| 372 | + /> | ||
| 373 | + </div> | ||
| 374 | +</template> | ||
| 375 | + | ||
| 376 | +<script setup> | ||
| 377 | +import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue' | ||
| 378 | +import { Search, Menu, DataLine, Setting, Document, Warning } from '@element-plus/icons-vue' | ||
| 379 | +import { ElMessage } from 'element-plus' | ||
| 380 | +import OeeDialog from '../components/OeeDialog.vue' | ||
| 381 | +import SettingDialog from '../components/SettingDialog.vue' | ||
| 382 | +import UtilizationDialog from '../components/UtilizationDialog.vue' | ||
| 383 | +import CountDialog from '../components/CountDialog.vue' | ||
| 384 | + | ||
| 385 | +const selectedFactory = ref('金马') | ||
| 386 | +const searchKeyword = ref('') | ||
| 387 | +const currentStatus = ref('realtime') | ||
| 388 | +const lampStateFilter = ref('') // 灯状态筛选: ''=全部, '1'=绿, '2'=红, '3'=黄, '0'=灭灯 | ||
| 389 | + | ||
| 390 | +// 各颜色数量(接口返回后更新,初始默认值) | ||
| 391 | +const totalCounts = reactive({ all: 0, red: 0, yellow: 0, green: 0, blue: 0, gray: 0 }) | ||
| 392 | + | ||
| 393 | +// 点击筛选标签 | ||
| 394 | +function filterByLampState(lampState) { | ||
| 395 | + if (lampStateFilter.value === lampState) return | ||
| 396 | + lampStateFilter.value = lampState | ||
| 397 | + currentPage.value = 1 | ||
| 398 | + fetchDeviceList() | ||
| 399 | +} | ||
| 400 | + | ||
| 401 | +// 搜索 | ||
| 402 | +function doSearch() { | ||
| 403 | + currentPage.value = 1 | ||
| 404 | + fetchDeviceList() | ||
| 405 | +} | ||
| 406 | + | ||
| 407 | +const statusTabs = [ | ||
| 408 | + { key: 'realtime', label: '实时状态' }, | ||
| 409 | + { key: 'timeseries', label: '时序状态' }, | ||
| 410 | + { key: 'utilization', label: '稼动率' }, | ||
| 411 | + { key: 'startup', label: '开机率' } | ||
| 412 | +] | ||
| 413 | + | ||
| 414 | +// lampState → 卡片状态映射: 0=灭灯, 1=红, 2=黄, 3=绿, 4=蓝 | ||
| 415 | +function mapLampStatus(lampState) { | ||
| 416 | + if (lampState === '3') return 'green' | ||
| 417 | + if (lampState === '1') return 'red' | ||
| 418 | + if (lampState === '2') return 'yellow' | ||
| 419 | + if (lampState === '4') return 'blue' | ||
| 420 | + return 'gray' // 0 或其他 → 灭灯/灰灯 | ||
| 421 | +} | ||
| 422 | + | ||
| 423 | +// 状态 → 灯色名称 | ||
| 424 | +const LAMP_LABEL_MAP = { green: '绿灯', yellow: '黄灯', red: '红灯', gray: '灭灯' } | ||
| 425 | +function getLampLabel(status) { | ||
| 426 | + return LAMP_LABEL_MAP[status] || '灭灯' | ||
| 427 | +} | ||
| 428 | + | ||
| 429 | +const PAGE_SIZE = 12 | ||
| 430 | +const currentPage = ref(1) | ||
| 431 | +const deviceList = ref([]) | ||
| 432 | +const totalDevices = ref(0) | ||
| 433 | +const totalPages = computed(() => Math.ceil(totalDevices.value / PAGE_SIZE) || 1) | ||
| 434 | + | ||
| 435 | +// 分页数据直接来自接口返回的list(服务端分页) | ||
| 436 | +const pagedDevices = computed(() => deviceList.value) | ||
| 437 | + | ||
| 438 | +// 页码显示逻辑 | ||
| 439 | +const visiblePages = computed(() => { | ||
| 440 | + const pages = [] | ||
| 441 | + const maxVisible = 5 | ||
| 442 | + const cp = currentPage.value | ||
| 443 | + const tp = totalPages.value | ||
| 444 | + let start = Math.max(1, cp - Math.floor(maxVisible / 2)) | ||
| 445 | + let end = Math.min(tp, start + maxVisible - 1) | ||
| 446 | + if (end - start + 1 < maxVisible) start = Math.max(1, end - maxVisible + 1) | ||
| 447 | + if (start > 1) { pages.push(1); if (start > 2) pages.push('...') } | ||
| 448 | + for (let i = start; i <= end; i++) pages.push(i) | ||
| 449 | + if (end < tp) { if (end < tp - 1) pages.push('...'); pages.push(tp) } | ||
| 450 | + return pages | ||
| 451 | +}) | ||
| 452 | + | ||
| 453 | +// 获取设备列表 | ||
| 454 | +async function fetchDeviceList() { | ||
| 455 | + try { | ||
| 456 | + const params = new URLSearchParams({ | ||
| 457 | + pageNo: currentPage.value, | ||
| 458 | + pageSize: PAGE_SIZE, | ||
| 459 | + }) | ||
| 460 | + if (searchKeyword.value) params.append('deviceName', searchKeyword.value) | ||
| 461 | + if (lampStateFilter.value) params.append('lampState', lampStateFilter.value) | ||
| 462 | + | ||
| 463 | + const res = await fetch(`/api/device/list?${params}`) | ||
| 464 | + const data = await res.json() | ||
| 465 | + deviceList.value = (data.list || []).map(item => ({ | ||
| 466 | + id: item.id, | ||
| 467 | + name: item.deviceName || item.dtuSn, | ||
| 468 | + status: mapLampStatus(item.lampState), | ||
| 469 | + utilization: parseFloat(item.utilizationRate) || 0, | ||
| 470 | + lightTime: item.duration || '0分', | ||
| 471 | + displayValue: '000000', | ||
| 472 | + _raw: item, | ||
| 473 | + })) | ||
| 474 | + totalDevices.value = data.total || 0 | ||
| 475 | + | ||
| 476 | + // 刷新统计数据 | ||
| 477 | + await fetchStats() | ||
| 478 | + } catch (err) { | ||
| 479 | + console.error('获取设备列表失败:', err) | ||
| 480 | + } | ||
| 481 | +} | ||
| 482 | + | ||
| 483 | +// 获取灯状态统计 | ||
| 484 | +async function fetchStats() { | ||
| 485 | + try { | ||
| 486 | + const res = await fetch('/api/device/stats') | ||
| 487 | + const data = await res.json() | ||
| 488 | + totalCounts.all = data.all || 0 | ||
| 489 | + totalCounts.red = data.red || 0 | ||
| 490 | + totalCounts.yellow = data.yellow || 0 | ||
| 491 | + totalCounts.green = data.green || 0 | ||
| 492 | + totalCounts.blue = data.blue || 0 | ||
| 493 | + totalCounts.gray = data.off || 0 // 接口off → 灭灯/灰灯 | ||
| 494 | + } catch (err) { | ||
| 495 | + console.error('获取统计失败:', err) | ||
| 496 | + } | ||
| 497 | +} | ||
| 498 | + | ||
| 499 | +// 切页时重新请求 | ||
| 500 | +watch(currentPage, () => { | ||
| 501 | + fetchDeviceList() | ||
| 502 | +}) | ||
| 503 | + | ||
| 504 | +// 初始加载 | ||
| 505 | +onMounted(() => { | ||
| 506 | + fetchDeviceList() | ||
| 507 | +}) | ||
| 508 | + | ||
| 509 | +const dialogVisible = reactive({ | ||
| 510 | + oee: false, | ||
| 511 | + setting: false, | ||
| 512 | + count: false, | ||
| 513 | + utilization: false | ||
| 514 | +}) | ||
| 515 | +const currentDevice = ref(null) | ||
| 516 | + | ||
| 517 | +function openDetail(type, device) { | ||
| 518 | + currentDevice.value = device | ||
| 519 | + dialogVisible[type] = true | ||
| 520 | +} | ||
| 521 | + | ||
| 522 | +// ========== 时序状态数据 ========== | ||
| 523 | +const tsQueryMode = ref('day') | ||
| 524 | +const tsDate = ref(null) | ||
| 525 | +const tsLoading = ref(false) | ||
| 526 | +const tsPageNo = ref(1) | ||
| 527 | +const tsPageSize = 20 | ||
| 528 | +const tsTotal = ref(0) | ||
| 529 | +const timeSeriesData = ref([]) | ||
| 530 | + | ||
| 531 | +// 最大页码限制 | ||
| 532 | +const TS_MAX_PAGES = 20 | ||
| 533 | +const tsTotalPages = computed(() => Math.min(Math.ceil(tsTotal.value / tsPageSize) || 1, TS_MAX_PAGES)) | ||
| 534 | + | ||
| 535 | +// 可见页码 | ||
| 536 | +const tsVisiblePages = computed(() => { | ||
| 537 | + const pages = [] | ||
| 538 | + const maxVisible = 5 | ||
| 539 | + const cp = tsPageNo.value | ||
| 540 | + const tp = tsTotalPages.value | ||
| 541 | + let start = Math.max(1, cp - Math.floor(maxVisible / 2)) | ||
| 542 | + let end = Math.min(tp, start + maxVisible - 1) | ||
| 543 | + if (end - start + 1 < maxVisible) start = Math.max(1, end - maxVisible + 1) | ||
| 544 | + if (start > 1) { pages.push(1); if (start > 2) pages.push('...') } | ||
| 545 | + for (let i = start; i <= end; i++) pages.push(i) | ||
| 546 | + if (end < tp) { if (end < tp - 1) pages.push('...'); pages.push(tp) } | ||
| 547 | + return pages | ||
| 548 | +}) | ||
| 549 | + | ||
| 550 | +// 日期禁用:超过今天不可选 | ||
| 551 | +function disabledDate(time) { | ||
| 552 | + return time.getTime() > Date.now() | ||
| 553 | +} | ||
| 554 | + | ||
| 555 | +// Canvas refs & 缩放参数(参考 OeeDialog) | ||
| 556 | +const ganttCanvasRef = ref(null) | ||
| 557 | +const ganttContainerRef = ref(null) | ||
| 558 | +const ganttZoomLevel = ref(1) | ||
| 559 | +const ganttViewOffsetX = ref(0) | ||
| 560 | +const GANTT_MIN_ZOOM = 0.02 // 最小 zoom(最大放大) | ||
| 561 | +const GANTT_MAX_ZOOM = 1 // 最大 zoom(最小放大,即原始大小) | ||
| 562 | + | ||
| 563 | +// Hover 状态 | ||
| 564 | +const ganttHoveredSeg = ref(null) | ||
| 565 | +const ganttTooltipPos = ref({ x: 0, y: 0 }) | ||
| 566 | + | ||
| 567 | +// 鼠标滚轮缩放(以鼠标位置为中心点)— 与 OeeDialog 一致的简洁坐标系统 | ||
| 568 | +function onGanttWheel(e) { | ||
| 569 | + const container = ganttContainerRef.value | ||
| 570 | + if (!container) return | ||
| 571 | + const rect = container.getBoundingClientRect() | ||
| 572 | + const PAD = 6 | ||
| 573 | + const LF = 230 // leftFixedWidth (nameCol 160 + rateCol 70) | ||
| 574 | + const mx = e.clientX - rect.left - PAD | ||
| 575 | + if (mx < LF || mx < 0) return | ||
| 576 | + | ||
| 577 | + const z = ganttZoomLevel.value | ||
| 578 | + const vo = ganttViewOffsetX.value | ||
| 579 | + | ||
| 580 | + // 坐标系(与 drawGanttChart 完全一致): | ||
| 581 | + // 数据空间 dataX → 屏幕 screenX = LF + (dataX - vo) / z | ||
| 582 | + // 反推(屏幕→数据):dataX = vo + (screenX - LF) * z | ||
| 583 | + const mouseDataX = vo + (mx - LF) * z | ||
| 584 | + | ||
| 585 | + // 向上滚动(deltaY < 0)放大(zoom 变小),向下(deltaY > 0)缩小(zoom 变大) | ||
| 586 | + // 与 OeeDialog 一致:deltaY<0 → delta=0.8, deltaY>0 → delta=1.25 | ||
| 587 | + const delta = e.deltaY < 0 ? 0.8 : 1.25 | ||
| 588 | + const nextZ = Math.max(GANTT_MIN_ZOOM, Math.min(GANTT_MAX_ZOOM, z * delta)) | ||
| 589 | + | ||
| 590 | + // 保持鼠标下数据点不变:LF + (mouseDataX - vo_new)/nextZ = mx | ||
| 591 | + // 解得:vo_new = mouseDataX - (mx - LF) * nextZ | ||
| 592 | + ganttViewOffsetX.value = mouseDataX - (mx - LF) * nextZ | ||
| 593 | + ganttZoomLevel.value = nextZ | ||
| 594 | + drawGanttChart() | ||
| 595 | +} | ||
| 596 | + | ||
| 597 | +// 鼠标移动检测 hover 条形 — 与 OeeDialog 一致的坐标系统 | ||
| 598 | +function onGanttMouseMove(e) { | ||
| 599 | + const container = ganttContainerRef.value | ||
| 600 | + if (!container) return | ||
| 601 | + const rect = container.getBoundingClientRect() | ||
| 602 | + const PAD = 6 | ||
| 603 | + const LF = 230 // leftFixedWidth (nameCol 160 + rateCol 70) | ||
| 604 | + const mx = e.clientX - rect.left - PAD | ||
| 605 | + const my = e.clientY - rect.top - PAD | ||
| 606 | + | ||
| 607 | + if (mx < LF || mx < 0) { | ||
| 608 | + if (ganttHoveredSeg.value) { ganttHoveredSeg.value = null; drawGanttChart() } | ||
| 609 | + return | ||
| 610 | + } | ||
| 611 | + | ||
| 612 | + const z = ganttZoomLevel.value | ||
| 613 | + const vo = ganttViewOffsetX.value | ||
| 614 | + const rowHeight = 36 | ||
| 615 | + const headerHeight = 40 | ||
| 616 | + | ||
| 617 | + // 屏幕X → 数据空间X(与 drawGanttChart 完全一致的公式:dataX = vo + (screenX - LF)*z) | ||
| 618 | + const mouseDataX = vo + (mx - LF) * z | ||
| 619 | + | ||
| 620 | + // 检测哪一行(Y轴不受缩放影响) | ||
| 621 | + const rowIdx = Math.floor((my - headerHeight) / rowHeight) | ||
| 622 | + if (rowIdx < 0 || rowIdx >= timeSeriesData.value.length) { | ||
| 623 | + if (ganttHoveredSeg.value) { ganttHoveredSeg.value = null; drawGanttChart() } | ||
| 624 | + return | ||
| 625 | + } | ||
| 626 | + | ||
| 627 | + const dev = timeSeriesData.value[rowIdx] | ||
| 628 | + if (!dev.segments || dev.segments.length === 0) { | ||
| 629 | + if (ganttHoveredSeg.value) { ganttHoveredSeg.value = null; drawGanttChart() } | ||
| 630 | + return | ||
| 631 | + } | ||
| 632 | + | ||
| 633 | + // 计算参数(与 drawGanttChart 完全一致) | ||
| 634 | + const { startTime: tStart, endTime: tEnd } = getTimeBounds() | ||
| 635 | + const totalMs = tEnd - tStart || (48 * 60 * 60 * 1000) | ||
| 636 | + const containerW = container.clientWidth - 12 | ||
| 637 | + const plotW = containerW - LF | ||
| 638 | + const barPad = 4 | ||
| 639 | + const innerPlotW = plotW - barPad * 2 | ||
| 640 | + | ||
| 641 | + // Y 轴条形范围检查(与 drawGanttChart 中的 barY/barH 一致) | ||
| 642 | + const rowY = headerHeight + rowIdx * rowHeight | ||
| 643 | + const barY = rowY + (rowHeight - 18) / 2 // = rowY + 9 | ||
| 644 | + const barH = 18 | ||
| 645 | + if (my < barY || my > barY + barH) { | ||
| 646 | + if (ganttHoveredSeg.value) { ganttHoveredSeg.value = null; drawGanttChart() } | ||
| 647 | + return | ||
| 648 | + } | ||
| 649 | + | ||
| 650 | + // 全部在数据空间比较(与 drawGanttChart 绘制条形时使用同一坐标系) | ||
| 651 | + let hitSeg = null | ||
| 652 | + for (let i = dev.segments.length - 1; i >= 0; i--) { | ||
| 653 | + const seg = dev.segments[i] | ||
| 654 | + const segStartMs = parseStartTime(seg.startTime) | ||
| 655 | + // duration 单位是秒 → 毫秒(与 OeeDialog 一致:durSec * 1000) | ||
| 656 | + const durMs = (seg.duration || 0) * 1000 | ||
| 657 | + const pct = Math.max(0, (segStartMs - tStart) / totalMs) | ||
| 658 | + const wPct = durMs / totalMs | ||
| 659 | + // 数据空间中的条形位置和宽度(与 drawGanttChart 一致) | ||
| 660 | + const dataSx = barPad + pct * innerPlotW | ||
| 661 | + const dataSw = Math.max(wPct * innerPlotW, 1) | ||
| 662 | + | ||
| 663 | + if (mouseDataX >= dataSx && mouseDataX <= dataSx + dataSw) { | ||
| 664 | + hitSeg = seg; break | ||
| 665 | + } | ||
| 666 | + } | ||
| 667 | + | ||
| 668 | + if (hitSeg !== ganttHoveredSeg.value) { | ||
| 669 | + ganttHoveredSeg.value = hitSeg | ||
| 670 | + if (hitSeg) { | ||
| 671 | + ganttTooltipPos.value = { x: e.clientX - rect.left + 12, y: e.clientY - rect.top + 12 } | ||
| 672 | + } | ||
| 673 | + drawGanttChart() | ||
| 674 | + } else if (hitSeg) { | ||
| 675 | + ganttTooltipPos.value = { x: e.clientX - rect.left + 12, y: e.clientY - rect.top + 12 } | ||
| 676 | + } | ||
| 677 | +} | ||
| 678 | + | ||
| 679 | +function onGanttMouseLeave() { | ||
| 680 | + if (ganttHoveredSeg.value) { ganttHoveredSeg.value = null; drawGanttChart() } | ||
| 681 | +} | ||
| 682 | + | ||
| 683 | +// Hover tooltip 格式化函数 | ||
| 684 | +function getLampLabelName(lampState) { | ||
| 685 | + if (lampState === 3) return '绿灯' | ||
| 686 | + if (lampState === 2) return '黄灯' | ||
| 687 | + if (lampState === 1) return '红灯' | ||
| 688 | + return '灭灯' | ||
| 689 | +} | ||
| 690 | +function formatDuration(dur) { | ||
| 691 | + if (!dur && dur !== 0) return '' | ||
| 692 | + if (dur > 3600) { | ||
| 693 | + const h = Math.floor(dur / 3600), m = Math.floor((dur % 3600) / 60) | ||
| 694 | + return `${h}时${m}分` | ||
| 695 | + } | ||
| 696 | + if (dur > 60) { const m = Math.floor(dur / 60), s = Math.floor(dur % 60); return `${m}分${s}秒` } | ||
| 697 | + return `${Math.floor(dur)}秒` | ||
| 698 | +} | ||
| 699 | +function formatTimeRange(start, end) { | ||
| 700 | + const fmt = (ts) => { | ||
| 701 | + const d = ts ? new Date(ts.replace(/-/g, '/')) : new Date() | ||
| 702 | + return `${d.getFullYear()}/${String(d.getMonth()+1).padStart(2,'0')}/${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}` | ||
| 703 | + } | ||
| 704 | + return `${fmt(start)} ~ ${fmt(end || start)}` | ||
| 705 | +} | ||
| 706 | + | ||
| 707 | +// 灯状态颜色映射: lampState → 颜色 | ||
| 708 | +function getLampColor(lampState) { | ||
| 709 | + if (lampState === 3) return '#67c23a' // 绿 | ||
| 710 | + if (lampState === 2) return '#e6a23c' // 黄 | ||
| 711 | + if (lampState === 1) return '#f56c6c' // 红 | ||
| 712 | + return '#909399' // 灭灯/灰 | ||
| 713 | +} | ||
| 714 | + | ||
| 715 | +// 获取时间轴起止时间(基于当天00:00到次日00:00) | ||
| 716 | +function getTimeBounds() { | ||
| 717 | + const dateStr = tsDate.value || '' | ||
| 718 | + if (!dateStr) { | ||
| 719 | + const today = new Date() | ||
| 720 | + today.setHours(0, 0, 0, 0) | ||
| 721 | + const tomorrow = new Date(today) | ||
| 722 | + tomorrow.setDate(tomorrow.getDate() + 1) | ||
| 723 | + return { startTime: today.getTime(), endTime: tomorrow.getTime() } | ||
| 724 | + } | ||
| 725 | + const startDate = new Date(dateStr) | ||
| 726 | + startDate.setHours(0, 0, 0, 0) | ||
| 727 | + const endDate = new Date(dateStr) | ||
| 728 | + endDate.setHours(23, 59, 59, 999) | ||
| 729 | + return { startTime: startDate.getTime(), endTime: endDate.getTime() + 1 } | ||
| 730 | +} | ||
| 731 | + | ||
| 732 | +// 解析startTime字符串为时间戳 | ||
| 733 | +function parseStartTime(timeStr) { | ||
| 734 | + // 格式如 "2026-05-13 23:59:11" 或 "2026-05-13T..." | ||
| 735 | + const d = new Date(timeStr.replace(/-/g, '/')) | ||
| 736 | + return d.getTime() | ||
| 737 | +} | ||
| 738 | + | ||
| 739 | +// 绘制Canvas甘特图(支持缩放/平移)- 固定列与时间轴区域隔离 | ||
| 740 | +function drawGanttChart() { | ||
| 741 | + const canvas = ganttCanvasRef.value | ||
| 742 | + const container = ganttContainerRef.value | ||
| 743 | + if (!canvas || !container) return | ||
| 744 | + | ||
| 745 | + const containerW = container.clientWidth - 12 | ||
| 746 | + const containerH = container.clientHeight - 12 | ||
| 747 | + const rowHeight = 36 | ||
| 748 | + const headerHeight = 40 | ||
| 749 | + const nameColWidth = 160 | ||
| 750 | + const rateColWidth = 70 | ||
| 751 | + const leftFixedWidth = nameColWidth + rateColWidth | ||
| 752 | + const plotW = containerW - leftFixedWidth // 时间轴区域宽度(数据空间) | ||
| 753 | + const totalHeight = Math.max(headerHeight + timeSeriesData.value.length * rowHeight, containerH) | ||
| 754 | + | ||
| 755 | + // 不使用 DPR 缩放(避免模糊),直接用 CSS 像素尺寸 | ||
| 756 | + canvas.width = containerW | ||
| 757 | + canvas.height = totalHeight | ||
| 758 | + canvas.style.width = containerW + 'px' | ||
| 759 | + canvas.style.height = totalHeight + 'px' | ||
| 760 | + | ||
| 761 | + const ctx = canvas.getContext('2d') | ||
| 762 | + const W = containerW | ||
| 763 | + const H = Math.max(totalHeight, 200) | ||
| 764 | + | ||
| 765 | + // 清空背景 | ||
| 766 | + ctx.fillStyle = '#fff' | ||
| 767 | + ctx.fillRect(0, 0, W, H) | ||
| 768 | + | ||
| 769 | + const { startTime: tStart, endTime: tEnd } = getTimeBounds() | ||
| 770 | + const totalMs = tEnd - tStart || (48 * 60 * 60 * 1000) | ||
| 771 | + const totalSec = totalMs / 1000 | ||
| 772 | + | ||
| 773 | + // 缩放参数 | ||
| 774 | + const z = ganttZoomLevel.value | ||
| 775 | + const vo = ganttViewOffsetX.value | ||
| 776 | + | ||
| 777 | + // ========== 坐标系定义(OeeDialog 同款简洁方式)========== | ||
| 778 | + // 数据空间 dataX → 屏幕像素 screenX = leftFixedWidth + (dataX - vo) / z | ||
| 779 | + // 反推:数据空间 dataX = vo + (screenX - leftFixedWidth) * z | ||
| 780 | + // 所有绘制都用手动坐标转换,不用 ctx.translate/scale 链 | ||
| 781 | + | ||
| 782 | + // ========== 第一部分:固定列区域(不受缩放影响) ========== | ||
| 783 | + ctx.fillStyle = '#fafafa' | ||
| 784 | + ctx.fillRect(0, 0, leftFixedWidth, headerHeight) | ||
| 785 | + ctx.strokeStyle = '#e0e0e0'; ctx.lineWidth = 2 | ||
| 786 | + ctx.beginPath(); ctx.moveTo(0, headerHeight); ctx.lineTo(leftFixedWidth, headerHeight); ctx.stroke() | ||
| 787 | + ctx.fillStyle = '#333'; ctx.font = 'bold 13px sans-serif' | ||
| 788 | + ctx.textAlign = 'center'; ctx.textBaseline = 'middle' | ||
| 789 | + ctx.fillText('设备名称', nameColWidth / 2, headerHeight / 2) | ||
| 790 | + ctx.fillText('稼动率', nameColWidth + rateColWidth / 2, headerHeight / 2) | ||
| 791 | + ctx.strokeStyle = '#ebeef5'; ctx.lineWidth = 1 | ||
| 792 | + ctx.beginPath(); ctx.moveTo(nameColWidth, 0); ctx.lineTo(nameColWidth, headerHeight); ctx.stroke() | ||
| 793 | + ctx.beginPath(); ctx.moveTo(leftFixedWidth, 0); ctx.lineTo(leftFixedWidth, H); ctx.stroke() | ||
| 794 | + | ||
| 795 | + timeSeriesData.value.forEach((dev, idx) => { | ||
| 796 | + const y = headerHeight + idx * rowHeight | ||
| 797 | + ctx.fillStyle = idx % 2 === 0 ? '#fff' : '#fafbfc' | ||
| 798 | + ctx.fillRect(0, y, leftFixedWidth, rowHeight) | ||
| 799 | + ctx.strokeStyle = '#ebeef5'; ctx.lineWidth = 1 | ||
| 800 | + ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(leftFixedWidth, y); ctx.stroke() | ||
| 801 | + ctx.beginPath(); ctx.moveTo(nameColWidth, y); ctx.lineTo(nameColWidth, y + rowHeight); ctx.stroke() | ||
| 802 | + ctx.fillStyle = '#409eff'; ctx.font = '12px sans-serif' | ||
| 803 | + ctx.textAlign = 'left'; ctx.textBaseline = 'middle' | ||
| 804 | + const dn = dev.name.length > 14 ? dev.name.slice(0, 14) + '..' : dev.name | ||
| 805 | + ctx.fillText(dn, 12, y + rowHeight / 2) | ||
| 806 | + ctx.fillStyle = '#333'; ctx.font = 'bold 12px sans-serif' | ||
| 807 | + ctx.textAlign = 'center' | ||
| 808 | + ctx.fillText(dev.rate + '%', nameColWidth + rateColWidth / 2, y + rowHeight / 2) | ||
| 809 | + }) | ||
| 810 | + | ||
| 811 | + // ========== 第二部分:时间轴区域(手动坐标映射) ========== | ||
| 812 | + // 时间轴表头背景 | ||
| 813 | + ctx.fillStyle = '#fafafa' | ||
| 814 | + ctx.fillRect(leftFixedWidth, 0, plotW, headerHeight) | ||
| 815 | + ctx.strokeStyle = '#e0e0e0'; ctx.lineWidth = 2 | ||
| 816 | + ctx.beginPath(); ctx.moveTo(leftFixedWidth, headerHeight); ctx.lineTo(W, headerHeight); ctx.stroke() | ||
| 817 | + | ||
| 818 | + // ----- 时间刻度 ----- | ||
| 819 | + // 可见时间范围:z 越小(放大越多)→ 可见范围越小 → 刻度越精细 | ||
| 820 | + const visMs = Math.max(1000, totalMs * z) | ||
| 821 | + let stepMs = 2 * 3600 * 1000 // 默认2小时 | ||
| 822 | + if (visMs <= 2000) stepMs = 1000 // ≤2s → 每秒 | ||
| 823 | + else if (visMs <= 6000) stepMs = 2000 // ≤6s → 每2秒 | ||
| 824 | + else if (visMs <= 15000) stepMs = 5000 // ≤15s → 每5秒 | ||
| 825 | + else if (visMs <= 30000) stepMs = 10000 // ≤30s → 每10秒 | ||
| 826 | + else if (visMs <= 60000) stepMs = 15000 // ≤1min → 每15秒 | ||
| 827 | + else if (visMs <= 120000) stepMs = 30000 // ≤2min → 每30秒 | ||
| 828 | + else if (visMs <= 300000) stepMs = 60 * 1000 // ≤5min → 每分钟 | ||
| 829 | + else if (visMs <= 600000) stepMs = 2 * 60 * 1000 // ≤10min→ 每2分钟 | ||
| 830 | + else if (visMs <= 1800000) stepMs = 5 * 60 * 1000 // ≤30min→ 每5分钟 | ||
| 831 | + else if (visMs <= 3600000) stepMs = 10 * 60 * 1000 // ≤1h → 每10分钟 | ||
| 832 | + else if (visMs <= 7200000) stepMs = 15 * 60 * 1000 // ≤2h → 每15分钟 | ||
| 833 | + else if (visMs <= 14400000) stepMs = 30 * 60 * 1000 // ≤4h → 每30分钟 | ||
| 834 | + else if (visMs <= 28800000) stepMs = 3600 * 1000 // ≤8h → 每小时 | ||
| 835 | + else stepMs = 2 * 3600 * 1000 // >8h → 每2小时 | ||
| 836 | + | ||
| 837 | + // 视口左边界对应的时间偏移 → 对齐到 stepMs 边界 | ||
| 838 | + const leftMs = (vo / plotW) * totalMs | ||
| 839 | + let firstMs = Math.floor(leftMs / stepMs) * stepMs | ||
| 840 | + if (firstMs < 0) firstMs = 0 | ||
| 841 | + | ||
| 842 | + ctx.font = '11px sans-serif'; ctx.fillStyle = '#666' | ||
| 843 | + | ||
| 844 | + for (let ms = firstMs; ms <= totalMs + stepMs * 2; ms += stepMs) { | ||
| 845 | + // 数据空间的 x 坐标 | ||
| 846 | + const dataX = (ms / totalMs) * plotW | ||
| 847 | + // 手动转换到屏幕坐标:screenX = LF + (dataX - vo) / z | ||
| 848 | + const screenX = leftFixedWidth + (dataX - vo) / z | ||
| 849 | + if (screenX < leftFixedWidth - 80 || screenX > W + 80) continue | ||
| 850 | + | ||
| 851 | + // 时间文字 | ||
| 852 | + ctx.save() | ||
| 853 | + ctx.textAlign = 'center'; ctx.textBaseline = 'middle' | ||
| 854 | + const dt = new Date(tStart + ms) | ||
| 855 | + if (stepMs < 60000) { | ||
| 856 | + ctx.fillText(`${String(dt.getHours()).padStart(2,'0')}:${String(dt.getMinutes()).padStart(2,'0')}:${String(dt.getSeconds()).padStart(2,'0')}`, screenX, headerHeight / 2) | ||
| 857 | + } else { | ||
| 858 | + ctx.fillText(`${String(dt.getHours()).padStart(2,'0')}:${String(dt.getMinutes()).padStart(2,'0')}`, screenX, headerHeight / 2) | ||
| 859 | + } | ||
| 860 | + ctx.restore() | ||
| 861 | + | ||
| 862 | + // 垂直刻度线(屏幕坐标) | ||
| 863 | + ctx.strokeStyle = '#f0f0f0'; ctx.lineWidth = 0.5 | ||
| 864 | + ctx.beginPath(); ctx.moveTo(screenX, headerHeight); ctx.lineTo(screenX, H); ctx.stroke() | ||
| 865 | + } | ||
| 866 | + | ||
| 867 | + // ----- 设备行背景 + 条形 ----- | ||
| 868 | + timeSeriesData.value.forEach((dev, idx) => { | ||
| 869 | + const y = headerHeight + idx * rowHeight | ||
| 870 | + // 行背景(屏幕坐标,覆盖时间轴区域) | ||
| 871 | + ctx.fillStyle = idx % 2 === 0 ? '#fff' : '#fafbfc' | ||
| 872 | + ctx.fillRect(leftFixedWidth, y, W - leftFixedWidth, rowHeight) | ||
| 873 | + // 行顶分割线 | ||
| 874 | + ctx.strokeStyle = '#ebeef5'; ctx.lineWidth = 1 | ||
| 875 | + ctx.beginPath(); ctx.moveTo(leftFixedWidth, y); ctx.lineTo(W, y); ctx.stroke() | ||
| 876 | + | ||
| 877 | + // 甘特图条形 | ||
| 878 | + if (dev.segments && dev.segments.length > 0) { | ||
| 879 | + const barY = y + (rowHeight - 18) / 2 | ||
| 880 | + const barH = 18 | ||
| 881 | + let barY_hover = barY | ||
| 882 | + let barH_hover = barH | ||
| 883 | + const pad = 4 | ||
| 884 | + const innerPlotW = plotW - pad * 2 | ||
| 885 | + | ||
| 886 | + // 裁剪区域:条形不能溢出到左侧固定列 | ||
| 887 | + ctx.save() | ||
| 888 | + ctx.beginPath() | ||
| 889 | + ctx.rect(leftFixedWidth, headerHeight, W - leftFixedWidth, H - headerHeight) | ||
| 890 | + ctx.clip() | ||
| 891 | + | ||
| 892 | + dev.segments.forEach(seg => { | ||
| 893 | + const segStartMs = parseStartTime(seg.startTime) | ||
| 894 | + // duration 单位是秒 → 毫秒(与 OeeDialog 一致) | ||
| 895 | + const durMs = (seg.duration || 0) * 1000 | ||
| 896 | + const pct = Math.max(0, (segStartMs - tStart) / totalMs) | ||
| 897 | + const wPct = durMs / totalMs | ||
| 898 | + // 数据空间坐标 | ||
| 899 | + const dataSx = pad + pct * innerPlotW | ||
| 900 | + const dataSw = Math.max(wPct * innerPlotW, 1) | ||
| 901 | + // 转换为屏幕坐标 | ||
| 902 | + const sx = leftFixedWidth + (dataSx - vo) / z | ||
| 903 | + const sw = dataSw / z | ||
| 904 | + | ||
| 905 | + // 可见性裁剪(屏幕坐标) | ||
| 906 | + if (sx + sw < leftFixedWidth - 20 || sx > W + 20) return | ||
| 907 | + | ||
| 908 | + const isHovered = ganttHoveredSeg.value === seg | ||
| 909 | + | ||
| 910 | + ctx.fillStyle = getLampColor(seg.lampState) | ||
| 911 | + if (isHovered) { | ||
| 912 | + ctx.globalAlpha = 0.7 | ||
| 913 | + barH_hover = barH + 4; barY_hover = barY - 2 | ||
| 914 | + ctx.fillRect(sx, barY_hover, sw, barH_hover) | ||
| 915 | + ctx.globalAlpha = 1 | ||
| 916 | + } else { | ||
| 917 | + ctx.fillRect(sx, barY, sw, barH) | ||
| 918 | + } | ||
| 919 | + // 边框 | ||
| 920 | + ctx.strokeStyle = isHovered ? 'rgba(0,0,0,0.5)' : 'rgba(255,255,255,0.35)' | ||
| 921 | + ctx.lineWidth = isHovered ? 1.5 : 0.5 | ||
| 922 | + ctx.strokeRect(sx, isHovered ? barY_hover : barY, sw, isHovered ? barH_hover : barH) | ||
| 923 | + }) | ||
| 924 | + | ||
| 925 | + // 恢复裁剪区域,不影响后续行的绘制 | ||
| 926 | + ctx.restore() | ||
| 927 | + } | ||
| 928 | + }) | ||
| 929 | +} | ||
| 930 | + | ||
| 931 | +// 获取时序数据 | ||
| 932 | +async function fetchTimeSeriesData() { | ||
| 933 | + if (!tsDate.value) { | ||
| 934 | + ElMessage.warning('请选择查询日期') | ||
| 935 | + return | ||
| 936 | + } | ||
| 937 | + | ||
| 938 | + tsLoading.value = true | ||
| 939 | + try { | ||
| 940 | + const params = new URLSearchParams({ | ||
| 941 | + startDate: tsDate.value, | ||
| 942 | + endDate: tsDate.value, | ||
| 943 | + pageNo: tsPageNo.value, | ||
| 944 | + pageSize: tsPageSize, | ||
| 945 | + }) | ||
| 946 | + const res = await fetch(`/api/device/oeeTimeline?${params}`) | ||
| 947 | + const data = await res.json() | ||
| 948 | + | ||
| 949 | + if (data.data && data.data.records) { | ||
| 950 | + const records = data.data.records || [] | ||
| 951 | + tsTotal.value = data.data.total || records.length * tsPageSize | ||
| 952 | + timeSeriesData.value = records.map(record => ({ | ||
| 953 | + name: record.deviceName || record.dtuSn || '', | ||
| 954 | + rate: parseFloat(record.availabilityRatio || 0).toFixed(1), | ||
| 955 | + greenDuration: record.greenDuration || '', | ||
| 956 | + segments: (record.lampData || []).map(item => ({ | ||
| 957 | + duration: item.duration, | ||
| 958 | + lampState: item.lampState, | ||
| 959 | + startTime: item.startTime, | ||
| 960 | + endTime: item.endTime || '', | ||
| 961 | + })), | ||
| 962 | + })) | ||
| 963 | + } | ||
| 964 | + | ||
| 965 | + // 数据加载后绘制Canvas | ||
| 966 | + await nextTick() | ||
| 967 | + drawGanttChart() | ||
| 968 | + } catch (err) { | ||
| 969 | + console.error('获取时序数据失败:', err) | ||
| 970 | + ElMessage.error('获取时序数据失败') | ||
| 971 | + } finally { | ||
| 972 | + tsLoading.value = false | ||
| 973 | + } | ||
| 974 | +} | ||
| 975 | + | ||
| 976 | +// 监听tab切换到时序状态时触发查询 | ||
| 977 | +watch(currentStatus, (newVal) => { | ||
| 978 | + if (newVal === 'timeseries') { | ||
| 979 | + if (!tsDate.value) { | ||
| 980 | + // 默认选今天 | ||
| 981 | + const today = new Date() | ||
| 982 | + tsDate.value = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}` | ||
| 983 | + } | ||
| 984 | + fetchTimeSeriesData() | ||
| 985 | + } | ||
| 986 | +}) | ||
| 987 | + | ||
| 988 | +// ========== 稼动率数据(Canvas绘制) ========== | ||
| 989 | +const utilQueryMode = ref('day') | ||
| 990 | +const utilDate = ref('') | ||
| 991 | +const utilWeekDate = ref('') | ||
| 992 | +const utilMonthDate = ref('') | ||
| 993 | +const utilLoading = ref(false) | ||
| 994 | +const sortMode = ref('rate') | ||
| 995 | + | ||
| 996 | +// 周查询显示格式:2026-第20周 | ||
| 997 | +const utilWeekDisplayFormat = computed(() => { | ||
| 998 | + if (!utilWeekDate.value) return '' | ||
| 999 | + const d = new Date(utilWeekDate.value) | ||
| 1000 | + return `${d.getFullYear()}-${String(getWeekNumber(d)).padStart(2, '0')}周` | ||
| 1001 | +}) | ||
| 1002 | + | ||
| 1003 | +// 获取日期所在年的第几周(ISO标准:周一为每周起始) | ||
| 1004 | +function getWeekNumber(date) { | ||
| 1005 | + const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())) | ||
| 1006 | + const dayNum = d.getUTCDay() || 7 | ||
| 1007 | + d.setUTCDate(d.getUTCDate() + 4 - dayNum) | ||
| 1008 | + const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)) | ||
| 1009 | + return Math.ceil(((d - yearStart) / 86400000 + 1) / 7) | ||
| 1010 | +} | ||
| 1011 | + | ||
| 1012 | +// 获取某日所在自然周的周一和周日 { start: YYYY-MM-DD, end: YYYY-MM-DD } | ||
| 1013 | +function getWeekRange(dateStr) { | ||
| 1014 | + if (!dateStr) return { start: '', end: '' } | ||
| 1015 | + const d = new Date(dateStr) | ||
| 1016 | + // 调整到本周一 | ||
| 1017 | + const day = d.getDay() || 7 | ||
| 1018 | + const diff = d.getDate() - day + 1 | ||
| 1019 | + const monday = new Date(d.setDate(diff)) | ||
| 1020 | + const sunday = new Date(monday) | ||
| 1021 | + sunday.setDate(monday.getDate() + 6) | ||
| 1022 | + return { | ||
| 1023 | + start: formatYMD(monday), | ||
| 1024 | + end: formatYMD(sunday), | ||
| 1025 | + } | ||
| 1026 | +} | ||
| 1027 | + | ||
| 1028 | +// 获取某月的1号和最后一天 | ||
| 1029 | +function getMonthRange(ymStr) { | ||
| 1030 | + if (!ymStr) return { start: '', end: '' } | ||
| 1031 | + const [y, m] = ymStr.split('-').map(Number) | ||
| 1032 | + const lastDay = new Date(y, m, 0).getDate() | ||
| 1033 | + return { start: `${ymStr}-01`, end: `${ymStr}-${String(lastDay).padStart(2,'0')}` } | ||
| 1034 | +} | ||
| 1035 | + | ||
| 1036 | +// 格式化为 YYYY-MM-DD | ||
| 1037 | +function formatYMD(d) { | ||
| 1038 | + return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}` | ||
| 1039 | +} | ||
| 1040 | + | ||
| 1041 | +// 获取当前自然周的周一日期字符串 | ||
| 1042 | +function getCurrentMonday() { | ||
| 1043 | + const today = new Date() | ||
| 1044 | + const day = today.getDay() || 7 | ||
| 1045 | + today.setDate(today.getDate() - day + 1) | ||
| 1046 | + return formatYMD(today) | ||
| 1047 | +} | ||
| 1048 | + | ||
| 1049 | +// 周选择器只允许选周一 | ||
| 1050 | +function disableNonMonday(date) { | ||
| 1051 | + return date.getDay() !== 1 | ||
| 1052 | +} | ||
| 1053 | + | ||
| 1054 | +// 获取当前年月字符串 | ||
| 1055 | +function getCurrentYM() { | ||
| 1056 | + const now = new Date() | ||
| 1057 | + return `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}` | ||
| 1058 | +} | ||
| 1059 | + | ||
| 1060 | +// Canvas refs | ||
| 1061 | +const utilPieTotalRef = ref(null) | ||
| 1062 | +const utilPieRateRef = ref(null) | ||
| 1063 | +const utilPieStatusRef = ref(null) | ||
| 1064 | +const utilAbnormalRef = ref(null) | ||
| 1065 | +const utilStackRef = ref(null) | ||
| 1066 | + | ||
| 1067 | +// API 数据 | ||
| 1068 | +const utilData = reactive({ | ||
| 1069 | + totalDuration: { green: {}, yellow: {}, red: {}, off: {}, blue: {} }, | ||
| 1070 | + availabilityRate: '0%', | ||
| 1071 | + currentStatus: { green: 0, red: 0, off: 0, blue: 0, yellow: 0 }, | ||
| 1072 | + abnormalRanking: [], | ||
| 1073 | + deviceList: [], | ||
| 1074 | +}) | ||
| 1075 | + | ||
| 1076 | +// 饼图总时长分段(用于图例) | ||
| 1077 | +const utilTotalSegments = computed(() => { | ||
| 1078 | + const td = utilData.totalDuration | ||
| 1079 | + const totalSec = (td.green?.seconds||0)+(td.yellow?.seconds||0)+(td.red?.seconds||0)+(td.off?.seconds||0) | ||
| 1080 | + if (!totalSec) return [] | ||
| 1081 | + return [ | ||
| 1082 | + { label: '绿灯', color: '#67c23a', duration: td.green?.duration||'', pct: ((td.green.seconds||0)/totalSec*100).toFixed(2)+'%' }, | ||
| 1083 | + { label: '黄灯', color: '#e6a23c', duration: td.yellow?.duration||'', pct: ((td.yellow.seconds||0)/totalSec*100).toFixed(2)+'%' }, | ||
| 1084 | + { label: '红灯', color: '#f56c6c', duration: td.red?.duration||'', pct: ((td.red.seconds||0)/totalSec*100).toFixed(2)+'%' }, | ||
| 1085 | + { label: '灭灯', color: '#909399', duration: td.off?.duration||'', pct: ((td.off.seconds||0)/totalSec*100).toFixed(2)+'%' }, | ||
| 1086 | + ] | ||
| 1087 | +}) | ||
| 1088 | + | ||
| 1089 | +// 异常排名底部文字 | ||
| 1090 | +const utilAbnormalFooterText = computed(() => { | ||
| 1091 | + const list = utilData.abnormalRanking || [] | ||
| 1092 | + if (list.length === 0) return '' | ||
| 1093 | + // 取前5个的rygTotalDuration | ||
| 1094 | + return list.slice(0,5).map((item,i) => | ||
| 1095 | + `${item.deviceName||item.dtuSn} ${item.availabilityRatio}` | ||
| 1096 | + ).join(' | ') | ||
| 1097 | +}) | ||
| 1098 | + | ||
| 1099 | +// 饼图 hover 状态(canvas级别,按key+segIdx跟踪) | ||
| 1100 | +const utilHoveredPieKey = ref('') | ||
| 1101 | +const utilHoveredPieSegIdx = ref(-1) | ||
| 1102 | +const utilPieTip = reactive({ show: false, x: 0, y: 0, title: '', lines: [] }) | ||
| 1103 | + | ||
| 1104 | +// 饼图通用 mousemove 处理(DPR模式下用逻辑坐标) | ||
| 1105 | +function onUtilPieMove(pieKey, e) { | ||
| 1106 | + const canvasMap = { total: utilPieTotalRef, rate: utilPieRateRef, status: utilPieStatusRef } | ||
| 1107 | + const anglesMap = { total: utilTotalAngles, rate: utilRateAngles, status: utilStatusAngles } | ||
| 1108 | + const canvas = canvasMap[pieKey]?.value | ||
| 1109 | + if (!canvas) return | ||
| 1110 | + | ||
| 1111 | + const rect = canvas.getBoundingClientRect() | ||
| 1112 | + // setupPieCanvas 已将 ctx.scale(dpr,dpr),所以直接用 CSS 像素坐标即可(逻辑坐标) | ||
| 1113 | + const mx = e.clientX - rect.left | ||
| 1114 | + const my = e.clientY - rect.top | ||
| 1115 | + | ||
| 1116 | + const W = 320, H = 280 // 与 setupPieCanvas 一致 | ||
| 1117 | + const cx = W / 2, cy = H / 2, r = 100 | ||
| 1118 | + const angles = anglesMap[pieKey] | ||
| 1119 | + if (!angles || angles.length === 0) return | ||
| 1120 | + | ||
| 1121 | + const hitIdx = hitTestPie(mx, my, cx, cy, r, angles) | ||
| 1122 | + | ||
| 1123 | + if (hitIdx !== utilHoveredPieSegIdx.value || utilHoveredPieKey.value !== pieKey) { | ||
| 1124 | + utilHoveredPieKey.value = pieKey | ||
| 1125 | + utilHoveredPieSegIdx.value = hitIdx | ||
| 1126 | + | ||
| 1127 | + if (hitIdx >= 0 && pieKey !== 'status') { | ||
| 1128 | + const seg = angles[hitIdx] | ||
| 1129 | + const total = angles.reduce((s, a) => s + a.value, 0) | ||
| 1130 | + // 根据不同饼图设置标题和内容 | ||
| 1131 | + const titles = { total: '总时长', rate: '稼动率', status: '当前机台运行状态' } | ||
| 1132 | + utilPieTip.title = titles[pieKey] || seg.label || '' | ||
| 1133 | + utilPieTip.lines = [ | ||
| 1134 | + { color: seg.color, label: `${seg.label} ${seg.rawDur || ''}`, value: '' }, | ||
| 1135 | + { color: seg.color, label: '时长占比', value: ((seg.value / total) * 100).toFixed(2) + '%' }, | ||
| 1136 | + ] | ||
| 1137 | + utilPieTip.x = e.clientX + 12 | ||
| 1138 | + utilPieTip.y = e.clientY + 12 | ||
| 1139 | + utilPieTip.show = true | ||
| 1140 | + } else { | ||
| 1141 | + utilPieTip.show = false | ||
| 1142 | + } | ||
| 1143 | + drawAllUtilCharts() | ||
| 1144 | + } else if (hitIdx >= 0) { | ||
| 1145 | + utilPieTip.x = e.clientX + 12 | ||
| 1146 | + utilPieTip.y = e.clientY + 12 | ||
| 1147 | + } | ||
| 1148 | +} | ||
| 1149 | + | ||
| 1150 | +// 饼图 mouseleave | ||
| 1151 | +function onUtilPieLeave(pieKey) { | ||
| 1152 | + if (utilHoveredPieKey.value === pieKey) { | ||
| 1153 | + utilHoveredPieKey.value = '' | ||
| 1154 | + utilHoveredPieSegIdx.value = -1 | ||
| 1155 | + utilPieTip.show = false | ||
| 1156 | + drawAllUtilCharts() | ||
| 1157 | + } | ||
| 1158 | +} | ||
| 1159 | + | ||
| 1160 | +// 异常机台 hover 状态 | ||
| 1161 | +const utilAbnTip = reactive({ show: false, x: 0, y: 0, name: '', yellowDur: '', yellowPct: '', redDur: '', redPct: '' }) | ||
| 1162 | +let utilAbnHitIdx = -1 | ||
| 1163 | + | ||
| 1164 | +// 堆叠柱状图 hover 状态 | ||
| 1165 | +const utilStackTip = reactive({ show: false, x: 0, y: 0, name: '', lines: [] }) | ||
| 1166 | +let utilStackHitIdx = -1 | ||
| 1167 | + | ||
| 1168 | +// ---------- 绘制工具函数 ---------- | ||
| 1169 | +// 绘制实心饼图(filled pie) - 返回扇区角度数组用于 hit-test | ||
| 1170 | +function drawPie(ctx, cx, cy, r, segments, options = {}) { | ||
| 1171 | + const { activeIdx = -1 } = options | ||
| 1172 | + const total = segments.reduce((s, seg) => s + seg.value, 0) | ||
| 1173 | + if (total <= 0) return [] | ||
| 1174 | + let angle = -Math.PI / 2 | ||
| 1175 | + const angles = [] | ||
| 1176 | + | ||
| 1177 | + segments.forEach((seg, i) => { | ||
| 1178 | + const sweep = (seg.value / total) * Math.PI * 2 | ||
| 1179 | + const isActive = i === activeIdx | ||
| 1180 | + | ||
| 1181 | + ctx.beginPath() | ||
| 1182 | + ctx.moveTo(cx, cy) | ||
| 1183 | + ctx.arc(cx, cy, r, angle, angle + sweep) | ||
| 1184 | + ctx.closePath() | ||
| 1185 | + ctx.fillStyle = seg.color | ||
| 1186 | + ctx.fill() | ||
| 1187 | + | ||
| 1188 | + if (isActive) { | ||
| 1189 | + ctx.save() | ||
| 1190 | + ctx.shadowColor = 'rgba(0,0,0,0.35)' | ||
| 1191 | + ctx.shadowBlur = 12 | ||
| 1192 | + ctx.strokeStyle = '#fff' | ||
| 1193 | + ctx.lineWidth = 2 | ||
| 1194 | + ctx.stroke() | ||
| 1195 | + ctx.restore() | ||
| 1196 | + } else { | ||
| 1197 | + ctx.strokeStyle = 'rgba(255,255,255,0.5)' | ||
| 1198 | + ctx.lineWidth = 0.8 | ||
| 1199 | + ctx.stroke() | ||
| 1200 | + } | ||
| 1201 | + | ||
| 1202 | + // 在扇区中心位置绘制白色文字标签(字体缩小2号) | ||
| 1203 | + if (seg.value > 0) { | ||
| 1204 | + const midAngle = angle + sweep / 2 | ||
| 1205 | + const labelR = r * 0.6 | ||
| 1206 | + const lx = cx + Math.cos(midAngle) * labelR | ||
| 1207 | + const ly = cy + Math.sin(midAngle) * labelR | ||
| 1208 | + | ||
| 1209 | + ctx.save() | ||
| 1210 | + ctx.fillStyle = '#fff' | ||
| 1211 | + ctx.textAlign = 'center' | ||
| 1212 | + ctx.textBaseline = 'middle' | ||
| 1213 | + | ||
| 1214 | + const pct = ((seg.value / total) * 100).toFixed(2) + '%' | ||
| 1215 | + ctx.font = isActive ? 'bold 13px sans-serif' : '12px sans-serif' | ||
| 1216 | + ctx.fillText(pct, lx, ly - 6) | ||
| 1217 | + | ||
| 1218 | + ctx.font = isActive ? 'bold 11px sans-serif' : '10px sans-serif' | ||
| 1219 | + ctx.fillText(seg.textLabel || '', lx, ly + 7) | ||
| 1220 | + ctx.restore() | ||
| 1221 | + } | ||
| 1222 | + | ||
| 1223 | + angles.push({ startAngle: angle, endAngle: angle + sweep, ...seg }) | ||
| 1224 | + angle += sweep | ||
| 1225 | + }) | ||
| 1226 | + return angles | ||
| 1227 | +} | ||
| 1228 | + | ||
| 1229 | +// 饼图 DPR 缩放辅助:设置 canvas 物理尺寸并返回缩放后的 context | ||
| 1230 | +function setupPieCanvas(canvas) { | ||
| 1231 | + const dpr = window.devicePixelRatio || 1 | ||
| 1232 | + const w = 320, h = 280 | ||
| 1233 | + canvas.style.width = w + 'px' | ||
| 1234 | + canvas.style.height = h + 'px' | ||
| 1235 | + canvas.width = Math.round(w * dpr) | ||
| 1236 | + canvas.height = Math.round(h * dpr) | ||
| 1237 | + const ctx = canvas.getContext('2d') | ||
| 1238 | + ctx.scale(dpr, dpr) | ||
| 1239 | + return { ctx, W: w, H: h } | ||
| 1240 | +} | ||
| 1241 | + | ||
| 1242 | +// 饼图 hit-test: 判断鼠标是否在某个扇区(实心饼图) | ||
| 1243 | +function hitTestPie(mx, my, cx, cy, r, angles) { | ||
| 1244 | + const dx = mx - cx, dy = my - cy | ||
| 1245 | + const dist = Math.sqrt(dx * dx + dy * dy) | ||
| 1246 | + if (dist > r) return -1 | ||
| 1247 | + let ang = Math.atan2(dy, dx) | ||
| 1248 | + if (ang < -Math.PI / 2) ang += Math.PI * 2 | ||
| 1249 | + for (let i = 0; i < angles.length; i++) { | ||
| 1250 | + let sa = angles[i].startAngle, ea = angles[i].endAngle | ||
| 1251 | + if (sa < -Math.PI / 2) sa += Math.PI * 2 | ||
| 1252 | + if (ea < -Math.PI / 2) ea += Math.PI * 2 | ||
| 1253 | + if (ang >= sa && ang < ea) return i | ||
| 1254 | + } | ||
| 1255 | + return -1 | ||
| 1256 | +} | ||
| 1257 | + | ||
| 1258 | +// 格式化秒数为 "xxx.xx时" | ||
| 1259 | +function fmtSecHour(sec) { | ||
| 1260 | + sec = Math.max(0, sec || 0) | ||
| 1261 | + return (sec / 3600).toFixed(2) + '时' | ||
| 1262 | +} | ||
| 1263 | + | ||
| 1264 | +// 格式化秒数为可读时长 | ||
| 1265 | +function fmtSec(sec) { | ||
| 1266 | + sec = Math.max(0, sec | 0) | ||
| 1267 | + const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60 | ||
| 1268 | + if (h > 0) return `${h}时${m}分${s}秒` | ||
| 1269 | + if (m > 0) return `${m}分${s}秒` | ||
| 1270 | + return `${s}秒` | ||
| 1271 | +} | ||
| 1272 | + | ||
| 1273 | +// ---------- 绘制总时长饼图 ---------- | ||
| 1274 | +let utilTotalAngles = [] | ||
| 1275 | +function drawUtilPieTotal() { | ||
| 1276 | + const canvas = utilPieTotalRef.value | ||
| 1277 | + if (!canvas) return | ||
| 1278 | + const { ctx, W, H } = setupPieCanvas(canvas) | ||
| 1279 | + const cx = W / 2, cy = H / 2, r = 100 | ||
| 1280 | + | ||
| 1281 | + const td = utilData.totalDuration | ||
| 1282 | + const gSec = td.green?.seconds || 0, ySec = td.yellow?.seconds || 0 | ||
| 1283 | + const rSec = td.red?.seconds || 0, oSec = td.off?.seconds || 0 | ||
| 1284 | + const totalSec = gSec + ySec + rSec + oSec | ||
| 1285 | + | ||
| 1286 | + if (totalSec <= 0) { | ||
| 1287 | + ctx.fillStyle = '#999'; ctx.font = '13px sans-serif'; ctx.textAlign = 'center' | ||
| 1288 | + ctx.fillText('暂无数据', cx, cy); return | ||
| 1289 | + } | ||
| 1290 | + | ||
| 1291 | + const segments = [ | ||
| 1292 | + { label:'绿灯', value:gSec, color:'#67c23a', textLabel: `绿灯`, rawDur: fmtSec(gSec) }, | ||
| 1293 | + { label:'黄灯', value:ySec, color:'#e6a23c', textLabel: `黄灯:${fmtSecHour(ySec)}`, rawDur: fmtSec(ySec) }, | ||
| 1294 | + { label:'红灯', value:rSec, color:'#f56c6c', textLabel: `红灯:${fmtSecHour(rSec)}`, rawDur: fmtSec(rSec) }, | ||
| 1295 | + { label:'灭灯', value:oSec, color:'#909399', textLabel: `灭灯:${fmtSecHour(oSec)}`, rawDur: fmtSec(oSec) }, | ||
| 1296 | + ] | ||
| 1297 | + utilTotalAngles = drawPie(ctx, cx, cy, r, segments, { | ||
| 1298 | + activeIdx: utilHoveredPieKey.value === 'total' ? utilHoveredPieSegIdx.value : -1 | ||
| 1299 | + }) | ||
| 1300 | +} | ||
| 1301 | + | ||
| 1302 | +// ---------- 绘制稼动率饼图 ---------- | ||
| 1303 | +let utilRateAngles = [] | ||
| 1304 | +function drawUtilPieRate() { | ||
| 1305 | + const canvas = utilPieRateRef.value | ||
| 1306 | + if (!canvas) return | ||
| 1307 | + const { ctx, W, H } = setupPieCanvas(canvas) | ||
| 1308 | + const cx = W / 2, cy = H / 2, r = 100 | ||
| 1309 | + | ||
| 1310 | + const td = utilData.totalDuration | ||
| 1311 | + const gSec = td.green?.seconds || 0, ySec = td.yellow?.seconds || 0 | ||
| 1312 | + const rSec = td.red?.seconds || 0 | ||
| 1313 | + const rygTotal = gSec + ySec + rSec | ||
| 1314 | + if (rygTotal <= 0) { ctx.fillStyle='#999';ctx.font='13px sans-serif';ctx.textAlign='center';ctx.fillText('暂无数据',cx,cy); return } | ||
| 1315 | + | ||
| 1316 | + const segments = [ | ||
| 1317 | + { label:'绿灯', value:gSec, color:'#67c23a', textLabel: `绿灯:${fmtSecHour(gSec)}`, rawDur: fmtSec(gSec) }, | ||
| 1318 | + { label:'黄灯', value:ySec, color:'#e6a23c', textLabel: `黄灯:${fmtSecHour(ySec)}`, rawDur: fmtSec(ySec) }, | ||
| 1319 | + { label:'红灯', value:rSec, color:'#f56c6c', textLabel: `红灯:${fmtSecHour(rSec)}`, rawDur: fmtSec(rSec) }, | ||
| 1320 | + ] | ||
| 1321 | + utilRateAngles = drawPie(ctx, cx, cy, r, segments, { | ||
| 1322 | + activeIdx: utilHoveredPieKey.value === 'rate' ? utilHoveredPieSegIdx.value : -1 | ||
| 1323 | + }) | ||
| 1324 | +} | ||
| 1325 | + | ||
| 1326 | +// ---------- 绘制当前机台运行状态饼图 ---------- | ||
| 1327 | +let utilStatusAngles = [] | ||
| 1328 | +function drawUtilPieStatus() { | ||
| 1329 | + const canvas = utilPieStatusRef.value | ||
| 1330 | + if (!canvas) return | ||
| 1331 | + const { ctx, W, H } = setupPieCanvas(canvas) | ||
| 1332 | + const cx = W / 2, cy = H / 2, r = 100 | ||
| 1333 | + | ||
| 1334 | + const cs = utilData.currentStatus | ||
| 1335 | + const gN = cs.green||0, yN = cs.yellow||0, rN = cs.red||0, oN = cs.off||0 | ||
| 1336 | + const total = gN + yN + rN + oN | ||
| 1337 | + if (total <= 0) { ctx.fillStyle='#999';ctx.font='13px sans-serif';ctx.textAlign='center';ctx.fillText('暂无数据',cx,cy); return } | ||
| 1338 | + | ||
| 1339 | + const segments = [ | ||
| 1340 | + { label:`绿灯`, value:gN, color:'#67c23a', textLabel: `绿灯${gN}台`, rawDur: `${gN}台` }, | ||
| 1341 | + { label:`黄灯`, value:yN, color:'#e6a23c', textLabel: `黄灯${yN}台`, rawDur: `${yN}台` }, | ||
| 1342 | + { label:`红灯`, value:rN, color:'#f56c6c', textLabel: `红灯${rN}台`, rawDur: `${rN}台` }, | ||
| 1343 | + { label:`灭灯`, value:oN, color:'#909399', textLabel: `灭灯${oN}台`, rawDur: `${oN}台` }, | ||
| 1344 | + ] | ||
| 1345 | + utilStatusAngles = drawPie(ctx, cx, cy, r, segments, { | ||
| 1346 | + activeIdx: utilHoveredPieKey.value === 'status' ? utilHoveredPieSegIdx.value : -1 | ||
| 1347 | + }) | ||
| 1348 | +} | ||
| 1349 | + | ||
| 1350 | +// ---------- 绘制异常机台排名水平柱状图 ---------- | ||
| 1351 | +function drawUtilAbnormal() { | ||
| 1352 | + const canvas = utilAbnormalRef.value | ||
| 1353 | + if (!canvas) return | ||
| 1354 | + const container = canvas.parentElement | ||
| 1355 | + if (!container) return | ||
| 1356 | + const cw = container.clientWidth, ch = 320 | ||
| 1357 | + canvas.width = cw; canvas.height = ch | ||
| 1358 | + const ctx = canvas.getContext('2d') | ||
| 1359 | + ctx.clearRect(0, 0, cw, ch) | ||
| 1360 | + | ||
| 1361 | + let list = (utilData.abnormalRanking || []).filter(item => { | ||
| 1362 | + const yS = item.yellowSeconds || 0, rS = item.redSeconds || 0 | ||
| 1363 | + return (yS + rS) > 0 | ||
| 1364 | + }) | ||
| 1365 | + if (list.length === 0) { | ||
| 1366 | + ctx.fillStyle='#999'; ctx.font='13px sans-serif'; ctx.textAlign='center' | ||
| 1367 | + ctx.fillText('暂无异常数据', cw/2, ch/2); return | ||
| 1368 | + } | ||
| 1369 | + | ||
| 1370 | + const PAD_L = 60, PAD_R = 20, PAD_T = 28, PAD_B = 8 | ||
| 1371 | + const plotW = cw - PAD_L - PAD_R, plotH = ch - PAD_T - PAD_B | ||
| 1372 | + const rowH = Math.min(22, plotH / list.length) | ||
| 1373 | + const barH = rowH - 4 | ||
| 1374 | + | ||
| 1375 | + // 找最大值(黄+红总秒数) | ||
| 1376 | + let maxVal = 1 | ||
| 1377 | + list.forEach(item => { maxVal = Math.max(maxVal, (item.yellowSeconds||0)+(item.redSeconds||0)) }) | ||
| 1378 | + | ||
| 1379 | + // X轴时间刻度(放在顶部) | ||
| 1380 | + const tickCount = 5 | ||
| 1381 | + ctx.strokeStyle = '#ddd'; ctx.lineWidth = 1; ctx.fillStyle = '#666'; ctx.font = '11px sans-serif'; ctx.textAlign = 'center' | ||
| 1382 | + for (let i = 0; i <= tickCount; i++) { | ||
| 1383 | + const val = maxVal * i / tickCount | ||
| 1384 | + const x = PAD_L + (plotW * i / tickCount) | ||
| 1385 | + ctx.fillText(fmtSec(Math.round(val)), x, 16) | ||
| 1386 | + if (i > 0) { | ||
| 1387 | + ctx.beginPath(); ctx.moveTo(x, PAD_T); ctx.lineTo(x, ch - PAD_B); ctx.stroke() | ||
| 1388 | + } | ||
| 1389 | + } | ||
| 1390 | + // 基线加粗(顶部基线) | ||
| 1391 | + ctx.strokeStyle = '#bbb'; ctx.lineWidth = 1.5 | ||
| 1392 | + ctx.beginPath(); ctx.moveTo(PAD_L, PAD_T); ctx.lineTo(cw - PAD_R, PAD_T); ctx.stroke() | ||
| 1393 | + | ||
| 1394 | + // 每行 | ||
| 1395 | + list.forEach((item, idx) => { | ||
| 1396 | + const y = PAD_T + idx * rowH | ||
| 1397 | + // 设备名 | ||
| 1398 | + ctx.fillStyle = '#333' | ||
| 1399 | + ctx.font = '12px sans-serif' | ||
| 1400 | + ctx.textAlign = 'right'; ctx.textBaseline = 'middle' | ||
| 1401 | + const dn = (item.deviceName || item.dtuSn || '') | ||
| 1402 | + ctx.fillText(dn.length > 7 ? dn.slice(0,7) + '..' : dn, PAD_L - 6, y + rowH / 2) | ||
| 1403 | + | ||
| 1404 | + const yS = item.yellowSeconds || 0, rS = item.redSeconds || 0 | ||
| 1405 | + | ||
| 1406 | + const yW = (yS / maxVal) * plotW | ||
| 1407 | + const rW = (rS / maxVal) * plotW | ||
| 1408 | + | ||
| 1409 | + // 黄色段 | ||
| 1410 | + ctx.fillStyle = '#e6a23c' | ||
| 1411 | + ctx.fillRect(PAD_L, y + 2, yW, barH) | ||
| 1412 | + // 红色段 | ||
| 1413 | + ctx.fillStyle = '#f56c6c' | ||
| 1414 | + ctx.fillRect(PAD_L + yW, y + 2, rW, barH) | ||
| 1415 | + }) | ||
| 1416 | +} | ||
| 1417 | + | ||
| 1418 | +// 异常机台 mousemove | ||
| 1419 | +function onUtilAbnormalMove(e) { | ||
| 1420 | + const canvas = utilAbnormalRef.value | ||
| 1421 | + if (!canvas) return | ||
| 1422 | + const rect = canvas.getBoundingClientRect() | ||
| 1423 | + const mx = e.clientX - rect.left, my = e.clientY - rect.top | ||
| 1424 | + const rawList = utilData.abnormalRanking || [] | ||
| 1425 | + // 与 drawUtilAbnormal 保持一致:过滤掉数据为0的项 | ||
| 1426 | + let list = rawList.filter(item => { | ||
| 1427 | + const yS = item.yellowSeconds || 0, rS = item.redSeconds || 0 | ||
| 1428 | + return (yS + rS) > 0 | ||
| 1429 | + }) | ||
| 1430 | + | ||
| 1431 | + const PAD_L = 60, PAD_T = 28, PAD_B = 8 | ||
| 1432 | + const rowH = Math.min(22, (canvas.clientHeight - PAD_T - PAD_B) / Math.max(list.length, 1)) | ||
| 1433 | + const rowIdx = Math.floor((my - PAD_T) / rowH) | ||
| 1434 | + | ||
| 1435 | + if (rowIdx < 0 || rowIdx >= list.length) { | ||
| 1436 | + if (utilAbnTip.show) { utilAbnTip.show = false; drawUtilAbnormal() } | ||
| 1437 | + return | ||
| 1438 | + } | ||
| 1439 | + | ||
| 1440 | + if (rowIdx !== utilAbnHitIdx) { | ||
| 1441 | + utilAbnHitIdx = rowIdx | ||
| 1442 | + const item = list[rowIdx] | ||
| 1443 | + const yS = item.yellowSeconds || 0, rS = item.redSeconds || 0, totalS = yS + rS | ||
| 1444 | + utilAbnTip.name = item.deviceName || item.dtuSn || '' | ||
| 1445 | + utilAbnTip.yellowDur = fmtSec(yS) | ||
| 1446 | + utilAbnTip.yellowPct = totalS > 0 ? ((yS/totalS)*100).toFixed(2)+'%' : '0%' | ||
| 1447 | + utilAbnTip.redDur = fmtSec(rS) | ||
| 1448 | + utilAbnTip.redPct = totalS > 0 ? ((rS/totalS)*100).toFixed(2)+'%' : '0%' | ||
| 1449 | + utilAbnTip.x = e.clientX + 12 | ||
| 1450 | + utilAbnTip.y = e.clientY + 12 | ||
| 1451 | + utilAbnTip.show = true | ||
| 1452 | + } else { | ||
| 1453 | + utilAbnTip.x = e.clientX + 12 | ||
| 1454 | + utilAbnTip.y = e.clientY + 12 | ||
| 1455 | + } | ||
| 1456 | +} | ||
| 1457 | +function onUtilAbnormalLeave() { | ||
| 1458 | + utilAbnTip.show = false; utilAbnHitIdx = -1 | ||
| 1459 | +} | ||
| 1460 | + | ||
| 1461 | +// ---------- 绘制堆叠柱状图 ---------- | ||
| 1462 | +let utilStackBars = [] // 存储每根柱子的位置信息用于hit-test | ||
| 1463 | +function drawUtilStackBar() { | ||
| 1464 | + const canvas = utilStackRef.value | ||
| 1465 | + if (!canvas) return | ||
| 1466 | + const wrap = canvas.parentElement | ||
| 1467 | + if (!wrap) return | ||
| 1468 | + const cw = wrap.clientWidth, ch = 300 | ||
| 1469 | + canvas.width = cw; canvas.height = ch | ||
| 1470 | + const ctx = canvas.getContext('2d') | ||
| 1471 | + ctx.clearRect(0, 0, cw, ch) | ||
| 1472 | + | ||
| 1473 | + const list = utilData.deviceList || [] | ||
| 1474 | + if (list.length === 0) { | ||
| 1475 | + ctx.fillStyle='#999'; ctx.font='13px sans-serif'; ctx.textAlign='center' | ||
| 1476 | + ctx.fillText('暂无数据', cw/2, ch/2); return | ||
| 1477 | + } | ||
| 1478 | + | ||
| 1479 | + // 排序 | ||
| 1480 | + const sorted = sortMode.value === 'rate' | ||
| 1481 | + ? [...list].sort((a,b) => parseFloat(b.availabilityRatio||0) - parseFloat(a.availabilityRatio||0)) | ||
| 1482 | + : [...list].sort((a,b) => (b.greenSeconds||0) - (a.greenSeconds||0)) | ||
| 1483 | + | ||
| 1484 | + const PAD_L = 36, PAD_R = 20, PAD_T = 24, PAD_B = 42 | ||
| 1485 | + const plotW = cw - PAD_L - PAD_R, plotH = ch - PAD_T - PAD_B | ||
| 1486 | + const colW = Math.min(32, Math.floor(plotW / Math.max(sorted.length, 1) * 0.85)) | ||
| 1487 | + const gap = Math.max(2, (plotW - colW * sorted.length) / (sorted.length + 1)) | ||
| 1488 | + | ||
| 1489 | + // 找最大值 | ||
| 1490 | + let maxVal = 1 | ||
| 1491 | + sorted.forEach(item => { | ||
| 1492 | + const t = (item.greenSeconds||0)+(item.yellowSeconds||0)+(item.redSeconds||0)+(item.offSeconds||0) | ||
| 1493 | + maxVal = Math.max(maxVal, t) | ||
| 1494 | + }) | ||
| 1495 | + | ||
| 1496 | + // Y轴刻度(小时) | ||
| 1497 | + const maxHours = maxVal / 3600 | ||
| 1498 | + const yTicks = [Math.ceil(maxHours), Math.ceil(maxHours*0.75), Math.ceil(maxHours*0.5), Math.ceil(maxHours*0.25), 0] | ||
| 1499 | + ctx.strokeStyle = '#eee'; ctx.lineWidth = 1; ctx.fillStyle = '#999'; ctx.font = '10px sans-serif'; ctx.textAlign = 'end' | ||
| 1500 | + | ||
| 1501 | + yTicks.forEach(val => { | ||
| 1502 | + if (val <= maxHours) { | ||
| 1503 | + const py = PAD_T + plotH * (1 - val / maxHours) | ||
| 1504 | + ctx.fillText(val + '时', PAD_L - 4, py + 3) | ||
| 1505 | + ctx.beginPath(); ctx.moveTo(PAD_L, py); ctx.lineTo(cw - PAD_R, py); ctx.stroke() | ||
| 1506 | + } | ||
| 1507 | + }) | ||
| 1508 | + | ||
| 1509 | + // 基线 | ||
| 1510 | + ctx.strokeStyle = '#ddd'; ctx.lineWidth = 1 | ||
| 1511 | + ctx.beginPath(); ctx.moveTo(PAD_L, PAD_T + plotH); ctx.lineTo(cw - PAD_R, PAD_T + plotH); ctx.stroke() | ||
| 1512 | + | ||
| 1513 | + // 绘制每根柱子 | ||
| 1514 | + utilStackBars = [] | ||
| 1515 | + sorted.forEach((item, idx) => { | ||
| 1516 | + const px = PAD_L + gap + idx * (colW + gap) | ||
| 1517 | + const gS = item.greenSeconds||0, yS = item.yellowSeconds||0, rS = item.redSeconds||0, oS = item.offSeconds||0 | ||
| 1518 | + const total = gS+yS+rS+oS | ||
| 1519 | + if (total <= 0) return | ||
| 1520 | + | ||
| 1521 | + const scale = plotH / maxVal | ||
| 1522 | + let curY = PAD_T + plotH // 从底部往上堆叠 | ||
| 1523 | + | ||
| 1524 | + // 绿灯(底部) | ||
| 1525 | + const gH = gS * scale | ||
| 1526 | + curY -= gH; ctx.fillStyle='#67c23a'; ctx.fillRect(px, curY, colW, gH) | ||
| 1527 | + // 黄灯 | ||
| 1528 | + const yH = yS * scale | ||
| 1529 | + curY -= yH; ctx.fillStyle='#e6a23c'; ctx.fillRect(px, curY, colW, yH) | ||
| 1530 | + // 红灯 | ||
| 1531 | + const rH = rS * scale | ||
| 1532 | + curY -= rH; ctx.fillStyle='#f56c6c'; ctx.fillRect(px, curY, colW, rH) | ||
| 1533 | + // 灭灯(顶部) | ||
| 1534 | + const oH = oS * scale | ||
| 1535 | + curY -= oH; ctx.fillStyle='#909399'; ctx.fillRect(px, curY, colW, oH) | ||
| 1536 | + | ||
| 1537 | + // 设备名标签 | ||
| 1538 | + ctx.fillStyle = '#666'; ctx.font = '9px sans-serif'; ctx.textAlign = 'center' | ||
| 1539 | + ctx.save() | ||
| 1540 | + ctx.translate(px + colW / 2, PAD_T + plotH + 10) | ||
| 1541 | + ctx.rotate(-Math.PI / 6) | ||
| 1542 | + const dn = (item.deviceName || item.dtuSn || '').replace(/中速|高速|号机/g,'').slice(0,6) | ||
| 1543 | + ctx.fillText(dn, 0, 0) | ||
| 1544 | + ctx.restore() | ||
| 1545 | + | ||
| 1546 | + // 存储hit区域 | ||
| 1547 | + utilStackBars.push({ | ||
| 1548 | + x: px, y: curY, w: colW, h: gH+yH+rH+oH, | ||
| 1549 | + data: item, | ||
| 1550 | + name: item.deviceName || item.dtuSn, | ||
| 1551 | + gSec: gS, ySec: yS, rSec: rS, oSec: oS, | ||
| 1552 | + }) | ||
| 1553 | + | ||
| 1554 | + // hover 高亮 | ||
| 1555 | + if (idx === utilStackHitIdx) { | ||
| 1556 | + ctx.strokeStyle='#333'; ctx.lineWidth=1.5 | ||
| 1557 | + ctx.strokeRect(px, curY, colW, gH+yH+rH+oH) | ||
| 1558 | + ctx.fillStyle='rgba(64,158,255,0.08)' | ||
| 1559 | + ctx.fillRect(px-2, curY-2, colW+4, gH+yH+rH+oH+4) | ||
| 1560 | + } | ||
| 1561 | + }) | ||
| 1562 | +} | ||
| 1563 | + | ||
| 1564 | +// 堆叠柱状图 mousemove | ||
| 1565 | +function onUtilStackMove(e) { | ||
| 1566 | + const canvas = utilStackRef.value | ||
| 1567 | + if (!canvas) return | ||
| 1568 | + const rect = canvas.getBoundingClientRect() | ||
| 1569 | + const mx = e.clientX - rect.left, my = e.clientY - rect.top | ||
| 1570 | + | ||
| 1571 | + let hitIdx = -1 | ||
| 1572 | + for (let i = 0; i < utilStackBars.length; i++) { | ||
| 1573 | + const b = utilStackBars[i] | ||
| 1574 | + if (mx >= b.x && mx <= b.x + b.w && my >= b.y && my <= b.y + b.h) { | ||
| 1575 | + hitIdx = i; break | ||
| 1576 | + } | ||
| 1577 | + } | ||
| 1578 | + | ||
| 1579 | + if (hitIdx === -1 && my > 200) { | ||
| 1580 | + // 也检查标签区域 | ||
| 1581 | + for (let i = 0; i < utilStackBars.length; i++) { | ||
| 1582 | + const b = utilStackBars[i] | ||
| 1583 | + if (mx >= b.x && mx <= b.x + b.w) { hitIdx = i; break } | ||
| 1584 | + } | ||
| 1585 | + } | ||
| 1586 | + | ||
| 1587 | + if (hitIdx !== utilStackHitIdx) { | ||
| 1588 | + utilStackHitIdx = hitIdx | ||
| 1589 | + if (hitIdx >= 0) { | ||
| 1590 | + const b = utilStackBars[hitIdx] | ||
| 1591 | + const total = b.gSec+b.ySec+b.rSec+b.oSec | ||
| 1592 | + utilStackTip.name = b.name | ||
| 1593 | + utilStackTip.lines = [] | ||
| 1594 | + if (b.gSec>0) utilStackTip.lines.push({label:'绿灯',color:'#67c23a',dur:fmtSec(b.gSec),pct:(b.gSec/total*100).toFixed(2)+'%'}) | ||
| 1595 | + if (b.ySec>0) utilStackTip.lines.push({label:'黄灯',color:'#e6a23c',dur:fmtSec(b.ySec),pct:(b.ySec/total*100).toFixed(2)+'%'}) | ||
| 1596 | + if (b.rSec>0) utilStackTip.lines.push({label:'红灯',color:'#f56c6c',dur:fmtSec(b.rSec),pct:(b.rSec/total*100).toFixed(2)+'%'}) | ||
| 1597 | + if (b.oSec>0) utilStackTip.lines.push({label:'灭灯',color:'#909399',dur:fmtSec(b.oSec),pct:(b.oSec/total*100).toFixed(2)+'%'}) | ||
| 1598 | + utilStackTip.x = e.clientX + 12 | ||
| 1599 | + utilStackTip.y = e.clientY - 10 | ||
| 1600 | + utilStackTip.show = true | ||
| 1601 | + } else { | ||
| 1602 | + utilStackTip.show = false | ||
| 1603 | + } | ||
| 1604 | + drawUtilStackBar() | ||
| 1605 | + } else if (hitIdx >= 0) { | ||
| 1606 | + utilStackTip.x = e.clientX + 12 | ||
| 1607 | + utilStackTip.y = e.clientY - 10 | ||
| 1608 | + } | ||
| 1609 | +} | ||
| 1610 | +function onUtilStackLeave() { utilStackTip.show = false; utilStackHitIdx = -1; drawUtilStackBar() } | ||
| 1611 | + | ||
| 1612 | +// ---------- 绘制所有图表 ---------- | ||
| 1613 | +function drawAllUtilCharts() { | ||
| 1614 | + drawUtilPieTotal() | ||
| 1615 | + drawUtilPieRate() | ||
| 1616 | + drawUtilPieStatus() | ||
| 1617 | + drawUtilAbnormal() | ||
| 1618 | + drawUtilStackBar() | ||
| 1619 | +} | ||
| 1620 | + | ||
| 1621 | +// ---------- 获取稼动率数据 ---------- | ||
| 1622 | +async function fetchUtilData() { | ||
| 1623 | + let startDate = '', endDate = '' | ||
| 1624 | + | ||
| 1625 | + if (utilQueryMode.value === 'day') { | ||
| 1626 | + const dateStr = utilDate.value | ||
| 1627 | + if (!dateStr) { ElMessage.warning('请选择查询日期'); return } | ||
| 1628 | + startDate = endDate = dateStr | ||
| 1629 | + } else if (utilQueryMode.value === 'week') { | ||
| 1630 | + if (!utilWeekDate.value) { ElMessage.warning('请选择查询周'); return } | ||
| 1631 | + const range = getWeekRange(utilWeekDate.value) | ||
| 1632 | + startDate = range.start | ||
| 1633 | + endDate = range.end | ||
| 1634 | + } else if (utilQueryMode.value === 'month') { | ||
| 1635 | + if (!utilMonthDate.value) { ElMessage.warning('请选择查询月份'); return } | ||
| 1636 | + const range = getMonthRange(utilMonthDate.value) | ||
| 1637 | + startDate = range.start | ||
| 1638 | + endDate = range.end | ||
| 1639 | + } | ||
| 1640 | + | ||
| 1641 | + utilLoading.value = true | ||
| 1642 | + try { | ||
| 1643 | + const res = await fetch(`/api/device/lampStatistics?startDate=${startDate}&endDate=${endDate}`) | ||
| 1644 | + const data = await res.json() | ||
| 1645 | + | ||
| 1646 | + if (data.totalDuration) Object.assign(utilData.totalDuration, data.totalDuration) | ||
| 1647 | + if (data.availabilityRate) utilData.availabilityRate = data.availabilityRate | ||
| 1648 | + if (data.currentStatus) Object.assign(utilData.currentStatus, data.currentStatus) | ||
| 1649 | + if (data.abnormalRanking) utilData.abnormalRanking = data.abnormalRanking | ||
| 1650 | + if (data.deviceList) utilData.deviceList = data.deviceList | ||
| 1651 | + | ||
| 1652 | + await nextTick() | ||
| 1653 | + drawAllUtilCharts() | ||
| 1654 | + } catch (err) { | ||
| 1655 | + console.error('获取稼动率数据失败:', err) | ||
| 1656 | + ElMessage.error('获取数据失败') | ||
| 1657 | + } finally { | ||
| 1658 | + utilLoading.value = false | ||
| 1659 | + } | ||
| 1660 | +} | ||
| 1661 | + | ||
| 1662 | +// 监听tab切换 | ||
| 1663 | +watch(currentStatus, async (newVal) => { | ||
| 1664 | + if (newVal === 'utilization') { | ||
| 1665 | + setUtilDefaultDate() | ||
| 1666 | + fetchUtilData() | ||
| 1667 | + } | ||
| 1668 | +}) | ||
| 1669 | + | ||
| 1670 | +// 监听查询方式切换,自动设置默认日期并触发查询 | ||
| 1671 | +watch(utilQueryMode, () => { | ||
| 1672 | + setUtilDefaultDate() | ||
| 1673 | + fetchUtilData() | ||
| 1674 | +}) | ||
| 1675 | + | ||
| 1676 | +// 根据当前查询模式设置默认日期 | ||
| 1677 | +function setUtilDefaultDate() { | ||
| 1678 | + if (utilQueryMode.value === 'day') { | ||
| 1679 | + if (!utilDate.value) { | ||
| 1680 | + const today = new Date() | ||
| 1681 | + utilDate.value = formatYMD(today) | ||
| 1682 | + } | ||
| 1683 | + } else if (utilQueryMode.value === 'week') { | ||
| 1684 | + if (!utilWeekDate.value) { | ||
| 1685 | + utilWeekDate.value = getCurrentMonday() | ||
| 1686 | + } | ||
| 1687 | + } else if (utilQueryMode.value === 'month') { | ||
| 1688 | + if (!utilMonthDate.value) { | ||
| 1689 | + utilMonthDate.value = getCurrentYM() | ||
| 1690 | + } | ||
| 1691 | + } | ||
| 1692 | +} | ||
| 1693 | + | ||
| 1694 | +// ========== 开机率数据 ========== | ||
| 1695 | +const startupQueryMode = ref('day') | ||
| 1696 | +const startupDate = ref('') | ||
| 1697 | +const startupWeekDate = ref('') | ||
| 1698 | +const startupMonthDate = ref('') | ||
| 1699 | +const startupLoading = ref(false) | ||
| 1700 | +const startupCanvasRef = ref(null) | ||
| 1701 | + | ||
| 1702 | +// 周查询显示格式 | ||
| 1703 | +const startupWeekDisplayFormat = computed(() => { | ||
| 1704 | + if (!startupWeekDate.value) return '' | ||
| 1705 | + const d = new Date(startupWeekDate.value) | ||
| 1706 | + return `${d.getFullYear()}-${String(getWeekNumber(d)).padStart(2, '0')}周` | ||
| 1707 | +}) | ||
| 1708 | + | ||
| 1709 | +// API 数据 | ||
| 1710 | +const startupList = ref([]) | ||
| 1711 | +const startupSummary = reactive({ | ||
| 1712 | + totalDevices: 0, | ||
| 1713 | + overallBootRate: '0%', | ||
| 1714 | + totalDuration: '', | ||
| 1715 | + onDuration: '', | ||
| 1716 | + offDuration: '', | ||
| 1717 | + dataDays: 0, | ||
| 1718 | +}) | ||
| 1719 | + | ||
| 1720 | +// Tooltip | ||
| 1721 | +const startupTip = reactive({ show: false, x: 0, y: 0, name: '', bootRate: '', onDuration: '', offDuration: '', totalDuration: '' }) | ||
| 1722 | +let startupHitIdx = -1 | ||
| 1723 | + | ||
| 1724 | +// 获取开机率日期范围 | ||
| 1725 | +function getStartupDateRange() { | ||
| 1726 | + let startDate = '', endDate = '' | ||
| 1727 | + if (startupQueryMode.value === 'day') { | ||
| 1728 | + const dateStr = startupDate.value | ||
| 1729 | + if (!dateStr) return { startDate: '', endDate: '' } | ||
| 1730 | + startDate = endDate = dateStr | ||
| 1731 | + } else if (startupQueryMode.value === 'week') { | ||
| 1732 | + if (!startupWeekDate.value) return { startDate: '', endDate: '' } | ||
| 1733 | + const range = getWeekRange(startupWeekDate.value) | ||
| 1734 | + startDate = range.start | ||
| 1735 | + endDate = range.end | ||
| 1736 | + } else if (startupQueryMode.value === 'month') { | ||
| 1737 | + if (!startupMonthDate.value) return { startDate: '', endDate: '' } | ||
| 1738 | + const range = getMonthRange(startupMonthDate.value) | ||
| 1739 | + startDate = range.start | ||
| 1740 | + endDate = range.end | ||
| 1741 | + } | ||
| 1742 | + return { startDate, endDate } | ||
| 1743 | +} | ||
| 1744 | + | ||
| 1745 | +// 获取开机率数据 | ||
| 1746 | +async function fetchStartupData() { | ||
| 1747 | + const { startDate, endDate } = getStartupDateRange() | ||
| 1748 | + if (!startDate || !endDate) { | ||
| 1749 | + ElMessage.warning('请选择查询时间'); return | ||
| 1750 | + } | ||
| 1751 | + | ||
| 1752 | + startupLoading.value = true | ||
| 1753 | + try { | ||
| 1754 | + const res = await fetch(`/api/device/bootRate?startDate=${startDate}&endDate=${endDate}`) | ||
| 1755 | + const data = await res.json() | ||
| 1756 | + | ||
| 1757 | + startupList.value = data.list || [] | ||
| 1758 | + if (data.summary) Object.assign(startupSummary, data.summary) | ||
| 1759 | + | ||
| 1760 | + await nextTick() | ||
| 1761 | + drawStartupChart() | ||
| 1762 | + } catch (err) { | ||
| 1763 | + console.error('获取开机率数据失败:', err) | ||
| 1764 | + ElMessage.error('获取数据失败') | ||
| 1765 | + } finally { | ||
| 1766 | + startupLoading.value = false | ||
| 1767 | + } | ||
| 1768 | +} | ||
| 1769 | + | ||
| 1770 | +// 设置开机率默认日期 | ||
| 1771 | +function setStartupDefaultDate() { | ||
| 1772 | + if (startupQueryMode.value === 'day') { | ||
| 1773 | + if (!startupDate.value) { | ||
| 1774 | + startupDate.value = formatYMD(new Date()) | ||
| 1775 | + } | ||
| 1776 | + } else if (startupQueryMode.value === 'week') { | ||
| 1777 | + if (!startupWeekDate.value) { | ||
| 1778 | + startupWeekDate.value = getCurrentMonday() | ||
| 1779 | + } | ||
| 1780 | + } else if (startupQueryMode.value === 'month') { | ||
| 1781 | + if (!startupMonthDate.value) { | ||
| 1782 | + startupMonthDate.value = getCurrentYM() | ||
| 1783 | + } | ||
| 1784 | + } | ||
| 1785 | +} | ||
| 1786 | + | ||
| 1787 | +// 监听tab切换 | ||
| 1788 | +watch(currentStatus, async (newVal) => { | ||
| 1789 | + if (newVal === 'startup') { | ||
| 1790 | + setStartupDefaultDate() | ||
| 1791 | + fetchStartupData() | ||
| 1792 | + } | ||
| 1793 | +}) | ||
| 1794 | + | ||
| 1795 | +// 监听查询方式切换 | ||
| 1796 | +watch(startupQueryMode, () => { | ||
| 1797 | + setStartupDefaultDate() | ||
| 1798 | + fetchStartupData() | ||
| 1799 | +}) | ||
| 1800 | + | ||
| 1801 | +// ---------- 绘制开机率柱状图 ---------- | ||
| 1802 | +let startupBars = [] | ||
| 1803 | +function drawStartupChart() { | ||
| 1804 | + const canvas = startupCanvasRef.value | ||
| 1805 | + if (!canvas) return | ||
| 1806 | + const container = canvas.parentElement | ||
| 1807 | + if (!container) return | ||
| 1808 | + const cw = container.clientWidth | ||
| 1809 | + const ch = Math.min(400, Math.max(320, startupList.value.length * 28 + 80)) | ||
| 1810 | + | ||
| 1811 | + // DPR 缩放提升清晰度 | ||
| 1812 | + const dpr = window.devicePixelRatio || 1 | ||
| 1813 | + canvas.style.width = cw + 'px' | ||
| 1814 | + canvas.style.height = ch + 'px' | ||
| 1815 | + canvas.width = Math.round(cw * dpr) | ||
| 1816 | + canvas.height = Math.round(ch * dpr) | ||
| 1817 | + const ctx = canvas.getContext('2d') | ||
| 1818 | + ctx.scale(dpr, dpr) | ||
| 1819 | + ctx.clearRect(0, 0, cw, ch) | ||
| 1820 | + | ||
| 1821 | + const list = startupList.value | ||
| 1822 | + if (list.length === 0) { | ||
| 1823 | + ctx.fillStyle = '#999' | ||
| 1824 | + ctx.font = '13px sans-serif' | ||
| 1825 | + ctx.textAlign = 'center' | ||
| 1826 | + ctx.fillText('暂无数据', cw / 2, ch / 2) | ||
| 1827 | + return | ||
| 1828 | + } | ||
| 1829 | + | ||
| 1830 | + const PAD_L = 50, PAD_R = 20, PAD_T = 24, PAD_B = 50 | ||
| 1831 | + const plotW = cw - PAD_L - PAD_R, plotH = ch - PAD_T - PAD_B | ||
| 1832 | + const colW = Math.min(32, Math.floor(plotW / Math.max(list.length, 1) * 0.85)) | ||
| 1833 | + const gap = Math.max(3, (plotW - colW * list.length) / (list.length + 1)) | ||
| 1834 | + | ||
| 1835 | + // Y轴刻度 | ||
| 1836 | + ctx.strokeStyle = '#eee' | ||
| 1837 | + ctx.lineWidth = 1 | ||
| 1838 | + ctx.fillStyle = '#999' | ||
| 1839 | + ctx.font = '10px sans-serif' | ||
| 1840 | + ctx.textAlign = 'end' | ||
| 1841 | + | ||
| 1842 | + for (let i = 0; i <= 5; i++) { | ||
| 1843 | + const val = i * 20 | ||
| 1844 | + const py = PAD_T + plotH * (1 - val / 100) | ||
| 1845 | + ctx.fillText(val + '%', PAD_L - 5, py + 3) | ||
| 1846 | + if (i > 0) { | ||
| 1847 | + ctx.beginPath() | ||
| 1848 | + ctx.moveTo(PAD_L, py) | ||
| 1849 | + ctx.lineTo(cw - PAD_R, py) | ||
| 1850 | + ctx.stroke() | ||
| 1851 | + } | ||
| 1852 | + } | ||
| 1853 | + | ||
| 1854 | + // 基线 | ||
| 1855 | + ctx.strokeStyle = '#ddd' | ||
| 1856 | + ctx.lineWidth = 1.5 | ||
| 1857 | + ctx.beginPath() | ||
| 1858 | + ctx.moveTo(PAD_L, PAD_T + plotH) | ||
| 1859 | + ctx.lineTo(cw - PAD_R, PAD_T + plotH) | ||
| 1860 | + ctx.stroke() | ||
| 1861 | + | ||
| 1862 | + // 绘制柱子背景(统一样式) | ||
| 1863 | + list.forEach((item, idx) => { | ||
| 1864 | + const px = PAD_L + gap + idx * (colW + gap) | ||
| 1865 | + ctx.fillStyle = '#f2f3f5' | ||
| 1866 | + roundRect(ctx, px, PAD_T, colW, plotH, 3) | ||
| 1867 | + ctx.fill() | ||
| 1868 | + }) | ||
| 1869 | + | ||
| 1870 | + // 绘制柱子 | ||
| 1871 | + startupBars = [] | ||
| 1872 | + list.forEach((item, idx) => { | ||
| 1873 | + const px = PAD_L + gap + idx * (colW + gap) | ||
| 1874 | + const rate = item.bootRateValue || 0 | ||
| 1875 | + const barH = rate / 100 * plotH | ||
| 1876 | + const barY = PAD_T + plotH - barH | ||
| 1877 | + | ||
| 1878 | + let color = '#67c23a' | ||
| 1879 | + if (rate >= 95) color = '#4caf50' | ||
| 1880 | + else if (rate < 50) color = '#91cc75' | ||
| 1881 | + | ||
| 1882 | + const isHovered = idx === startupHitIdx | ||
| 1883 | + | ||
| 1884 | + if (isHovered) { | ||
| 1885 | + // hover效果:阴影+放大+提亮+边框 | ||
| 1886 | + ctx.save() | ||
| 1887 | + ctx.shadowColor = 'rgba(103,194,58,0.5)' | ||
| 1888 | + ctx.shadowBlur = 16 | ||
| 1889 | + ctx.shadowOffsetY = 3 | ||
| 1890 | + ctx.fillStyle = lightenColor(color, 30) | ||
| 1891 | + const hoverPad = 2 | ||
| 1892 | + roundRect(ctx, px - hoverPad, barY - hoverPad, colW + hoverPad * 2, barH + hoverPad * 2, 4) | ||
| 1893 | + ctx.fill() | ||
| 1894 | + ctx.restore() | ||
| 1895 | + // 边框 | ||
| 1896 | + ctx.strokeStyle = '#3d8b3d' | ||
| 1897 | + ctx.lineWidth = 1.5 | ||
| 1898 | + roundRect(ctx, px - hoverPad, barY - hoverPad, colW + hoverPad * 2, barH + hoverPad * 2, 4) | ||
| 1899 | + ctx.stroke() | ||
| 1900 | + } else { | ||
| 1901 | + roundRect(ctx, px, barY, colW, barH, 3) | ||
| 1902 | + ctx.fillStyle = color | ||
| 1903 | + ctx.fill() | ||
| 1904 | + } | ||
| 1905 | + | ||
| 1906 | + ctx.fillStyle = '#666' | ||
| 1907 | + ctx.font = '9px sans-serif' | ||
| 1908 | + ctx.textAlign = 'center' | ||
| 1909 | + ctx.save() | ||
| 1910 | + ctx.translate(px + colW / 2, PAD_T + plotH + 12) | ||
| 1911 | + ctx.rotate(-Math.PI / 6) | ||
| 1912 | + const dn = (item.deviceName || item.dtuSn || '').slice(0, 8) | ||
| 1913 | + ctx.fillText(dn, 0, 0) | ||
| 1914 | + ctx.restore() | ||
| 1915 | + | ||
| 1916 | + // 始终用基础坐标存储hit区域,扩大到整列方便hover | ||
| 1917 | + startupBars.push({ | ||
| 1918 | + x: px - 2, y: PAD_T, w: colW + 4, h: plotH, | ||
| 1919 | + name: item.deviceName || item.dtuSn, | ||
| 1920 | + bootRate: item.bootRate, | ||
| 1921 | + onDuration: item.onDuration, | ||
| 1922 | + offDuration: item.offDuration, | ||
| 1923 | + totalDuration: item.totalDuration, | ||
| 1924 | + }) | ||
| 1925 | + }) | ||
| 1926 | +} | ||
| 1927 | + | ||
| 1928 | +// 圆角矩形辅助函数 | ||
| 1929 | +function roundRect(ctx, x, y, w, h, r) { | ||
| 1930 | + r = Math.min(r, w / 2, h / 2) | ||
| 1931 | + ctx.beginPath() | ||
| 1932 | + ctx.moveTo(x + r, y) | ||
| 1933 | + ctx.lineTo(x + w - r, y) | ||
| 1934 | + ctx.arcTo(x + w, y, x + w, y + r, r) | ||
| 1935 | + ctx.lineTo(x + w, y + h - r) | ||
| 1936 | + ctx.arcTo(x + w, y + h, x + w - r, y + h, r) | ||
| 1937 | + ctx.lineTo(x + r, y + h) | ||
| 1938 | + ctx.arcTo(x, y + h, x, y + h - r, r) | ||
| 1939 | + ctx.lineTo(x, y + r) | ||
| 1940 | + ctx.arcTo(x, y, x + r, y, r) | ||
| 1941 | + ctx.closePath() | ||
| 1942 | +} | ||
| 1943 | + | ||
| 1944 | +// 颜色提亮工具函数 | ||
| 1945 | +function lightenColor(hex, amount) { | ||
| 1946 | + let r = parseInt(hex.slice(1, 3), 16) | ||
| 1947 | + let g = parseInt(hex.slice(3, 5), 16) | ||
| 1948 | + let b = parseInt(hex.slice(5, 7), 16) | ||
| 1949 | + r = Math.min(255, r + amount) | ||
| 1950 | + g = Math.min(255, g + amount) | ||
| 1951 | + b = Math.min(255, b + amount) | ||
| 1952 | + return `rgb(${r},${g},${b})` | ||
| 1953 | +} | ||
| 1954 | + | ||
| 1955 | +function onStartupMove(e) { | ||
| 1956 | + const canvas = startupCanvasRef.value | ||
| 1957 | + if (!canvas) return | ||
| 1958 | + const rect = canvas.getBoundingClientRect() | ||
| 1959 | + const mx = e.clientX - rect.left | ||
| 1960 | + const my = e.clientY - rect.top | ||
| 1961 | + | ||
| 1962 | + let hitIdx = -1 | ||
| 1963 | + for (let i = 0; i < startupBars.length; i++) { | ||
| 1964 | + const b = startupBars[i] | ||
| 1965 | + if (mx >= b.x && mx <= b.x + b.w && my >= b.y && my <= b.y + b.h) { | ||
| 1966 | + hitIdx = i; break | ||
| 1967 | + } | ||
| 1968 | + } | ||
| 1969 | + | ||
| 1970 | + if (hitIdx !== startupHitIdx) { | ||
| 1971 | + startupHitIdx = hitIdx | ||
| 1972 | + if (hitIdx >= 0) { | ||
| 1973 | + const b = startupBars[hitIdx] | ||
| 1974 | + startupTip.name = b.name | ||
| 1975 | + startupTip.bootRate = b.bootRate | ||
| 1976 | + startupTip.onDuration = b.onDuration | ||
| 1977 | + startupTip.offDuration = b.offDuration | ||
| 1978 | + startupTip.totalDuration = b.totalDuration | ||
| 1979 | + startupTip.x = e.clientX + 12 | ||
| 1980 | + startupTip.y = e.clientY - 10 | ||
| 1981 | + startupTip.show = true | ||
| 1982 | + } else { | ||
| 1983 | + startupTip.show = false | ||
| 1984 | + } | ||
| 1985 | + drawStartupChart() | ||
| 1986 | + } else if (hitIdx >= 0) { | ||
| 1987 | + startupTip.x = e.clientX + 12 | ||
| 1988 | + startupTip.y = e.clientY - 10 | ||
| 1989 | + } | ||
| 1990 | +} | ||
| 1991 | +function onStartupLeave() { startupTip.show = false; startupHitIdx = -1; drawStartupChart() } | ||
| 1992 | +</script> | ||
| 1993 | + | ||
| 1994 | +<style scoped> | ||
| 1995 | +.smart-light-page { | ||
| 1996 | + min-height: 100%; | ||
| 1997 | + height: calc(100vh - 0px); | ||
| 1998 | + display: flex; | ||
| 1999 | + flex-direction: column; | ||
| 2000 | + background-color: #f0f2f5; | ||
| 2001 | +} | ||
| 2002 | +.device-grid { | ||
| 2003 | + flex: 1; | ||
| 2004 | + padding: 16px 20px; | ||
| 2005 | + display: grid; | ||
| 2006 | + grid-template-columns: repeat(6, 1fr); | ||
| 2007 | + gap: 16px; | ||
| 2008 | + align-content: start; | ||
| 2009 | +} | ||
| 2010 | +.device-grid.grid-full { | ||
| 2011 | + align-content: stretch; | ||
| 2012 | +} | ||
| 2013 | +.device-grid.grid-normal { | ||
| 2014 | + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); | ||
| 2015 | +} | ||
| 2016 | +.top-toolbar { | ||
| 2017 | + background: #fff; | ||
| 2018 | + padding: 0 20px; | ||
| 2019 | + display: flex; | ||
| 2020 | + align-items: center; | ||
| 2021 | + justify-content: space-between; | ||
| 2022 | + border-bottom: 1px solid #e8e8e8; | ||
| 2023 | +} | ||
| 2024 | +.status-tabs { | ||
| 2025 | + display: flex; | ||
| 2026 | + gap: 4px; | ||
| 2027 | +} | ||
| 2028 | +.status-tab { | ||
| 2029 | + padding: 14px 18px; | ||
| 2030 | + cursor: pointer; | ||
| 2031 | + font-size: 13px; | ||
| 2032 | + color: #666; | ||
| 2033 | + position: relative; | ||
| 2034 | + transition: all 0.2s; | ||
| 2035 | +} | ||
| 2036 | +.status-tab:hover { | ||
| 2037 | + color: #409eff; | ||
| 2038 | +} | ||
| 2039 | +.status-tab.active { | ||
| 2040 | + color: #409eff; | ||
| 2041 | + font-weight: bold; | ||
| 2042 | +} | ||
| 2043 | +.status-tab.active::after { | ||
| 2044 | + content: ''; | ||
| 2045 | + position: absolute; | ||
| 2046 | + bottom: 0; | ||
| 2047 | + left: 50%; | ||
| 2048 | + transform: translateX(-50%); | ||
| 2049 | + width: 60%; | ||
| 2050 | + height: 2px; | ||
| 2051 | + background: #409eff; | ||
| 2052 | +} | ||
| 2053 | +.toolbar-right { | ||
| 2054 | + display: flex; | ||
| 2055 | + align-items: center; | ||
| 2056 | + gap: 8px; | ||
| 2057 | +} | ||
| 2058 | +.filter-bar { | ||
| 2059 | + background: #fff; | ||
| 2060 | + padding: 10px 20px; | ||
| 2061 | + display: flex; | ||
| 2062 | + align-items: center; | ||
| 2063 | + justify-content: space-between; | ||
| 2064 | + border-bottom: 1px solid #e8e8e8; | ||
| 2065 | +} | ||
| 2066 | +.filter-label { | ||
| 2067 | + font-size: 13px; | ||
| 2068 | + color: #999; | ||
| 2069 | +} | ||
| 2070 | +.filter-tags { | ||
| 2071 | + display: flex; | ||
| 2072 | + gap: 16px; | ||
| 2073 | +} | ||
| 2074 | +.tag-item { | ||
| 2075 | + font-size: 12px; | ||
| 2076 | + display: flex; | ||
| 2077 | + align-items: center; | ||
| 2078 | + gap: 4px; | ||
| 2079 | + cursor: pointer; | ||
| 2080 | + padding: 2px 6px; | ||
| 2081 | + border-radius: 4px; | ||
| 2082 | + transition: background 0.2s; | ||
| 2083 | +} | ||
| 2084 | +.tag-item:hover { | ||
| 2085 | + background: rgba(0,0,0,0.05); | ||
| 2086 | +} | ||
| 2087 | +.tag-item.active { | ||
| 2088 | + font-weight: bold; | ||
| 2089 | + background: rgba(64,158,255,0.08); | ||
| 2090 | +} | ||
| 2091 | +.tag-item i { | ||
| 2092 | + width: 10px; | ||
| 2093 | + height: 10px; | ||
| 2094 | + display: inline-block; | ||
| 2095 | + border-radius: 2px; | ||
| 2096 | +} | ||
| 2097 | +.tag-item.black i { background: #333; } | ||
| 2098 | +.tag-item.red i { background: #f56c6c; } | ||
| 2099 | +.tag-item.yellow i { background: #e6a23c; } | ||
| 2100 | +.tag-item.green i { background: #67c23a; } | ||
| 2101 | +.tag-item.blue i { background: #409eff; } | ||
| 2102 | +.tag-item.gray i { background: #909399; } | ||
| 2103 | + | ||
| 2104 | +.device-card { | ||
| 2105 | + height: 340px; | ||
| 2106 | + border-radius: 12px; | ||
| 2107 | + overflow: hidden; | ||
| 2108 | + transition: transform 0.2s; | ||
| 2109 | + display: flex; | ||
| 2110 | + flex-direction: column; | ||
| 2111 | +} | ||
| 2112 | +.device-card.green { | ||
| 2113 | + background: linear-gradient(160deg, #5cb85c 0%, #449d44 50%, #3d8b3d 100%); | ||
| 2114 | + box-shadow: 0 4px 16px rgba(92,184,92,0.35); | ||
| 2115 | +} | ||
| 2116 | +.device-card.yellow { | ||
| 2117 | + background: linear-gradient(160deg, #f5a623 0%, #e69811 50%, #cc8409 100%); | ||
| 2118 | + box-shadow: 0 4px 16px rgba(245,166,35,0.35); | ||
| 2119 | +} | ||
| 2120 | +.device-card.red { | ||
| 2121 | + background: linear-gradient(160deg, #e74c3c 0%, #c0392b 50%, #a93226 100%); | ||
| 2122 | + box-shadow: 0 4px 16px rgba(231,76,60,0.35); | ||
| 2123 | +} | ||
| 2124 | +.device-card.gray { | ||
| 2125 | + background: linear-gradient(160deg, #8a9a9a 0%, #6b7b7b 50%, #5a6a6a 100%); | ||
| 2126 | + box-shadow: 0 4px 16px rgba(100,110,110,0.25); | ||
| 2127 | +} | ||
| 2128 | +.device-card:hover { | ||
| 2129 | + transform: translateY(-3px); | ||
| 2130 | +} | ||
| 2131 | +.device-card.green:hover { | ||
| 2132 | + box-shadow: 0 6px 24px rgba(92,184,92,0.45); | ||
| 2133 | +} | ||
| 2134 | +.device-card.yellow:hover { | ||
| 2135 | + box-shadow: 0 6px 24px rgba(245,166,35,0.5); | ||
| 2136 | +} | ||
| 2137 | +.device-card.red:hover { | ||
| 2138 | + box-shadow: 0 6px 24px rgba(231,76,60,0.5); | ||
| 2139 | +} | ||
| 2140 | +.device-card.gray:hover { | ||
| 2141 | + box-shadow: 0 6px 24px rgba(100,110,110,0.35); | ||
| 2142 | +} | ||
| 2143 | + | ||
| 2144 | +/* 标题栏 */ | ||
| 2145 | +.card-header { | ||
| 2146 | + padding: 12px 16px; | ||
| 2147 | + display: flex; | ||
| 2148 | + justify-content: space-between; | ||
| 2149 | + align-items: center; | ||
| 2150 | + font-size: 14px; | ||
| 2151 | + font-weight: bold; | ||
| 2152 | + color: #fff; | ||
| 2153 | + flex-shrink: 0; | ||
| 2154 | +} | ||
| 2155 | +.menu-icon { | ||
| 2156 | + cursor: pointer; | ||
| 2157 | + color: rgba(255,255,255,0.85); | ||
| 2158 | + font-size: 18px; | ||
| 2159 | +} | ||
| 2160 | + | ||
| 2161 | +/* 内容区 */ | ||
| 2162 | +.card-body { | ||
| 2163 | + padding: 16px 14px 12px; | ||
| 2164 | + display: flex; | ||
| 2165 | + gap: 12px; | ||
| 2166 | + align-items: stretch; | ||
| 2167 | + flex: 1; | ||
| 2168 | +} | ||
| 2169 | + | ||
| 2170 | +/* 左侧状态条 - 四块色块 */ | ||
| 2171 | +.status-bar { | ||
| 2172 | + width: 28px; | ||
| 2173 | + border-radius: 14px; | ||
| 2174 | + flex-shrink: 0; | ||
| 2175 | + overflow: hidden; | ||
| 2176 | + display: flex; | ||
| 2177 | + flex-direction: column; | ||
| 2178 | + gap: 3px; | ||
| 2179 | + padding: 3px 0; | ||
| 2180 | +} | ||
| 2181 | +.bar-block { | ||
| 2182 | + flex: 1; | ||
| 2183 | + border-radius: 6px; | ||
| 2184 | + transition: opacity 0.2s, filter 0.2s; | ||
| 2185 | +} | ||
| 2186 | +/* 默认暗态(未激活)- 全部灰色 */ | ||
| 2187 | +.block-red { background: rgba(120, 130, 140, 0.35); } | ||
| 2188 | +.block-yellow { background: rgba(120, 130, 140, 0.35); } | ||
| 2189 | +.block-green { background: rgba(120, 130, 140, 0.35); } | ||
| 2190 | +.block-blue { background: rgba(120, 130, 140, 0.35); } | ||
| 2191 | +/* 激活亮态 - 纯色实心与背景区分 */ | ||
| 2192 | +.block-red.active { background: #c0392b; } | ||
| 2193 | +.block-yellow.active { background: #e67e22; } | ||
| 2194 | +.block-green.active { background: #1a7a37; } | ||
| 2195 | +.block-blue.active { background: #2463aa; } | ||
| 2196 | + | ||
| 2197 | +/* 右侧内容 */ | ||
| 2198 | +.card-content { | ||
| 2199 | + flex: 1; | ||
| 2200 | + display: flex; | ||
| 2201 | + flex-direction: column; | ||
| 2202 | + gap: 10px; | ||
| 2203 | + justify-content: center; | ||
| 2204 | +} | ||
| 2205 | + | ||
| 2206 | +.info-row { | ||
| 2207 | + font-size: 14px; | ||
| 2208 | + color: rgba(255,255,255,0.95); | ||
| 2209 | + line-height: 1.8; | ||
| 2210 | +} | ||
| 2211 | +.value-highlight { | ||
| 2212 | + color: #fff; | ||
| 2213 | + font-weight: bold; | ||
| 2214 | + font-size: 15px; | ||
| 2215 | +} | ||
| 2216 | + | ||
| 2217 | +/* 数字显示屏 */ | ||
| 2218 | +.digital-display { | ||
| 2219 | + background: #1a1a2e; | ||
| 2220 | + padding: 10px 14px; | ||
| 2221 | + border-radius: 8px; | ||
| 2222 | + display: flex; | ||
| 2223 | + justify-content: center; | ||
| 2224 | + gap: 3px; | ||
| 2225 | + margin-top: 30px; | ||
| 2226 | +} | ||
| 2227 | +.digit { | ||
| 2228 | + color: #00ff88; | ||
| 2229 | + font-family: 'Courier New', monospace; | ||
| 2230 | + font-size: 24px; | ||
| 2231 | + font-weight: bold; | ||
| 2232 | + text-shadow: 0 0 10px rgba(0,255,136,0.5); | ||
| 2233 | + min-width: 16px; | ||
| 2234 | + text-align: center; | ||
| 2235 | +} | ||
| 2236 | + | ||
| 2237 | +/* 底部按钮 */ | ||
| 2238 | +.card-footer { | ||
| 2239 | + padding: 10px 14px 12px; | ||
| 2240 | + display: grid; | ||
| 2241 | + grid-template-columns: 1fr 1fr; | ||
| 2242 | + gap: 8px; | ||
| 2243 | + flex-shrink: 0; | ||
| 2244 | +} | ||
| 2245 | +.action-btn { | ||
| 2246 | + display: flex; | ||
| 2247 | + align-items: center; | ||
| 2248 | + justify-content: center; | ||
| 2249 | + gap: 4px; | ||
| 2250 | + padding: 7px 8px; | ||
| 2251 | + border-radius: 5px; | ||
| 2252 | + font-size: 13px; | ||
| 2253 | + cursor: pointer; | ||
| 2254 | + transition: all 0.2s; | ||
| 2255 | + background: rgba(255,255,255,0.18); | ||
| 2256 | + border: 1.5px solid rgba(255,255,255,0.4); | ||
| 2257 | + color: #fff; | ||
| 2258 | + backdrop-filter: blur(4px); | ||
| 2259 | +} | ||
| 2260 | +.action-btn:hover { | ||
| 2261 | + background: rgba(255,255,255,0.3); | ||
| 2262 | + border-color: rgba(255,255,255,0.65); | ||
| 2263 | +} | ||
| 2264 | + | ||
| 2265 | +/* ========== 自定义分页 ========== */ | ||
| 2266 | +.pagination-wrapper { | ||
| 2267 | + display: flex; | ||
| 2268 | + align-items: center; | ||
| 2269 | + justify-content: flex-end; | ||
| 2270 | + padding: 14px 20px; | ||
| 2271 | + border-top: 1px solid #e8e8e8; | ||
| 2272 | +} | ||
| 2273 | +.pagination-info { font-size: 13px; color: #666; } | ||
| 2274 | +.pagination-info strong { color: #333; } | ||
| 2275 | + | ||
| 2276 | +.pagination-controls { | ||
| 2277 | + display: flex; | ||
| 2278 | + align-items: center; | ||
| 2279 | + gap: 4px; | ||
| 2280 | +} | ||
| 2281 | +.page-size-select { | ||
| 2282 | + height: 30px; | ||
| 2283 | + padding: 2px 8px; | ||
| 2284 | + border: 1px solid #dcdfe6; | ||
| 2285 | + border-radius: 4px; | ||
| 2286 | + background: #fff; | ||
| 2287 | + font-size: 13px; | ||
| 2288 | + color: #606266; | ||
| 2289 | + outline: none; | ||
| 2290 | + cursor: pointer; | ||
| 2291 | +} | ||
| 2292 | +.page-btn { | ||
| 2293 | + display: inline-flex; | ||
| 2294 | + align-items: center; | ||
| 2295 | + justify-content: center; | ||
| 2296 | + width: 32px; | ||
| 2297 | + height: 32px; | ||
| 2298 | + border: 1px solid #dcdfe6; | ||
| 2299 | + border-radius: 4px; | ||
| 2300 | + background: #fff; | ||
| 2301 | + color: #606266; | ||
| 2302 | + font-size: 13px; | ||
| 2303 | + cursor: pointer; | ||
| 2304 | + transition: all 0.15s; | ||
| 2305 | +} | ||
| 2306 | +.page-btn:hover:not(:disabled) { | ||
| 2307 | + color: #409eff; | ||
| 2308 | + border-color: #409eff; | ||
| 2309 | +} | ||
| 2310 | +.page-btn.active { | ||
| 2311 | + background-color: #409eff; | ||
| 2312 | + border-color: #409eff; | ||
| 2313 | + color: #fff; | ||
| 2314 | +} | ||
| 2315 | +.page-btn:disabled { | ||
| 2316 | + opacity: 0.45; | ||
| 2317 | + cursor: not-allowed; | ||
| 2318 | +} | ||
| 2319 | +.page-dots { | ||
| 2320 | + display: inline-flex; | ||
| 2321 | + align-items: center; | ||
| 2322 | + justify-content: center; | ||
| 2323 | + width: 24px; | ||
| 2324 | + color: #999; | ||
| 2325 | + font-size: 13px; | ||
| 2326 | +} | ||
| 2327 | + | ||
| 2328 | +/* ========== Tab内容区通用 ========== */ | ||
| 2329 | +.tab-content { | ||
| 2330 | + flex: 1; | ||
| 2331 | + display: flex; | ||
| 2332 | + flex-direction: column; | ||
| 2333 | + overflow: hidden; | ||
| 2334 | +} | ||
| 2335 | + | ||
| 2336 | +/* ========== 时序状态 ========== */ | ||
| 2337 | +.timeseries-view { | ||
| 2338 | + background: #f0f2f5; | ||
| 2339 | +} | ||
| 2340 | +.ts-toolbar { | ||
| 2341 | + background: #fff; | ||
| 2342 | + padding: 12px 16px; | ||
| 2343 | + display: flex; | ||
| 2344 | + align-items: center; | ||
| 2345 | + gap: 8px; | ||
| 2346 | + border-bottom: 1px solid #ebeef5; | ||
| 2347 | +} | ||
| 2348 | +.ts-label { | ||
| 2349 | + font-size: 13px; color: #666; white-space: nowrap; | ||
| 2350 | +} | ||
| 2351 | +.ts-table-wrap { | ||
| 2352 | + flex: 1; | ||
| 2353 | + overflow: hidden; | ||
| 2354 | + margin: 12px 20px; | ||
| 2355 | + /* 与 OeeDialog timeline-chart 风格一致 */ | ||
| 2356 | + background: #fff; | ||
| 2357 | + border: 1px solid #e0e0e0; | ||
| 2358 | + border-radius: 4px; | ||
| 2359 | + padding: 6px; | ||
| 2360 | +} | ||
| 2361 | +.ts-header-row { | ||
| 2362 | + display: flex; | ||
| 2363 | + align-items: flex-end; | ||
| 2364 | + position: sticky; | ||
| 2365 | + top: 0; | ||
| 2366 | + background: #fafafa; | ||
| 2367 | + border-bottom: 2px solid #e0e0e0; | ||
| 2368 | + z-index: 2; | ||
| 2369 | +} | ||
| 2370 | +.ts-col-name { | ||
| 2371 | + width: 160px; | ||
| 2372 | + padding: 8px 12px; | ||
| 2373 | + font-size: 13px; | ||
| 2374 | + font-weight: bold; | ||
| 2375 | + color: #333; | ||
| 2376 | + flex-shrink: 0; | ||
| 2377 | + text-align: center; | ||
| 2378 | +} | ||
| 2379 | +.ts-sub-col { width: 70px; } | ||
| 2380 | +.ts-timeline-area { | ||
| 2381 | + flex: 1; | ||
| 2382 | + min-width: 800px; | ||
| 2383 | +} | ||
| 2384 | +.ts-row { | ||
| 2385 | + display: flex; | ||
| 2386 | + align-items: center; | ||
| 2387 | + border-bottom: 1px solid #f0f0f0; | ||
| 2388 | + min-height: 36px; | ||
| 2389 | +} | ||
| 2390 | +.ts-row.row-gray .ts-cell-name { background: #f5f5f5; } | ||
| 2391 | +.ts-cell-name { | ||
| 2392 | + width: 160px; | ||
| 2393 | + padding: 6px 12px; | ||
| 2394 | + flex-shrink: 0; | ||
| 2395 | + font-size: 12px; | ||
| 2396 | +} | ||
| 2397 | +.ts-link { color: #409eff; cursor: pointer; } | ||
| 2398 | +.ts-link:hover { text-decoration: underline; } | ||
| 2399 | +.ts-cell-rate { | ||
| 2400 | + width: 70px; | ||
| 2401 | + padding: 6px 4px; | ||
| 2402 | + text-align: center; | ||
| 2403 | + font-size: 12px; | ||
| 2404 | + font-weight: bold; | ||
| 2405 | + color: #333; | ||
| 2406 | + flex-shrink: 0; | ||
| 2407 | +} | ||
| 2408 | +.ts-row.row-gray .ts-cell-rate { background: #f0f0f0; } | ||
| 2409 | +.ts-cell-bars { | ||
| 2410 | + flex: 1; | ||
| 2411 | + min-width: 800px; | ||
| 2412 | + padding: 4px 8px; | ||
| 2413 | +} | ||
| 2414 | +.bar-track { | ||
| 2415 | + height: 22px; | ||
| 2416 | + background: #f5f5f5; | ||
| 2417 | + border-radius: 3px; | ||
| 2418 | + position: relative; | ||
| 2419 | + overflow: hidden; | ||
| 2420 | +} | ||
| 2421 | +.bar-seg { | ||
| 2422 | + position: absolute; | ||
| 2423 | + top: 0; | ||
| 2424 | + height: 100%; | ||
| 2425 | + border-radius: 0 2px 2px 0; | ||
| 2426 | +} | ||
| 2427 | +.seg-g { background: #67c23a; } | ||
| 2428 | +.seg-y { background: #e6a23c; } | ||
| 2429 | +.seg-r { background: #f56c6c; } | ||
| 2430 | +.seg-gy { background: #909399; } | ||
| 2431 | + | ||
| 2432 | +/* Canvas甘特图 */ | ||
| 2433 | +.gantt-canvas { | ||
| 2434 | + width: 100%; | ||
| 2435 | + display: block; | ||
| 2436 | +} | ||
| 2437 | + | ||
| 2438 | +/* Hover Tooltip */ | ||
| 2439 | +.gantt-tooltip { | ||
| 2440 | + position: absolute; | ||
| 2441 | + z-index: 10; | ||
| 2442 | + background: rgba(32, 40, 51, 0.92); | ||
| 2443 | + color: #fff; | ||
| 2444 | + padding: 8px 12px; | ||
| 2445 | + border-radius: 4px; | ||
| 2446 | + font-size: 12px; | ||
| 2447 | + line-height: 1.6; | ||
| 2448 | + pointer-events: none; | ||
| 2449 | + white-space: nowrap; | ||
| 2450 | + box-shadow: 0 2px 12px rgba(0,0,0,0.2); | ||
| 2451 | +} | ||
| 2452 | +.gtt-row { | ||
| 2453 | + display: flex; | ||
| 2454 | + align-items: center; | ||
| 2455 | + gap: 5px; | ||
| 2456 | +} | ||
| 2457 | +.gtt-dot { | ||
| 2458 | + display: inline-block; | ||
| 2459 | + width: 9px; | ||
| 2460 | + height: 9px; | ||
| 2461 | + border-radius: 2px; | ||
| 2462 | + flex-shrink: 0; | ||
| 2463 | +} | ||
| 2464 | +.gtt-sub { | ||
| 2465 | + font-size: 11px; | ||
| 2466 | + color: #bbb; | ||
| 2467 | +} | ||
| 2468 | + | ||
| 2469 | +/* ========== 稼动率 (Canvas) ========== */ | ||
| 2470 | +.util-view { | ||
| 2471 | + background: #f5f7fa; | ||
| 2472 | + overflow-y: auto; | ||
| 2473 | +} | ||
| 2474 | +.util-toolbar { | ||
| 2475 | + background: #fff; | ||
| 2476 | + padding: 10px 20px; | ||
| 2477 | + display: flex; | ||
| 2478 | + align-items: center; | ||
| 2479 | + gap: 10px; | ||
| 2480 | + border-bottom: 1px solid #e8e8e8; | ||
| 2481 | +} | ||
| 2482 | +.util-label { | ||
| 2483 | + font-size: 13px; color: #666; font-weight: bold; | ||
| 2484 | +} | ||
| 2485 | +.util-top-charts { | ||
| 2486 | + display: grid; | ||
| 2487 | + grid-template-columns: repeat(4, 1fr); | ||
| 2488 | + gap: 14px; | ||
| 2489 | + padding: 14px 20px; | ||
| 2490 | +} | ||
| 2491 | +.pie-card, .bar-card { | ||
| 2492 | + background: #fff; | ||
| 2493 | + border-radius: 6px; | ||
| 2494 | + box-shadow: 0 1px 4px rgba(0,0,0,0.06); | ||
| 2495 | + padding: 12px 14px; | ||
| 2496 | + display: flex; | ||
| 2497 | + flex-direction: column; | ||
| 2498 | + align-items: center; | ||
| 2499 | + position: relative; | ||
| 2500 | +} | ||
| 2501 | +.pie-title { | ||
| 2502 | + font-size: 13px; | ||
| 2503 | + font-weight: bold; | ||
| 2504 | + color: #333; | ||
| 2505 | + margin-bottom: 6px; | ||
| 2506 | + flex-shrink: 0; | ||
| 2507 | + align-self: flex-start; | ||
| 2508 | + width: 100%; | ||
| 2509 | +} | ||
| 2510 | +/* Canvas 饼图 */ | ||
| 2511 | +.util-canvas-pie { | ||
| 2512 | + width: 320px; | ||
| 2513 | + height: 280px; | ||
| 2514 | + display: block; | ||
| 2515 | +} | ||
| 2516 | + | ||
| 2517 | +/* 饼图加载转圈 */ | ||
| 2518 | +.util-loading-overlay { | ||
| 2519 | + position: absolute; | ||
| 2520 | + top: 0; left: 0; right: 0; bottom: 0; | ||
| 2521 | + background: rgba(255,255,255,0.85); | ||
| 2522 | + display: flex; | ||
| 2523 | + align-items: center; | ||
| 2524 | + justify-content: center; | ||
| 2525 | + z-index: 5; | ||
| 2526 | + border-radius: 6px; | ||
| 2527 | +} | ||
| 2528 | +.util-spinner { | ||
| 2529 | + width: 32px; height: 32px; | ||
| 2530 | + border: 3px solid #e0e0e0; | ||
| 2531 | + border-top-color: #409eff; | ||
| 2532 | + border-radius: 50%; | ||
| 2533 | + animation: utilSpin 0.8s linear infinite; | ||
| 2534 | +} | ||
| 2535 | +@keyframes utilSpin { | ||
| 2536 | + to { transform: rotate(360deg); } | ||
| 2537 | +} | ||
| 2538 | +.util-total-leg { | ||
| 2539 | + margin-top: 4px; | ||
| 2540 | + display: flex; | ||
| 2541 | + flex-wrap: wrap; | ||
| 2542 | + gap: 8px; | ||
| 2543 | + font-size: 11px; | ||
| 2544 | + color: #666; | ||
| 2545 | + line-height: 1.5; | ||
| 2546 | +} | ||
| 2547 | +.util-total-leg .leg-sub { color: #999; font-size: 10px; } | ||
| 2548 | +.pie-legend { | ||
| 2549 | + margin-top: 6px; | ||
| 2550 | + display: flex; | ||
| 2551 | + gap: 8px; | ||
| 2552 | + font-size: 16px; | ||
| 2553 | + color: #666; | ||
| 2554 | + line-height: 1.5; | ||
| 2555 | + flex-wrap: wrap; | ||
| 2556 | +} | ||
| 2557 | +.center-leg { justify-content: center; } | ||
| 2558 | +.leg-item { | ||
| 2559 | + display: inline-flex; | ||
| 2560 | + align-items: center; | ||
| 2561 | + gap: 3px; | ||
| 2562 | + cursor: pointer; | ||
| 2563 | + transition: background 0.15s; | ||
| 2564 | + border-radius: 3px; | ||
| 2565 | + padding: 1px 4px; | ||
| 2566 | +} | ||
| 2567 | +.leg-item:hover { background: rgba(64,158,255,0.06); } | ||
| 2568 | +.dot { display: inline-block; width: 9px; height: 9px; border-radius: 2px; flex-shrink: 0; } | ||
| 2569 | +.dot.g { background: #67c23a; } | ||
| 2570 | +.dot.y { background: #e6a23c; } | ||
| 2571 | +.dot.r { background: #f56c6c; } | ||
| 2572 | +.dot.gy { background: #909399; } | ||
| 2573 | + | ||
| 2574 | +/* 异常机台排名 Canvas */ | ||
| 2575 | +.util-canvas-abnormal { | ||
| 2576 | + flex: 1; | ||
| 2577 | + min-height: 280px; | ||
| 2578 | + width: 100%; | ||
| 2579 | + display: block; | ||
| 2580 | +} | ||
| 2581 | +.abn-footer { | ||
| 2582 | + margin-top: 6px; | ||
| 2583 | + font-size: 10px; | ||
| 2584 | + color: #bbb; | ||
| 2585 | + text-align: right; | ||
| 2586 | + white-space: nowrap; | ||
| 2587 | + overflow: hidden; | ||
| 2588 | + text-overflow: ellipsis; | ||
| 2589 | +} | ||
| 2590 | +.abn-legend { | ||
| 2591 | + margin-top: 8px; | ||
| 2592 | + font-size: 15px; | ||
| 2593 | + color: #555; | ||
| 2594 | + display: flex; | ||
| 2595 | + gap: 16px; | ||
| 2596 | + justify-content: center; | ||
| 2597 | + padding: 6px 0; | ||
| 2598 | +} | ||
| 2599 | +.abn-legend .dot { width: 14px; height: 14px; border-radius: 3px; } | ||
| 2600 | + | ||
| 2601 | +/* 堆叠柱状图 Canvas */ | ||
| 2602 | +.util-bottom-chart { | ||
| 2603 | + margin: 0 20px 14px; | ||
| 2604 | + background: #fff; | ||
| 2605 | + border-radius: 6px; | ||
| 2606 | + box-shadow: 0 1px 4px rgba(0,0,0,0.06); | ||
| 2607 | + padding: 14px; | ||
| 2608 | +} | ||
| 2609 | +.stack-bar-toolbar { | ||
| 2610 | + display: flex; | ||
| 2611 | + align-items: center; | ||
| 2612 | + gap: 10px; | ||
| 2613 | + margin-bottom: 10px; | ||
| 2614 | + font-size: 13px; | ||
| 2615 | + color: #666; | ||
| 2616 | +} | ||
| 2617 | +.stack-bar-legend { | ||
| 2618 | + display: flex; | ||
| 2619 | + gap: 18px; | ||
| 2620 | + margin-bottom: 8px; | ||
| 2621 | + font-size: 12px; | ||
| 2622 | + color: #666; | ||
| 2623 | +} | ||
| 2624 | +.stack-bar-canvas-wrap { | ||
| 2625 | + overflow-x: auto; | ||
| 2626 | +} | ||
| 2627 | +.util-stack-canvas { | ||
| 2628 | + width: 100%; | ||
| 2629 | + height: auto; | ||
| 2630 | + display: block; | ||
| 2631 | +} | ||
| 2632 | + | ||
| 2633 | +/* 统一 Tooltip 样式 - 使用 fixed 定位,坐标基于页面 */ | ||
| 2634 | +.util-tooltip { | ||
| 2635 | + position: fixed; | ||
| 2636 | + z-index: 1000; | ||
| 2637 | + background: rgba(32,40,51,0.92); | ||
| 2638 | + color: #fff; | ||
| 2639 | + padding: 8px 12px; | ||
| 2640 | + border-radius: 4px; | ||
| 2641 | + font-size: 12px; | ||
| 2642 | + line-height: 1.7; | ||
| 2643 | + pointer-events: none; | ||
| 2644 | + box-shadow: 0 2px 12px rgba(0,0,0,0.2); | ||
| 2645 | + max-width: 260px; | ||
| 2646 | +} | ||
| 2647 | +.utip-wide { max-width: 300px; } | ||
| 2648 | +.utip-title { | ||
| 2649 | + font-weight: bold; | ||
| 2650 | + font-size: 13px; | ||
| 2651 | + margin-bottom: 4px; | ||
| 2652 | + border-bottom: 1px solid rgba(255,255,255,0.2); | ||
| 2653 | + padding-bottom: 4px; | ||
| 2654 | +} | ||
| 2655 | +.utip-line { | ||
| 2656 | + display: flex; | ||
| 2657 | + align-items: center; | ||
| 2658 | + gap: 5px; | ||
| 2659 | +} | ||
| 2660 | +.utip-line i { | ||
| 2661 | + display: inline-block; | ||
| 2662 | + width: 8px; | ||
| 2663 | + height: 8px; | ||
| 2664 | + border-radius: 2px; | ||
| 2665 | + flex-shrink: 0; | ||
| 2666 | +} | ||
| 2667 | + | ||
| 2668 | +/* ========== 开机率 ========== */ | ||
| 2669 | +.startup-view { | ||
| 2670 | + background: #f5f7fa; | ||
| 2671 | + overflow-y: auto; | ||
| 2672 | +} | ||
| 2673 | +.startup-toolbar { | ||
| 2674 | + background: #fff; | ||
| 2675 | + padding: 10px 20px; | ||
| 2676 | + display: flex; | ||
| 2677 | + align-items: center; | ||
| 2678 | + gap: 10px; | ||
| 2679 | + border-bottom: 1px solid #e8e8e8; | ||
| 2680 | +} | ||
| 2681 | +.startup-label { | ||
| 2682 | + font-size: 13px; color: #666; font-weight: bold; | ||
| 2683 | +} | ||
| 2684 | +.startup-summary { | ||
| 2685 | + background: #fff; | ||
| 2686 | + margin: 12px 20px 0; | ||
| 2687 | + padding: 10px 16px; | ||
| 2688 | + border-radius: 4px; | ||
| 2689 | + box-shadow: 0 1px 3px rgba(0,0,0,0.05); | ||
| 2690 | + display: flex; | ||
| 2691 | + gap: 24px; | ||
| 2692 | + font-size: 12px; | ||
| 2693 | + color: #666; | ||
| 2694 | + flex-wrap: wrap; | ||
| 2695 | +} | ||
| 2696 | +.startup-summary b { color: #333; } | ||
| 2697 | +.startup-legend { | ||
| 2698 | + padding: 10px 24px 0; | ||
| 2699 | + font-size: 13px; | ||
| 2700 | + color: #666; | ||
| 2701 | + display: flex; | ||
| 2702 | + align-items: center; | ||
| 2703 | + gap: 6px; | ||
| 2704 | +} | ||
| 2705 | +.startup-chart { | ||
| 2706 | + margin: 0 20px 20px; | ||
| 2707 | + background: #fff; | ||
| 2708 | + border-radius: 6px; | ||
| 2709 | + box-shadow: 0 1px 4px rgba(0,0,0,0.06); | ||
| 2710 | + padding: 14px; | ||
| 2711 | + min-height: 340px; | ||
| 2712 | +} | ||
| 2713 | +.startup-canvas { | ||
| 2714 | + width: 100%; | ||
| 2715 | + height: auto; | ||
| 2716 | + display: block; | ||
| 2717 | +} | ||
| 2718 | +</style> |
vite.config.js
0 → 100644
| 1 | +import { fileURLToPath, URL } from 'node:url' | ||
| 2 | + | ||
| 3 | +import { defineConfig } from 'vite' | ||
| 4 | +import vue from '@vitejs/plugin-vue' | ||
| 5 | +import vueDevTools from 'vite-plugin-vue-devtools' | ||
| 6 | + | ||
| 7 | +// https://vite.dev/config/ | ||
| 8 | +export default defineConfig({ | ||
| 9 | + plugins: [ | ||
| 10 | + vue(), | ||
| 11 | + vueDevTools(), | ||
| 12 | + ], | ||
| 13 | + server: { | ||
| 14 | + proxy: { | ||
| 15 | + '/api': { | ||
| 16 | + target: 'http://127.0.0.1:8080', | ||
| 17 | + changeOrigin: true, | ||
| 18 | + rewrite: (path) => path.replace(/^\/api/, ''), | ||
| 19 | + }, | ||
| 20 | + }, | ||
| 21 | + }, | ||
| 22 | + resolve: { | ||
| 23 | + alias: { | ||
| 24 | + '@': fileURLToPath(new URL('./src', import.meta.url)) | ||
| 25 | + }, | ||
| 26 | + }, | ||
| 27 | +}) |