记录与数据处理
This commit is contained in:
parent
73f948d4fc
commit
a228647152
46
bun.lock
46
bun.lock
@ -7,6 +7,7 @@
|
||||
"dependencies": {
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"echarts": "^6.0.0",
|
||||
"katex": "^0.16.22",
|
||||
"lucide-vue-next": "^0.562.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"vue": "^3.5.24",
|
||||
@ -16,6 +17,7 @@
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"sass-embedded": "^1.97.2",
|
||||
"ssh2-sftp-client": "^11.0.0",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.2.4",
|
||||
"vue-tsc": "^3.1.4",
|
||||
@ -209,8 +211,16 @@
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
|
||||
|
||||
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
|
||||
|
||||
"buffer-builder": ["buffer-builder@0.2.0", "", {}, "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"buildcheck": ["buildcheck@0.0.7", "", {}, "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA=="],
|
||||
|
||||
"camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
||||
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
@ -223,6 +233,12 @@
|
||||
|
||||
"colorjs.io": ["colorjs.io@0.5.2", "", {}, "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw=="],
|
||||
|
||||
"commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
||||
"concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="],
|
||||
|
||||
"cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
|
||||
@ -237,6 +253,8 @@
|
||||
|
||||
"entities": ["entities@7.0.0", "", {}, "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ=="],
|
||||
|
||||
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
|
||||
|
||||
"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=="],
|
||||
@ -253,12 +271,16 @@
|
||||
|
||||
"immutable": ["immutable@5.1.4", "", {}, "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="],
|
||||
|
||||
"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=="],
|
||||
@ -267,6 +289,8 @@
|
||||
|
||||
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
|
||||
|
||||
"nan": ["nan@2.25.0", "", {}, "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g=="],
|
||||
|
||||
"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=="],
|
||||
@ -289,18 +313,28 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
|
||||
|
||||
"qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"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=="],
|
||||
@ -345,8 +379,14 @@
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"ssh2": ["ssh2@1.17.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.23.0" } }, "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ=="],
|
||||
|
||||
"ssh2-sftp-client": ["ssh2-sftp-client@11.0.0", "", { "dependencies": { "concat-stream": "^2.0.0", "promise-retry": "^2.0.1", "ssh2": "^1.15.0" } }, "sha512-lOjgNYtioYquhtgyHwPryFNhllkuENjvCKkUXo18w/Q4UpEffCnEUBfiOTlwFdKIhG1rhrOGnA6DeKPSF2CP6w=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"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=="],
|
||||
@ -359,10 +399,16 @@
|
||||
|
||||
"tslib": ["tslib@2.3.0", "", {}, "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="],
|
||||
|
||||
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
|
||||
|
||||
"typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@ -2,9 +2,8 @@
|
||||
<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>
|
||||
<title>自动声速测定仪 - 首页</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@ -6,11 +6,14 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"upload": "bun scripts/deploy.mjs",
|
||||
"deploy": "bun run build && bun run upload"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"echarts": "^6.0.0",
|
||||
"katex": "^0.16.22",
|
||||
"lucide-vue-next": "^0.562.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"vue": "^3.5.24"
|
||||
@ -20,6 +23,7 @@
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"sass-embedded": "^1.97.2",
|
||||
"ssh2-sftp-client": "^11.0.0",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.2.4",
|
||||
"vue-tsc": "^3.1.4"
|
||||
|
||||
@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
49
scripts/deploy.mjs
Normal file
49
scripts/deploy.mjs
Normal file
@ -0,0 +1,49 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import Client from 'ssh2-sftp-client';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const HOST = process.env.DEPLOY_HOST ?? '192.168.1.71';
|
||||
const USERNAME = process.env.DEPLOY_USER ?? 'feie9454';
|
||||
const REMOTE_DIR = process.env.DEPLOY_PATH ?? '/var/www/html';
|
||||
const PASSWORD = process.env.DEPLOY_PASSWORD;
|
||||
|
||||
if (!PASSWORD) {
|
||||
console.error('Missing DEPLOY_PASSWORD env var.');
|
||||
console.error('Example (PowerShell): $env:DEPLOY_PASSWORD="<your password>"; npm run deploy');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const localDist = path.resolve(__dirname, '..', 'dist');
|
||||
|
||||
const sftp = new Client();
|
||||
|
||||
try {
|
||||
console.log(`Deploying ${localDist} -> ${USERNAME}@${HOST}:${REMOTE_DIR}`);
|
||||
|
||||
await sftp.connect({
|
||||
host: HOST,
|
||||
username: USERNAME,
|
||||
password: PASSWORD,
|
||||
readyTimeout: 20000,
|
||||
});
|
||||
|
||||
// Ensure remote directory exists
|
||||
await sftp.mkdir(REMOTE_DIR, true);
|
||||
|
||||
// Upload dist contents into REMOTE_DIR
|
||||
await sftp.uploadDir(localDist, REMOTE_DIR);
|
||||
|
||||
console.log('Deploy complete.');
|
||||
} catch (err) {
|
||||
console.error('Deploy failed:', err?.message ?? err);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
try {
|
||||
await sftp.end();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
216
src/App.vue
216
src/App.vue
@ -1,12 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, type Ref, watch } from 'vue';
|
||||
import * as api from './api.ts'
|
||||
import type { HistoryItem } from './types.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() });
|
||||
|
||||
@ -26,18 +24,21 @@ const showLogin = ref(false);
|
||||
const showManualAdjust = ref(false);
|
||||
const targetDis = ref('');
|
||||
|
||||
import { Delete, RefreshCcw, Settings, ChevronRight } from 'lucide-vue-next';
|
||||
import { Delete, RefreshCcw, Settings, ChevronRight, LogOut } 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 Home from './pages/Home.vue';
|
||||
import Measure from './pages/Measure.vue';
|
||||
import TrendChart from './components/TrendChart.vue';
|
||||
import DataProcess from './pages/DataProcess.vue';
|
||||
|
||||
const showSettings = ref(false);
|
||||
const showSpeedSettings = ref(false);
|
||||
const targetSpeed = ref('');
|
||||
|
||||
const historyData = ref<any[]>([]);
|
||||
const historyData = ref<HistoryItem[]>([]);
|
||||
|
||||
// 监听新的测量数据
|
||||
watch(() => state.last_measurement, (newVal, oldVal) => {
|
||||
@ -116,14 +117,20 @@ function locationReload() {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
// 'home' | 'measure' | 'control' | 'history'
|
||||
const pageIndex: Ref<0 | 1 | 2 | 3> = ref(0);
|
||||
function exitSystem() {
|
||||
document.exitFullscreen();
|
||||
window.close();
|
||||
}
|
||||
|
||||
const mountedPages = ref([true, false, false, false]);
|
||||
const pageNames = ['首页', '波形', '控制', '记录', '处理'];
|
||||
const pageIndex: Ref<0 | 1 | 2 | 3 | 4> = ref(0);
|
||||
|
||||
const mountedPages = ref([true, false, false, false, false]);
|
||||
let cleanupTimer: any = null;
|
||||
|
||||
watch(pageIndex, (newVal, oldVal) => {
|
||||
// Ensure target page is mounted
|
||||
document.title = `自动声速测定仪 - ${pageNames[newVal]}`;
|
||||
mountedPages.value[newVal] = true;
|
||||
|
||||
// Ensure old page stays mounted during transition
|
||||
@ -134,7 +141,7 @@ watch(pageIndex, (newVal, oldVal) => {
|
||||
if (cleanupTimer) clearTimeout(cleanupTimer);
|
||||
cleanupTimer = setTimeout(() => {
|
||||
// Keep only the current page mounted to save resources
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
mountedPages.value[i] = (i === pageIndex.value);
|
||||
}
|
||||
}, 400); // 0.3s transition + buffer
|
||||
@ -145,12 +152,16 @@ watch(pageIndex, (newVal, oldVal) => {
|
||||
<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 === 1 }" @click="pageIndex = 1">波形</Button>
|
||||
<Button :class="{ current: pageIndex === 2 }" @click="pageIndex = 2">控制</Button>
|
||||
<Button :class="{ current: pageIndex === 3 }" @click="pageIndex = 3">历史</Button>
|
||||
<Button :class="{ current: pageIndex === 3 }" @click="pageIndex = 3">记录</Button>
|
||||
<Button :class="{ current: pageIndex === 4 }" @click="pageIndex = 4">处理</Button>
|
||||
</div>
|
||||
|
||||
<div class="page-title">
|
||||
<div class="connection-state" :class="{ connected: state.connected, disconnected: !state.connected }"></div>
|
||||
<span>自动声速测定仪</span>
|
||||
自动声速测定仪
|
||||
</div>
|
||||
<div class="actions">
|
||||
<Button class="reload" @click="locationReload">
|
||||
<RefreshCcw />
|
||||
@ -163,57 +174,22 @@ watch(pageIndex, (newVal, oldVal) => {
|
||||
</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>
|
||||
<Home v-if="mountedPages[0]" :state="state" :history-data="historyData"
|
||||
@show-manual-adjust="showManualAdjust = true" @change-page="pageIndex = $event" />
|
||||
</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>
|
||||
<Measure v-if="mountedPages[1]" :state="state" />
|
||||
</div>
|
||||
<div class="page page-control" :style="{ transform: `translateX(${(-pageIndex + 2) * 100}%)` }">
|
||||
<Control v-if="mountedPages[2]" :state="state" @start="clearHistory" />
|
||||
<Control v-if="mountedPages[2]" :state="state" @start="clearHistory"
|
||||
@show-motor-speed-setting="showSpeedSettings = true" @show-manual-adjust="showManualAdjust = true" />
|
||||
</div>
|
||||
<div class="page page-history" :style="{ transform: `translateX(${(-pageIndex + 3) * 100}%)` }">
|
||||
<History v-if="mountedPages[3]" :history="historyData" @clear="clearHistory" />
|
||||
</div>
|
||||
<div class="page page-data-process" :style="{ transform: `translateX(${(-pageIndex + 4) * 100}%)` }">
|
||||
<DataProcess v-if="mountedPages[4]" :history-data="historyData" />
|
||||
</div>
|
||||
</main>
|
||||
<Window v-model="showLogin" title="扫码登录">
|
||||
<img :src="qrcodeSrc" alt="" class="qr-img">
|
||||
@ -239,6 +215,12 @@ watch(pageIndex, (newVal, oldVal) => {
|
||||
<ChevronRight :size="20" class="arrow" />
|
||||
</div>
|
||||
</Button>
|
||||
<Button class="settings-item" @click="exitSystem">
|
||||
<span class="settings-label">退出系统</span>
|
||||
<div class="settings-value">
|
||||
<LogOut :size="20" class="arrow" />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</Window>
|
||||
|
||||
@ -262,22 +244,8 @@ header {
|
||||
align-items: center;
|
||||
color: white;
|
||||
position: relative;
|
||||
padding-left: 40px;
|
||||
|
||||
.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;
|
||||
@ -305,6 +273,29 @@ header {
|
||||
}
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.connection-state {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: gray;
|
||||
|
||||
&.connected {
|
||||
background-color: limegreen;
|
||||
}
|
||||
|
||||
&.disconnected {
|
||||
background-color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-label {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
@ -314,7 +305,7 @@ header {
|
||||
gap: 4px;
|
||||
|
||||
.btn {
|
||||
width: 64px;
|
||||
width: 56px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
|
||||
@ -354,52 +345,10 @@ header {
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
height: calc(100% - 36px);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@ -419,43 +368,6 @@ main {
|
||||
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;
|
||||
|
||||
71
src/api.ts
71
src/api.ts
@ -1,4 +1,8 @@
|
||||
const API_BASE = 'http://localhost:8000';
|
||||
import type { AppState, BatchTask } from './types';
|
||||
|
||||
const url = new URL(window.location.origin);
|
||||
url.port = '8000';
|
||||
const API_BASE = url.toString().endsWith('/') ? url.toString().slice(0, -1) : url.toString();
|
||||
|
||||
async function ping() {
|
||||
return await fetch(`${API_BASE}/ping`);
|
||||
@ -8,55 +12,36 @@ async function bee() {
|
||||
return await fetch(`${API_BASE}/bee`);
|
||||
}
|
||||
|
||||
const _state = {
|
||||
// 连接状态
|
||||
const _state: AppState = {
|
||||
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[],
|
||||
tasks: [],
|
||||
last_measurement: {
|
||||
ts: 0,
|
||||
idn: null,
|
||||
points_mode: 'NORM',
|
||||
n: 0,
|
||||
tscale: 0,
|
||||
toffs: 0,
|
||||
f0_hz: 0,
|
||||
amp1_pp_adc: 0,
|
||||
amp2_pp_adc: 0,
|
||||
phi1_rad: 0,
|
||||
phi2_rad: 0,
|
||||
dphi_rad: 0,
|
||||
dphi_deg: 0,
|
||||
dt_s: 0,
|
||||
wave1: [],
|
||||
wave2: [],
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: ((state: typeof _state) => void)[] = [];
|
||||
const listeners: ((state: AppState) => void)[] = [];
|
||||
|
||||
function notifyListeners() {
|
||||
for (const listener of listeners) {
|
||||
@ -65,7 +50,7 @@ function notifyListeners() {
|
||||
}
|
||||
|
||||
function initSSE() {
|
||||
let reconnectTimer: NodeJS.Timeout;
|
||||
let reconnectTimer: any;
|
||||
|
||||
const connect = () => {
|
||||
const eventSource = new EventSource(`${API_BASE}/events`);
|
||||
@ -114,7 +99,7 @@ function getState() {
|
||||
return _state;
|
||||
}
|
||||
|
||||
async function onStateChange(callback: (state: typeof _state) => void) {
|
||||
async function onStateChange(callback: (state: AppState) => void) {
|
||||
listeners.push(callback);
|
||||
// 立即回调当前状态
|
||||
callback(_state);
|
||||
@ -175,7 +160,7 @@ async function measure() {
|
||||
});
|
||||
}
|
||||
|
||||
async function batch(tasks: any[]) {
|
||||
async function batch(tasks: BatchTask[]) {
|
||||
return await fetch(`${API_BASE}/action/batch`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
||||
Binary file not shown.
@ -1,35 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@ -1,8 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'PingFang SC';
|
||||
src: url('_-.woff2') format('woff2');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 496 B |
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 436 KiB |
43
src/components/Formula.vue
Normal file
43
src/components/Formula.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import katex from 'katex';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
tex: string;
|
||||
displayMode?: boolean;
|
||||
}>(),
|
||||
{
|
||||
displayMode: false,
|
||||
}
|
||||
);
|
||||
|
||||
const html = computed(() => {
|
||||
try {
|
||||
return katex.renderToString(props.tex, {
|
||||
displayMode: props.displayMode,
|
||||
throwOnError: false,
|
||||
strict: 'ignore',
|
||||
trust: false,
|
||||
output: 'html',
|
||||
});
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="formula" :class="{ block: displayMode }" v-html="html"></span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.formula {
|
||||
display: inline-block;
|
||||
vertical-align: -0.12em;
|
||||
}
|
||||
|
||||
.formula.block {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@ -84,6 +84,7 @@ img {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.machine {
|
||||
|
||||
50
src/components/ManualAdjust.vue
Normal file
50
src/components/ManualAdjust.vue
Normal file
@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import * as api from '../api.ts';
|
||||
import Button from './Button.vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'showInput'): void;
|
||||
}>();
|
||||
import { ArrowLeft, ArrowRight } from 'lucide-vue-next';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="manual-adjust">
|
||||
<div class="line">
|
||||
<Button class="man-adj" @long-press="api.move(-40)"><ArrowLeft /></Button>
|
||||
<span>手动<br>调整</span>
|
||||
<Button class="man-adj" @long-press="api.move(40)"><ArrowRight /></Button>
|
||||
</div>
|
||||
<Button class="input-dis" @click="emit('showInput')">
|
||||
输入
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.manual-adjust {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
|
||||
.line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.man-adj {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
font-size: 24px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.input-dis {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -29,7 +29,7 @@ const inputNumber = (val: string, current: string) => {
|
||||
<div class="numpad-container">
|
||||
<div class="label">{{ label }}</div>
|
||||
<div class="input-wrapper">
|
||||
<input type="text" :value="modelValue" disabled class="target-input">
|
||||
<div class="target-input" aria-disabled="true">{{ modelValue || '\u00A0' }}</div>
|
||||
<span class="unit">{{ unit }}</span>
|
||||
</div>
|
||||
<div class="input-panel">
|
||||
@ -86,10 +86,12 @@ const inputNumber = (val: string, current: string) => {
|
||||
.target-input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
border: 2px solid #808080;
|
||||
background-color: white;
|
||||
background-color: white !important;
|
||||
padding: 4px 8px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
|
||||
@ -19,29 +19,41 @@ 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;
|
||||
let chartIndexToRawIndex: number[] = [];
|
||||
let rawIndexToChartIndex: number[] = [];
|
||||
|
||||
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 dataPhase: ([(string | number), number] | null)[] = [];
|
||||
const dataP2P = props.data.map(item => [(item.currentDis / 1600).toFixed(3), item.amp2_pp_adc]);
|
||||
chartIndexToRawIndex = [];
|
||||
rawIndexToChartIndex = [];
|
||||
|
||||
for (let i = 0; i < props.data.length; i++) {
|
||||
const item = props.data[i]!;
|
||||
const x = (item.currentDis / 1600).toFixed(3);
|
||||
const y = item.dphi_deg;
|
||||
|
||||
// 检测相位跳变(阈值设为 180 度)
|
||||
if (i > 0 && Math.abs(y - props.data[i - 1]!.dphi_deg) > 180) {
|
||||
// 在跳变处插入 null 以断开线条
|
||||
dataPhase.push(null);
|
||||
}
|
||||
|
||||
rawIndexToChartIndex[i] = dataPhase.length;
|
||||
chartIndexToRawIndex[dataPhase.length] = i;
|
||||
dataPhase.push([x, y]);
|
||||
}
|
||||
|
||||
const option = {
|
||||
animation: false,
|
||||
@ -112,6 +124,7 @@ const updateChart = () => {
|
||||
smooth: 0.3,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
connectNulls: false,
|
||||
itemStyle: { color: '#c23531' },
|
||||
lineStyle: { color: '#c23531', width: 2 }
|
||||
},
|
||||
@ -134,10 +147,12 @@ const updateChart = () => {
|
||||
|
||||
const showTip = (index: number) => {
|
||||
if (!chartInstance) return;
|
||||
const chartDataIndex = rawIndexToChartIndex[index];
|
||||
if (chartDataIndex === undefined) return;
|
||||
chartInstance.dispatchAction({
|
||||
type: 'showTip',
|
||||
seriesIndex: 0,
|
||||
dataIndex: index
|
||||
dataIndex: chartDataIndex
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -3,13 +3,16 @@
|
||||
class="window"
|
||||
:style="{ left: position.x + 'px', top: position.y + 'px' }"
|
||||
v-if="modelValue"
|
||||
ref="windowEl"
|
||||
>
|
||||
<div
|
||||
class="title-bar"
|
||||
@pointerdown="startDrag"
|
||||
>
|
||||
<div class="title-text">{{ title }}</div>
|
||||
<Button class="close-btn" @click="close">×</Button>
|
||||
<Button class="close-btn" @click="close">
|
||||
<X color="black" />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="window-body">
|
||||
<slot></slot>
|
||||
@ -18,8 +21,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted, onUnmounted } from 'vue';
|
||||
import { reactive, onMounted, onUnmounted, ref } from 'vue';
|
||||
import Button from './Button.vue';
|
||||
import { X } from 'lucide-vue-next'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@ -35,6 +39,8 @@ const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
}>();
|
||||
|
||||
const windowEl = ref<HTMLElement | null>(null);
|
||||
|
||||
const position = reactive({ x: 0, y: 0 });
|
||||
const dragState = reactive({
|
||||
isDragging: false,
|
||||
@ -42,12 +48,64 @@ const dragState = reactive({
|
||||
startY: 0,
|
||||
initialX: 0,
|
||||
initialY: 0,
|
||||
invLinearMatrix: null as DOMMatrix | null,
|
||||
});
|
||||
|
||||
const getLinearTransformMatrix = (el: HTMLElement): DOMMatrix => {
|
||||
const style = window.getComputedStyle(el);
|
||||
const t = style.transform;
|
||||
const matrix = t && t !== 'none' ? new DOMMatrix(t) : new DOMMatrix();
|
||||
// 只需要线性部分(旋转/缩放/倾斜),平移对“增量向量”没有影响
|
||||
matrix.m41 = 0;
|
||||
matrix.m42 = 0;
|
||||
matrix.m43 = 0;
|
||||
return matrix;
|
||||
};
|
||||
|
||||
const findTransformedAncestor = (el: HTMLElement | null): HTMLElement | null => {
|
||||
let cur = el?.parentElement ?? null;
|
||||
while (cur) {
|
||||
const style = window.getComputedStyle(cur);
|
||||
if (
|
||||
(style.transform && style.transform !== 'none') ||
|
||||
(style.perspective && style.perspective !== 'none') ||
|
||||
(style.filter && style.filter !== 'none')
|
||||
) {
|
||||
return cur;
|
||||
}
|
||||
cur = cur.parentElement;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getCumulativeLinearMatrixToViewport = (fromEl: HTMLElement | null): DOMMatrix => {
|
||||
// 累乘 fromEl 以及其祖先的 transform(只保留线性部分),得到“本地 -> 屏幕”的线性变换
|
||||
let cur = fromEl;
|
||||
let combined = new DOMMatrix();
|
||||
while (cur) {
|
||||
const t = getLinearTransformMatrix(cur);
|
||||
combined = t.multiply(combined);
|
||||
cur = cur.parentElement;
|
||||
}
|
||||
return combined;
|
||||
};
|
||||
|
||||
const screenDeltaToLocalDelta = (invLinearMatrix: DOMMatrix | null, dx: number, dy: number) => {
|
||||
if (!invLinearMatrix) return { x: dx, y: dy };
|
||||
// 用两个点相减的方式消掉平移(虽然我们已将平移清零,但这样更稳)
|
||||
const p0 = new DOMPoint(0, 0);
|
||||
const p1 = new DOMPoint(dx, dy);
|
||||
const lp0 = p0.matrixTransform(invLinearMatrix);
|
||||
const lp1 = p1.matrixTransform(invLinearMatrix);
|
||||
return { x: lp1.x - lp0.x, y: lp1.y - lp0.y };
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 居中显示
|
||||
position.x = (window.innerWidth - 300) / 2;
|
||||
position.y = (window.innerHeight - 250) / 2;
|
||||
// 居中显示:优先使用“最近的变换祖先”的本地尺寸(更符合 rotate 容器内的坐标系)
|
||||
const container = findTransformedAncestor(windowEl.value) ?? windowEl.value?.parentElement;
|
||||
const w = container?.clientWidth ?? window.innerWidth;
|
||||
position.x = (w - 400) / 2;
|
||||
position.y = 50;
|
||||
});
|
||||
|
||||
const startDrag = (e: PointerEvent) => {
|
||||
@ -58,6 +116,15 @@ const startDrag = (e: PointerEvent) => {
|
||||
dragState.initialX = position.x;
|
||||
dragState.initialY = position.y;
|
||||
|
||||
// 若父容器(或其祖先)有 rotate/scale 等 transform,需要把“屏幕增量”逆变换回本地增量
|
||||
const container = findTransformedAncestor(windowEl.value);
|
||||
const toViewport = getCumulativeLinearMatrixToViewport(container);
|
||||
try {
|
||||
dragState.invLinearMatrix = toViewport.inverse();
|
||||
} catch {
|
||||
dragState.invLinearMatrix = null;
|
||||
}
|
||||
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
document.addEventListener('pointermove', onDrag);
|
||||
document.addEventListener('pointerup', stopDrag);
|
||||
@ -69,12 +136,14 @@ const onDrag = (e: PointerEvent) => {
|
||||
const deltaX = e.clientX - dragState.startX;
|
||||
const deltaY = e.clientY - dragState.startY;
|
||||
|
||||
position.x = dragState.initialX + deltaX;
|
||||
position.y = dragState.initialY + deltaY;
|
||||
const localDelta = screenDeltaToLocalDelta(dragState.invLinearMatrix, deltaX, deltaY);
|
||||
position.x = dragState.initialX + localDelta.x;
|
||||
position.y = dragState.initialY + localDelta.y;
|
||||
};
|
||||
|
||||
const stopDrag = () => {
|
||||
dragState.isDragging = false;
|
||||
dragState.invLinearMatrix = null;
|
||||
document.removeEventListener('pointermove', onDrag);
|
||||
document.removeEventListener('pointerup', stopDrag);
|
||||
};
|
||||
@ -108,6 +177,7 @@ onUnmounted(() => {
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
height: 32px;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
@ -123,8 +193,8 @@ onUnmounted(() => {
|
||||
|
||||
.window-body {
|
||||
padding: 12px;
|
||||
background-color: rgb(221, 221, 221);
|
||||
border-top: 1px solid #808080;
|
||||
background-color: rgb(226, 226, 226);
|
||||
border-top: 1px solid #c8c8c8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
18
src/main.ts
18
src/main.ts
@ -1,9 +1,25 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import './assets/stylesheet.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import App from './App.vue'
|
||||
|
||||
function resize() {
|
||||
const app = document.getElementById('app');
|
||||
if (!app) return;
|
||||
let { innerWidth: w, innerHeight: h } = window;
|
||||
let isRotate = false;
|
||||
if (h > w) { [w, h] = [h, w]; isRotate = true; };
|
||||
let scaleRatio = 1
|
||||
if (h < 500) { scaleRatio = h / 500; h = 500; w = w / scaleRatio; }
|
||||
app.style.width = w + 'px';
|
||||
app.style.height = h + 'px';
|
||||
app.style.transform = isRotate ? `translateX(${h * scaleRatio}px) rotate(90deg) scale(${scaleRatio})` : 'none';
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
createApp(App).mount('#app')
|
||||
|
||||
document.body.addEventListener('contextmenu', event => event.preventDefault());
|
||||
// alert(window.innerWidth + ", " + window.innerHeight);
|
||||
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import * as api from '../api.ts';
|
||||
import type { AppState, BatchTask } from '../types.ts';
|
||||
import Panel from '../components/Panel.vue';
|
||||
import Button from '../components/Button.vue';
|
||||
import Machine from '../components/Machine.vue';
|
||||
import ManualAdjust from '../components/ManualAdjust.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;
|
||||
state: AppState;
|
||||
}>();
|
||||
|
||||
const controlStart = ref('10');
|
||||
@ -46,6 +48,8 @@ function confirmInput() {
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'start'): void;
|
||||
(e: 'showManualAdjust'): void;
|
||||
(e: 'showMotorSpeedSetting'): void;
|
||||
}>();
|
||||
|
||||
function startBatch(currentDis: number) {
|
||||
@ -64,7 +68,7 @@ function startBatch(currentDis: number) {
|
||||
|
||||
emit('start');
|
||||
|
||||
const tasks = [];
|
||||
const tasks: BatchTask[] = [];
|
||||
const currentSteps = currentDis;
|
||||
const startSteps = start * 1600;
|
||||
const diff = startSteps - currentSteps;
|
||||
@ -103,8 +107,9 @@ function startBatch(currentDis: number) {
|
||||
<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 })" />
|
||||
<Machine :dis="state.dis" :task="state.tasks.find(t => t.type == 'move')" />
|
||||
<ManualAdjust @show-input="emit('showManualAdjust')" class="manual-adjust-wrapper" />
|
||||
|
||||
</Panel>
|
||||
|
||||
<!-- 下方:李萨如 + 控制面板 -->
|
||||
@ -118,25 +123,34 @@ function startBatch(currentDis: number) {
|
||||
<div class="input-container">
|
||||
<div class="input-group">
|
||||
<label>起点</label>
|
||||
<div class="input-wrapper" @click="openInput('start')">
|
||||
<div class="input-display">{{ controlStart || '10' }}</div>
|
||||
<div class="input-wrapper">
|
||||
<Button class="input-display" @click="openInput('start')">{{ controlStart || '10' }}</Button>
|
||||
<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>
|
||||
<div class="input-wrapper">
|
||||
<Button class="input-display" @click="openInput('end')">{{ controlEnd || '30' }}</Button>
|
||||
<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>
|
||||
<div class="input-wrapper">
|
||||
<Button class="input-display" @click="openInput('step')">{{ controlStep || '0.1' }}</Button>
|
||||
<span class="unit">mm</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>电机速度</label>
|
||||
<div class="input-wrapper">
|
||||
<Button class="input-display" @click="emit('showMotorSpeedSetting')">{{ state.speed ? Math.round(state.speed / 1.6) : '--'
|
||||
}}</Button>
|
||||
<span class="unit">μm/s</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button @click="startBatch(state.dis)" bg="limegreen" class="start-btn">开始任务</Button>
|
||||
</div>
|
||||
</Panel>
|
||||
@ -153,11 +167,12 @@ function startBatch(currentDis: number) {
|
||||
<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
|
||||
{{ (task.steps / 1600).toFixed(3) }}mm
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="state.tasks.length === 0" class="empty-tasks">暂无任务</div>
|
||||
</div>
|
||||
<Button class="action-btn" @click="api.stopAll()" bg="red">停止</Button>
|
||||
</Panel>
|
||||
</div>
|
||||
|
||||
@ -204,6 +219,11 @@ function startBatch(currentDis: number) {
|
||||
.machine-panel-wrapper {
|
||||
flex: 0 0 auto; // 不要压缩
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.manual-adjust-wrapper {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.bottom-row {
|
||||
@ -238,7 +258,7 @@ function startBatch(currentDis: number) {
|
||||
|
||||
|
||||
.inputs-panel-wrapper {
|
||||
flex: 2; // 控制区
|
||||
flex: 2.5; // 控制区
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@ -248,7 +268,7 @@ function startBatch(currentDis: number) {
|
||||
.input-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -261,7 +281,7 @@ function startBatch(currentDis: number) {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
width: 50px;
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
@ -273,13 +293,6 @@ function startBatch(currentDis: number) {
|
||||
.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 {
|
||||
@ -290,8 +303,8 @@ function startBatch(currentDis: number) {
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
margin-top: 10px;
|
||||
height: 48px;
|
||||
margin-top: 4px;
|
||||
height: 36px;
|
||||
width: 100%;
|
||||
font-size: 18px;
|
||||
border-radius: 6px;
|
||||
|
||||
1006
src/pages/DataProcess.vue
Normal file
1006
src/pages/DataProcess.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,19 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, computed, nextTick } from 'vue';
|
||||
import type { HistoryItem } from '../types.ts';
|
||||
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;
|
||||
}
|
||||
import { ArrowLeft, ArrowRight } from 'lucide-vue-next';
|
||||
|
||||
const props = defineProps<{
|
||||
history: HistoryItem[];
|
||||
@ -52,22 +45,14 @@ const next = () => {
|
||||
}
|
||||
};
|
||||
|
||||
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 });
|
||||
|
||||
@ -77,7 +62,7 @@ watch(() => props.history, () => {
|
||||
<div class="history-page">
|
||||
<div class="charts-section">
|
||||
<Panel class="trend-chart-panel">
|
||||
<TrendChart ref="trendChartRef" :data="history" @select="onSelectPoint" />
|
||||
<TrendChart ref="trendChartRef" :data="history" />
|
||||
</Panel>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
@ -113,9 +98,13 @@ watch(() => props.history, () => {
|
||||
<div class="controls">
|
||||
<div class="nav-btns">
|
||||
<Button @long-press="prev" :long-press-interval="100" :disabled="selectedIndex <= 0"
|
||||
class="nav-btn">◀</Button>
|
||||
class="nav-btn">
|
||||
<ArrowLeft />
|
||||
</Button>
|
||||
<Button @long-press="next" :long-press-interval="100"
|
||||
:disabled="selectedIndex >= history.length - 1" class="nav-btn">▶</Button>
|
||||
:disabled="selectedIndex >= history.length - 1" class="nav-btn">
|
||||
<ArrowRight />
|
||||
</Button>
|
||||
</div>
|
||||
<Button bg="red" @click="$emit('clear')" class="clear-btn">清除历史</Button>
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import * as api from '../api.ts';
|
||||
import type { AppState, HistoryItem } from '../types.ts';
|
||||
import Button from '../components/Button.vue';
|
||||
import Panel from '../components/Panel.vue';
|
||||
import Machine from '../components/Machine.vue';
|
||||
import ManualAdjust from '../components/ManualAdjust.vue';
|
||||
import WaveformChart from '../components/WaveformChart.vue';
|
||||
|
||||
import { RefreshCcw } from 'lucide-vue-next';
|
||||
|
||||
defineProps<{
|
||||
state: AppState;
|
||||
historyData: HistoryItem[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'showManualAdjust'): void;
|
||||
(e: 'changePage', page: 2): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-home-content">
|
||||
<div class="left">
|
||||
<Panel style="flex: 1;">
|
||||
<WaveformChart :data="state.last_measurement" />
|
||||
<Button @click="api.measure()" style="position: absolute; top: 10px; right: 10px; width: 32px; height: 32px;">
|
||||
<RefreshCcw />
|
||||
</Button>
|
||||
</Panel>
|
||||
<Panel>
|
||||
<Machine :dis="state.dis" :task="state.tasks.find(t => t.type == 'move')" />
|
||||
</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">
|
||||
<ManualAdjust @show-input="emit('showManualAdjust')" />
|
||||
<div class="right">
|
||||
<Button class="action-btn" @click="emit('changePage', 2)" bg="limegreen">开始实验</Button>
|
||||
<Button class="action-btn" @click="api.stopAll()" bg="red">停止</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-home-content {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.left {
|
||||
flex: 3;
|
||||
position: relative;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
28
src/pages/Measure.vue
Normal file
28
src/pages/Measure.vue
Normal file
@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import * as api from '../api.ts';
|
||||
import type { AppState } from '../types.ts';
|
||||
import Button from '../components/Button.vue';
|
||||
import Oscilloscope from '../components/Oscilloscope.vue';
|
||||
|
||||
defineProps<{
|
||||
state: AppState;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-measure-content">
|
||||
<Oscilloscope :data="state.last_measurement">
|
||||
<template #controls>
|
||||
<Button @click="api.measure()" bg="limegreen"
|
||||
style="height: 52px; width: 100px; border-radius: 8px;">单次测量</Button>
|
||||
</template>
|
||||
</Oscilloscope>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-measure-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -1,15 +1,26 @@
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-touch-callout: none !important;
|
||||
/* 禁用链接/图片长按菜单 */
|
||||
-webkit-user-select: none !important;
|
||||
/* 禁止文本选择(最关键) */
|
||||
box-sizing: border-box;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
font-family: 'PingFang SC', sans-serif;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 500px;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
61
src/types.ts
Normal file
61
src/types.ts
Normal file
@ -0,0 +1,61 @@
|
||||
// 测量数据类型
|
||||
export interface MeasurementData {
|
||||
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[];
|
||||
}
|
||||
|
||||
// 任务类型
|
||||
export type Task = {
|
||||
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;
|
||||
};
|
||||
|
||||
// 应用状态类型
|
||||
export interface AppState {
|
||||
connected: boolean;
|
||||
dis: number;
|
||||
phase: number;
|
||||
freq: number;
|
||||
p2p: number;
|
||||
speed: number;
|
||||
total_tasks: number;
|
||||
tasks: Task[];
|
||||
last_measurement: MeasurementData;
|
||||
}
|
||||
|
||||
// 历史记录项类型
|
||||
export interface HistoryItem extends MeasurementData {
|
||||
currentDis: number;
|
||||
}
|
||||
|
||||
// 批量任务类型
|
||||
export interface BatchTask {
|
||||
cmd: 'move' | 'move_measure';
|
||||
args: {
|
||||
steps: number;
|
||||
};
|
||||
repeat: number;
|
||||
}
|
||||
@ -5,7 +5,9 @@ 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 app = document.getElementById('app');
|
||||
if (!app) return;
|
||||
app.appendChild(container);
|
||||
|
||||
const destroy = () => {
|
||||
render(null, container);
|
||||
|
||||
@ -4,4 +4,14 @@ import vue from '@vitejs/plugin-vue'
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user