移动端适配,开关改善select
This commit is contained in:
parent
2fd050ec6d
commit
c4a7d0d5a2
59
src/App.vue
59
src/App.vue
@ -16,7 +16,7 @@ const worldRef = ref<HTMLDivElement | null>(null)
|
|||||||
const showAI = ref(true)
|
const showAI = ref(true)
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
scale: 1,
|
scale: 0.8,
|
||||||
minScale: 0.2,
|
minScale: 0.2,
|
||||||
maxScale: 3,
|
maxScale: 3,
|
||||||
translateX: 0,
|
translateX: 0,
|
||||||
@ -128,7 +128,8 @@ function onCanvasDrop(e: DragEvent) {
|
|||||||
const props: Record<string, number | string> = {}
|
const props: Record<string, number | string> = {}
|
||||||
; (item.propertySchemas || []).forEach((p) => { props[p.key] = p.default })
|
; (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 }
|
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)
|
instances.push(inst)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -464,7 +465,8 @@ function buildNet(): BuiltNet {
|
|||||||
vsrcs.push({ inst, nPlus: n0, nMinus: nInt, V })
|
vsrcs.push({ inst, nPlus: n0, nMinus: nInt, V })
|
||||||
resistors.push({ inst, nPlus: nInt, nMinus: n1, R: Math.max(1e-9, Rint) })
|
resistors.push({ inst, nPlus: nInt, nMinus: n1, R: Math.max(1e-9, Rint) })
|
||||||
} else if (key === 'switch') {
|
} 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 })
|
vsrcs.push({ inst, nPlus: n0, nMinus: n1, V: 0 })
|
||||||
}
|
}
|
||||||
// off = open, 忽略
|
// off = open, 忽略
|
||||||
@ -820,6 +822,13 @@ function loadFromJSONText(text: string) {
|
|||||||
rotation: safeNumber(r.rotation, 0),
|
rotation: safeNumber(r.rotation, 0),
|
||||||
props: rawProps,
|
props: rawProps,
|
||||||
state: r.state === 'on' || r.state === 'off' ? r.state : undefined,
|
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)以便后续重建导线
|
// 暂存连接信息在实例对象上(不入 props)以便后续重建导线
|
||||||
; (inst as any).__savedConnections = savedConnections
|
; (inst as any).__savedConnections = savedConnections
|
||||||
@ -951,10 +960,10 @@ function deleteSelected() {
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- 画布视口(可平移、缩放、接收拖拽) -->
|
<!-- 画布视口(可平移、缩放、接收拖拽) -->
|
||||||
<main class="viewport" ref="viewportRef" @mousedown="onViewportMouseDown"
|
<main class="viewport" ref="viewportRef" @pointerdown="onViewportMouseDown"
|
||||||
@mousemove="(e) => { onViewportMouseMove(e); onViewportMouseMoveDrag(e); onViewportMouseMoveTrack(e); onViewportMouseMoveSlider(e) }"
|
@pointermove="(e) => { onViewportMouseMove(e); onViewportMouseMoveDrag(e); onViewportMouseMoveTrack(e); onViewportMouseMoveSlider(e) }"
|
||||||
@mouseup="() => { onViewportMouseUp(); onViewportMouseUpDrag(); onViewportMouseUpSlider() }"
|
@pointerup="() => { onViewportMouseUp(); onViewportMouseUpDrag(); onViewportMouseUpSlider() }"
|
||||||
@mouseleave="() => { onViewportMouseUp(); onViewportMouseUpDrag(); onViewportMouseUpSlider() }" @wheel="onWheel"
|
@pointerleave="() => { onViewportMouseUp(); onViewportMouseUpDrag(); onViewportMouseUpSlider() }" @wheel="onWheel"
|
||||||
@dragover="onCanvasDragOver" @drop="onCanvasDrop" @click="clearSelection">
|
@dragover="onCanvasDragOver" @drop="onCanvasDrop" @click="clearSelection">
|
||||||
<!-- 世界平面:网格背景,应用 transform -->
|
<!-- 世界平面:网格背景,应用 transform -->
|
||||||
<div class="world" :style="worldStyle" ref="worldRef">
|
<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 }"
|
<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)' }"
|
: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">
|
:title="inst.key">
|
||||||
<div class="inst-box" :style="{ width: inst.size + 'px', height: inst.size + 'px' }">
|
<div class="inst-box" :style="{ width: inst.size + 'px', height: inst.size + 'px' }">
|
||||||
<img
|
<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"
|
: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"
|
<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="{
|
:src="(elements.find(e => e.key === inst.key) as any)?.pinUrl" alt="pin" draggable="false" :style="{
|
||||||
left: ((getRheostatPinOffset(inst) / inst.size) * 100) + '%',
|
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"
|
<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',
|
left: getConnectionPointWorldPos(inst, idx).x + 'px',
|
||||||
top: getConnectionPointWorldPos(inst, idx).y + 'px'
|
top: getConnectionPointWorldPos(inst, idx).y + 'px'
|
||||||
}" @click.stop="(e) => onEndpointClick(inst.id, idx, e)"
|
}" @click.stop="(e) => onEndpointClick(inst.id, idx, e)"
|
||||||
@mouseenter="() => { hoveredEndpoint = { instId: inst.id, cpIndex: idx } }"
|
@pointerenter="() => { hoveredEndpoint = { instId: inst.id, cpIndex: idx } }"
|
||||||
@mouseleave="() => { if (hoveredEndpoint && hoveredEndpoint.instId === inst.id && hoveredEndpoint.cpIndex === idx) hoveredEndpoint = null }"
|
@pointerleave="() => { if (hoveredEndpoint && hoveredEndpoint.instId === inst.id && hoveredEndpoint.cpIndex === idx) hoveredEndpoint = null }"
|
||||||
:title="cp.name || ('P' + (idx + 1))"></div>
|
:title="cp.name || ('P' + (idx + 1))"></div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -1083,8 +1105,13 @@ function deleteSelected() {
|
|||||||
<span class="label">{{ schema.label }}</span>
|
<span class="label">{{ schema.label }}</span>
|
||||||
<input v-if="schema.type === 'number'" type="number" step="any"
|
<input v-if="schema.type === 'number'" type="number" step="any"
|
||||||
v-model.number="selectedInst!.props[schema.key]" />
|
v-model.number="selectedInst!.props[schema.key]" />
|
||||||
<input v-else type="text" v-model="selectedInst!.props[schema.key] as string" />
|
<input v-else-if="schema.type === 'text'" type="text" v-model="selectedInst!.props[schema.key] as string" />
|
||||||
<span class="unit" v-if="schema.unit">{{ schema.unit }}</span>
|
<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>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -1105,6 +1132,8 @@ function deleteSelected() {
|
|||||||
/* 左栏 / 画布 / AI 辅助 */
|
/* 左栏 / 画布 / AI 辅助 */
|
||||||
grid-template-rows: 100vh;
|
grid-template-rows: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
|
|||||||
@ -35,13 +35,27 @@ export type CircuitElement = {
|
|||||||
pinUrl?: string
|
pinUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PropertySchema = {
|
export type PropertySchema =
|
||||||
|
| {
|
||||||
key: string
|
key: string
|
||||||
label: string
|
label: string
|
||||||
type: 'number' | 'text'
|
type: 'number'
|
||||||
unit?: string
|
unit?: string
|
||||||
default: number | 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 = {
|
export type Preset = {
|
||||||
@ -85,11 +99,13 @@ export const elements: CircuitElement[] = [
|
|||||||
defaultSize: 130,
|
defaultSize: 130,
|
||||||
connectionPoints: [
|
connectionPoints: [
|
||||||
{ x: 0.05, y: 0.7, name: 'A' },
|
{ 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 显示
|
// 点击元件切换 on/off 显示
|
||||||
stateImages: { on: switchOn, off: switchOff },
|
stateImages: { on: switchOn, off: switchOff },
|
||||||
// 开关没有额外可编辑属性
|
propertySchemas: [
|
||||||
|
{ key: 'state', label: '状态', type: 'select', options: ['off', 'on'], default: 'off' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'light_bulb',
|
key: 'light_bulb',
|
||||||
|
|||||||
@ -4,4 +4,7 @@ import vue from '@vitejs/plugin-vue'
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
|
server:{
|
||||||
|
host: '0.0.0.0'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user