修复电表renderF安全策略

This commit is contained in:
feie9454 2025-09-13 16:11:05 +08:00
parent a94ce15731
commit 61f76c338b
3 changed files with 65 additions and 95 deletions

View File

@ -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,8 +1109,7 @@ 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="{
<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',
@ -1138,24 +1120,20 @@ function editTextBox(inst: Instance) {
<!-- 滑动变阻器滑块 -->
<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)">{{
<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>
</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>
</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">{{
<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>

View File

@ -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
View File

@ -1 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}