移动端适配,开关改善select

This commit is contained in:
feie9456 2025-09-12 15:57:55 +08:00
parent 2fd050ec6d
commit c4a7d0d5a2
3 changed files with 73 additions and 25 deletions

View File

@ -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)
}
@ -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 {

View File

@ -35,13 +35,27 @@ export type CircuitElement = {
pinUrl?: string
}
export type PropertySchema = {
export type PropertySchema =
| {
key: string
label: string
type: 'number' | 'text'
type: 'number'
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 = {
@ -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',

View File

@ -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'
}
})