From dc740e80270cd3e7a0a79373938abfae33164004 Mon Sep 17 00:00:00 2001 From: leon_mac Date: Sun, 18 Oct 2026 15:11:44 +0800 Subject: [PATCH] =?UTF-8?q?chore(=E7=AE=A1=E7=90=86=E7=AB=AF):=20=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96=E5=B7=A5=E7=A8=8B=E4=BB=93=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 3 + .gitignore | 10 + Dockerfile | 9 + index.html | 1 + nginx.conf | 28 + package-lock.json | 1805 ++++++++++++++++++++++++++ package.json | 25 + src/App.vue | 16 + src/api.ts | 7 + src/auth.ts | 5 + src/components/CallNotifications.vue | 18 + src/contract.ts | 9 + src/main.ts | 9 + src/notifications/core.ts | 51 + src/notifications/realtime.ts | 46 + src/notifications/speech.ts | 22 + src/notifications/store.ts | 62 + src/style.css | 3 + src/views/Login.vue | 6 + src/views/Patterns.vue | 8 + src/views/Settings.vue | 9 + src/views/Tasks.vue | 10 + tests/api.test.ts | 5 + tests/client.test.ts | 8 + tests/notifications.test.ts | 16 + tests/realtime.test.ts | 11 + tsconfig.json | 1 + vite.config.ts | 3 + 28 files changed, 2206 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 index.html create mode 100644 nginx.conf create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/App.vue create mode 100644 src/api.ts create mode 100644 src/auth.ts create mode 100644 src/components/CallNotifications.vue create mode 100644 src/contract.ts create mode 100644 src/main.ts create mode 100644 src/notifications/core.ts create mode 100644 src/notifications/realtime.ts create mode 100644 src/notifications/speech.ts create mode 100644 src/notifications/store.ts create mode 100644 src/style.css create mode 100644 src/views/Login.vue create mode 100644 src/views/Patterns.vue create mode 100644 src/views/Settings.vue create mode 100644 src/views/Tasks.vue create mode 100644 tests/api.test.ts create mode 100644 tests/client.test.ts create mode 100644 tests/notifications.test.ts create mode 100644 tests/realtime.test.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..54850f3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +dist +.env* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f210c28 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +node_modules/ +dist/ +.env +.env.local +.env.*.local +!.env.example +*.log +*.tsbuildinfo +coverage/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..13ffe39 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM node:24-alpine AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +RUN npm run build +FROM nginx:1.28-alpine +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/index.html b/index.html new file mode 100644 index 0000000..6fd9cae --- /dev/null +++ b/index.html @@ -0,0 +1 @@ +豆间 · 门店工作台
diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..2943e6d --- /dev/null +++ b/nginx.conf @@ -0,0 +1,28 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + add_header X-Content-Type-Options nosniff always; + add_header X-Frame-Options DENY always; + # WS protocol upgrade; heartbeat every 20s keeps the tunnel live. + location = /api/ws { + proxy_pass http://server:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 75s; + proxy_send_timeout 10s; + proxy_buffering off; + } + location /api/ { + proxy_pass http://server:8080; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + client_max_body_size 4m; + } + location / { try_files $uri $uri/ /index.html; } + location ~ /\. { deny all; } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1a2a500 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1805 @@ +{ + "name": "perler-beads-webui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "perler-beads-webui", + "version": "0.1.0", + "dependencies": { + "@element-plus/icons-vue": "2.3.2", + "element-plus": "2.14.7", + "pinia": "4.0.3", + "vue": "3.5.43", + "vue-router": "5.3.1" + }, + "devDependencies": { + "@vitejs/plugin-vue": "6.0.9", + "typescript": "5.9.3", + "vite": "8.3.2", + "vue-tsc": "3.3.12" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.1.tgz", + "integrity": "sha512-Vh5uy9Y1JBMId6J1f68TMXnZrTCvNdo9Fu2k0d1YnJngR+sM5kWTKl441c/3TFs+cG0NWOjRWgQ/iaWhbYx+7A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", + "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.9.tgz", + "integrity": "sha512-rD/MORlhaZMlXWW0rEn4FB4wMinWC0z/D7Ye160S5+Rs1mC8ZDAdcDp4SjKfUhG5t8q1ktcPVw4xuTkPRlzrSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue-macros/common": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.4.tgz", + "integrity": "sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-sfc": "^3.5.22", + "ast-kit": "^2.1.2", + "local-pkg": "^1.1.2", + "magic-string-ast": "^1.0.2", + "unplugin-utils": "^0.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/vue-macros" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.2.25" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.43.tgz", + "integrity": "sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.43", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.43.tgz", + "integrity": "sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.43.tgz", + "integrity": "sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.43", + "@vue/compiler-dom": "3.5.43", + "@vue/compiler-ssr": "3.5.43", + "@vue/shared": "3.5.43", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.28", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.43.tgz", + "integrity": "sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/devtools-api": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz", + "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/devtools-kit": "^8.2.1" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz", + "integrity": "sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.2.1", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz", + "integrity": "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.12.tgz", + "integrity": "sha512-k7uIKR0n4CBidk233y7cac9osnDQ5WRidTl6nsrNvpZ3nf8cx9iJxRVsRzVsoPRHbowT/iKgGMTS9RGov3jeIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.2.1", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.43.tgz", + "integrity": "sha512-G/c9GyOZNI2jVaaS6OX1EF1SSFSv7H0ERqNTl4+DTFMlZmB5eVAB53aLQNam/7NL2NPtaDD7RdVrzf8uJzMuOA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.43.tgz", + "integrity": "sha512-hU6U6VnVhBGQDpvlnnDlIB8ZGJBiOcgk2lh/0InltHiz3D8oSkluvuvY+do1G2H3+udeKFsmaBlgVYP7gXQzEw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.43.tgz", + "integrity": "sha512-Bb2Jc0YjjJdMt1SJmb9b2L/IWd3I8lIT9x9eS/xvvP9CiVgna0ffua74xKRmt4/uSJ+0r4iN8ex1jrqkhQGWQw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.43", + "@vue/runtime-core": "3.5.43", + "@vue/shared": "3.5.43", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.43.tgz", + "integrity": "sha512-l2Ygjv9NehV94PSBxNWsAHC0j/eIIKbn92mBuWXAPGLnn6HfJ6MH5ubsd+Nk0YoZ5FRuxWI1P2VSoh+dbPQhCQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.43", + "@vue/runtime-dom": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.43.tgz", + "integrity": "sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-15.0.0.tgz", + "integrity": "sha512-Zv67h5mrk7YHDJsRObx+DXaCBz6RKBUpxqLesJ+3xZNPG6yuQs7ja7kg3veFrrouYLWAqydQ7CwGEbLVxB43Ng==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "15.0.0", + "@vueuse/shared": "15.0.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-15.0.0.tgz", + "integrity": "sha512-sfWE5LADAKJ0KnjtGliMdSN8Ij/L7uP6rXIZlgPloAI4zvvefbw6H5Lt7KJA6RqQOYgVqWF5Guj50HHMIzygBg==", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-15.0.0.tgz", + "integrity": "sha512-vbNXhGrbV53xPOMSyOzMHQRvhMgZf7oNDw2RChUetVxwTIEkInQ1joZ9JbT0G3z2n2AJNjvmCWeM9ySYrnvogA==", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/alien-signals": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/ast-kit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", + "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/ast-walker-scope": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.9.0.tgz", + "integrity": "sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@babel/types": "^7.29.0", + "ast-kit": "^2.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/element-plus": { + "version": "2.14.7", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.14.7.tgz", + "integrity": "sha512-lrG7pZyCS+mpCNVpzxxyu5r35+T6V0hzkAeIwn7HA6to772XSTHKuYCZG51OfzlqaxGbNegLxs650zAF1j43gA==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.1", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.8.0", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "15.0.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.20", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.3.11" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/exsolve": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", + "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "devOptional": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/local-pkg": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz", + "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magic-string-ast": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz", + "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==", + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.19" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/nostics": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/nostics/-/nostics-1.3.0.tgz", + "integrity": "sha512-wzRsrJwNlFbqIRQt8aCeL2WE8USip/uuvGSOZV1D02CxX8ABTMbw8IywKjH7yWHETMXbWJI4OBpihg9rmz2Vyg==", + "license": "MIT" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-4.0.3.tgz", + "integrity": "sha512-XMQqpvjgG7LMqVhhFUzKLT4KEbsYbfZZ0CZU9PgdFm1O2VKBmIkykL8+SfNhOaT7sR0wT6BEgKT0YNDYWgmVRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "nostics": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/devtools-api": "^8.1.5", + "typescript": ">=5.6.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "@vue/devtools-api": { + "optional": false + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.3.tgz", + "integrity": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==", + "license": "MIT", + "dependencies": { + "confbox": "^0.3.1", + "exsolve": "^1.1.1", + "pathe": "^2.0.3" + } + }, + "node_modules/pkg-types/node_modules/confbox": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz", + "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rolldown": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", + "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.152.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "license": "MIT" + }, + "node_modules/unplugin": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.4.0.tgz", + "integrity": "sha512-9skdIFlCsPdFV7wUfZxNsFInlW+7nJmGu2gkTu0OUhF56aXGsHab9x52/QhdJ4lC7ZDPWTxbiC4ANqVlsuaW3w==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.7", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@farmfe/core": "*", + "@rsbuild/core": "*", + "@rspack/core": "*", + "bun-types-no-globals": "*", + "esbuild": "*", + "rolldown": "*", + "rollup": "*", + "unloader": "*", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "@farmfe/core": { + "optional": true + }, + "@rsbuild/core": { + "optional": true + }, + "@rspack/core": { + "optional": true + }, + "bun-types-no-globals": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "unloader": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/unplugin-utils": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.2.tgz", + "integrity": "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==", + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/vite": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz", + "integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.11", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.43.tgz", + "integrity": "sha512-o5qZoksdnjIKvW1srZ3ab7pcDNYAerBjRe54D0LBLfRdCYFrSgBHVXokMas35czQc0//lmx4/tuY4ZNQ+Rf2Ng==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.43", + "@vue/compiler-sfc": "3.5.43", + "@vue/runtime-dom": "3.5.43", + "@vue/server-renderer": "3.5.43", + "@vue/shared": "3.5.43" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.12.tgz", + "integrity": "sha512-HwCqmgTfLFskQc4P5TRnbduCMyPat2Ezu0elLWOGn0dD/6BKHP15B5bxce6aDi+E4kblPtru8amIBQzu0Di3qQ==", + "license": "MIT" + }, + "node_modules/vue-router": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.3.1.tgz", + "integrity": "sha512-GDBZzgmILxA/kFnkFbJjQZdZ2QQbngnIMMuoUcjhZIfH1RGMaPjPwX5ASnV38qamuA9uhO0RDjSBHTDNG2uXyQ==", + "license": "MIT", + "dependencies": { + "@vue-macros/common": "^3.1.3", + "@vue/devtools-api": "^8.1.5", + "ast-walker-scope": "^0.9.0", + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "local-pkg": "^1.2.1", + "magic-string": "^0.30.21", + "mlly": "^1.8.2", + "muggle-string": "^0.4.1", + "nostics": "^1.1.4", + "pathe": "^2.0.3", + "picomatch": "^4.0.5", + "scule": "^1.3.0", + "tinyglobby": "^0.2.17", + "unplugin": "^3.3.0", + "unplugin-utils": "^0.3.2" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@pinia/colada": ">=0.21.2", + "@vue/compiler-sfc": "^3.5.34 || ^4.0.0", + "pinia": "^3.0.4 || ^4.0.2", + "vite": "^7.3.0 || ^8.0.0", + "vue": "^3.5.34 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@pinia/colada": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "pinia": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vue-tsc": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.12.tgz", + "integrity": "sha512-kbpaz18O9jPVkXVHD1BEjpUXM6WhXSu+62L1sGGr6Wbzd8GbvkBQWjoj3KZ/C0aq2MZllce7UdtWzwCIYsBMGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.3.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9602aa5 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "perler-beads-webui", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1 --port 5174", + "build": "vue-tsc --noEmit && vite build", + "typecheck": "vue-tsc --noEmit", + "test": "node --experimental-strip-types --test tests/*.test.ts" + }, + "dependencies": { + "@element-plus/icons-vue": "2.3.2", + "element-plus": "2.14.7", + "pinia": "4.0.3", + "vue": "3.5.43", + "vue-router": "5.3.1" + }, + "devDependencies": { + "@vitejs/plugin-vue": "6.0.9", + "typescript": "5.9.3", + "vite": "8.3.2", + "vue-tsc": "3.3.12" + } +} diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..36ed9ef --- /dev/null +++ b/src/App.vue @@ -0,0 +1,16 @@ + + diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..d61b4ae --- /dev/null +++ b/src/api.ts @@ -0,0 +1,7 @@ +import { unwrap, ApiError } from './contract.ts' +export async function api(path:string,method='GET',data?:unknown,signal?:AbortSignal):Promise{ + let token=sessionStorage.getItem('perler-admin-token'); let response=await fetch('/api'+path,{method,headers:{'Content-Type':'application/json',...(token?{Authorization:'Bearer '+token}:{})},body:data===undefined?undefined:JSON.stringify(data),signal}); + if(!response.ok)throw new Error('服务连接失败,请稍后重试'); + try{return unwrap(await response.json())}catch(e){if(e instanceof ApiError&&e.code===403){sessionStorage.removeItem('perler-admin-token');window.dispatchEvent(new Event('auth-expired'))}throw e} +} +export async function downloadPrint(id:string){let response=await fetch('/api/patterns/'+encodeURIComponent(id)+'/print?pitchMm=5',{headers:{Authorization:'Bearer '+(sessionStorage.getItem('perler-admin-token')||'')}});if(!response.ok)throw new Error('下载连接失败');if(response.headers.get('content-type')?.includes('application/json')){unwrap(await response.json());throw new Error('未返回打印文件')}let blob=await response.blob();let url=URL.createObjectURL(blob);let a=document.createElement('a');a.href=url;a.download='pattern-'+id+'.svg';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000)} diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..e3903bd --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,5 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { api } from './api' +import type { User } from './contract' +export let useAuth=defineStore('auth',()=>{let user=ref(null);async function verify(){if(!sessionStorage.getItem('perler-admin-token'))return false; try{let current=await api('/me');if(!['admin','staff'].includes(current.role)||current.storeId!=='store-001'){logout();return false}user.value=current;return true}catch(e){user.value=null;throw e}}function logout(){user.value=null;sessionStorage.removeItem('perler-admin-token')}async function login(username:string,password:string){let result=await api<{token:string;user:User}>('/auth/admin','POST',{username,password});sessionStorage.setItem('perler-admin-token',result.token);await verify()}return{user,verify,logout,login}}) diff --git a/src/components/CallNotifications.vue b/src/components/CallNotifications.vue new file mode 100644 index 0000000..1b0f4e0 --- /dev/null +++ b/src/components/CallNotifications.vue @@ -0,0 +1,18 @@ + + diff --git a/src/contract.ts b/src/contract.ts new file mode 100644 index 0000000..bf64c97 --- /dev/null +++ b/src/contract.ts @@ -0,0 +1,9 @@ +export class ApiError extends Error { code: number; constructor(code:number,msg:string){super(msg);this.code=code} } +export function unwrap(body:{code:number;msg:string;data:T}):T { if(body.code!==200) throw new ApiError(body.code,body.msg||'请求失败'); return body.data } +export function nextStatus(status:string){return status==='pending'?'accepted':status==='accepted'?'completed':null} +export interface User {id:string;name:string;role:string;storeId:string} +export interface Color {code:string;name:string;hex:string;location:string;available:boolean} +export interface Pattern {id:string;title:string;category:string;width:number;height:number;cells:(string|null)[];colors:Color[];visibility:string;beadCount:number;demo?:boolean} +export interface Task {id:string;tableCode:string;kind:string;note:string;status:string;patternId?:string;createdAt:string;updatedAt:string} +export interface Store {id:string;name:string;address:string;hours:string;notice:string;localBaseUrl:string;publicBaseUrl:string} +export interface Table {code:string;label:string;enabled:boolean} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..dea5451 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,9 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import { createRouter, createWebHistory } from 'vue-router' +import { ElButton, ElIcon, ElInput, ElForm, ElFormItem, ElAlert, ElTable, ElTableColumn, ElSelect, ElOption, ElSwitch, ElSkeleton, ElTag, ElDrawer, ElEmpty } from 'element-plus' +import 'element-plus/dist/index.css' +import './style.css' +import App from './App.vue' +import {useAuth} from './auth' +let app=createApp(App);app.use(createPinia());let router=createRouter({history:createWebHistory(),routes:[{path:'/login',component:()=>import('./views/Login.vue')},{path:'/',redirect:'/tasks'},{path:'/tasks',component:()=>import('./views/Tasks.vue')},{path:'/patterns',component:()=>import('./views/Patterns.vue'),meta:{admin:true}},{path:'/palette',component:()=>import('./views/Settings.vue'),meta:{admin:true}},{path:'/tables',component:()=>import('./views/Settings.vue'),meta:{admin:true}},{path:'/store',component:()=>import('./views/Settings.vue'),meta:{admin:true}},{path:'/:pathMatch(.*)*',redirect:'/tasks'}]});router.beforeEach(async to=>{let auth=useAuth();if(to.path==='/login')return true;try{if(!await auth.verify())return'/login';if(to.meta.admin&&auth.user?.role!=='admin')return'/tasks'}catch{return'/login'}});window.addEventListener('auth-expired',()=>{useAuth().logout();void router.replace('/login')});app.use(router);for(let component of [ElButton,ElIcon,ElInput,ElForm,ElFormItem,ElAlert,ElTable,ElTableColumn,ElSelect,ElOption,ElSwitch,ElSkeleton,ElTag,ElDrawer,ElEmpty])app.use(component);app.mount('#app') diff --git a/src/notifications/core.ts b/src/notifications/core.ts new file mode 100644 index 0000000..d39ce06 --- /dev/null +++ b/src/notifications/core.ts @@ -0,0 +1,51 @@ +import type { Task } from '../contract.ts' + +export function announcement(task:Task){ + let kinds:Record={help:'呼叫员工',refill:'需要补充豆料',ironing:'需要熨烫作品',print:'需要打印图纸'} + return `${task.tableCode.slice(0,40)} 桌,${kinds[task.kind]||'需要服务'}。` +} +export function selectVoice(voices:T[]):T|undefined{ + let local=voices.filter(v=>v.localService&&/^zh(?:[-_]|$)/i.test(v.lang)) + return local.find(v=>/^zh[-_]CN$/i.test(v.lang))||local[0] +} +export class CallTracker { + private seen=new Set() + private initialized=false + accept(tasks:Task[]){ + let fresh=tasks.filter(t=>!this.seen.has(t.id)&&t.status==='pending') + for(let task of tasks)this.seen.add(task.id) + if(!this.initialized){this.initialized=true;return []} + return fresh.sort((a,b)=>Date.parse(a.createdAt)-Date.parse(b.createdAt)) + } + reset(){this.seen.clear();this.initialized=false} +} +export interface SpeechDriver {speak(text:string,done:(error?:string)=>void):void;cancel():void} +interface SpeechJob {id?:string;text:string;done?:()=>void} +export class SpeechQueue { + private driver:SpeechDriver + private onError:(error:string)=>void + private waiting:SpeechJob[]=[] + private current:SpeechJob|undefined + private generation=0 + constructor(driver:SpeechDriver,onError:(error:string)=>void){this.driver=driver;this.onError=onError} + add(tasks:Task[]){ + if(this.waiting.length+tasks.length>32){this.stop();this.onError('待播报呼叫较多,请先查看服务队列,再重新开启语音。');return} + this.waiting.push(...tasks.map(t=>({id:t.id,text:announcement(t)})));this.next() + } + preview(text:string,done:()=>void){this.waiting.push({text,done});this.next()} + retain(ids:Set){ + this.waiting=this.waiting.filter(t=>!t.id||ids.has(t.id)) + if(this.current?.id&&!ids.has(this.current.id)){this.generation++;this.current=undefined;this.driver.cancel();this.next()} + } + stop(){this.generation++;this.waiting=[];this.current=undefined;this.driver.cancel()} + private next(){ + if(this.current||!this.waiting.length)return + this.current=this.waiting.shift();let job=this.current!,generation=this.generation + try{this.driver.speak(job.text,error=>{ + if(generation!==this.generation)return + this.current=undefined + if(error){this.stop();this.onError(error);return} + job.done?.();this.next() + })}catch{this.stop();this.onError('语音播放失败,请检查浏览器后重新开启。')} + } +} diff --git a/src/notifications/realtime.ts b/src/notifications/realtime.ts new file mode 100644 index 0000000..daccf18 --- /dev/null +++ b/src/notifications/realtime.ts @@ -0,0 +1,46 @@ +export type ConnectionState='connecting'|'online'|'retrying'|'closed' +interface SocketLike { + onopen:((event:any)=>void)|null;onmessage:((event:any)=>void)|null;onclose:((event:any)=>void)|null;onerror:((event:any)=>void)|null + send(data:string):void;close():void +} +export interface LinkOptions { + socket:()=>SocketLike;token:()=>string;state:(state:ConnectionState)=>void;sync:()=>void;expired:()=>void + later?:(fn:()=>void,ms:number)=>ReturnType;clear?:(timer:ReturnType)=>void +} +export class RealtimeLink { + private options:LinkOptions + private socket:SocketLike|undefined + private timer:ReturnType|undefined + private watchdog:ReturnType|undefined + private stopped=true + private attempts=0 + constructor(options:LinkOptions){this.options=options} + private later(fn:()=>void,ms:number){return (this.options.later||setTimeout)(fn,ms)} + private clear(timer:ReturnType|undefined){if(timer!==undefined)(this.options.clear||clearTimeout)(timer)} + start(){this.stop();this.stopped=false;this.attempts=0;this.connect()} + stop(){this.stopped=true;this.clear(this.timer);this.clear(this.watchdog);this.detach();this.options.state('closed')} + private detach(){if(this.socket){let socket=this.socket;this.socket=undefined;socket.onopen=null;socket.onmessage=null;socket.onclose=null;socket.onerror=null;socket.close()}} + private connect(){ + if(this.stopped)return + this.options.state(this.attempts?'retrying':'connecting') + let socket:SocketLike + try{socket=this.options.socket();this.socket=socket}catch{this.retry();return} + let ready=false + let arm=(ms:number)=>{this.clear(this.watchdog);this.watchdog=this.later(()=>{this.detach();this.retry()},ms)} + arm(10000) + socket.onopen=()=>socket.send(JSON.stringify({type:'auth',token:this.options.token()})) + socket.onmessage=event=>{ + let data:any;try{data=JSON.parse(event.data)}catch{return} + if(data.type==='ready'){ready=true;this.attempts=0;this.options.state('online');arm(45000);this.options.sync()} + else if(ready&&data.type==='heartbeat')arm(45000) + else if(ready&&data.type==='tasks.changed'){arm(45000);this.options.sync()} + } + socket.onerror=()=>socket.close() + socket.onclose=event=>{ + this.clear(this.watchdog);this.detach() + if(event.code===4403||event.code===4401){this.stop();this.options.expired();return} + this.retry() + } + } + private retry(){if(this.stopped)return;this.options.state('retrying');this.clear(this.watchdog);this.clear(this.timer);let delay=Math.min(30000,1000*2**Math.min(this.attempts++,5));this.timer=this.later(()=>this.connect(),delay)} +} diff --git a/src/notifications/speech.ts b/src/notifications/speech.ts new file mode 100644 index 0000000..85f53a7 --- /dev/null +++ b/src/notifications/speech.ts @@ -0,0 +1,22 @@ +import {selectVoice,type SpeechDriver} from './core' + +// 显式选择本机语音;浏览器提供的远程音色不自动启用。 +export function browserSpeech(onStart:(text:string)=>void):SpeechDriver { + let timer:ReturnType|undefined + let active:SpeechSynthesisUtterance|undefined + function cancel(){clearTimeout(timer);if(active){active.onend=null;active.onerror=null;active.onstart=null;active=undefined}window.speechSynthesis?.cancel()} + return {cancel,speak(text,done){ + let synth=window.speechSynthesis + let voice=synth&&selectVoice(synth.getVoices()) + if(!voice){done('没有可用的本机中文语音,请在操作系统安装中文语音后重新开启。');return} + let utterance=new SpeechSynthesisUtterance(text);active=utterance + utterance.voice=voice;utterance.lang=voice.lang;utterance.rate=1;utterance.volume=1 + let finished=false + function finish(error?:string){if(finished)return;finished=true;clearTimeout(timer);utterance.onend=null;utterance.onerror=null;utterance.onstart=null;active=undefined;done(error)} + utterance.onstart=()=>onStart(text) + utterance.onend=()=>finish() + utterance.onerror=e=>finish(e.error==='not-allowed'?'浏览器阻止了播放,请点击「开启语音」重试。':'语音播放失败,请检查系统语音与声音输出后重试。') + timer=setTimeout(()=>{finish('语音播放超时,请检查声音输出后重新开启。');synth.cancel()},20000) + synth.speak(utterance) + }} +} diff --git a/src/notifications/store.ts b/src/notifications/store.ts new file mode 100644 index 0000000..899ce1b --- /dev/null +++ b/src/notifications/store.ts @@ -0,0 +1,62 @@ +import {defineStore} from 'pinia' +import {ref} from 'vue' +import {api} from '../api' +import type {Task} from '../contract' +import {CallTracker,SpeechQueue,selectVoice} from './core' +import {browserSpeech} from './speech' +import {RealtimeLink,type ConnectionState} from './realtime' + +export let useCalls=defineStore('calls',()=>{ + let tasks=ref([]),loading=ref(true),error=ref(''),last=ref('') + let connection=ref('closed'),enabled=ref(false),arming=ref(false),voiceError=ref(''),lastSpoken=ref(''),voiceName=ref('') + let supported=ref(false),tracker=new CallTracker(),scope='',generation=0,dirty=false + let controller:AbortController|undefined,pending:Promise|undefined,timer:ReturnType|undefined + let driver=browserSpeech(text=>{lastSpoken.value=text}) + let queue=new SpeechQueue(driver,message=>{enabled.value=false;arming.value=false;voiceError.value=message}) + let link=new RealtimeLink({socket(){let url=new URL('/api/ws',location.href);url.protocol=location.protocol==='https:'?'wss:':'ws:';return new WebSocket(url)},token:()=>sessionStorage.getItem('perler-admin-token')||'',state:value=>{connection.value=value},sync:()=>{void refresh()},expired:()=>window.dispatchEvent(new Event('auth-expired'))}) + function voices(){supported.value='speechSynthesis' in window&&'SpeechSynthesisUtterance' in window;voiceName.value=supported.value?selectVoice(window.speechSynthesis.getVoices())?.name||'':''} + function mute(){enabled.value=false;arming.value=false;queue.stop()} + function enable(){ + voices();voiceError.value='' + if(!supported.value){voiceError.value='当前浏览器不支持语音,请使用支持语音合成的桌面浏览器。';return} + if(!voiceName.value){voiceError.value='没有可用的本机中文语音,请在操作系统安装中文语音后重试。';return} + arming.value=true;enabled.value=true;let epoch=generation + // 直接在点击事件中启动试听,满足浏览器对用户操作的要求。 + queue.preview('门店语音提醒已开启。',()=>{if(epoch===generation)arming.value=false}) + } + async function refresh():Promise{ + if(!scope)return + if(pending){dirty=true;return pending} + let epoch=generation,abort=new AbortController();controller=abort + let deadline=setTimeout(()=>abort.abort(),12000) + pending=(async()=>{try{ + let result=await api('/service-requests','GET',undefined,abort.signal) + if(epoch!==generation)return + tasks.value=result;error.value='';last.value=new Date().toLocaleTimeString('zh-CN') + let fresh=tracker.accept(result) + queue.retain(new Set(result.filter(t=>t.status==='pending').map(t=>t.id))) + if(enabled.value)queue.add(fresh) + }catch(e){if(epoch===generation)error.value=e instanceof DOMException&&e.name==='AbortError'?'同步超时,正在等待重试':e instanceof Error?e.message:'同步失败'}finally{ + clearTimeout(deadline) + if(epoch===generation){controller=undefined;pending=undefined;loading.value=false;if(dirty){dirty=false;void refresh()}} + }})() + return pending + } + function start(key:string){ + if(key===scope)return + stop();scope=key;voices();window.speechSynthesis?.addEventListener('voiceschanged',voices) + void refresh();link.start() + // WS 是主路径;低频补查覆盖断线、漏通知和代理异常。 + timer=setInterval(()=>{void refresh()},60000) + window.addEventListener('online',recover);document.addEventListener('visibilitychange',visible) + } + function recover(){if(!scope)return;if(connection.value!=='online')link.start();void refresh()} + function visible(){if(!document.hidden)recover()} + function stop(){ + scope='';generation++;link.stop();mute();controller?.abort();controller=undefined;pending=undefined;dirty=false + clearInterval(timer);window.removeEventListener('online',recover);document.removeEventListener('visibilitychange',visible) + window.speechSynthesis?.removeEventListener('voiceschanged',voices) + tracker.reset();tasks.value=[];loading.value=true;error.value='';last.value='';lastSpoken.value='';voiceError.value='' + } + return {tasks,loading,error,last,connection,enabled,arming,voiceError,lastSpoken,voiceName,supported,refresh,start,stop,enable,mute} +}) diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..a270fdd --- /dev/null +++ b/src/style.css @@ -0,0 +1,3 @@ +:root{font-family:"PingFang SC","Microsoft YaHei",sans-serif;color:#29352d;background:#f5f4ec;font-synthesis:none;--forest:#294b38;--muted:#677169;--line:#e3e6dc;--el-color-primary:#365a42;--el-color-primary-light-3:#64816c;--el-color-primary-light-5:#8ca28f;--el-color-primary-light-7:#c1cebf;--el-color-primary-light-9:#eef3ec;--el-border-radius-base:6px;--el-font-family:inherit;--el-text-color-primary:#29352d;--el-border-color:#dce1d7}*{box-sizing:border-box}body{margin:0}button,input,textarea{font:inherit}a{color:inherit;text-decoration:none}button:focus-visible,a:focus-visible,input:focus-visible{outline:3px solid #bd8739;outline-offset:3px}h1{font-size:1.85rem;letter-spacing:.02em;margin:.6rem 0}h2{font-size:1rem;margin:0}p{line-height:1.7}small,.muted{color:var(--muted)}.workspace{display:flex;min-height:100vh}.workspace aside{width:14.5rem;background:#243e30;color:#e8eee1;flex-shrink:0;padding:2rem 1.25rem;display:flex;flex-direction:column;position:fixed;height:100vh}.brand{display:flex;align-items:center;gap:.7rem;font-size:1.6rem;font-weight:700;letter-spacing:.1em}.brand small{display:block;font-size:.6rem;letter-spacing:.18em;color:inherit;opacity:.72;margin-top:.3rem}.bead-mark{font-size:2.6rem;color:#d7d6a0}.store-badge{margin:2.5rem 0 1.75rem;padding:1rem .6rem;border-top:1px solid #536250;border-bottom:1px solid #536250;font-size:.85rem}.store-badge small{display:block;margin:.5rem 0 0 1rem;color:#bfcbbc}.dot{display:inline-block;width:.45rem;height:.45rem;border-radius:50%;background:#d7d6a0;margin-right:.5rem}nav{display:grid;gap:.5rem}nav a{display:flex;align-items:center;gap:.8rem;padding:.85rem 1rem;border-radius:6px;font-size:.9rem;color:#cdd8c9}nav a:hover{background:#344d3c}nav a.router-link-active{background:#e6eadb;color:#294b38;font-weight:600}nav .el-icon{font-size:1.1rem}.sidebar-note{margin-top:auto;font-size:.85rem;line-height:1.9;color:#aebfa9;border-top:1px solid #536250;padding-top:1.5rem}.body{margin-left:14.5rem;width:calc(100% - 14.5rem);min-width:0}.topbar{height:4.5rem;padding:0 2rem;background:#fafbf5;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;font-size:.85rem}.separator{color:#a7aca1;margin:0 .75rem}.account{display:flex;align-items:center;gap:1rem}main#main{padding:2rem;min-height:calc(100vh - 8rem)}.page-heading{display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-bottom:1.75rem}.page-heading p{color:var(--muted);font-size:.9rem;margin:0}.eyebrow{font-size:.65rem;font-weight:700;letter-spacing:.18em;color:#6d7e65}.summary-strip{display:grid;grid-template-columns:repeat(3,1fr);background:#e8eddf;border:1px solid #dce2d3;border-radius:6px;margin-bottom:1.5rem}.summary-strip>div{padding:1.25rem 1.75rem;border-right:1px solid #d6decc;display:grid;grid-template-columns:1fr auto;gap:.6rem}.summary-strip>div:last-child{border:0}.summary-strip span{font-size:.9rem}.summary-strip strong{font-size:2rem;grid-row:span 2;font-weight:600}.summary-strip small{font-size:.7rem}.panel{background:#fffefa;border:1px solid var(--line);border-radius:6px;overflow:hidden}.panel-toolbar{min-height:4.5rem;display:flex;align-items:center;justify-content:space-between;padding:1.25rem 1.5rem;border-bottom:1px solid var(--line);gap:1rem}.panel-toolbar h2 small{font-weight:400;margin-left:.5rem}.sync{display:flex;gap:.5rem;align-items:center;font-size:.75rem}.sync span{color:var(--muted)}.filters{display:flex;gap:.75rem;padding:1.25rem 1.5rem}.filters .el-select{width:11rem;flex-shrink:0}.filters .el-input{max-width:18rem}.el-table{--el-table-header-bg-color:#f7f8f0;--el-table-row-hover-bg-color:#f4f6ed;--el-table-border-color:#edf0e7;font-size:.85rem}.el-table th.el-table__cell{font-weight:500;color:#65715f;height:3rem}.el-table td.el-table__cell{height:4.1rem}.table-code{display:inline-flex;background:#eef1e5;padding:.4rem .5rem;border-radius:4px;color:#405b3b}.status{display:inline-flex;padding:.3rem .65rem;border-radius:4px;font-size:.75rem;background:#eef0e9;color:#606d5a}.status.pending{background:#f8e9ca;color:#77561e}.status.accepted{background:#e5efdf;color:#3f6234}.status.completed{background:#e8eded;color:#526667}.table-footer{padding:1rem 1.5rem;border-top:1px solid var(--line);font-size:.75rem;display:flex;justify-content:space-between;gap:1rem;color:#687461;line-height:1.6}.notice{display:flex;align-items:center;gap:1rem;border:1px dashed #d6d8c7;padding:1.1rem 1.5rem;margin-top:1.5rem;border-radius:6px;font-size:.8rem}.notice span{flex:1;color:var(--muted)}.notice .el-button+.el-button{margin-left:0}footer{padding:1.25rem 2rem;color:#78816f;font-size:.7rem;display:flex;justify-content:space-between}footer span{letter-spacing:.15em}.el-skeleton{padding:1.5rem}.el-alert{margin:1rem 0}.search{max-width:18rem}.swatch{display:block;width:2rem;height:2rem;border-radius:4px;border:1px solid #90988b}.store-details{display:grid;grid-template-columns:9rem 1fr;gap:1.5rem;padding:1.5rem;margin:0;font-size:.9rem;line-height:1.7}.store-details dt{color:var(--muted)}.store-details dd{margin:0;overflow-wrap:anywhere}.login{min-height:100vh;display:grid;grid-template-columns:1fr 1fr}.login-story{background:#294b38;color:#f3f2df;padding:4rem;display:flex;flex-direction:column;justify-content:center}.login-story .brand{margin-bottom:auto}.login-story h1{font-family:"Songti SC",serif;font-size:2.6rem;font-weight:500;line-height:1.5}.login-story p{color:#cbd8c2;margin-bottom:3rem}.login-story>small{color:#cbd8c2}.pixel-flower{font-size:7rem;color:#d6cc8c;line-height:1;margin:2.5rem 0 1rem}.login-form{max-width:29rem;padding:3rem;width:100%;margin:auto}.login-form h2{font-size:1.4rem;margin:1rem 0}.login-form p{font-size:.85rem;margin-bottom:2rem}.login-submit{width:100%;margin:1rem 0 1.5rem;height:2.75rem}.login-form>small{font-size:.7rem;line-height:1.7;display:block}.mobile-menu{display:none!important}.skip{position:fixed;left:1rem;top:-5rem;z-index:99;background:white;padding:1rem}.skip:focus{top:1rem}@media(max-width:1024px){.workspace aside{width:12rem}.body{margin-left:12rem;width:calc(100% - 12rem)}main#main{padding:1.5rem}.notice{flex-wrap:wrap}.notice span{flex-basis:65%}.summary-strip>div{padding:1rem}.table-footer{flex-wrap:wrap}.login-story{padding:2.5rem}.login-story h1{font-size:2rem}}@media(max-width:768px){.workspace aside{transform:translateX(-100%);z-index:20;width:14.5rem}.workspace aside.open{transform:translateX(0);top:4.5rem;height:calc(100vh - 4.5rem)}.body{margin-left:0;width:100%}.mobile-menu{display:inline-flex!important}.topbar{padding:0 1rem;gap:.5rem;position:sticky;top:0;z-index:22}.topbar>span{font-size:.7rem}.account{gap:.25rem}.account>span{display:none}main#main{padding:1.25rem 1rem}.page-heading{align-items:flex-start}h1{font-size:1.55rem}.page-heading p{font-size:.8rem}.eyebrow{font-size:.55rem}.summary-strip>div{padding:.85rem .7rem;display:flex;flex-direction:column}.summary-strip strong{font-size:1.6rem}.summary-strip span{font-size:.75rem}.summary-strip small{font-size:.6rem}.filters{padding:1rem;flex-wrap:wrap}.filters .el-select{width:calc(50% - .4rem);flex-shrink:1}.filters .el-input{max-width:none}.panel-toolbar{padding:1rem;flex-wrap:wrap}.sync{font-size:.65rem}.notice{padding:1rem;gap:.75rem}.notice span{flex-basis:100%;line-height:1.7}.store-details{grid-template-columns:1fr;gap:.5rem}.store-details dd{margin-bottom:1rem}.login{grid-template-columns:1fr}.login-story{padding:2rem}.login-story .brand{margin:0}.pixel-flower{display:none}.login-story h1{font-size:1.6rem;margin:1.5rem 0 .5rem}.login-story p{margin-bottom:.75rem;font-size:.8rem}.login-form{padding:2rem}.search{max-width:none}footer{padding:1rem}.el-button{min-height:2.5rem}}@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}} + +.call-notifications{border:1px solid var(--line);border-radius:6px;background:#fffefa;padding:1rem 1.25rem;margin-bottom:1.75rem}.call-notification-row,.call-notification-title,.call-notification-actions{display:flex;align-items:center;gap:.65rem;flex-wrap:wrap}.call-notification-row{justify-content:space-between}.call-notification-title{font-size:.85rem}.call-notification-actions{font-size:.75rem;color:var(--muted)}.call-notification-actions .el-button+.el-button{margin-left:0}.connection-state{font-size:.7rem;color:#7d5a20;background:#faf0d9;padding:.3rem .5rem;border-radius:4px}.connection-state.online{color:#35583c;background:#e9f1e4}.call-notification-hint,.call-last-spoken{font-size:.75rem;color:var(--muted);line-height:1.7;margin:.6rem 0 0}.call-last-spoken{color:var(--forest)}@media(max-width:768px){.call-notifications{padding:1rem}.call-notification-actions{width:100%;margin-top:.5rem}} diff --git a/src/views/Login.vue b/src/views/Login.vue new file mode 100644 index 0000000..6804504 --- /dev/null +++ b/src/views/Login.vue @@ -0,0 +1,6 @@ + + diff --git a/src/views/Patterns.vue b/src/views/Patterns.vue new file mode 100644 index 0000000..e0bcc82 --- /dev/null +++ b/src/views/Patterns.vue @@ -0,0 +1,8 @@ + + diff --git a/src/views/Settings.vue b/src/views/Settings.vue new file mode 100644 index 0000000..8d750d0 --- /dev/null +++ b/src/views/Settings.vue @@ -0,0 +1,9 @@ + + diff --git a/src/views/Tasks.vue b/src/views/Tasks.vue new file mode 100644 index 0000000..3d2ee2e --- /dev/null +++ b/src/views/Tasks.vue @@ -0,0 +1,10 @@ + + diff --git a/tests/api.test.ts b/tests/api.test.ts new file mode 100644 index 0000000..2d8c1f8 --- /dev/null +++ b/tests/api.test.ts @@ -0,0 +1,5 @@ +import { test } from 'node:test' +import { strict as assert } from 'node:assert' +import { unwrap, nextStatus } from '../src/contract.ts' +test('HTTP 200 envelopes still reject unauthenticated and forbidden operations',()=>{assert.equal(unwrap({code:200,msg:'ok',data:3}),3); for(let code of [401,403,409]) assert.throws(()=>unwrap({code,msg:'拒绝',data:null}), /拒绝/)}) +test('task actions follow pending accepted completed sequence',()=>{assert.equal(nextStatus('pending'),'accepted');assert.equal(nextStatus('accepted'),'completed');assert.equal(nextStatus('completed'),null);assert.equal(nextStatus('cancelled'),null)}) diff --git a/tests/client.test.ts b/tests/client.test.ts new file mode 100644 index 0000000..58e8ca0 --- /dev/null +++ b/tests/client.test.ts @@ -0,0 +1,8 @@ +import {test} from 'node:test' +import {strict as assert} from 'node:assert' +import {api} from '../src/api.ts' +test('client uses centralized same-origin API, Bearer token, JSON body and invalidates expired sessions',async()=>{ + let stored:string|null='test-session',events:string[]=[];Object.defineProperty(globalThis,'sessionStorage',{value:{getItem:()=>stored,removeItem:()=>{stored=null}},configurable:true});Object.defineProperty(globalThis,'window',{value:{dispatchEvent:(event:Event)=>events.push(event.type)},configurable:true});let previous=globalThis.fetch;let calls:{url:string;init:RequestInit}[]=[]; + globalThis.fetch=async(url,init)=>{calls.push({url:String(url),init:init!});return new Response(JSON.stringify({code:200,msg:'ok',data:{status:'accepted'}}),{headers:{'Content-Type':'application/json'}})}; + try {assert.deepEqual(await api('/service-requests/task','PATCH',{status:'accepted'}),{status:'accepted'});assert.equal(calls[0]!.url,'/api/service-requests/task');assert.equal((calls[0]!.init.headers as Record).Authorization,'Bearer test-session');assert.equal(calls[0]!.init.body,'{"status":"accepted"}');globalThis.fetch=async()=>new Response(JSON.stringify({code:403,msg:'会话过期',data:null}));await assert.rejects(api('/me'),/会话过期/);assert.equal(stored,null);assert.deepEqual(events,['auth-expired'])}finally{globalThis.fetch=previous} +}) diff --git a/tests/notifications.test.ts b/tests/notifications.test.ts new file mode 100644 index 0000000..e8eacd3 --- /dev/null +++ b/tests/notifications.test.ts @@ -0,0 +1,16 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { CallTracker, announcement, SpeechQueue, selectVoice } from '../src/notifications/core.ts' +import type { Task } from '../src/contract.ts' +function task(id:string,status='pending'):Task{return{id,status,tableCode:'T01',kind:'help',note:'私人备注不要播报',createdAt:'2026-10-03T09:00:00Z',updatedAt:''}} +test('首次只建基线;重复轮询和已处理任务不播报',()=>{let t=new CallTracker();assert.deepEqual(t.accept([task('old')]),[]);assert.deepEqual(t.accept([task('old'),task('new'),task('done','accepted')]).map(x=>x.id),['new']);assert.deepEqual(t.accept([task('new')]),[]);assert.deepEqual(t.accept([task('old')]),[])}) +test('静音快照仍去重;重置后历史任务不重播',()=>{let t=new CallTracker();t.accept([]);t.accept([task('muted')]);assert.deepEqual(t.accept([task('muted'),task('new')]).map(x=>x.id),['new']);t.reset();assert.deepEqual(t.accept([task('new')]),[])}) +test('播报只包括桌位与服务类型,不包含顾客备注',()=>{assert.equal(announcement(task('x')),'T01 桌,呼叫员工。');assert.equal(announcement({...task('x'),kind:'refill'}),'T01 桌,需要补充豆料。')}) +test('仅选择本机中文语音,优先普通话',()=>{let remote={lang:'zh-CN',localService:false};let cantonese={lang:'zh-HK',localService:true};let mandarin={lang:'zh-CN',localService:true};assert.equal(selectVoice([remote,cantonese,mandarin]),mandarin);assert.equal(selectVoice([remote,{lang:'en-US',localService:true}]),undefined)}) +function harness(){let spoken:string[]=[];let callbacks:Array<(error?:string)=>void>=[];let cancelled=0;let errors:string[]=[];let q=new SpeechQueue({speak(text,done){spoken.push(text);callbacks.push(done)},cancel(){cancelled++}},e=>errors.push(e));return {q,spoken,callbacks,errors,get cancelled(){return cancelled}}} +test('队列串行播报且待接单任务被取消后不再播报',()=>{let h=harness();h.q.add([task('a'),task('b'),task('c')]);assert.equal(h.spoken.length,1);h.q.retain(new Set(['a','c']));h.callbacks[0]();assert.equal(h.spoken.length,2);h.q.stop();h.callbacks[1]();assert.equal(h.spoken.length,2);assert.equal(h.cancelled,1)}) +test('关闭后的旧回调不影响新队列;播报错误清空并上报',()=>{let h=harness();h.q.add([task('a')]);h.q.stop();h.q.add([task('b'),task('c')]);h.callbacks[0]();assert.equal(h.spoken.length,2);h.callbacks[1]('播放被浏览器阻止');assert.deepEqual(h.errors,['播放被浏览器阻止']);assert.equal(h.spoken.length,2)}) + +test('队列过载明确提示,不产生无限语音积压',()=>{let h=harness();h.q.add(Array.from({length:33},(_,i)=>task(String(i))));assert.equal(h.spoken.length,0);assert.equal(h.errors.length,1)}) + +test('试听期间收到的新呼叫排队保留,已开启时试听也不清空任务',()=>{let h=harness();let completed=0;h.q.preview('语音开启',()=>completed++);h.q.add([task('new')]);h.callbacks[0]();assert.equal(completed,1);assert.equal(h.spoken[1],'T01 桌,呼叫员工。');h.q.preview('再次试听',()=>completed++);h.q.add([task('second')]);h.callbacks[1]();assert.equal(h.spoken[2],'再次试听');h.callbacks[2]();assert.equal(h.spoken.length,4);h.q.stop()}) diff --git a/tests/realtime.test.ts b/tests/realtime.test.ts new file mode 100644 index 0000000..d6c7e2b --- /dev/null +++ b/tests/realtime.test.ts @@ -0,0 +1,11 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import {RealtimeLink} from '../src/notifications/realtime.ts' +function harness(){ + let sockets:any[]=[],states:string[]=[],syncs=0,expired=0,timers=new Mapvoid,ms:number}>(),id=0 + let link=new RealtimeLink({socket(){let s:any={onopen:null,onmessage:null,onclose:null,onerror:null,sent:[],send(data:string){this.sent.push(JSON.parse(data))},close(){}};sockets.push(s);return s},token:()=> 'test-token',state:s=>states.push(s),sync:()=>syncs++,expired:()=>expired++,later(fn,ms){let key=++id as any;timers.set(key,{fn,ms});return key},clear(key){timers.delete(key)}}) + return {link,sockets,states,timers,get syncs(){return syncs},get expired(){return expired}} +} +test('首帧鉴权,就绪与推送都触发补查,心跳不重复拉取',()=>{let h=harness();h.link.start();let s=h.sockets[0];s.onopen({});assert.deepEqual(s.sent,[{type:'auth',token:'test-token'}]);s.onmessage({data:'{"type":"tasks.changed"}'});assert.equal(h.syncs,0);s.onmessage({data:'{"type":"ready"}'});s.onmessage({data:'{"type":"tasks.changed"}'});s.onmessage({data:'{"type":"heartbeat"}'});assert.equal(h.syncs,2);assert.equal(h.states.at(-1),'online');h.link.stop();assert.equal(h.timers.size,0)}) +test('网络断线退避重连;认证失效停止连接',()=>{let h=harness();h.link.start();h.sockets[0].onclose({code:1006});let retry=[...h.timers.values()].find(t=>t.ms===1000)!;assert.ok(retry);retry.fn();assert.equal(h.sockets.length,2);h.sockets[1].onclose({code:4403});assert.equal(h.expired,1);assert.equal(h.states.at(-1),'closed');assert.equal(h.timers.size,0)}) +test('未收到心跳时主动重连,关闭后不残留监听',()=>{let h=harness();h.link.start();let s=h.sockets[0];s.onmessage({data:'{"type":"ready"}'});[...h.timers.values()].find(t=>t.ms===45000)!.fn();assert.equal(h.states.at(-1),'retrying');assert.equal(s.onmessage,null);h.link.stop();assert.equal(h.timers.size,0)}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a58c63a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1 @@ +{"compilerOptions":{"target":"ES2022","module":"ESNext","moduleResolution":"Bundler","strict":true,"lib":["ES2022","DOM"],"skipLibCheck":true,"allowImportingTsExtensions":true,"noEmit":true},"include":["src/**/*.ts","src/**/*.vue","vite.config.ts"]} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..23b7d91 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +export default defineConfig({ plugins: [vue()], server: { host:'127.0.0.1',port:5174,strictPort:true,proxy:{'/api':{target:'http://127.0.0.1:8080',ws:true}} } })