移动端适配,开关改善select
This commit is contained in:
parent
2fd050ec6d
commit
c4a7d0d5a2
61
src/App.vue
61
src/App.vue
@ -16,7 +16,7 @@ const worldRef = ref<HTMLDivElement | null>(null)
|
||||
const showAI = ref(true)
|
||||
|
||||
const state = reactive({
|
||||
scale: 1,
|
||||
scale: 0.8,
|
||||
minScale: 0.2,
|
||||
maxScale: 3,
|
||||
translateX: 0,
|
||||
@ -128,7 +128,8 @@ function onCanvasDrop(e: DragEvent) {
|
||||
const props: Record<string, number | string> = {}
|
||||
; (item.propertySchemas || []).forEach((p) => { props[p.key] = p.default })
|
||||
const inst: Instance = { id: String(++idSeq), key: item.key, url: item.url, x: x, y: y, size: defSize, rotation: 0, props }
|
||||
if (item.stateImages) inst.state = 'off'
|
||||
// 仍保留 state 字段用于灯泡等展示,但对于开关用 props.state 作为真值
|
||||
if (item.stateImages) inst.state = String(props['state'] ?? 'off') as any
|
||||
instances.push(inst)
|
||||
}
|
||||
|
||||
@ -447,7 +448,7 @@ function buildNet(): BuiltNet {
|
||||
const n1 = getNode(inst, 1)
|
||||
|
||||
const key = meta.key
|
||||
if (key === 'battery') {
|
||||
if (key === 'battery') {
|
||||
const V = safeNum(inst.props?.['voltage'], 0)
|
||||
const Rint = safeNum(inst.props?.['internalResistance'], 0.1)
|
||||
// 用内部节点将电压源与内阻串联:n0 --[V]--> nInt --[Rint]--> n1
|
||||
@ -464,7 +465,8 @@ function buildNet(): BuiltNet {
|
||||
vsrcs.push({ inst, nPlus: n0, nMinus: nInt, V })
|
||||
resistors.push({ inst, nPlus: nInt, nMinus: n1, R: Math.max(1e-9, Rint) })
|
||||
} else if (key === 'switch') {
|
||||
if (inst.state === 'on') {
|
||||
const sw = (inst.props?.['state'] as any) === 'on' || inst.state === 'on'
|
||||
if (sw) {
|
||||
vsrcs.push({ inst, nPlus: n0, nMinus: n1, V: 0 })
|
||||
}
|
||||
// off = open, 忽略
|
||||
@ -820,6 +822,13 @@ function loadFromJSONText(text: string) {
|
||||
rotation: safeNumber(r.rotation, 0),
|
||||
props: rawProps,
|
||||
state: r.state === 'on' || r.state === 'off' ? r.state : undefined,
|
||||
}
|
||||
// 兼容老文件:若为开关且 props.state 缺失,则使用 legacy state 或默认 off
|
||||
if (inst.key === 'switch') {
|
||||
const cur = String((inst.props as any)['state'] ?? '')
|
||||
if (cur !== 'on' && cur !== 'off') {
|
||||
(inst.props as any)['state'] = (inst.state === 'on' ? 'on' : 'off')
|
||||
}
|
||||
}
|
||||
// 暂存连接信息在实例对象上(不入 props)以便后续重建导线
|
||||
; (inst as any).__savedConnections = savedConnections
|
||||
@ -951,10 +960,10 @@ function deleteSelected() {
|
||||
</aside>
|
||||
|
||||
<!-- 画布视口(可平移、缩放、接收拖拽) -->
|
||||
<main class="viewport" ref="viewportRef" @mousedown="onViewportMouseDown"
|
||||
@mousemove="(e) => { onViewportMouseMove(e); onViewportMouseMoveDrag(e); onViewportMouseMoveTrack(e); onViewportMouseMoveSlider(e) }"
|
||||
@mouseup="() => { onViewportMouseUp(); onViewportMouseUpDrag(); onViewportMouseUpSlider() }"
|
||||
@mouseleave="() => { onViewportMouseUp(); onViewportMouseUpDrag(); onViewportMouseUpSlider() }" @wheel="onWheel"
|
||||
<main class="viewport" ref="viewportRef" @pointerdown="onViewportMouseDown"
|
||||
@pointermove="(e) => { onViewportMouseMove(e); onViewportMouseMoveDrag(e); onViewportMouseMoveTrack(e); onViewportMouseMoveSlider(e) }"
|
||||
@pointerup="() => { onViewportMouseUp(); onViewportMouseUpDrag(); onViewportMouseUpSlider() }"
|
||||
@pointerleave="() => { onViewportMouseUp(); onViewportMouseUpDrag(); onViewportMouseUpSlider() }" @wheel="onWheel"
|
||||
@dragover="onCanvasDragOver" @drop="onCanvasDrop" @click="clearSelection">
|
||||
<!-- 世界平面:网格背景,应用 transform -->
|
||||
<div class="world" :style="worldStyle" ref="worldRef">
|
||||
@ -980,19 +989,32 @@ function deleteSelected() {
|
||||
<!-- 已放置的元件(可拖动) -->
|
||||
<div v-for="inst in instances" :key="inst.id" class="inst" :class="{ selected: selectedId === inst.id }"
|
||||
:style="{ left: inst.x + 'px', top: inst.y + 'px', transform: 'translate(-50%, -50%) rotate(' + (inst.rotation || 0) + 'deg)' }"
|
||||
@mousedown="(e) => onInstanceMouseDown(e, inst)" @click.stop="() => selectInstance(inst.id)"
|
||||
@pointerdown="(e) => onInstanceMouseDown(e, inst)" @click.stop="() => selectInstance(inst.id)"
|
||||
:title="inst.key">
|
||||
<div class="inst-box" :style="{ width: inst.size + 'px', height: inst.size + 'px' }">
|
||||
<img
|
||||
:src="(elements.find(e => e.key === inst.key)?.stateImages ? (inst.state === 'on' ? elements.find(e => e.key === inst.key)!.stateImages!.on : elements.find(e => e.key === inst.key)!.stateImages!.off) : inst.url)"
|
||||
:src="(elements.find(e => e.key === inst.key)?.stateImages
|
||||
? (((inst.key === 'switch' ? (inst.props['state'] as string) : inst.state) === 'on')
|
||||
? elements.find(e => e.key === inst.key)!.stateImages!.on
|
||||
: elements.find(e => e.key === inst.key)!.stateImages!.off)
|
||||
: inst.url)"
|
||||
:alt="inst.key" :draggable="false"
|
||||
@contextmenu.prevent="() => { const meta = elements.find(e => e.key === inst.key); if (meta?.stateImages) inst.state = inst.state === 'on' ? 'off' : 'on' }" />
|
||||
@contextmenu.prevent="() => {
|
||||
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'
|
||||
}
|
||||
}" />
|
||||
|
||||
<!-- 滑动变阻器滑块 -->
|
||||
<img v-if="inst.key === 'sliding_rheostat'" class="rheo-pin"
|
||||
:src="(elements.find(e => e.key === inst.key) as any)?.pinUrl" alt="pin" draggable="false" :style="{
|
||||
left: ((getRheostatPinOffset(inst) / inst.size) * 100) + '%',
|
||||
}" @mousedown.stop="() => { sliderDraggingId = inst.id }" />
|
||||
}" @pointerdown.stop="() => { sliderDraggingId = inst.id }" />
|
||||
|
||||
<!-- 灯泡发散光线叠加层 -->
|
||||
<svg v-if="inst.key === 'light_bulb'" class="bulb-glow" :width="inst.size" :height="inst.size"
|
||||
@ -1031,8 +1053,8 @@ function deleteSelected() {
|
||||
left: getConnectionPointWorldPos(inst, idx).x + 'px',
|
||||
top: getConnectionPointWorldPos(inst, idx).y + 'px'
|
||||
}" @click.stop="(e) => onEndpointClick(inst.id, idx, e)"
|
||||
@mouseenter="() => { hoveredEndpoint = { instId: inst.id, cpIndex: idx } }"
|
||||
@mouseleave="() => { if (hoveredEndpoint && hoveredEndpoint.instId === inst.id && hoveredEndpoint.cpIndex === idx) hoveredEndpoint = null }"
|
||||
@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))"></div>
|
||||
</template>
|
||||
</div>
|
||||
@ -1083,8 +1105,13 @@ function deleteSelected() {
|
||||
<span class="label">{{ schema.label }}</span>
|
||||
<input v-if="schema.type === 'number'" type="number" step="any"
|
||||
v-model.number="selectedInst!.props[schema.key]" />
|
||||
<input v-else type="text" v-model="selectedInst!.props[schema.key] as string" />
|
||||
<span class="unit" v-if="schema.unit">{{ schema.unit }}</span>
|
||||
<input v-else-if="schema.type === 'text'" type="text" v-model="selectedInst!.props[schema.key] as string" />
|
||||
<select v-else-if="schema.type === 'select'" v-model="selectedInst!.props[schema.key] as string">
|
||||
<option v-for="opt in (Array.isArray((schema as any).options) ? (schema as any).options : [])" :key="typeof opt === 'string' ? opt : (opt.value)" :value="typeof opt === 'string' ? opt : (opt.value)">
|
||||
{{ typeof opt === 'string' ? opt : (opt.label || opt.value) }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="unit" v-if="schema.type === 'number' && (schema as any).unit">{{ (schema as any).unit }}</span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
@ -1105,6 +1132,8 @@ function deleteSelected() {
|
||||
/* 左栏 / 画布 / AI 辅助 */
|
||||
grid-template-rows: 100vh;
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
|
||||
@ -35,13 +35,27 @@ export type CircuitElement = {
|
||||
pinUrl?: string
|
||||
}
|
||||
|
||||
export type PropertySchema = {
|
||||
key: string
|
||||
label: string
|
||||
type: 'number' | 'text'
|
||||
unit?: string
|
||||
default: number | string
|
||||
}
|
||||
export type PropertySchema =
|
||||
| {
|
||||
key: string
|
||||
label: string
|
||||
type: 'number'
|
||||
unit?: string
|
||||
default: number
|
||||
}
|
||||
| {
|
||||
key: string
|
||||
label: string
|
||||
type: 'text'
|
||||
default: string
|
||||
}
|
||||
| {
|
||||
key: string
|
||||
label: string
|
||||
type: 'select'
|
||||
options: Array<{ label?: string; value: string }> | string[]
|
||||
default: string
|
||||
}
|
||||
|
||||
// 预设定义:展示名称 + 要覆盖的属性值
|
||||
export type Preset = {
|
||||
@ -85,11 +99,13 @@ export const elements: CircuitElement[] = [
|
||||
defaultSize: 130,
|
||||
connectionPoints: [
|
||||
{ x: 0.05, y: 0.7, name: 'A' },
|
||||
{ x: 0.83, y: 0.7, name: 'B' },
|
||||
{ x: 0.95, y: 0.7, name: 'B' },
|
||||
],
|
||||
// 点击元件切换 on/off 显示
|
||||
stateImages: { on: switchOn, off: switchOff },
|
||||
// 开关没有额外可编辑属性
|
||||
propertySchemas: [
|
||||
{ key: 'state', label: '状态', type: 'select', options: ['off', 'on'], default: 'off' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'light_bulb',
|
||||
|
||||
@ -4,4 +4,7 @@ import vue from '@vitejs/plugin-vue'
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server:{
|
||||
host: '0.0.0.0'
|
||||
}
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user