Commit f80a2a45f25f09cf88f2400a9ebfef25ac04ed3d

Authored by 杨鸣坤
0 parents

chore: 初始化项目

  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
... ...
  1 +{
  2 + "recommendations": ["Vue.volar"]
  3 +}
... ...
  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 +```
... ...
  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>
... ...
  1 +{
  2 + "compilerOptions": {
  3 + "paths": {
  4 + "@/*": ["./src/*"]
  5 + }
  6 + },
  7 + "exclude": ["node_modules", "dist"]
  8 +}
... ...
  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 +}
... ...
  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 +}
... ...
No preview for this file type
  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>
... ...
  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 +}
... ...
  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>
... ...
  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 +}
... ...
  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>
... ...
  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>&nbsp;
  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>&nbsp;
  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>
... ...
  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>
... ...
  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>
... ...
  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 &nbsp; B相 120 &nbsp; 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">视在功率 &nbsp;&nbsp; {{ 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>
... ...
  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>
... ...
  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>
... ...
  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>
... ...
  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 + &nbsp;({{ 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 + &nbsp;({{ 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>
... ...
  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>
... ...
  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>
... ...
  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>
... ...
  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>
... ...
  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>
... ...
  1 +<template>
  2 + <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
  3 + <path
  4 + d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
  5 + />
  6 + </svg>
  7 +</template>
... ...
  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>
... ...
  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')
... ...
  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
... ...
  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>待机 &nbsp; <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>
... ...
  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()">&lt;</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()">&gt;</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>黄灯 &nbsp; <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>
... ...
  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 +})
... ...