diff --git a/TODO b/TODO new file mode 100644 index 0000000..95959ff --- /dev/null +++ b/TODO @@ -0,0 +1,2 @@ +- 添加图像识别 +- 添加分享大厅 \ No newline at end of file diff --git a/bun.lock b/bun.lock index e6bae65..673e8a1 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "dependencies": { "openai": "^5.20.0", "vue": "^3.5.18", + "vue-router": "4", }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.1", @@ -143,6 +144,8 @@ "@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="], + "@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="], + "@vue/language-core": ["@vue/language-core@3.0.6", "", { "dependencies": { "@volar/language-core": "2.4.23", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^2.0.5", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-e2RRzYWm+qGm8apUHW1wA5RQxzNhkqbbKdbKhiDUcmMrNAZGyM8aTiL3UrTqkaFI5s7wJRGGrp4u3jgusuBp2A=="], "@vue/reactivity": ["@vue/reactivity@3.5.21", "", { "dependencies": { "@vue/shared": "3.5.21" } }, "sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA=="], @@ -205,6 +208,8 @@ "vue": ["vue@3.5.21", "", { "dependencies": { "@vue/compiler-dom": "3.5.21", "@vue/compiler-sfc": "3.5.21", "@vue/runtime-dom": "3.5.21", "@vue/server-renderer": "3.5.21", "@vue/shared": "3.5.21" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA=="], + "vue-router": ["vue-router@4.5.1", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw=="], + "vue-tsc": ["vue-tsc@3.0.6", "", { "dependencies": { "@volar/typescript": "2.4.23", "@vue/language-core": "3.0.6" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-Tbs8Whd43R2e2nxez4WXPvvdjGbW24rOSgRhLOHXzWiT4pcP4G7KeWh0YCn18rF4bVwv7tggLLZ6MJnO6jXPBg=="], } } diff --git a/package.json b/package.json index 2a44f47..44c6019 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ }, "dependencies": { "openai": "^5.20.0", - "vue": "^3.5.18" + "vue": "^3.5.18", + "vue-router": "4" }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.1", diff --git a/src/App.vue b/src/App.vue index 594e46b..b7d7195 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,1560 +1,3 @@ - - - - - - - - { onViewportMouseMove(e); onViewportMouseMoveDrag(e); onViewportMouseMoveTrack(e); onViewportMouseMoveSlider(e) }" - @pointerup="() => { onViewportMouseUp(); onViewportMouseUpDrag(); onViewportMouseUpSlider() }" - @pointerleave="() => { onViewportMouseUp(); onViewportMouseUpDrag(); onViewportMouseUpSlider() }" @wheel="onWheel" - @dragover="onCanvasDragOver" @drop="onCanvasDrop" @click="clearSelection"> - - - - - - removeWire(w.id, e)"> - - - - - - - - - - onInstanceMouseDown(e, inst)" @click.stop="() => selectInstance(inst.id)" - :title="inst.key"> - - { - const meta = elements.find(e => e.key === inst.key); - if (!meta?.stateImages) return; - if (inst.key === 'switch') { - const cur = String(inst.props['state'] ?? 'off'); - inst.props['state'] = cur === 'on' ? 'off' : 'on' - } else { - inst.state = inst.state === 'on' ? 'off' : 'on' - } - }" /> - - - { sliderDraggingId = inst.id }" /> - - - - - - - - - - - {{ meterText(inst) }} - - - - {{ instLive[inst.id].v?.toFixed(2) }} V - {{ instLive[inst.id].i?.toFixed(2) }} A - - - - - - - - onEndpointClick(inst.id, idx, e)" - @pointerenter="() => { hoveredEndpoint = { instId: inst.id, cpIndex: idx } }" - @pointerleave="() => { if (hoveredEndpoint && hoveredEndpoint.instId === inst.id && hoveredEndpoint.cpIndex === idx) hoveredEndpoint = null }" - :title="cp.name || ('P' + (idx + 1))"> - - - - - - - - ◀ - - - - - - - - + + \ No newline at end of file diff --git a/src/circuit-ele/resistor.png b/src/circuit-ele/resistor.png deleted file mode 100644 index b5e4b5b..0000000 Binary files a/src/circuit-ele/resistor.png and /dev/null differ diff --git a/src/main.ts b/src/main.ts index fe5bae3..8345d45 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,19 @@ import { createApp } from 'vue' import App from './App.vue' import './style.css' +import { createRouter, createWebHistory } from 'vue-router' -createApp(App).mount('#app') +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + component: () => import('./views/Home.vue') + }, { + path: '/lab', + component: () => import('./views/VirtualLab.vue') + } + ] +}) + +createApp(App).use(router).mount('#app') diff --git a/src/views/Home.vue b/src/views/Home.vue new file mode 100644 index 0000000..63cf83f --- /dev/null +++ b/src/views/Home.vue @@ -0,0 +1,3 @@ + + Home + \ No newline at end of file diff --git a/src/views/VirtualLab.vue b/src/views/VirtualLab.vue new file mode 100644 index 0000000..539df35 --- /dev/null +++ b/src/views/VirtualLab.vue @@ -0,0 +1,1727 @@ + + + + + + + + + { onViewportMouseMove(e); onViewportMouseMoveDrag(e); onViewportMouseMoveTrack(e); onViewportMouseMoveSlider(e) }" + @pointerup="() => { onViewportMouseUp(); onViewportMouseUpDrag(); onViewportMouseUpSlider() }" + @pointerleave="() => { onViewportMouseUp(); onViewportMouseUpDrag(); onViewportMouseUpSlider() }" + @wheel="onWheel" @dragover="onCanvasDragOver" @drop="onCanvasDrop" + @click="clearSelection"> + + + + + + removeWire(w.id, e)"> + + + + + + + + + + onInstanceMouseDown(e, inst)" + @click.stop="() => selectInstance(inst.id)" :title="(elements.find(e => e.key === inst.key)?.name || inst.key)"> + + { + const meta = elements.find(e => e.key === inst.key); + if (!meta?.stateImages) return; + if (inst.key === 'switch') { + const cur = String(inst.props['state'] ?? 'off'); + inst.props['state'] = cur === 'on' ? 'off' : 'on' + } else { + inst.state = inst.state === 'on' ? 'off' : 'on' + } + }" /> + + + + {{ String(inst.props?.['text'] ?? '') }} + + + + + + + + + { sliderDraggingId = inst.id }" /> + + + + + + + + + + + {{ meterText(inst) }} + + + + {{ instLive[inst.id]?.v?.toFixed(2) }} V + {{ (-(instLive[inst.id]?.i ?? 0)).toFixed(2) }} A + + + + + + + + onEndpointClick(inst.id, idx, e)" + @pointerenter="() => { hoveredEndpoint = { instId: inst.id, cpIndex: idx } }" + @pointerleave="() => { if (hoveredEndpoint && hoveredEndpoint.instId === inst.id && hoveredEndpoint.cpIndex === idx) hoveredEndpoint = null }" + :title="cp.name || ('P' + (idx + 1))"> + + + + + + + + ◀ + + + + + + + + diff --git a/src/circuit-ele/battery.png b/src/views/circuit-ele/battery.png similarity index 100% rename from src/circuit-ele/battery.png rename to src/views/circuit-ele/battery.png diff --git a/src/circuit-ele/capacitor.png b/src/views/circuit-ele/capacitor.png similarity index 100% rename from src/circuit-ele/capacitor.png rename to src/views/circuit-ele/capacitor.png diff --git a/src/circuit-ele/inductor.png b/src/views/circuit-ele/inductor.png similarity index 100% rename from src/circuit-ele/inductor.png rename to src/views/circuit-ele/inductor.png diff --git a/src/circuit-ele/light_bulb.png b/src/views/circuit-ele/light_bulb.png similarity index 100% rename from src/circuit-ele/light_bulb.png rename to src/views/circuit-ele/light_bulb.png diff --git a/src/circuit-ele/light_bulb_grow.png b/src/views/circuit-ele/light_bulb_grow.png similarity index 100% rename from src/circuit-ele/light_bulb_grow.png rename to src/views/circuit-ele/light_bulb_grow.png diff --git a/src/circuit-ele/meter.png b/src/views/circuit-ele/meter.png similarity index 100% rename from src/circuit-ele/meter.png rename to src/views/circuit-ele/meter.png diff --git a/src/circuit-ele/power_supply.png b/src/views/circuit-ele/power_supply.png similarity index 100% rename from src/circuit-ele/power_supply.png rename to src/views/circuit-ele/power_supply.png diff --git a/src/circuit-ele/resistance_box.png b/src/views/circuit-ele/resistance_box.png similarity index 100% rename from src/circuit-ele/resistance_box.png rename to src/views/circuit-ele/resistance_box.png diff --git a/src/views/circuit-ele/resistor.png b/src/views/circuit-ele/resistor.png new file mode 100644 index 0000000..43700e2 Binary files /dev/null and b/src/views/circuit-ele/resistor.png differ diff --git a/src/circuit-ele/sliding_rheostat.png b/src/views/circuit-ele/sliding_rheostat.png similarity index 100% rename from src/circuit-ele/sliding_rheostat.png rename to src/views/circuit-ele/sliding_rheostat.png diff --git a/src/circuit-ele/sliding_rheostat_pin.png b/src/views/circuit-ele/sliding_rheostat_pin.png similarity index 100% rename from src/circuit-ele/sliding_rheostat_pin.png rename to src/views/circuit-ele/sliding_rheostat_pin.png diff --git a/src/circuit-ele/switch_off.png b/src/views/circuit-ele/switch_off.png similarity index 100% rename from src/circuit-ele/switch_off.png rename to src/views/circuit-ele/switch_off.png diff --git a/src/circuit-ele/switch_on.png b/src/views/circuit-ele/switch_on.png similarity index 100% rename from src/circuit-ele/switch_on.png rename to src/views/circuit-ele/switch_on.png diff --git a/src/views/circuit-ele/text_box.svg b/src/views/circuit-ele/text_box.svg new file mode 100644 index 0000000..77fcad2 --- /dev/null +++ b/src/views/circuit-ele/text_box.svg @@ -0,0 +1,10 @@ + + + + + + + + + T + diff --git a/src/elements.ts b/src/views/elements.ts similarity index 82% rename from src/elements.ts rename to src/views/elements.ts index 4e9c893..cc717cb 100644 --- a/src/elements.ts +++ b/src/views/elements.ts @@ -13,6 +13,7 @@ import switchOn from './circuit-ele/switch_on.png' import meter from './circuit-ele/meter.png' import slidingRheostat from './circuit-ele/sliding_rheostat.png' import slidingRheostatPin from './circuit-ele/sliding_rheostat_pin.png' +import textBox from './circuit-ele/text_box.svg' export type ConnectionPoint = { // 以百分比(0..1)定义在图片盒子内的位置 @@ -49,6 +50,12 @@ export type PropertySchema = type: 'text' default: string } + | { + key: string + label: string + type: 'color' + default: string + } | { key: string label: string @@ -64,14 +71,28 @@ export type Preset = { } export const elements: CircuitElement[] = [ + { + key: 'text_box', + name: '文本框', + url: textBox, + defaultSize: 0, + // 文本框不参与电路连接,无端点 + connectionPoints: [], + propertySchemas: [ + { key: 'text', label: '文本', type: 'text', default: '双击编辑' }, + { key: 'fontSize', label: '字体大小', type: 'number', unit: 'px', default: 24 }, + { key: 'color', label: '颜色', type: 'color', default: '#111827' }, + // 可选:是否粗体/对齐方式等,后续再加 + ], + }, { key: 'battery', name: '电池', url: battery, defaultSize: 110, connectionPoints: [ - { x: 0.05, y: 0.5, name: 'A' }, // 左侧 - { x: 0.95, y: 0.5, name: 'B' }, // 右侧 + { x: 0.95, y: 0.5, name: '正极' }, // 右侧 + { x: 0.05, y: 0.5, name: '负极' }, // 左侧 ], propertySchemas: [ { key: 'voltage', label: '电压', type: 'number', unit: 'V', default: 3 }, @@ -84,8 +105,8 @@ export const elements: CircuitElement[] = [ url: powerSupply, defaultSize: 180, connectionPoints: [ - { x: 0.2, y: 1, name: 'A' }, - { x: 0.8, y: 1, name: 'B' }, + { x: 0.8, y: 1, name: '正极' }, + { x: 0.2, y: 1, name: '负极' }, ], propertySchemas: [ { key: 'voltage', label: '电压', type: 'number', unit: 'V', default: 5 }, @@ -122,32 +143,6 @@ export const elements: CircuitElement[] = [ // 当电流超过阈值时在前端根据 state 切换图片 stateImages: { on: lightBulbGrow, off: lightBulb }, }, - { - key: 'capacitor', - name: '电容', - url: capacitor, - defaultSize: 120, - connectionPoints: [ - { x: 0.6, y: 0.96, name: 'A' }, - { x: 0.4, y: 0.9, name: 'B' }, - ], - propertySchemas: [ - { key: 'capacitance', label: '电容', type: 'number', unit: 'F', default: 0.000001 }, - ], - }, - { - key: 'inductor', - name: '电感', - url: inductor, - defaultSize: 100, - connectionPoints: [ - { x: 0.05, y: 0.5, name: 'A' }, - { x: 0.95, y: 0.5, name: 'B' }, - ], - propertySchemas: [ - { key: 'inductance', label: '电感', type: 'number', unit: 'H', default: 0.001 }, - ], - }, { key: 'sliding_rheostat', name: '滑动变阻器', @@ -155,10 +150,10 @@ export const elements: CircuitElement[] = [ pinUrl: slidingRheostatPin, defaultSize: 180, connectionPoints: [ - { x: 0.15, y: 0.28, name: 'A' }, - { x: 0.85, y: 0.28, name: 'B' }, - { x: 0.15, y: 0.76, name: 'C' }, - { x: 0.85, y: 0.76, name: 'D' }, + { x: 0.15, y: 0.28, name: '下部左侧' }, + { x: 0.85, y: 0.28, name: '下部右侧' }, + { x: 0.15, y: 0.76, name: '上部左侧' }, + { x: 0.85, y: 0.76, name: '上部右侧' }, ], propertySchemas: [ { key: 'maxResistance', label: '最大电阻', type: 'number', unit: 'Ω', default: 100 }, @@ -197,8 +192,8 @@ export const elements: CircuitElement[] = [ url: meter, defaultSize: 150, connectionPoints: [ - { x: 0.25, y: 1, name: 'A' }, - { x: 0.45, y: 1, name: 'B' }, + { x: 0.25, y: 1, name: '正极' }, + { x: 0.45, y: 1, name: '负极' }, ], preset: [{ name: '微安电流表', propertyValues: { resistance: 0.05, renderFunc: '(i) => `${(i * 1000000).toFixed(0)} uA`' } @@ -214,4 +209,30 @@ export const elements: CircuitElement[] = [ { key: 'renderFunc', label: '显示函数', type: 'text', default: '(i) => `${(i).toFixed(2)} A`' }, ], }, + { + key: 'capacitor', + name: '电容', + url: capacitor, + defaultSize: 120, + connectionPoints: [ + { x: 0.4, y: 0.9, name: '正极' }, + { x: 0.6, y: 0.96, name: '负极' }, + ], + propertySchemas: [ + { key: 'capacitance', label: '电容', type: 'number', unit: 'F', default: 0.000001 }, + ], + }, + { + key: 'inductor', + name: '电感', + url: inductor, + defaultSize: 100, + connectionPoints: [ + { x: 0.05, y: 0.5, name: 'A' }, + { x: 0.95, y: 0.5, name: 'B' }, + ], + propertySchemas: [ + { key: 'inductance', label: '电感', type: 'number', unit: 'H', default: 0.001 }, + ], + }, ] diff --git a/src/utils.ts b/src/views/utils.ts similarity index 100% rename from src/utils.ts rename to src/views/utils.ts