commit 73f948d4fc9e9f0d8e1b9fc3c2c0cce3c1a74fa8 Author: feie9454 Date: Wed Jan 28 14:42:14 2026 +0800 260128 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -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? diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/README.md @@ -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 ` + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..1c3ef42 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..1ca6172 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,480 @@ + + + + + diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..448df79 --- /dev/null +++ b/src/api.ts @@ -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 }; \ No newline at end of file diff --git a/src/assets/_-.woff2 b/src/assets/_-.woff2 new file mode 100644 index 0000000..626f5d9 Binary files /dev/null and b/src/assets/_-.woff2 differ diff --git a/src/assets/apparatus.svg b/src/assets/apparatus.svg new file mode 100644 index 0000000..c46e29d --- /dev/null +++ b/src/assets/apparatus.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + S1 发射 + + + + + + + + + + + + + + S2 接收 + + + L=Moving + + \ No newline at end of file diff --git a/src/assets/stylesheet.css b/src/assets/stylesheet.css new file mode 100644 index 0000000..34c122d --- /dev/null +++ b/src/assets/stylesheet.css @@ -0,0 +1,8 @@ +@font-face { + font-family: 'PingFang SC'; + src: url('_-.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + diff --git a/src/assets/vue.svg b/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/仪器.png b/src/assets/仪器.png new file mode 100644 index 0000000..5be0e37 Binary files /dev/null and b/src/assets/仪器.png differ diff --git a/src/assets/仪器臂.png b/src/assets/仪器臂.png new file mode 100644 index 0000000..efcb53f Binary files /dev/null and b/src/assets/仪器臂.png differ diff --git a/src/assets/发射器.png b/src/assets/发射器.png new file mode 100644 index 0000000..112c89e Binary files /dev/null and b/src/assets/发射器.png differ diff --git a/src/assets/接收器.png b/src/assets/接收器.png new file mode 100644 index 0000000..e86110d Binary files /dev/null and b/src/assets/接收器.png differ diff --git a/src/components/Button.vue b/src/components/Button.vue new file mode 100644 index 0000000..b0e7698 --- /dev/null +++ b/src/components/Button.vue @@ -0,0 +1,148 @@ + + + + + \ No newline at end of file diff --git a/src/components/Lissajous.vue b/src/components/Lissajous.vue new file mode 100644 index 0000000..8ac9e8b --- /dev/null +++ b/src/components/Lissajous.vue @@ -0,0 +1,145 @@ + + + + + \ No newline at end of file diff --git a/src/components/Machine.vue b/src/components/Machine.vue new file mode 100644 index 0000000..b392909 --- /dev/null +++ b/src/components/Machine.vue @@ -0,0 +1,106 @@ + + + + + \ No newline at end of file diff --git a/src/components/Numpad.vue b/src/components/Numpad.vue new file mode 100644 index 0000000..c11559d --- /dev/null +++ b/src/components/Numpad.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/src/components/Oscilloscope.vue b/src/components/Oscilloscope.vue new file mode 100644 index 0000000..1b59f33 --- /dev/null +++ b/src/components/Oscilloscope.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/src/components/Panel.vue b/src/components/Panel.vue new file mode 100644 index 0000000..4b7e552 --- /dev/null +++ b/src/components/Panel.vue @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/src/components/TrendChart.vue b/src/components/TrendChart.vue new file mode 100644 index 0000000..76cf3ad --- /dev/null +++ b/src/components/TrendChart.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/src/components/WaveformChart.vue b/src/components/WaveformChart.vue new file mode 100644 index 0000000..8cf9440 --- /dev/null +++ b/src/components/WaveformChart.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/src/components/Window.vue b/src/components/Window.vue new file mode 100644 index 0000000..2c220af --- /dev/null +++ b/src/components/Window.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..323c78a --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,7 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..a0af020 --- /dev/null +++ b/src/main.ts @@ -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); \ No newline at end of file diff --git a/src/pages/Control.vue b/src/pages/Control.vue new file mode 100644 index 0000000..0185cb3 --- /dev/null +++ b/src/pages/Control.vue @@ -0,0 +1,403 @@ + + + + + \ No newline at end of file diff --git a/src/pages/History.vue b/src/pages/History.vue new file mode 100644 index 0000000..b08888c --- /dev/null +++ b/src/pages/History.vue @@ -0,0 +1,301 @@ + + + + + \ No newline at end of file diff --git a/src/pages/Home.vue b/src/pages/Home.vue new file mode 100644 index 0000000..e69de29 diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..0d64f3c --- /dev/null +++ b/src/style.css @@ -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; +} \ No newline at end of file diff --git a/src/utils/message.ts b/src/utils/message.ts new file mode 100644 index 0000000..1a1bb0b --- /dev/null +++ b/src/utils/message.ts @@ -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); +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..053db87 --- /dev/null +++ b/tsconfig.app.json @@ -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"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/tsconfig.node.json @@ -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"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..bbcf80c --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue()], +})