修复电表renderF安全策略
This commit is contained in:
parent
a94ce15731
commit
61f76c338b
@ -651,17 +651,10 @@ watchEffect(() => { solveMNA() })
|
||||
function meterText(inst: Instance) {
|
||||
const live = instLive[inst.id]
|
||||
const i = live?.i ?? 0
|
||||
const r = inst.props?.['resistance'] ? safeNum(inst.props?.['resistance'], 0) : 0
|
||||
const code = String(inst.props?.['renderFunc'] || '')
|
||||
if (!code) return `${(i).toFixed(6)} A`
|
||||
try {
|
||||
// 允许两种形态:"(i)=>..." 或 "(i,v,props)=>..."
|
||||
const fn = new Function('i', 'r', 'props', 'return (' + code + ')(i, r, props)')
|
||||
const out = fn(i, r, inst.props)
|
||||
return String(out)
|
||||
} catch {
|
||||
return `ERR_RF`
|
||||
}
|
||||
const num = Number(inst.props?.['dispNum'] || '')
|
||||
const fig = Number(inst.props?.['dispFig'] || '')
|
||||
const unit = String(inst.props?.['dispUnit'] || 'A')
|
||||
return `${(i * num).toFixed(fig)} ${unit}`
|
||||
}
|
||||
|
||||
// 灯泡发光参数:随电流变化增加线条数量、长度、透明度
|
||||
@ -1031,8 +1024,7 @@ function editTextBox(inst: Instance) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app" :style="{ '--ai-width': showAI ? '360px' : '0px' }"
|
||||
:class="{ 'preview-mode': isPreview }">
|
||||
<div class="app" :style="{ '--ai-width': showAI ? '360px' : '0px' }" :class="{ 'preview-mode': isPreview }">
|
||||
<!-- 左侧元件面板 -->
|
||||
<aside class="sidebar" v-if="!isPreview">
|
||||
<RouterLink class="back-to-home" to="/">
|
||||
@ -1041,9 +1033,8 @@ function editTextBox(inst: Instance) {
|
||||
|
||||
<h2>元件</h2>
|
||||
<div class="palette">
|
||||
<div v-for="item in palette" :key="item.key" class="palette-item"
|
||||
draggable="true" @dragstart="(e) => onPaletteDragStart(e, item)"
|
||||
:title="item.name">
|
||||
<div v-for="item in palette" :key="item.key" class="palette-item" draggable="true"
|
||||
@dragstart="(e) => onPaletteDragStart(e, item)" :title="item.name">
|
||||
<img :src="item.url" :alt="item.name" draggable="false" />
|
||||
<span class="label">{{ item.name }}</span>
|
||||
</div>
|
||||
@ -1054,8 +1045,8 @@ function editTextBox(inst: Instance) {
|
||||
<button class="btn" @click="triggerImport">导入 JSON</button>
|
||||
<!-- 上传模型到模型广场(需登录) -->
|
||||
<UploadButton :makeSave="makeSave" />
|
||||
<input ref="importInputRef" type="file" accept="application/json,.json"
|
||||
class="hidden-file" @change="onImportFileChange" />
|
||||
<input ref="importInputRef" type="file" accept="application/json,.json" class="hidden-file"
|
||||
@change="onImportFileChange" />
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@ -1063,40 +1054,33 @@ function editTextBox(inst: Instance) {
|
||||
<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">
|
||||
@pointerleave="() => { onViewportMouseUp(); onViewportMouseUpDrag(); onViewportMouseUpSlider() }" @wheel="onWheel"
|
||||
@dragover="onCanvasDragOver" @drop="onCanvasDrop" @click="clearSelection">
|
||||
<!-- 世界平面:网格背景,应用 transform -->
|
||||
<div class="world" :style="worldStyle" ref="worldRef">
|
||||
<!-- 导线(SVG 覆盖层) -->
|
||||
<svg class="wires" :width="state.worldSize" :height="state.worldSize"
|
||||
@mousemove="onViewportMouseMoveTrack">
|
||||
<svg class="wires" :width="state.worldSize" :height="state.worldSize" @mousemove="onViewportMouseMoveTrack">
|
||||
<g>
|
||||
<g v-for="w in wires" :key="w.id" class="wire-group"
|
||||
@click.stop="(e) => removeWire(w.id, e)">
|
||||
<line class="wire-base"
|
||||
:x1="getConnectionPointWorldPos(getInstance(w.a.instId)!, w.a.cpIndex).x"
|
||||
<g v-for="w in wires" :key="w.id" class="wire-group" @click.stop="(e) => removeWire(w.id, e)">
|
||||
<line class="wire-base" :x1="getConnectionPointWorldPos(getInstance(w.a.instId)!, w.a.cpIndex).x"
|
||||
:y1="getConnectionPointWorldPos(getInstance(w.a.instId)!, w.a.cpIndex).y"
|
||||
:x2="getConnectionPointWorldPos(getInstance(w.b.instId)!, w.b.cpIndex).x"
|
||||
:y2="getConnectionPointWorldPos(getInstance(w.b.instId)!, w.b.cpIndex).y"
|
||||
stroke-linecap="round" vector-effect="non-scaling-stroke" />
|
||||
:y2="getConnectionPointWorldPos(getInstance(w.b.instId)!, w.b.cpIndex).y" stroke-linecap="round"
|
||||
vector-effect="non-scaling-stroke" />
|
||||
</g>
|
||||
|
||||
<!-- 预览线:当选中一个端点时,显示到鼠标或吸附端点的线 -->
|
||||
<line v-if="pendingEndpoint"
|
||||
:x1="getConnectionPointWorldPos(getInstance(pendingEndpoint.instId)!, pendingEndpoint.cpIndex).x"
|
||||
:y1="getConnectionPointWorldPos(getInstance(pendingEndpoint.instId)!, pendingEndpoint.cpIndex).y"
|
||||
:x2="previewTarget.x" :y2="previewTarget.y"
|
||||
class="wire preview" />
|
||||
:x2="previewTarget.x" :y2="previewTarget.y" class="wire preview" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<!-- 已放置的元件(可拖动) -->
|
||||
<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)' }"
|
||||
@pointerdown="(e) => onInstanceMouseDown(e, inst)"
|
||||
@click.stop="() => selectInstance(inst.id)"
|
||||
@pointerdown="(e) => onInstanceMouseDown(e, inst)" @click.stop="() => selectInstance(inst.id)"
|
||||
:title="(elements.find(e => e.key === inst.key)?.name || inst.key)">
|
||||
<div class="inst-box"
|
||||
:style="{ width: inst.size > 0 ? inst.size + 'px' : 'auto', height: inst.size > 0 ? inst.size + 'px' : 'auto' }">
|
||||
@ -1104,8 +1088,7 @@ function editTextBox(inst: Instance) {
|
||||
? (((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="() => {
|
||||
: inst.url)" :alt="inst.key" :draggable="false" @contextmenu.prevent="() => {
|
||||
const meta = elements.find(e => e.key === inst.key);
|
||||
if (!meta?.stateImages) return;
|
||||
if (inst.key === 'switch') {
|
||||
@ -1126,36 +1109,31 @@ function editTextBox(inst: Instance) {
|
||||
|
||||
<!-- 电阻色环叠加层:根据实时电阻值渲染 4 个色带 -->
|
||||
<template v-if="inst.key === 'resistor'">
|
||||
<div v-for="(c, bi) in getResistorBandColors(inst)" :key="bi"
|
||||
class="res-band" :style="{
|
||||
left: getResBandX(inst, bi) + 'px',
|
||||
top: (inst.size * RES_BAND_TOP_FRAC) + 'px',
|
||||
width: (inst.size * RES_BAND_WIDTH_FRAC) + 'px',
|
||||
height: (inst.size * RES_BAND_HEIGHT_FRAC) + 'px',
|
||||
backgroundColor: c
|
||||
}"></div>
|
||||
<div v-for="(c, bi) in getResistorBandColors(inst)" :key="bi" class="res-band" :style="{
|
||||
left: getResBandX(inst, bi) + 'px',
|
||||
top: (inst.size * RES_BAND_TOP_FRAC) + 'px',
|
||||
width: (inst.size * RES_BAND_WIDTH_FRAC) + 'px',
|
||||
height: (inst.size * RES_BAND_HEIGHT_FRAC) + 'px',
|
||||
backgroundColor: c
|
||||
}"></div>
|
||||
</template>
|
||||
|
||||
<!-- 滑动变阻器滑块 -->
|
||||
<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) + '%',
|
||||
}" @pointerdown.stop="() => { sliderDraggingId = inst.id }" />
|
||||
|
||||
<!-- 灯泡发散光线叠加层 -->
|
||||
<svg v-if="inst.key === 'light_bulb'" class="bulb-glow"
|
||||
:width="inst.size" :height="inst.size"
|
||||
:viewBox="'0 0 ' + inst.size + ' ' + inst.size"
|
||||
aria-hidden="true">
|
||||
<svg v-if="inst.key === 'light_bulb'" class="bulb-glow" :width="inst.size" :height="inst.size"
|
||||
:viewBox="'0 0 ' + inst.size + ' ' + inst.size" aria-hidden="true">
|
||||
<g :opacity="getBulbGlow(inst).opacity">
|
||||
<line v-for="k in getBulbGlow(inst).count" :key="k"
|
||||
:x1="(inst.size / 2) + Math.cos(2 * Math.PI * (k - 1) / getBulbGlow(inst).count) * (inst.size * 0.42)"
|
||||
:y1="(inst.size / 2) + Math.sin(2 * Math.PI * (k - 1) / getBulbGlow(inst).count) * (inst.size * 0.42)"
|
||||
:x2="(inst.size / 2) + Math.cos(2 * Math.PI * (k - 1) / getBulbGlow(inst).count) * (inst.size * (0.42 + getBulbGlow(inst).lenFrac))"
|
||||
:y2="(inst.size / 2) + Math.sin(2 * Math.PI * (k - 1) / getBulbGlow(inst).count) * (inst.size * (0.42 + getBulbGlow(inst).lenFrac))"
|
||||
stroke="#f59e0b" stroke-width="2" stroke-linecap="round"
|
||||
vector-effect="non-scaling-stroke" />
|
||||
stroke="#f59e0b" stroke-width="2" stroke-linecap="round" vector-effect="non-scaling-stroke" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
@ -1173,8 +1151,7 @@ function editTextBox(inst: Instance) {
|
||||
|
||||
<!-- 连接点(显示为虚线圆圈) -->
|
||||
<div v-for="inst in instances" :key="inst.id + '-cps'">
|
||||
<template
|
||||
v-for="(cp, idx) in (elements.find(e => e.key === inst.key)?.connectionPoints || [])"
|
||||
<template v-for="(cp, idx) in (elements.find(e => e.key === inst.key)?.connectionPoints || [])"
|
||||
:key="inst.id + '-' + idx">
|
||||
<div class="endpoint" :class="{
|
||||
active: pendingEndpoint && pendingEndpoint.instId === inst.id && pendingEndpoint.cpIndex === idx,
|
||||
@ -1194,49 +1171,39 @@ function editTextBox(inst: Instance) {
|
||||
|
||||
<!-- 右侧 AI 助手面板 -->
|
||||
<aside v-if="!isPreview && showAI" class="ai-aside">
|
||||
<AIAssistant :state="aiState" @error="onAiError"
|
||||
@close="showAI = false" />
|
||||
<AIAssistant :state="aiState" @error="onAiError" @close="showAI = false" />
|
||||
</aside>
|
||||
<button v-else-if="!isPreview" class="ai-reopen" title="打开 AI 助手"
|
||||
@click="showAI = true">◀</button>
|
||||
<button v-else-if="!isPreview" class="ai-reopen" title="打开 AI 助手" @click="showAI = true">◀</button>
|
||||
|
||||
<!-- 底部属性面板:编辑选中元件的业务属性(非尺寸) -->
|
||||
<footer v-if="selectedInst" class="bottom-panel"
|
||||
:style="{ right: showAI ? '360px' : '0' }">
|
||||
<footer v-if="selectedInst" class="bottom-panel" :style="{ right: showAI ? '360px' : '0' }">
|
||||
<div class="panel-inner">
|
||||
<div class="prop-title">{{ selectedInst.key }} 属性</div>
|
||||
<!-- 预设按钮区域:显示当前元件的预设,点击一键应用(当未隐藏属性时) -->
|
||||
<div
|
||||
v-if="selectedMeta?.preset?.length && String(selectedInst!.props['hideProps'] ?? 'off') !== 'on'"
|
||||
<div v-if="selectedMeta?.preset?.length && String(selectedInst!.props['hideProps'] ?? 'off') !== 'on'"
|
||||
class="presets">
|
||||
<span class="preset-label">预设</span>
|
||||
<button v-for="p in selectedMeta!.preset" :key="p.name"
|
||||
class="preset-btn" @click.stop="applyPreset(p)">{{
|
||||
p.name
|
||||
}}</button>
|
||||
<button v-for="p in selectedMeta!.preset" :key="p.name" class="preset-btn" @click.stop="applyPreset(p)">{{
|
||||
p.name
|
||||
}}</button>
|
||||
</div>
|
||||
<div class="props">
|
||||
<!-- 实时电学量(在隐藏属性=on时不显示) -->
|
||||
<label class="prop-row"
|
||||
v-if="String(selectedInst!.props['hideProps'] ?? 'off') !== 'on'">
|
||||
<label class="prop-row" v-if="String(selectedInst!.props['hideProps'] ?? 'off') !== 'on'">
|
||||
<span class="label">电压</span>
|
||||
<span>{{ formatValue(instLive[selectedInst!.id]?.v ?? null, 'V')
|
||||
}}</span>
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="prop-row"
|
||||
v-if="String(selectedInst!.props['hideProps'] ?? 'off') !== 'on'">
|
||||
<label class="prop-row" v-if="String(selectedInst!.props['hideProps'] ?? 'off') !== 'on'">
|
||||
<span class="label">电流</span>
|
||||
<span>{{ formatValue(instLive[selectedInst!.id]?.i ?? null, 'A')
|
||||
}}</span>
|
||||
}}</span>
|
||||
</label>
|
||||
<!-- 旋转控制(在隐藏属性=on时不显示) -->
|
||||
<label class="prop-row"
|
||||
v-if="String(selectedInst!.props['hideProps'] ?? 'off') !== 'on'">
|
||||
<label class="prop-row" v-if="String(selectedInst!.props['hideProps'] ?? 'off') !== 'on'">
|
||||
<span class="label">旋转</span>
|
||||
<input type="range" min="-180" max="180" step="1"
|
||||
v-model.number="selectedInst!.rotation" />
|
||||
<input type="number" class="deg-input" step="1"
|
||||
v-model.number="selectedInst!.rotation" />
|
||||
<input type="range" min="-180" max="180" step="1" v-model.number="selectedInst!.rotation" />
|
||||
<input type="number" class="deg-input" step="1" v-model.number="selectedInst!.rotation" />
|
||||
<span class="unit">deg</span>
|
||||
<button class="rot-btn"
|
||||
@click="selectedInst!.rotation = ((selectedInst!.rotation || 0) - 45 + 360) % 360">-45°</button>
|
||||
@ -1252,8 +1219,7 @@ function editTextBox(inst: Instance) {
|
||||
</select>
|
||||
</label>
|
||||
<!-- 业务属性编辑:当未隐藏属性时显示 -->
|
||||
<template
|
||||
v-if="String(selectedInst!.props['hideProps'] ?? 'off') !== 'on'"
|
||||
<template v-if="String(selectedInst!.props['hideProps'] ?? 'off') !== 'on'"
|
||||
v-for="schema in (elements.find(e => e.key === selectedInst!.key)?.propertySchemas || [])"
|
||||
:key="schema.key">
|
||||
<label class="prop-row" v-if="schema.key !== 'hideProps'">
|
||||
@ -1264,25 +1230,21 @@ function editTextBox(inst: Instance) {
|
||||
v-model="selectedInst!.props[schema.key] as string" />
|
||||
<input v-else-if="schema.type === 'color'" type="color"
|
||||
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 : [])"
|
||||
<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>
|
||||
<span class="unit" v-if="schema.type === 'number' && (schema as any).unit">{{
|
||||
(schema as any).unit }}</span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 右侧操作区:删除按钮 -->
|
||||
<div class="panel-actions">
|
||||
<button class="danger-btn" title="删除此元件及其所有连接的导线"
|
||||
@click="deleteSelected">删除元件</button>
|
||||
<button class="danger-btn" title="删除此元件及其所有连接的导线" @click="deleteSelected">删除元件</button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@ -196,17 +196,19 @@ export const elements: CircuitElement[] = [
|
||||
{ x: 0.45, y: 1, name: '负极' },
|
||||
],
|
||||
preset: [{
|
||||
name: '微安电流表', propertyValues: { resistance: 0.05, renderFunc: '(i) => `${(i * 1000000).toFixed(0)} uA`' }
|
||||
name: '微安电流表', propertyValues: { resistance: 0.05, dispNum: 1000000, dispFig: 2, dispUnit: 'uA' }
|
||||
}, {
|
||||
name: '毫安电流表', propertyValues: { resistance: 0.5, renderFunc: '(i) => `${(i * 1000).toFixed(1)} mA`' }
|
||||
name: '毫安电流表', propertyValues: { resistance: 0.5, dispNum: 1000, dispFig: 1, dispUnit: 'mA' }
|
||||
}, {
|
||||
name: '电流表', propertyValues: { resistance: 0.1, renderFunc: '(i) => `${(i).toFixed(2)} A`' }
|
||||
name: '电流表', propertyValues: { resistance: 0.1, dispNum: 1, dispFig: 2, dispUnit: 'A' }
|
||||
}, {
|
||||
name: '电压表', propertyValues: { resistance: 10000000, renderFunc: '(i, r) => `${(i * r).toFixed(2)} V`' }
|
||||
name: '电压表', propertyValues: { resistance: 10000000, dispNum: 10000000, dispFig: 2, dispUnit: 'V' }
|
||||
}],
|
||||
propertySchemas: [
|
||||
{ key: 'resistance', label: '电阻', type: 'number', unit: 'Ω', default: 0.1 },
|
||||
{ key: 'renderFunc', label: '显示函数', type: 'text', default: '(i) => `${(i).toFixed(2)} A`' },
|
||||
{ key: 'dispNum', label: '显示数值倍率', type: 'number', default: 1 },
|
||||
{ key: 'dispFig', label: '有效数字位数', type: 'number', default: 2 },
|
||||
{ key: 'dispUnit', label: '单位', type: 'text', default: 'A' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
6
src/vite-env.d.ts
vendored
6
src/vite-env.d.ts
vendored
@ -1 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user