This commit is contained in:
feie9454 2026-01-28 14:42:14 +08:00
commit 73f948d4fc
37 changed files with 3227 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

388
bun.lock Normal file
View File

@ -0,0 +1,388 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "motor-ui",
"dependencies": {
"@types/qrcode": "^1.5.6",
"echarts": "^6.0.0",
"lucide-vue-next": "^0.562.0",
"qrcode": "^1.5.4",
"vue": "^3.5.24",
},
"devDependencies": {
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.8.1",
"sass-embedded": "^1.97.2",
"typescript": "~5.9.3",
"vite": "^7.2.4",
"vue-tsc": "^3.1.4",
},
},
},
"packages": {
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="],
"@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="],
"@bufbuild/protobuf": ["@bufbuild/protobuf@2.10.2", "", {}, "sha512-uFsRXwIGyu+r6AMdz+XijIIZJYpoWeYzILt5yZ2d3mCjQrWUTVpVD9WL/jZAbvp+Ed04rOhrsk7FiTcEDseB5A=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@parcel/watcher": ["@parcel/watcher@2.5.4", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.4", "@parcel/watcher-darwin-arm64": "2.5.4", "@parcel/watcher-darwin-x64": "2.5.4", "@parcel/watcher-freebsd-x64": "2.5.4", "@parcel/watcher-linux-arm-glibc": "2.5.4", "@parcel/watcher-linux-arm-musl": "2.5.4", "@parcel/watcher-linux-arm64-glibc": "2.5.4", "@parcel/watcher-linux-arm64-musl": "2.5.4", "@parcel/watcher-linux-x64-glibc": "2.5.4", "@parcel/watcher-linux-x64-musl": "2.5.4", "@parcel/watcher-win32-arm64": "2.5.4", "@parcel/watcher-win32-ia32": "2.5.4", "@parcel/watcher-win32-x64": "2.5.4" } }, "sha512-WYa2tUVV5HiArWPB3ydlOc4R2ivq0IDrlqhMi3l7mVsFEXNcTfxYFPIHXHXIh/ca/y/V5N4E1zecyxdIBjYnkQ=="],
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.4", "", { "os": "android", "cpu": "arm64" }, "sha512-hoh0vx4v+b3BNI7Cjoy2/B0ARqcwVNrzN/n7DLq9ZB4I3lrsvhrkCViJyfTj/Qi5xM9YFiH4AmHGK6pgH1ss7g=="],
"@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kphKy377pZiWpAOyTgQYPE5/XEKVMaj6VUjKT5VkNyUJlr2qZAn8gIc7CPzx+kbhvqHDT9d7EqdOqRXT6vk0zw=="],
"@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-UKaQFhCtNJW1A9YyVz3Ju7ydf6QgrpNQfRZ35wNKUhTQ3dxJ/3MULXN5JN/0Z80V/KUBDGa3RZaKq1EQT2a2gg=="],
"@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Dib0Wv3Ow/m2/ttvLdeI2DBXloO7t3Z0oCp4bAb2aqyqOjKPPGrg10pMJJAQ7tt8P4V2rwYwywkDhUia/FgS+Q=="],
"@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.4", "", { "os": "linux", "cpu": "arm" }, "sha512-I5Vb769pdf7Q7Sf4KNy8Pogl/URRCKu9ImMmnVKYayhynuyGYMzuI4UOWnegQNa2sGpsPSbzDsqbHNMyeyPCgw=="],
"@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kGO8RPvVrcAotV4QcWh8kZuHr9bXi9a3bSZw7kFarYR0+fGliU7hd/zevhjw8fnvIKG3J9EO5G6sXNGCSNMYPQ=="],
"@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-KU75aooXhqGFY2W5/p8DYYHt4hrjHZod8AhcGAmhzPn/etTa+lYCDB2b1sJy3sWJ8ahFVTdy+EbqSBvMx3iFlw=="],
"@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qx8uNiIekVutnzbVdrgSanM+cbpDD3boB1f8vMtnuG5Zau4/bdDbXyKwIn0ToqFhIuob73bcxV9NwRm04/hzHQ=="],
"@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.4", "", { "os": "linux", "cpu": "x64" }, "sha512-UYBQvhYmgAv61LNUn24qGQdjtycFBKSK3EXr72DbJqX9aaLbtCOO8+1SkKhD/GNiJ97ExgcHBrukcYhVjrnogA=="],
"@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.4", "", { "os": "linux", "cpu": "x64" }, "sha512-YoRWCVgxv8akZrMhdyVi6/TyoeeMkQ0PGGOf2E4omODrvd1wxniXP+DBynKoHryStks7l+fDAMUBRzqNHrVOpg=="],
"@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-iby+D/YNXWkiQNYcIhg8P5hSjzXEHaQrk2SLrWOUD7VeC4Ohu0WQvmV+HDJokZVJ2UjJ4AGXW3bx7Lls9Ln4TQ=="],
"@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-vQN+KIReG0a2ZDpVv8cgddlf67J8hk1WfZMMP7sMeZmJRSmEax5xNDNWKdgqSe2brOKTQQAs3aCCUal2qBHAyg=="],
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.4", "", { "os": "win32", "cpu": "x64" }, "sha512-3A6efb6BOKwyw7yk9ro2vus2YTt2nvcd56AuzxdMiVOxL9umDyN5PKkKfZ/gZ9row41SjVmTVQNWQhaRRGpOKw=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.2", "", { "os": "android", "cpu": "arm" }, "sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.2", "", { "os": "android", "cpu": "arm64" }, "sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.2", "", { "os": "linux", "cpu": "arm" }, "sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.2", "", { "os": "linux", "cpu": "arm" }, "sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.2", "", { "os": "linux", "cpu": "none" }, "sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.2", "", { "os": "linux", "cpu": "none" }, "sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.2", "", { "os": "linux", "cpu": "none" }, "sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.2", "", { "os": "linux", "cpu": "none" }, "sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.2", "", { "os": "linux", "cpu": "x64" }, "sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.2", "", { "os": "win32", "cpu": "x64" }, "sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.2", "", { "os": "win32", "cpu": "x64" }, "sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="],
"@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="],
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.3", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.53" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w=="],
"@volar/language-core": ["@volar/language-core@2.4.27", "", { "dependencies": { "@volar/source-map": "2.4.27" } }, "sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ=="],
"@volar/source-map": ["@volar/source-map@2.4.27", "", {}, "sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg=="],
"@volar/typescript": ["@volar/typescript@2.4.27", "", { "dependencies": { "@volar/language-core": "2.4.27", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg=="],
"@vue/compiler-core": ["@vue/compiler-core@3.5.27", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/shared": "3.5.27", "entities": "^7.0.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ=="],
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.27", "", { "dependencies": { "@vue/compiler-core": "3.5.27", "@vue/shared": "3.5.27" } }, "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w=="],
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.27", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/compiler-core": "3.5.27", "@vue/compiler-dom": "3.5.27", "@vue/compiler-ssr": "3.5.27", "@vue/shared": "3.5.27", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ=="],
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.27", "", { "dependencies": { "@vue/compiler-dom": "3.5.27", "@vue/shared": "3.5.27" } }, "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw=="],
"@vue/language-core": ["@vue/language-core@3.2.2", "", { "dependencies": { "@volar/language-core": "2.4.27", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.0.0", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" } }, "sha512-5DAuhxsxBN9kbriklh3Q5AMaJhyOCNiQJvCskN9/30XOpdLiqZU9Q+WvjArP17ubdGEyZtBzlIeG5nIjEbNOrQ=="],
"@vue/reactivity": ["@vue/reactivity@3.5.27", "", { "dependencies": { "@vue/shared": "3.5.27" } }, "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ=="],
"@vue/runtime-core": ["@vue/runtime-core@3.5.27", "", { "dependencies": { "@vue/reactivity": "3.5.27", "@vue/shared": "3.5.27" } }, "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A=="],
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.27", "", { "dependencies": { "@vue/reactivity": "3.5.27", "@vue/runtime-core": "3.5.27", "@vue/shared": "3.5.27", "csstype": "^3.2.3" } }, "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg=="],
"@vue/server-renderer": ["@vue/server-renderer@3.5.27", "", { "dependencies": { "@vue/compiler-ssr": "3.5.27", "@vue/shared": "3.5.27" }, "peerDependencies": { "vue": "3.5.27" } }, "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA=="],
"@vue/shared": ["@vue/shared@3.5.27", "", {}, "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ=="],
"@vue/tsconfig": ["@vue/tsconfig@0.8.1", "", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g=="],
"alien-signals": ["alien-signals@3.1.2", "", {}, "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"buffer-builder": ["buffer-builder@0.2.0", "", {}, "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg=="],
"camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"colorjs.io": ["colorjs.io@0.5.2", "", {}, "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="],
"echarts": ["echarts@6.0.0", "", { "dependencies": { "tslib": "2.3.0", "zrender": "6.0.0" } }, "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"entities": ["entities@7.0.0", "", {}, "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ=="],
"esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"immutable": ["immutable@5.1.4", "", {}, "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"lucide-vue-next": ["lucide-vue-next@0.562.0", "", { "peerDependencies": { "vue": ">=3.0.1" } }, "sha512-LN0BLGKMFulv0lnfK29r14DcngRUhIqdcaL0zXTt2o0oS9odlrjCGaU3/X9hIihOjjN8l8e+Y9G/famcNYaI7Q=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="],
"rollup": ["rollup@4.55.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.2", "@rollup/rollup-android-arm64": "4.55.2", "@rollup/rollup-darwin-arm64": "4.55.2", "@rollup/rollup-darwin-x64": "4.55.2", "@rollup/rollup-freebsd-arm64": "4.55.2", "@rollup/rollup-freebsd-x64": "4.55.2", "@rollup/rollup-linux-arm-gnueabihf": "4.55.2", "@rollup/rollup-linux-arm-musleabihf": "4.55.2", "@rollup/rollup-linux-arm64-gnu": "4.55.2", "@rollup/rollup-linux-arm64-musl": "4.55.2", "@rollup/rollup-linux-loong64-gnu": "4.55.2", "@rollup/rollup-linux-loong64-musl": "4.55.2", "@rollup/rollup-linux-ppc64-gnu": "4.55.2", "@rollup/rollup-linux-ppc64-musl": "4.55.2", "@rollup/rollup-linux-riscv64-gnu": "4.55.2", "@rollup/rollup-linux-riscv64-musl": "4.55.2", "@rollup/rollup-linux-s390x-gnu": "4.55.2", "@rollup/rollup-linux-x64-gnu": "4.55.2", "@rollup/rollup-linux-x64-musl": "4.55.2", "@rollup/rollup-openbsd-x64": "4.55.2", "@rollup/rollup-openharmony-arm64": "4.55.2", "@rollup/rollup-win32-arm64-msvc": "4.55.2", "@rollup/rollup-win32-ia32-msvc": "4.55.2", "@rollup/rollup-win32-x64-gnu": "4.55.2", "@rollup/rollup-win32-x64-msvc": "4.55.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg=="],
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
"sass": ["sass@1.97.2", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw=="],
"sass-embedded": ["sass-embedded@1.97.2", "", { "dependencies": { "@bufbuild/protobuf": "^2.5.0", "buffer-builder": "^0.2.0", "colorjs.io": "^0.5.0", "immutable": "^5.0.2", "rxjs": "^7.4.0", "supports-color": "^8.1.1", "sync-child-process": "^1.0.2", "varint": "^6.0.0" }, "optionalDependencies": { "sass-embedded-all-unknown": "1.97.2", "sass-embedded-android-arm": "1.97.2", "sass-embedded-android-arm64": "1.97.2", "sass-embedded-android-riscv64": "1.97.2", "sass-embedded-android-x64": "1.97.2", "sass-embedded-darwin-arm64": "1.97.2", "sass-embedded-darwin-x64": "1.97.2", "sass-embedded-linux-arm": "1.97.2", "sass-embedded-linux-arm64": "1.97.2", "sass-embedded-linux-musl-arm": "1.97.2", "sass-embedded-linux-musl-arm64": "1.97.2", "sass-embedded-linux-musl-riscv64": "1.97.2", "sass-embedded-linux-musl-x64": "1.97.2", "sass-embedded-linux-riscv64": "1.97.2", "sass-embedded-linux-x64": "1.97.2", "sass-embedded-unknown-all": "1.97.2", "sass-embedded-win32-arm64": "1.97.2", "sass-embedded-win32-x64": "1.97.2" }, "bin": { "sass": "dist/bin/sass.js" } }, "sha512-lKJcskySwAtJ4QRirKrikrWMFa2niAuaGenY2ElHjd55IwHUiur5IdKu6R1hEmGYMs4Qm+6rlRW0RvuAkmcryg=="],
"sass-embedded-all-unknown": ["sass-embedded-all-unknown@1.97.2", "", { "dependencies": { "sass": "1.97.2" }, "cpu": [ "!arm", "!x64", "!arm64", ] }, "sha512-Fj75+vOIDv1T/dGDwEpQ5hgjXxa2SmMeShPa8yrh2sUz1U44bbmY4YSWPCdg8wb7LnwiY21B2KRFM+HF42yO4g=="],
"sass-embedded-android-arm": ["sass-embedded-android-arm@1.97.2", "", { "os": "android", "cpu": "arm" }, "sha512-BPT9m19ttY0QVHYYXRa6bmqmS3Fa2EHByNUEtSVcbm5PkIk1ntmYkG9fn5SJpIMbNmFDGwHx+pfcZMmkldhnRg=="],
"sass-embedded-android-arm64": ["sass-embedded-android-arm64@1.97.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pF6I+R5uThrscd3lo9B3DyNTPyGFsopycdx0tDAESN6s+dBbiRgNgE4Zlpv50GsLocj/lDLCZaabeTpL3ubhYA=="],
"sass-embedded-android-riscv64": ["sass-embedded-android-riscv64@1.97.2", "", { "os": "android", "cpu": "none" }, "sha512-fprI8ZTJdz+STgARhg8zReI2QhhGIT9G8nS7H21kc3IkqPRzhfaemSxEtCqZyvDbXPcgYiDLV7AGIReHCuATog=="],
"sass-embedded-android-x64": ["sass-embedded-android-x64@1.97.2", "", { "os": "android", "cpu": "x64" }, "sha512-RswwSjURZxupsukEmNt2t6RGvuvIw3IAD5sDq1Pc65JFvWFY3eHqCmH0lG0oXqMg6KJcF0eOxHOp2RfmIm2+4w=="],
"sass-embedded-darwin-arm64": ["sass-embedded-darwin-arm64@1.97.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xcsZNnU1XZh21RE/71OOwNqPVcGBU0qT9A4k4QirdA34+ts9cDIaR6W6lgHOBR/Bnnu6w6hXJR4Xth7oFrefPA=="],
"sass-embedded-darwin-x64": ["sass-embedded-darwin-x64@1.97.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-T/9DTMpychm6+H4slHCAsYJRJ6eM+9H9idKlBPliPrP4T8JdC2Cs+ZOsYqrObj6eOtAD0fGf+KgyNhnW3xVafA=="],
"sass-embedded-linux-arm": ["sass-embedded-linux-arm@1.97.2", "", { "os": "linux", "cpu": "arm" }, "sha512-yDRe1yifGHl6kibkDlRIJ2ZzAU03KJ1AIvsAh4dsIDgK5jx83bxZLV1ZDUv7a8KK/iV/80LZnxnu/92zp99cXQ=="],
"sass-embedded-linux-arm64": ["sass-embedded-linux-arm64@1.97.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Wh+nQaFer9tyE5xBPv5murSUZE/+kIcg8MyL5uqww6be9Iq+UmZpcJM7LUk+q8klQ9LfTmoDSNFA74uBqxD6IA=="],
"sass-embedded-linux-musl-arm": ["sass-embedded-linux-musl-arm@1.97.2", "", { "os": "linux", "cpu": "arm" }, "sha512-GIO6xfAtahJAWItvsXZ3MD1HM6s8cKtV1/HL088aUpKJaw/2XjTCveiOO2AdgMpLNztmq9DZ1lx5X5JjqhS45g=="],
"sass-embedded-linux-musl-arm64": ["sass-embedded-linux-musl-arm64@1.97.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-NfUqZSjHwnHvpSa7nyNxbWfL5obDjNBqhHUYmqbHUcmqBpFfHIQsUPgXME9DKn1yBlBc3mWnzMxRoucdYTzd2Q=="],
"sass-embedded-linux-musl-riscv64": ["sass-embedded-linux-musl-riscv64@1.97.2", "", { "os": "linux", "cpu": "none" }, "sha512-qtM4dJ5gLfvyTZ3QencfNbsTEShIWImSEpkThz+Y2nsCMbcMP7/jYOA03UWgPfEOKSehQQ7EIau7ncbFNoDNPQ=="],
"sass-embedded-linux-musl-x64": ["sass-embedded-linux-musl-x64@1.97.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ZAxYOdmexcnxGnzdsDjYmNe3jGj+XW3/pF/n7e7r8y+5c6D2CQRrCUdapLgaqPt1edOPQIlQEZF8q5j6ng21yw=="],
"sass-embedded-linux-riscv64": ["sass-embedded-linux-riscv64@1.97.2", "", { "os": "linux", "cpu": "none" }, "sha512-reVwa9ZFEAOChXpDyNB3nNHHyAkPMD+FTctQKECqKiVJnIzv2EaFF6/t0wzyvPgBKeatA8jszAIeOkkOzbYVkQ=="],
"sass-embedded-linux-x64": ["sass-embedded-linux-x64@1.97.2", "", { "os": "linux", "cpu": "x64" }, "sha512-bvAdZQsX3jDBv6m4emaU2OMTpN0KndzTAMgJZZrKUgiC0qxBmBqbJG06Oj/lOCoXGCxAvUOheVYpezRTF+Feog=="],
"sass-embedded-unknown-all": ["sass-embedded-unknown-all@1.97.2", "", { "dependencies": { "sass": "1.97.2" }, "os": [ "!linux", "!win32", "!darwin", "!android", ] }, "sha512-86tcYwohjPgSZtgeU9K4LikrKBJNf8ZW/vfsFbdzsRlvc73IykiqanufwQi5qIul0YHuu9lZtDWyWxM2dH/Rsg=="],
"sass-embedded-win32-arm64": ["sass-embedded-win32-arm64@1.97.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Cv28q8qNjAjZfqfzTrQvKf4JjsZ6EOQ5FxyHUQQeNzm73R86nd/8ozDa1Vmn79Hq0kwM15OCM9epanDuTG1ksA=="],
"sass-embedded-win32-x64": ["sass-embedded-win32-x64@1.97.2", "", { "os": "win32", "cpu": "x64" }, "sha512-DVxLxkeDCGIYeyHLAvWW3yy9sy5Ruk5p472QWiyfyyG1G1ASAR8fgfIY5pT0vE6Rv+VAKVLwF3WTspUYu7S1/Q=="],
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"sync-child-process": ["sync-child-process@1.0.2", "", { "dependencies": { "sync-message-port": "^1.0.0" } }, "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA=="],
"sync-message-port": ["sync-message-port@1.1.3", "", {}, "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tslib": ["tslib@2.3.0", "", {}, "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="],
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
"vue": ["vue@3.5.27", "", { "dependencies": { "@vue/compiler-dom": "3.5.27", "@vue/compiler-sfc": "3.5.27", "@vue/runtime-dom": "3.5.27", "@vue/server-renderer": "3.5.27", "@vue/shared": "3.5.27" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw=="],
"vue-tsc": ["vue-tsc@3.2.2", "", { "dependencies": { "@volar/typescript": "2.4.27", "@vue/language-core": "3.2.2" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-r9YSia/VgGwmbbfC06hDdAatH634XJ9nVl6Zrnz1iK4ucp8Wu78kawplXnIDa3MSu1XdQQePTHLXYwPDWn+nyQ=="],
"which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="],
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
"yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
"yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
"zrender": ["zrender@6.0.0", "", { "dependencies": { "tslib": "2.3.0" } }, "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg=="],
}
}

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>motor-ui</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "motor-ui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@types/qrcode": "^1.5.6",
"echarts": "^6.0.0",
"lucide-vue-next": "^0.562.0",
"qrcode": "^1.5.4",
"vue": "^3.5.24"
},
"devDependencies": {
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.8.1",
"sass-embedded": "^1.97.2",
"typescript": "~5.9.3",
"vite": "^7.2.4",
"vue-tsc": "^3.1.4"
}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

480
src/App.vue Normal file
View File

@ -0,0 +1,480 @@
<script setup lang="ts">
import { reactive, ref, type Ref, watch } from 'vue';
import * as api from './api.ts'
import Button from './components/Button.vue';
import Machine from './components/Machine.vue';
import QRCode from 'qrcode';
import Panel from './components/Panel.vue';
import Window from './components/Window.vue';
import Oscilloscope from './components/Oscilloscope.vue';
const state = reactive({ ...api.getState() });
api.onStateChange((newState) => {
Object.assign(state, newState);
});
const MachineId = '001';
const qrcodeSrc = ref('');
QRCode.toDataURL(`http://svme.xn--876a.net/login/${MachineId}`, {
}).then(url => {
qrcodeSrc.value = url;
});
const showLogin = ref(false);
const showManualAdjust = ref(false);
const targetDis = ref('');
import { Delete, RefreshCcw, Settings, ChevronRight } from 'lucide-vue-next';
import { showMessage } from './utils/message';
import Numpad from './components/Numpad.vue';
import Control from './pages/Control.vue';
import History from './pages/History.vue';
import TrendChart from './components/TrendChart.vue';
const showSettings = ref(false);
const showSpeedSettings = ref(false);
const targetSpeed = ref('');
const historyData = ref<any[]>([]);
//
watch(() => state.last_measurement, (newVal, oldVal) => {
if (newVal && newVal.ts && (!oldVal || newVal.ts !== oldVal.ts)) {
//
historyData.value.push({
...newVal,
currentDis: state.dis
});
}
});
function clearHistory() {
historyData.value = [];
}
function checkAndRun(callback: (val: number) => void) {
const value = targetDis.value;
if (!value) {
showMessage('请输入目标距离', 'warning');
return;
}
const num = parseFloat(value);
if (isNaN(num)) {
showMessage('请输入有效的数字格式', 'error');
return;
}
if (num < 0 || num > 300) {
showMessage('目标距离超出范围 (0-300mm)\n请重新输入', 'error');
return;
}
callback(num);
}
function checkSpeedAndSet() {
const value = targetSpeed.value;
if (!value) {
showMessage('请输入目标速度', 'warning');
return;
}
const num = parseFloat(value);
if (isNaN(num)) {
showMessage('请输入有效的数字格式', 'error');
return;
}
// : 60 - 2000 um/s ( 96 - 3200 Hz)
if (num < 60 || num > 2000) {
showMessage('速度超出范围 (60-2000 μm/s)\n请重新输入', 'error');
return;
}
// um/s -> Hz (1600 Hz = 1000 um/s => ratio = 1.6)
const hz = Math.round(num * 1.6);
api.setSpeed(hz);
showSpeedSettings.value = false;
showMessage('速度设置成功', 'success');
}
function openSettings() {
showSettings.value = true;
}
function openSpeedSettings() {
// Hz -> um/s
const currentUmPs = state.speed ? Math.round(state.speed / 1.6) : 0;
targetSpeed.value = currentUmPs.toString();
showSpeedSettings.value = true;
}
function locationReload() {
location.reload();
}
// 'home' | 'measure' | 'control' | 'history'
const pageIndex: Ref<0 | 1 | 2 | 3> = ref(0);
const mountedPages = ref([true, false, false, false]);
let cleanupTimer: any = null;
watch(pageIndex, (newVal, oldVal) => {
// Ensure target page is mounted
mountedPages.value[newVal] = true;
// Ensure old page stays mounted during transition
if (oldVal !== undefined) {
mountedPages.value[oldVal] = true;
}
if (cleanupTimer) clearTimeout(cleanupTimer);
cleanupTimer = setTimeout(() => {
// Keep only the current page mounted to save resources
for (let i = 0; i < 4; i++) {
mountedPages.value[i] = (i === pageIndex.value);
}
}, 400); // 0.3s transition + buffer
});
</script>
<template>
<header>
<div class="page-label">
<Button :class="{ current: pageIndex === 0 }" @click="pageIndex = 0">首页</Button>
<Button :class="{ current: pageIndex === 1 }" @click="pageIndex = 1">测量</Button>
<Button :class="{ current: pageIndex === 2 }" @click="pageIndex = 2">控制</Button>
<Button :class="{ current: pageIndex === 3 }" @click="pageIndex = 3">历史</Button>
</div>
<div class="connection-state" :class="{ connected: state.connected, disconnected: !state.connected }"></div>
<span>自动声速测定仪</span>
<div class="actions">
<Button class="reload" @click="locationReload">
<RefreshCcw />
</Button>
<Button class="settings" @click="openSettings">
<Settings />
</Button>
<Button class="login" @click="showLogin = true">登录</Button>
</div>
</header>
<main>
<div class="page page-home" :style="{ transform: `translateX(${(-pageIndex - 0) * 100}%)` }">
<template v-if="mountedPages[0]">
<div class="left">
<Panel style="flex: 1;">
<TrendChart :data="historyData" />
</Panel>
<Panel>
<Machine :dis="state.dis"
:task="state.tasks.reduce((acc, task) => (task.type == 'move' ? ({ remaining_steps: acc.remaining_steps + task.remaining_steps }) : acc), { remaining_steps: 0 })" />
</Panel>
</div>
<div class="right">
<div class="label">相位差</div>
<div class="data">{{ state.phase.toFixed(4) }} rad</div>
<div class="label">频率</div>
<div class="data">{{ state.freq }} KHz</div>
<div class="label">峰峰值</div>
<div class="data">{{ state.p2p }} ADC</div>
<div class="action">
<div class="left">
<div class="line">
<Button class="man-adj" @long-press="api.move(-40)"></Button>
<span>手动<br>调整</span>
<Button class="man-adj" @long-press="api.move(40)"></Button>
</div>
<Button class="input-dis" @click="showManualAdjust = true">
输入
</Button>
</div>
<div class="right">
<Button class="action-btn" @click="pageIndex = 2" bg="limegreen">开始实验</Button>
<!-- <Button class="action-btn" @click="api.measure()" bg="limegreen">单次测量</Button> -->
<Button class="action-btn" @click="api.stopAll()" bg="red">停止</Button>
</div>
</div>
</div>
</template>
</div>
<div class="page page-measure" :style="{ transform: `translateX(${(-pageIndex + 1) * 100}%)` }">
<Oscilloscope v-if="mountedPages[1]" :data="state.last_measurement">
<template #controls>
<Button @click="api.measure()" bg="limegreen" style="height: 52px; width: 100px; border-radius: 8px;">单次测量</Button>
</template>
</Oscilloscope>
</div>
<div class="page page-control" :style="{ transform: `translateX(${(-pageIndex + 2) * 100}%)` }">
<Control v-if="mountedPages[2]" :state="state" @start="clearHistory" />
</div>
<div class="page page-history" :style="{ transform: `translateX(${(-pageIndex + 3) * 100}%)` }">
<History v-if="mountedPages[3]" :history="historyData" @clear="clearHistory" />
</div>
</main>
<Window v-model="showLogin" title="扫码登录">
<img :src="qrcodeSrc" alt="" class="qr-img">
<Button @click="showLogin = false">游客模式</Button>
</Window>
<Window v-model="showManualAdjust" title="手动调整">
<div class="manual-adjust-content">
<Numpad v-model="targetDis" label="目标距离" unit="mm" />
<div class="manual-right">
<Button class="manual-btn" @click="checkAndRun((val) => api.setDis(val * 1600))">以目标值校准</Button>
<Button class="manual-btn" @click="showManualAdjust = false">取消</Button>
<Button class="manual-btn" bg="limegreen" @click="checkAndRun((val) => api.goTo(val * 1600))">启动</Button>
</div>
</div>
</Window>
<Window v-model="showSettings" title="系统设置">
<div class="settings-list">
<Button class="settings-item" @click="openSpeedSettings">
<span class="settings-label">电机速度</span>
<div class="settings-value">
{{ state.speed ? Math.round(state.speed / 1.6) : '--' }} μm/s
<ChevronRight :size="20" class="arrow" />
</div>
</Button>
</div>
</Window>
<Window v-model="showSpeedSettings" title="设置电机速度">
<div class="manual-adjust-content">
<Numpad v-model="targetSpeed" label="电机速度" unit="μm/s" />
<div class="manual-right">
<Button class="manual-btn" @click="showSpeedSettings = false">取消</Button>
<Button class="manual-btn" bg="limegreen" @click="checkSpeedAndSet()">确定</Button>
</div>
</div>
</Window>
</template>
<style scoped lang="scss">
header {
background-color: rgb(0, 0, 161);
height: 36px;
display: flex;
justify-content: center;
align-items: center;
color: white;
position: relative;
.connection-state {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: gray;
margin-right: 8px;
&.connected {
background-color: limegreen;
}
&.disconnected {
background-color: red;
}
}
.actions {
display: flex;
position: absolute;
right: 8px;
gap: 8px;
.btn {
border: none;
}
.login {
width: 48px;
height: 28px;
}
.reload {
width: 28px;
height: 28px;
}
.settings {
width: 28px;
height: 28px;
}
}
.page-label {
position: absolute;
left: 12px;
bottom: 0;
display: flex;
align-items: flex-end;
gap: 4px;
.btn {
width: 64px;
height: 28px;
border: none;
&.current {
background-color: #B5CDE4;
border-top: 3px solid forestgreen;
height: 32px;
}
}
}
}
.settings-list {
display: flex;
flex-direction: column;
gap: 12px;
padding: 8px;
min-width: 320px;
.settings-item {
width: 100%;
height: 52px;
justify-content: space-between;
padding: 0 12px;
}
.settings-value {
display: flex;
align-items: center;
gap: 8px;
color: #666;
font-weight: bold;
}
.arrow {
color: #999;
}
}
.action {
display: flex;
justify-content: space-between;
width: 100%;
.man-adj {
width: 52px;
height: 52px;
font-size: 24px;
margin-bottom: 6px;
}
.left {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
.line {
display: flex;
align-items: center;
gap: 4px;
}
}
.right {
display: flex;
flex-direction: column;
gap: 6px;
height: 150px;
justify-content: center;
}
.action-btn {
width: 140px;
height: 52px;
border-radius: 8px;
font-size: 20px;
}
}
main {
overflow: hidden;
height: calc(100vh - 36px);
width: 100vw;
position: relative;
}
.page {
position: absolute;
left: 0;
top: 0;
width: 100%;
background-color: #B5CDE4;
display: flex;
height: 100%;
transition: transform 0.3s ease-in-out;
}
.qr-img {
width: 160px;
height: 160px;
}
.page-home>.left,
.page-home>.right {
display: flex;
flex-direction: column;
padding: 10px;
}
.page-home>.left {
flex: 3;
position: relative;
align-items: stretch;
gap: 8px;
}
.page-home>.right {
flex: 2;
position: relative;
align-items: center;
.label {
align-self: flex-start;
font-size: 24px;
}
.label::after {
content: '';
margin-left: 4px;
}
.data {
font-size: 32px;
font-weight: bold;
margin-bottom: 12px;
margin-top: -6px;
}
}
.manual-adjust-content {
display: flex;
gap: 16px;
width: 100%;
.manual-right {
display: flex;
flex-direction: column;
gap: 12px;
justify-content: center;
min-width: 120px;
}
}
.manual-btn {
width: 100%;
height: 52px;
font-size: 16px;
border-radius: 6px;
}
</style>

188
src/api.ts Normal file
View File

@ -0,0 +1,188 @@
const API_BASE = 'http://localhost:8000';
async function ping() {
return await fetch(`${API_BASE}/ping`);
}
async function bee() {
return await fetch(`${API_BASE}/bee`);
}
const _state = {
// 连接状态
connected: false,
// 接收器距离 microsteps
dis: 16000,
// 相位差 rad
phase: 0,
// 频率 KHz
freq: 0,
// 峰峰值 mV
p2p: 0,
// 电机速度 Hz 1600 means 1mm/s or 1000 um/s
speed: 1200,
// 任务
total_tasks: 0,
tasks: [] as ({
id: string;
type: 'move';
steps: number;
remaining_steps: number;
status: 'running' | 'pending' | 'queued';
created_at: number;
} | {
id: string;
type: 'measure';
status: 'running' | 'pending' | 'queued';
created_at: number;
})[],
last_measurement:{}as {
"ts": number,
"idn": null,
"points_mode": 'NORM',
"n": number,
"tscale": number,
"toffs": number,
"f0_hz": number,
"amp1_pp_adc": number,
"amp2_pp_adc": number,
"phi1_rad": number,
"phi2_rad": number,
"dphi_rad": number,
"dphi_deg": number,
"dt_s": number,
"wave1": number[],
"wave2": number[],
}
}
const listeners: ((state: typeof _state) => void)[] = [];
function notifyListeners() {
for (const listener of listeners) {
listener(_state);
}
}
function initSSE() {
let reconnectTimer: NodeJS.Timeout;
const connect = () => {
const eventSource = new EventSource(`${API_BASE}/events`);
eventSource.onopen = () => {
console.log('SSE Connected');
_state.connected = true;
notifyListeners();
};
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// 简单地合并数据到 _state
Object.assign(_state, data);
notifyListeners();
} catch (e) {
console.error('SSE Parsing Error:', e);
}
};
eventSource.onerror = (err) => {
console.error('SSE Error:', err);
if (_state.connected) {
_state.connected = false;
notifyListeners();
}
eventSource.close();
clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(() => {
console.log('Attempting to reconnect SSE...');
connect();
}, 3000);
};
};
connect();
}
initSSE();
function getState() {
return _state;
}
async function onStateChange(callback: (state: typeof _state) => void) {
listeners.push(callback);
// 立即回调当前状态
callback(_state);
}
async function move(steps: number) {
const url = new URL(`${API_BASE}/action/move`);
url.searchParams.append('steps', steps.toString());
return await fetch(url.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
}
async function goTo(steps: number) {
const targetSteps = Math.round(steps - _state.dis);
return move(targetSteps);
}
async function setDis(steps: number) {
const url = new URL(`${API_BASE}/state/dis`);
url.searchParams.append('dis', Math.round(steps).toString());
return await fetch(url.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
}
async function setSpeed(hz: number) {
const url = new URL(`${API_BASE}/state/speed`);
url.searchParams.append('speed', Math.round(hz).toString());
return await fetch(url.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
}
async function stopAll() {
return await fetch(`${API_BASE}/action/cancel`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
}
async function measure() {
return await fetch(`${API_BASE}/action/measure`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
}
async function batch(tasks: any[]) {
return await fetch(`${API_BASE}/action/batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(tasks),
});
}
export { bee, ping, getState, onStateChange, move, goTo, setDis, setSpeed, stopAll, measure, batch };

BIN
src/assets/_-.woff2 Normal file

Binary file not shown.

35
src/assets/apparatus.svg Normal file
View File

@ -0,0 +1,35 @@
<svg width="600" height="200" viewBox="0 0 600 200" xmlns="http://www.w3.org/2000/svg">
<g id="rail_layer">
<line x1="50" y1="160" x2="550" y2="160" stroke="#4a5568" stroke-width="4" stroke-linecap="round"/>
<line x1="100" y1="160" x2="100" y2="170" stroke="#4a5568" stroke-width="2"/>
<line x1="200" y1="160" x2="200" y2="170" stroke="#4a5568" stroke-width="2"/>
<line x1="300" y1="160" x2="300" y2="170" stroke="#4a5568" stroke-width="2"/>
<line x1="400" y1="160" x2="400" y2="170" stroke="#4a5568" stroke-width="2"/>
<line x1="500" y1="160" x2="500" y2="170" stroke="#4a5568" stroke-width="2"/>
</g>
<g id="emitter_group" transform="translate(80, 100)">
<rect x="-15" y="30" width="30" height="30" rx="4" fill="#2b6cb0" />
<line x1="0" y1="30" x2="0" y2="0" stroke="#2b6cb0" stroke-width="4"/>
<circle cx="0" cy="0" r="18" fill="#4299e1" stroke="#2b6cb0" stroke-width="3"/>
<circle cx="0" cy="0" r="8" fill="#bee3f8"/>
<text x="0" y="-30" text-anchor="middle" font-family="Arial" font-size="12" fill="#4299e1" font-weight="bold">S1 发射</text>
</g>
<g id="waves" stroke="#63b3ed" stroke-width="2" stroke-opacity="0.5" fill="none">
<path d="M 110 100 Q 130 100 150 100" stroke-dasharray="4,4" />
</g>
<g id="receiver_group" transform="translate(300, 100)">
<line x1="-220" y1="0" x2="-18" y2="0" stroke="#ed8936" stroke-width="1" stroke-dasharray="5,5" opacity="0.6"/>
<rect x="-15" y="30" width="30" height="30" rx="4" fill="#c05621" />
<line x1="0" y1="30" x2="0" y2="0" stroke="#c05621" stroke-width="4"/>
<path d="M -16 0 L -8 -14 L 8 -14 L 16 0 L 8 14 L -8 14 Z" fill="#ed8936" stroke="#c05621" stroke-width="3"/>
<circle cx="0" cy="0" r="6" fill="#feebc8"/>
<text x="0" y="-30" text-anchor="middle" font-family="Arial" font-size="12" fill="#ed8936" font-weight="bold">S2 接收</text>
<rect x="-30" y="65" width="60" height="20" rx="4" fill="#4a5568"/>
<text x="0" y="79" text-anchor="middle" font-family="Monospace" font-size="10" fill="#ffffff">L=Moving</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,8 @@
@font-face {
font-family: 'PingFang SC';
src: url('_-.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}

1
src/assets/vue.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

BIN
src/assets/仪器.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

BIN
src/assets/仪器臂.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

BIN
src/assets/发射器.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
src/assets/接收器.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

148
src/components/Button.vue Normal file
View File

@ -0,0 +1,148 @@
<template>
<div @click="api.bee();" class="btn" :style="btnStyle" @pointerdown="pDown" @pointerup="pUp" @pointercancel="pUp"
@pointerleave="pUp">
<slot></slot>
</div>
</template>
<script setup lang="ts">
import * as api from '../api.ts'
import { computed, ref } from 'vue';
const props = withDefaults(
defineProps<{
bg?: string;
onLongPress?: () => void;
longPressInterval?: number;
}>(),
{
bg: '#ffffff',
longPressInterval: 200,
}
);
let longPressTimer: number | null = null;
type Rgb = { r: number; g: number; b: number };
const clamp255 = (v: number) => Math.max(0, Math.min(255, Math.round(v)));
const parseColor = (input: string): Rgb | null => {
const s = input.trim().toLowerCase();
const hex = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
if (hex && hex[1]) {
const h = hex[1];
if (h.length === 3) {
const r = parseInt(h[0]! + h[0]!, 16);
const g = parseInt(h[1]! + h[1]!, 16);
const b = parseInt(h[2]! + h[2]!, 16);
return { r, g, b };
}
const r = parseInt(h.slice(0, 2), 16);
const g = parseInt(h.slice(2, 4), 16);
const b = parseInt(h.slice(4, 6), 16);
return { r, g, b };
}
const rgb = s.match(
/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*(\d*\.?\d+)\s*)?\)$/
);
if (rgb) {
const r = clamp255(Number(rgb[1]));
const g = clamp255(Number(rgb[2]));
const b = clamp255(Number(rgb[3]));
return { r, g, b };
}
return null;
};
const relativeLuminance = ({ r, g, b }: Rgb) => {
const srgb = [r, g, b].map((v) => v / 255);
const lin = srgb.map((c) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)));
return 0.2126 * lin[0]! + 0.7152 * lin[1]! + 0.0722 * lin[2]!;
};
const mix = (a: number, b: number, t: number) => a + (b - a) * t;
const mixRgb = (c: Rgb, target: Rgb, t: number): Rgb => ({
r: clamp255(mix(c.r, target.r, t)),
g: clamp255(mix(c.g, target.g, t)),
b: clamp255(mix(c.b, target.b, t)),
});
const toCssRgb = (c: Rgb) => `rgb(${c.r}, ${c.g}, ${c.b})`;
const computeHover = (bg: string) => {
const parsed = parseColor(bg);
if (!parsed) return null;
const lum = relativeLuminance(parsed);
const t = 0.14;
const target = lum > 0.6 ? { r: 0, g: 0, b: 0 } : { r: 255, g: 255, b: 255 };
return toCssRgb(mixRgb(parsed, target, t));
};
const btnStyle = computed(() => {
const bg = props.bg;
const hover = computeHover(bg);
return {
'--btn-bg': bg,
'--btn-hover-bg': hover ?? `color-mix(in srgb, ${bg}, black 14%)`,
} as Record<string, string>;
});
const pDown = (e: PointerEvent) => {
(e.currentTarget as HTMLElement).classList.add('hover');
//
if (props.onLongPress) {
//
if (longPressTimer !== null) {
clearInterval(longPressTimer);
}
//
api.bee();
props.onLongPress();
//
longPressTimer = setInterval(() => {
if (props.onLongPress) {
if (props.longPressInterval > 150) api.bee();
props.onLongPress();
}
}, props.longPressInterval) as unknown as number;
}
};
const pUp = (e: PointerEvent) => {
(e.currentTarget as HTMLElement).classList.remove('hover');
//
if (longPressTimer !== null) {
clearInterval(longPressTimer);
longPressTimer = null;
}
};
</script>
<style scoped>
.btn {
cursor: pointer;
width: 96px;
height: 48px;
background-color: var(--btn-bg, white);
border: 1px solid black;
display: flex;
justify-content: center;
align-items: center;
color: #111111;
}
.btn.hover {
background-color: var(--btn-hover-bg, #DDDDDD);
}
</style>

View File

@ -0,0 +1,145 @@
<template>
<div ref="chart" class="echart-container"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch, nextTick } from 'vue';
import * as echarts from 'echarts';
// Define the shape based on provided data
interface MeasurementData {
ts: number;
n: number;
tscale: number;
toffs: number;
f0_hz: number;
amp1_pp_adc: number;
amp2_pp_adc: number;
phi1_rad: number;
phi2_rad: number;
dphi_rad: number;
dphi_deg: number;
wave1: number[];
wave2: number[];
[key: string]: any;
}
const props = defineProps<{
data: MeasurementData
}>();
const chart = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const initChart = () => {
if (chart.value && !chartInstance) {
chartInstance = echarts.init(chart.value);
}
updateChart();
};
const updateChart = () => {
if (!props.data || !props.data.wave1 || !props.data.wave2) {
chartInstance?.clear();
return;
}
const { wave1, wave2, n } = props.data;
const len = Math.min(wave1.length, wave2.length, n || 10000);
if (len === 0) return;
if (chartInstance) {
// Prepare [x, y] data
const lissajousData = [];
for (let i = 0; i < len; i++) {
lissajousData.push([wave1[i], wave2[i]]);
}
const option = {
animation: false,
tooltip: {
show: false
},
legend: {
show: false
},
grid: {
show: true,
backgroundColor: '#fff',
borderColor: '#333',
borderWidth: 1,
left: '4%',
right: '8%',
bottom: '4%',
top: '8%',
containLabel: false
},
xAxis: {
type: 'value',
name: 'CH1 (ADC)',
nameLocation: 'middle',
nameGap: 0,
nameTextStyle: { color: '#333', fontSize: 11 },
min: 0,
max: 255,
axisTick: { show: true },
axisLine: { show: true, lineStyle: { color: '#333' } },
axisLabel: { color: '#333' },
splitLine: { show: true, lineStyle: { color: '#ccc' } }
},
yAxis: {
type: 'value',
name: 'CH2 (ADC)',
nameLocation: 'middle',
nameGap: 0,
nameTextStyle: { color: '#333', fontSize: 11 },
min: 0,
max: 255,
axisTick: { show: true },
axisLine: { show: true, lineStyle: { color: '#333' } },
axisLabel: { color: '#333' },
splitLine: { show: true, lineStyle: { color: '#ccc' } }
},
series: [{
type: 'line',
smooth: 0.3,
symbol: 'none',
data: lissajousData,
lineStyle: {
color: '#749f83',
width: 2
}
}]
};
chartInstance.setOption(option);
}
};
onMounted(() => {
initChart();
window.addEventListener('resize', () => {
chartInstance?.resize();
});
});
watch(() => props.data, () => {
nextTick(() => {
if (!chartInstance) {
initChart();
} else {
updateChart();
}
});
}, { deep: true });
</script>
<style scoped>
.echart-container {
width: 100%;
height: 100%;
min-height: 200px;
overflow: hidden;
}
</style>

106
src/components/Machine.vue Normal file
View File

@ -0,0 +1,106 @@
<script setup lang="ts">
import { computed } from 'vue';
import { ArrowRight, ArrowLeft, MapPin } from 'lucide-vue-next';
const props = defineProps<{
dis: number; // 0 - 300
task?: {
remaining_steps: number;
} | undefined;
}>();
const targetDis = computed(() => {
if (!props.task) return null;
return (props.dis + props.task.remaining_steps) / 1600;
});
const isMovingRight = computed(() => {
return props.task && props.task.remaining_steps > 0;
});
</script>
<template>
<div class="machine-panel">
<div class="panel-title">装置位置示意图</div>
<div class="machine">
<div class="sender part">
发射器
<img src="../assets/发射器.png" alt="" draggable="false">
</div>
<div class="placeholder" :style="{ width: 80 + dis / 2 / 1600 + 'px' }">
<div class="current-val">{{ (dis / 1600).toFixed(3) }} mm</div>
<div class="arrow-container" :class="{ right: isMovingRight }"
v-if="task && task.remaining_steps !== 0">
<ArrowRight v-if="isMovingRight" :size="14" />
<ArrowLeft v-else :size="16" />
<span class="target-val">{{ targetDis?.toFixed(3) }}mm</span>
</div>
</div>
<div class="receiver part">
接收器
<img src="../assets/接收器.png" alt="" draggable="false">
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.machine-panel {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.part {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 10;
}
img {
user-select: none;
-webkit-user-drag: none;
left: 0;
top: 0;
height: 68px;
}
.placeholder {
position: relative;
border-bottom: 2px dashed #333333;
top: -19px;
height: 60px;
display: flex;
justify-content: center;
}
.current-val {
position: absolute;
bottom: 5px;
width: 100%;
text-align: center;
font-weight: bold;
}
.machine {
position: relative;
display: flex;
align-items: center;
padding-bottom: 6px 0;
margin-top: 20px;
}
.arrow-container {
display: flex;
justify-content: center;
align-items: center;
height: 30px;
font-size: 14px;
gap: 4px;
}
</style>

118
src/components/Numpad.vue Normal file
View File

@ -0,0 +1,118 @@
<script setup lang="ts">
import Button from './Button.vue';
import { Delete } from 'lucide-vue-next';
defineProps<{
modelValue: string;
label: string;
unit: string;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
}>();
const inputNumber = (val: string, current: string) => {
if (val === 'del') {
emit('update:modelValue', current.slice(0, -1));
} else if (val === '.') {
if (!current.includes('.')) {
emit('update:modelValue', current + val);
}
} else {
emit('update:modelValue', current + val);
}
};
</script>
<template>
<div class="numpad-container">
<div class="label">{{ label }}</div>
<div class="input-wrapper">
<input type="text" :value="modelValue" disabled class="target-input">
<span class="unit">{{ unit }}</span>
</div>
<div class="input-panel">
<Button class="num-key" @click="inputNumber('1', modelValue)">1</Button>
<Button class="num-key" @click="inputNumber('2', modelValue)">2</Button>
<Button class="num-key" @click="inputNumber('3', modelValue)">3</Button>
<Button class="num-key" @click="inputNumber('4', modelValue)">4</Button>
<Button class="num-key" @click="inputNumber('5', modelValue)">5</Button>
<Button class="num-key" @click="inputNumber('6', modelValue)">6</Button>
<Button class="num-key" @click="inputNumber('7', modelValue)">7</Button>
<Button class="num-key" @click="inputNumber('8', modelValue)">8</Button>
<Button class="num-key" @click="inputNumber('9', modelValue)">9</Button>
<Button class="num-key" @click="inputNumber('.', modelValue)">.</Button>
<Button class="num-key" @click="inputNumber('0', modelValue)">0</Button>
<Button class="num-key" @click="inputNumber('del', modelValue)">
<Delete :size="20" />
</Button>
</div>
</div>
</template>
<style scoped>
.numpad-container {
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
}
.label {
font-size: 24px;
}
.label::after {
content: '';
margin-left: 4px;
}
.input-wrapper {
position: relative;
width: 100%;
}
.unit {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
color: #666;
pointer-events: none;
}
.target-input {
width: 100%;
height: 40px;
font-size: 24px;
text-align: center;
border: 2px solid #808080;
background-color: white;
padding: 4px 8px;
box-sizing: border-box;
border-radius: 4px;
color: #000;
}
.input-panel {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
width: 100%;
}
.num-key {
width: 100%;
height: 48px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: bold;
cursor: pointer;
user-select: none;
}
</style>

View File

@ -0,0 +1,200 @@
<template>
<div class="oscilloscope-container">
<!-- Header / Metrics -->
<div class="top-row">
<Panel class="metrics-panel">
<div class="metric-item">
<div class="label">频率 F0</div>
<div class="value">{{ formatNum(data.f0_hz, 1) }} <span class="unit">Hz</span></div>
</div>
<div class="metric-item">
<div class="label">相位差 Δφ</div>
<div class="value">{{ formatNum(data.dphi_deg, 2) }} <span class="unit">°</span></div>
</div>
<div class="metric-item">
<div class="label">CH1 Vpp</div>
<div class="value">{{ formatNum(data.amp1_pp_adc, 0) }} <span class="unit">ADC</span></div>
</div>
<div class="metric-item">
<div class="label">CH2 Vpp</div>
<div class="value">{{ formatNum(data.amp2_pp_adc, 0) }} <span class="unit">ADC</span></div>
</div>
</Panel>
<div class="controls">
<slot name="controls"></slot>
</div>
</div>
<!-- Charts Table -->
<div class="charts-area">
<Panel class="chart-wrapper time-domain">
<div class="chart-header">
<div class="chart-title">原始波形</div>
<div class="chart-legend">
<span class="legend-item ch1">CH1</span>
<span class="legend-item ch2">CH2</span>
</div>
</div>
<WaveformChart :data="data" />
</Panel>
<Panel class="chart-wrapper lissajous">
<div class="chart-title">李萨如图形</div>
<Lissajous :data="data" />
</Panel>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted, nextTick } from 'vue';
import Panel from './Panel.vue';
import Lissajous from './Lissajous.vue';
import WaveformChart from './WaveformChart.vue';
// Define the shape based on provided data
interface MeasurementData {
ts: number;
n: number;
tscale: number;
toffs: number;
f0_hz: number;
amp1_pp_adc: number;
amp2_pp_adc: number;
phi1_rad: number;
phi2_rad: number;
dphi_rad: number;
dphi_deg: number;
wave1: number[];
wave2: number[];
[key: string]: any;
}
const props = defineProps<{
data: MeasurementData;
}>();
const formatNum = (num: number | undefined | null, digits: number) => {
if (num === undefined || num === null) return '--';
return num.toFixed(digits);
};
</script>
<style scoped lang="scss">
.oscilloscope-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 16px;
gap: 16px;
box-sizing: border-box;
// Inherits background from parent or set explicitly if needed
}
.top-row {
display: flex;
gap: 16px;
align-items: stretch;
}
.metrics-panel {
flex: 1;
display: flex;
justify-content: space-around;
padding: 12px 16px;
align-items: center;
.metric-item {
display: flex;
flex-direction: column;
align-items: center;
.label {
font-size: 14px;
color: #64748b;
margin-bottom: 4px;
}
.value {
font-size: 20px;
font-weight: 600;
color: #0f172a;
.unit {
font-size: 12px;
color: #94a3b8;
font-weight: normal;
margin-left: 2px;
}
}
}
}
.controls {
display: flex;
align-items: center;
justify-content: center;
}
.charts-area {
flex: 1;
display: flex;
gap: 16px;
min-height: 0; // Fix flex overflow
}
.chart-wrapper {
padding: 16px;
display: flex;
flex-direction: column;
&.time-domain {
flex: 3; // Takes more width
}
&.lissajous {
flex: 2; // Takes less width
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.chart-title {
font-weight: 600;
font-size: 16px;
color: #334155;
}
.chart-legend {
display: flex;
gap: 12px;
font-size: 12px;
.legend-item {
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
width: 12px;
height: 3px;
margin-right: 4px;
border-radius: 2px;
}
&.ch1::before { background-color: #f59e0b; }
&.ch2::before { background-color: #3b82f6; }
}
}
.echart-container {
flex: 1;
width: 100%;
min-height: 200px;
overflow: hidden;
}
}
</style>

16
src/components/Panel.vue Normal file
View File

@ -0,0 +1,16 @@
<template>
<div class="panel">
<slot></slot>
</div>
</template>
<style scoped>
.panel {
background-color: #ffffff;
padding: 4px 6px;
border: 1px solid #8ec4f7;
box-shadow: 0 0px 2px rgba(0, 0, 0, 0.384);
border-radius: 4px;
}
</style>

View File

@ -0,0 +1,169 @@
<template>
<div ref="chart" class="chart-container"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch, nextTick } from 'vue';
import * as echarts from 'echarts';
interface HistoryItem {
ts: number;
currentDis: number;
dphi_deg: number;
amp2_pp_adc: number;
f0_hz: number;
[key: string]: any;
}
const props = defineProps<{
data: HistoryItem[];
}>();
const emit = defineEmits<{
(e: 'select', index: number): void;
}>();
const chart = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const initChart = () => {
if (chart.value && !chartInstance) {
chartInstance = echarts.init(chart.value);
chartInstance.on('click', (params) => {
if (params.dataIndex !== undefined) {
emit('select', params.dataIndex);
}
});
}
};
const updateChart = () => {
if (!chartInstance) return;
const dataPhase = props.data.map(item => [(item.currentDis / 1600).toFixed(3), item.dphi_deg]);
const dataP2P = props.data.map(item => [(item.currentDis / 1600).toFixed(3), item.amp2_pp_adc]);
const option = {
animation: false,
legend: {
data: ['相位差 (Deg)', '峰峰值 (ADC)'],
show: true,
top: 0,
left: 'center',
textStyle: { color: '#333' }
},
grid: {
show: true,
backgroundColor: '#fff',
borderColor: '#333',
borderWidth: 1,
left: '8%',
right: '8%',
bottom: '10%',
top: '24',
containLabel: false
},
tooltip: {
trigger: 'axis',
showContent: false,
axisPointer: { type: 'cross' }
},
xAxis: {
type: 'category',
name: '距离 (mm)',
nameLocation: 'middle',
nameGap: 3,
nameTextStyle: { color: '#333', fontSize: 12 },
axisTick: { show: true, alignWithLabel: true },
axisLine: { show: true, lineStyle: { color: '#333' } },
axisLabel: { color: '#333' },
splitLine: { show: true, lineStyle: { color: '#ccc' } }
},
yAxis: [
{
type: 'value',
name: '相位差 (deg)',
nameLocation: 'middle',
nameGap: 45,
nameTextStyle: { color: '#333', fontSize: 12 },
position: 'left',
axisLine: { show: true, lineStyle: { color: '#333' } },
axisLabel: { color: '#333' },
splitLine: { show: true, lineStyle: { color: '#ccc' } }
},
{
type: 'value',
name: '峰峰值 (ADC)',
nameLocation: 'middle',
nameGap: 45,
nameTextStyle: { color: '#333', fontSize: 12 },
position: 'right',
axisLine: { show: true, lineStyle: { color: '#333' } },
axisLabel: { color: '#333' },
splitLine: { show: false }
}
],
series: [
{
name: '相位差 (Deg)',
type: 'line',
data: dataPhase,
yAxisIndex: 0,
smooth: 0.3,
symbol: 'circle',
symbolSize: 4,
itemStyle: { color: '#c23531' },
lineStyle: { color: '#c23531', width: 2 }
},
{
name: '峰峰值 (ADC)',
type: 'line',
data: dataP2P,
yAxisIndex: 1,
smooth: 0.3,
symbol: 'circle',
symbolSize: 4,
itemStyle: { color: '#2f4554' },
lineStyle: { color: '#2f4554', width: 2 }
}
]
};
chartInstance.setOption(option);
};
const showTip = (index: number) => {
if (!chartInstance) return;
chartInstance.dispatchAction({
type: 'showTip',
seriesIndex: 0,
dataIndex: index
});
};
onMounted(() => {
initChart();
window.addEventListener('resize', () => {
chartInstance?.resize();
});
if (props.data.length > 0) {
updateChart();
}
});
watch(() => props.data, () => {
nextTick(() => {
if (!chartInstance) initChart();
updateChart();
});
}, { deep: true });
defineExpose({ showTip });
</script>
<style scoped>
.chart-container {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,146 @@
<template>
<div ref="chart" class="echart-container"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch, nextTick } from 'vue';
import * as echarts from 'echarts';
interface MeasurementData {
ts: number;
n: number;
tscale: number;
toffs: number;
f0_hz: number;
amp1_pp_adc: number;
amp2_pp_adc: number;
phi1_rad: number;
phi2_rad: number;
dphi_rad: number;
dphi_deg: number;
wave1: number[];
wave2: number[];
[key: string]: any;
}
const props = defineProps<{
data: MeasurementData
}>();
const chart = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const initChart = () => {
if (chart.value && !chartInstance) {
chartInstance = echarts.init(chart.value);
}
updateChart();
};
const updateChart = () => {
if (!props.data || !props.data.wave1 || !props.data.wave2) {
chartInstance?.clear();
return;
}
const { wave1, wave2, n } = props.data;
const len = Math.min(wave1.length, wave2.length, n || 10000);
if (len === 0) return;
if (chartInstance) {
const optionTime = {
animation: false,
tooltip: {
show: false
},
legend: {
show: false
},
grid: {
show: true,
backgroundColor: '#fff',
borderColor: '#333',
borderWidth: 1,
left: '4%',
right: '8%',
bottom: '4%',
top: '8%',
containLabel: false
},
xAxis: {
type: 'category',
data: Array.from({length: len}, (_, i) => i),
boundaryGap: false,
name: '采样点',
nameLocation: 'middle',
nameGap: 0,
nameTextStyle: { color: '#333', fontSize: 11 },
axisTick: { show: true, alignWithLabel: true },
axisLine: { show: true, lineStyle: { color: '#333' } },
axisLabel: { show: true, interval: 'auto', color: '#333' },
splitLine: { show: true, lineStyle: { color: '#ccc' } }
},
yAxis: {
type: 'value',
name: 'ADC 值',
nameLocation: 'middle',
nameGap: 0,
nameTextStyle: { color: '#333', fontSize: 11 },
min: 0,
max: 255,
axisLine: { show: true, lineStyle: { color: '#333' } },
axisLabel: { color: '#333' },
splitLine: { show: true, lineStyle: { color: '#ccc' } },
inverse: true
},
series: [
{
name: 'CH1',
type: 'line',
data: wave1.slice(0, len),
smooth: 0.3,
symbol: 'none',
lineStyle: { width: 2, color: '#d48265' }
},
{
name: 'CH2',
type: 'line',
data: wave2.slice(0, len),
smooth: 0.3,
symbol: 'none',
lineStyle: { width: 2, color: '#61a0a8' }
}
]
};
chartInstance.setOption(optionTime);
}
};
onMounted(() => {
initChart();
window.addEventListener('resize', () => {
chartInstance?.resize();
});
});
watch(() => props.data, () => {
nextTick(() => {
if (!chartInstance) {
initChart();
} else {
updateChart();
}
});
}, { deep: true });
</script>
<style scoped>
.echart-container {
width: 100%;
height: 100%;
min-height: 200px;
overflow: hidden;
}
</style>

133
src/components/Window.vue Normal file
View File

@ -0,0 +1,133 @@
<template>
<div
class="window"
:style="{ left: position.x + 'px', top: position.y + 'px' }"
v-if="modelValue"
>
<div
class="title-bar"
@pointerdown="startDrag"
>
<div class="title-text">{{ title }}</div>
<Button class="close-btn" @click="close">×</Button>
</div>
<div class="window-body">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, onMounted, onUnmounted } from 'vue';
import Button from './Button.vue';
const props = withDefaults(
defineProps<{
title?: string;
modelValue: boolean;
}>(),
{
title: '窗口',
}
);
const emit = defineEmits<{
'update:modelValue': [value: boolean];
}>();
const position = reactive({ x: 0, y: 0 });
const dragState = reactive({
isDragging: false,
startX: 0,
startY: 0,
initialX: 0,
initialY: 0,
});
onMounted(() => {
//
position.x = (window.innerWidth - 300) / 2;
position.y = (window.innerHeight - 250) / 2;
});
const startDrag = (e: PointerEvent) => {
if ((e.target as HTMLElement).closest('.close-btn')) return;
dragState.isDragging = true;
dragState.startX = e.clientX;
dragState.startY = e.clientY;
dragState.initialX = position.x;
dragState.initialY = position.y;
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
document.addEventListener('pointermove', onDrag);
document.addEventListener('pointerup', stopDrag);
};
const onDrag = (e: PointerEvent) => {
if (!dragState.isDragging) return;
const deltaX = e.clientX - dragState.startX;
const deltaY = e.clientY - dragState.startY;
position.x = dragState.initialX + deltaX;
position.y = dragState.initialY + deltaY;
};
const stopDrag = () => {
dragState.isDragging = false;
document.removeEventListener('pointermove', onDrag);
document.removeEventListener('pointerup', stopDrag);
};
const close = () => {
emit('update:modelValue', false);
};
onUnmounted(() => {
document.removeEventListener('pointermove', onDrag);
document.removeEventListener('pointerup', stopDrag);
});
</script>
<style scoped>
.window {
position: fixed;
width: 400px;
border: 1px solid;
box-shadow: 1px 1px 0 #808080;
z-index: 1000;
}
.title-bar {
background: rgb(0, 0, 161);
color: white;
padding: 2px 2px 2px 8px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
user-select: none;
height: 32px;
}
.title-text {
font-size: 16px;
letter-spacing: 0.5px;
}
.close-btn {
width: 28px;
height: 28px;
}
.window-body {
padding: 12px;
background-color: rgb(221, 221, 221);
border-top: 1px solid #808080;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
</style>

7
src/env.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

9
src/main.ts Normal file
View File

@ -0,0 +1,9 @@
import { createApp } from 'vue'
import './style.css'
import './assets/stylesheet.css'
import App from './App.vue'
createApp(App).mount('#app')
document.body.addEventListener('contextmenu', event => event.preventDefault());
// alert(window.innerWidth + ", " + window.innerHeight);

403
src/pages/Control.vue Normal file
View File

@ -0,0 +1,403 @@
<script setup lang="ts">
import { ref } from 'vue';
import * as api from '../api.ts';
import Panel from '../components/Panel.vue';
import Button from '../components/Button.vue';
import Machine from '../components/Machine.vue';
import Lissajous from '../components/Lissajous.vue';
import Window from '../components/Window.vue';
import Numpad from '../components/Numpad.vue';
import { showMessage } from '../utils/message';
defineProps<{
state: any;
}>();
const controlStart = ref('10');
const controlEnd = ref('20');
const controlStep = ref('0.1');
const showNumpad = ref(false);
const numpadValue = ref('');
const numpadTitle = ref('');
let currentField: 'start' | 'end' | 'step' | null = null;
function openInput(field: 'start' | 'end' | 'step') {
currentField = field;
if (field === 'start') {
numpadTitle.value = '设置起点';
numpadValue.value = controlStart.value;
} else if (field === 'end') {
numpadTitle.value = '设置终点';
numpadValue.value = controlEnd.value;
} else if (field === 'step') {
numpadTitle.value = '设置步进';
numpadValue.value = controlStep.value;
}
showNumpad.value = true;
}
function confirmInput() {
if (currentField === 'start') controlStart.value = numpadValue.value;
else if (currentField === 'end') controlEnd.value = numpadValue.value;
else if (currentField === 'step') controlStep.value = numpadValue.value;
showNumpad.value = false;
}
const emit = defineEmits<{
(e: 'start'): void;
}>();
function startBatch(currentDis: number) {
const start = parseFloat(controlStart.value);
const end = parseFloat(controlEnd.value);
const step = parseFloat(controlStep.value);
if (isNaN(start) || isNaN(end) || isNaN(step)) {
showMessage('请输入有效的数字格式', 'error');
return;
}
if (step <= 0) {
showMessage('步进必须大于0', 'error');
return;
}
emit('start');
const tasks = [];
const currentSteps = currentDis;
const startSteps = start * 1600;
const diff = startSteps - currentSteps;
// 1.
if (Math.abs(diff) > 10) {
tasks.push({
cmd: 'move',
args: { steps: diff },
repeat: 1
});
}
// 2.
const totalDis = end - start;
const count = Math.floor(Math.abs(totalDis) / step);
if (count > 0) {
const dir = totalDis >= 0 ? 1 : -1;
const stepSteps = step * 1600 * dir;
tasks.push({
cmd: 'move_measure',
args: { steps: stepSteps },
repeat: count
});
}
api.batch(tasks).then(() => {
showMessage('批量测量任务已下发', 'success');
});
}
</script>
<template>
<div class="control-page-content">
<div class="left-section">
<!-- 顶部装置位置示意图 -->
<Panel class="machine-panel-wrapper">
<Machine :dis="state.dis"
:task="state.tasks.reduce((acc: any, task: any) => (task.type == 'move' ? ({ remaining_steps: acc.remaining_steps + task.remaining_steps }) : acc), { remaining_steps: 0 })" />
</Panel>
<!-- 下方李萨如 + 控制面板 -->
<div class="bottom-row">
<Panel class="oscilloscope-panel-wrapper">
<div class="chart-container">
<Lissajous :data="state.last_measurement" />
</div>
</Panel>
<Panel class="inputs-panel-wrapper">
<div class="input-container">
<div class="input-group">
<label>起点</label>
<div class="input-wrapper" @click="openInput('start')">
<div class="input-display">{{ controlStart || '10' }}</div>
<span class="unit">mm</span>
</div>
</div>
<div class="input-group">
<label>终点</label>
<div class="input-wrapper" @click="openInput('end')">
<div class="input-display">{{ controlEnd || '30' }}</div>
<span class="unit">mm</span>
</div>
</div>
<div class="input-group">
<label>步进</label>
<div class="input-wrapper" @click="openInput('step')">
<div class="input-display">{{ controlStep || '0.1' }}</div>
<span class="unit">mm</span>
</div>
</div>
<Button @click="startBatch(state.dis)" bg="limegreen" class="start-btn">开始任务</Button>
</div>
</Panel>
</div>
</div>
<div class="right-section">
<Panel class="task-list-panel">
<div class="task-head">任务列表 ({{ state.total_tasks }})</div>
<div class="task-list">
<div v-for="task in state.tasks.slice(-5)" :key="task.id" class="task-item">
<div class="task-info">
<span class="type" :class="task.type">{{ task.type === 'move' ? '移动' : '测量' }}</span>
<span class="status" :class="task.status">{{ task.status }}</span>
</div>
<div class="task-detail" v-if="task.type === 'move'">
{{ (task.steps / 1600).toFixed(1) }}mm
</div>
</div>
<div v-if="state.tasks.length === 0" class="empty-tasks">暂无任务</div>
</div>
</Panel>
</div>
<Window v-model="showNumpad" :title="numpadTitle">
<div class="numpad-content">
<Numpad v-model="numpadValue" :label="numpadTitle" unit="mm" />
<div class="numpad-actions">
<Button class="action-btn" @click="showNumpad = false">取消</Button>
<Button class="action-btn" bg="limegreen" @click="confirmInput">确定</Button>
</div>
</div>
</Window>
</div>
</template>
<style scoped lang="scss">
.control-page-content {
display: flex;
padding: 8px;
gap: 8px;
height: 100%;
box-sizing: border-box;
width: 100%;
overflow: hidden;
}
.left-section {
flex: 3;
display: flex;
flex-direction: column;
gap: 8px;
height: 100%;
min-width: 0; // Prevent flex overflow
}
.right-section {
flex: 1.5;
display: flex;
flex-direction: column;
height: 100%;
min-width: 0;
}
.machine-panel-wrapper {
flex: 0 0 auto; //
padding: 10px;
}
.bottom-row {
flex: 1;
display: flex;
gap: 8px;
min-height: 0; // Fix nested flex overflow
}
.oscilloscope-panel-wrapper {
flex: 3; //
overflow: hidden;
position: relative;
/* ensure inner absolute positioning works if needed, usually oscilloscope uses flex */
display: flex;
flex-direction: column;
padding: 4px;
}
.chart-title {
font-weight: 600;
font-size: 16px;
color: #334155;
margin-bottom: 8px;
}
.chart-container {
flex: 1;
min-height: 200px;
width: 100%;
}
.inputs-panel-wrapper {
flex: 2; //
display: flex;
flex-direction: column;
justify-content: center;
padding: 20px;
}
.input-container {
display: flex;
flex-direction: column;
gap: 16px;
width: 100%;
}
.input-group {
display: flex;
align-items: center;
justify-content: space-between;
label {
font-weight: bold;
font-size: 16px;
color: #333;
width: 50px;
}
.input-wrapper {
display: flex;
align-items: center;
gap: 6px;
}
.input-display {
width: 80px;
height: 40px;
border: 1px solid #ccc;
border-radius: 4px;
text-align: center;
font-size: 18px;
background: white;
line-height: 40px;
cursor: pointer;
}
.unit {
font-size: 14px;
color: #666;
width: 30px;
}
}
.start-btn {
margin-top: 10px;
height: 48px;
width: 100%;
font-size: 18px;
border-radius: 6px;
}
.numpad-content {
display: flex;
gap: 16px;
width: 100%;
}
.numpad-actions {
display: flex;
flex-direction: column;
justify-content: center;
gap: 12px;
min-width: 120px;
}
.action-btn {
width: 100%;
height: 52px;
font-size: 16px;
border-radius: 6px;
}
.task-list-panel {
flex: 1;
display: flex;
flex-direction: column;
padding: 10px;
height: 100%;
overflow: hidden;
.task-head {
font-weight: bold;
margin-bottom: 8px;
font-size: 16px;
border-bottom: 1px solid #eee;
padding-bottom: 6px;
}
.task-list {
flex: 1;
overflow-y: hidden;
display: flex;
flex-direction: column;
gap: 6px;
}
.task-item {
background: #f8fafc;
padding: 8px;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
border-left: 4px solid #cbd5e1;
.task-info {
display: flex;
gap: 8px;
align-items: center;
}
.type {
font-weight: bold;
font-size: 14px;
&.move {
color: #3b82f6;
}
&.measure {
color: #8b5cf6;
}
}
.status {
font-size: 12px;
padding: 2px 6px;
border-radius: 999px;
background: #e2e8f0;
&.running {
background: #dbface;
color: #166534;
border: 1px solid #166534;
}
&.pending {
background: #fef9c3;
color: #854d0e;
}
&.queued {
background: #e2e8f0;
color: #475569;
}
}
}
.empty-tasks {
text-align: center;
color: #999;
margin-top: 20px;
}
}
</style>

301
src/pages/History.vue Normal file
View File

@ -0,0 +1,301 @@
<script setup lang="ts">
import { ref, onMounted, watch, computed, nextTick } from 'vue';
import Panel from '../components/Panel.vue';
import Button from '../components/Button.vue';
import Lissajous from '../components/Lissajous.vue';
import WaveformChart from '../components/WaveformChart.vue';
import TrendChart from '../components/TrendChart.vue';
interface HistoryItem {
ts: number;
currentDis: number;
dphi_deg: number; // Phase
amp2_pp_adc: number; // P2P
f0_hz: number;
[key: string]: any;
}
const props = defineProps<{
history: HistoryItem[];
}>();
const emit = defineEmits<{
(e: 'clear'): void;
}>();
const selectedIndex = ref(0);
const trendChartRef = ref<InstanceType<typeof TrendChart> | null>(null);
const selectedItem = computed(() => {
if (selectedIndex.value >= 0 && selectedIndex.value < props.history.length) {
return props.history[selectedIndex.value];
}
return null;
});
const formatNum = (num: number | undefined, digits: number) => {
if (num === undefined || num === null) return '--';
return num.toFixed(digits);
};
const prev = () => {
if (selectedIndex.value > 0) {
selectedIndex.value--;
updateHighlight();
}
};
const next = () => {
if (selectedIndex.value < props.history.length - 1) {
selectedIndex.value++;
updateHighlight();
}
};
const onSelectPoint = (index: number) => {
selectedIndex.value = index;
updateHighlight();
};
const updateHighlight = () => {
trendChartRef.value?.showTip(selectedIndex.value);
};
watch(() => props.history, () => {
nextTick(() => {
// Auto select last point if none selected or just appended
if (props.history.length > 0 && (selectedIndex.value === -1 || selectedIndex.value === props.history.length - 2)) {
selectedIndex.value = props.history.length - 1;
updateHighlight();
}
});
}, { deep: true });
</script>
<template>
<div class="history-page">
<div class="charts-section">
<Panel class="trend-chart-panel">
<TrendChart ref="trendChartRef" :data="history" @select="onSelectPoint" />
</Panel>
</div>
<div class="detail-section">
<Panel class="waveform-panel">
<div class="waveform-container">
<WaveformChart v-if="selectedItem" :data="selectedItem" />
<div v-else class="no-data">暂无数据 / 未选择</div>
</div>
</Panel>
<Panel class="lissajous-panel">
<div class="lissajous-container">
<Lissajous v-if="selectedItem" :data="selectedItem" />
<div v-else class="no-data">暂无数据 / 未选择</div>
</div>
</Panel>
<Panel class="info-panel">
<div class="info-grid" v-if="selectedItem">
<div class="grid-item">
<div class="label">位置</div>
<div class="value">{{ (selectedItem.currentDis / 1600).toFixed(3) }} <span
class="unit">mm</span></div>
</div>
<div class="grid-item">
<div class="label">相位差</div>
<div class="value">{{ formatNum(selectedItem.dphi_deg, 2) }} <span class="unit">°</span></div>
</div>
<div class="grid-item">
<div class="label">CH2 Vpp</div>
<div class="value">{{ formatNum(selectedItem.amp2_pp_adc, 0) }} <span class="unit">ADC</span>
</div>
</div>
</div>
<div class="controls">
<div class="nav-btns">
<Button @long-press="prev" :long-press-interval="100" :disabled="selectedIndex <= 0"
class="nav-btn"></Button>
<Button @long-press="next" :long-press-interval="100"
:disabled="selectedIndex >= history.length - 1" class="nav-btn"></Button>
</div>
<Button bg="red" @click="$emit('clear')" class="clear-btn">清除历史</Button>
</div>
</Panel>
</div>
</div>
</template>
<style scoped lang="scss">
.history-page {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
gap: 8px;
padding: 8px;
box-sizing: border-box;
}
.charts-section {
flex: 3.5;
display: flex;
min-height: 0;
}
.trend-chart-panel {
flex: 1;
padding: 0px;
}
.chart {
width: 100%;
height: 100%;
}
.detail-section {
flex: 4;
display: flex;
gap: 8px;
min-height: 0;
}
.lissajous-panel {
flex: 1;
display: flex;
flex-direction: column;
padding: 10px;
.panel-title {
font-weight: bold;
color: #334155;
margin-bottom: 8px;
}
}
.lissajous-container {
flex: 1;
position: relative;
width: 100%;
}
.waveform-panel {
flex: 1;
display: flex;
flex-direction: column;
padding: 10px;
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.chart-title {
font-weight: 600;
font-size: 14px;
color: #334155;
}
.chart-legend {
display: flex;
gap: 12px;
font-size: 12px;
.legend-item {
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
width: 12px;
height: 3px;
margin-right: 4px;
border-radius: 2px;
}
&.ch1::before {
background-color: #f59e0b;
}
&.ch2::before {
background-color: #3b82f6;
}
}
}
}
.waveform-container {
flex: 1;
position: relative;
width: 100%;
}
.no-data {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #94a3b8;
}
.info-panel {
flex: 1;
display: flex;
flex-direction: column;
padding: 16px;
justify-content: space-between;
}
.info-grid {
display: flex;
flex-direction: column;
gap: 12px;
.grid-item {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #f1f5f9;
padding-bottom: 4px;
.label {
color: #64748b;
}
.value {
font-weight: bold;
font-size: 18px;
color: #0f172a;
}
.unit {
font-size: 12px;
color: #94a3b8;
font-weight: normal;
}
}
}
.controls {
display: flex;
flex-direction: column;
gap: 12px;
}
.nav-btns {
display: flex;
gap: 8px;
.nav-btn {
flex: 1;
height: 38px;
font-size: 20px;
}
}
.clear-btn {
width: 100%;
height: 34px;
}
</style>

0
src/pages/Home.vue Normal file
View File

15
src/style.css Normal file
View File

@ -0,0 +1,15 @@
body {
margin: 0;
}
* {
box-sizing: border-box;
touch-action: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
font-family: 'PingFang SC', sans-serif;
}
.panel-title{
font-size: 20px;
}

84
src/utils/message.ts Normal file
View File

@ -0,0 +1,84 @@
import { h, render, ref } from 'vue';
import Window from '../components/Window.vue';
import Button from '../components/Button.vue';
import { AlertTriangle, XOctagon, Info, CheckCircle2 } from 'lucide-vue-next';
export function showMessage(message: string, type: 'info' | 'error' | 'success' | 'warning' = 'info', title?: string) {
const container = document.createElement('div');
document.body.appendChild(container);
const destroy = () => {
render(null, container);
container.remove();
};
const onCloneWindow = () => {
// 先触发 Window 内部的关闭逻辑(如果是 v-if 控制,外部设为 false 即可)
// 这里直接销毁整个挂载节点
destroy();
}
let Icon = Info;
let color = '#3b82f6';
let defaultTitle = '提示';
switch (type) {
case 'error':
Icon = XOctagon;
color = '#ef4444';
defaultTitle = '错误';
break;
case 'success':
Icon = CheckCircle2;
color = '#22c55e';
defaultTitle = '成功';
break;
case 'warning':
Icon = AlertTriangle;
color = '#f59e0b';
defaultTitle = '警告';
break;
}
// Window 组件接收 modelValue 控制显示
// 我们创建一个 ref 传进去,虽然我们主要是靠销毁容器来关闭
const isVisible = ref(true);
const vnode = h(Window, {
modelValue: isVisible.value,
title: title || defaultTitle,
'onUpdate:modelValue': (val: boolean) => {
if (!val) onCloneWindow();
}
}, {
default: () => h('div', {
style: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: '20px',
gap: '15px',
minWidth: '240px'
}
}, [
h(Icon, { size: 48, color }),
h('div', {
style: {
fontSize: '18px',
color: '#333',
textAlign: 'center',
whiteSpace: 'pre-wrap'
}
}, message),
h(Button, {
onClick: onCloneWindow,
style: {
marginTop: '10px',
minWidth: '80px'
}
}, () => '确定')
])
});
render(vnode, container);
}

14
tsconfig.app.json Normal file
View File

@ -0,0 +1,14 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"],
/* Linting */
"strict": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/**/*.d.ts"]
}

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

26
tsconfig.node.json Normal file
View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

7
vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
})