Commit e65e953b authored by Developer's avatar Developer

feat: 参数类型系统 + 开关标签区分 + 初始状态同步

- 为参数定义添加 type 字段 (bool/int/real/string)
- 添加 coerceParamValue 工具函数进行类型转换
- 自定义组件编辑器新增类型下拉框
- PropertiesPanel bool 参数使用 true/false 下拉框
- 兼容旧节点: 从 constants 定义中查找参数类型
- 画布节点标题优先显示用户自定义 label
- 同类组件自动递增编号 (如 SW 开关1, SW 开关2)
- 硬件面板开关初始状态与画布 paramValues.closed 同步
- updateNodeData 同步更新 selectedNode
parent 3e05f33d
...@@ -424,7 +424,7 @@ export default function ComponentEditor() { ...@@ -424,7 +424,7 @@ export default function ComponentEditor() {
style={{ background: 'none', border: '1px solid #333', borderRadius: 3, color: '#6366f1', fontSize: 10, cursor: 'pointer', padding: '1px 6px' }} style={{ background: 'none', border: '1px solid #333', borderRadius: 3, color: '#6366f1', fontSize: 10, cursor: 'pointer', padding: '1px 6px' }}
onClick={() => { onClick={() => {
const params = [...(editingTemplate.params || [])]; const params = [...(editingTemplate.params || [])];
params.push({ key: `p${params.length + 1}`, label: '参数', unit: '', defaultValue: '' }); params.push({ key: `p${params.length + 1}`, label: '参数', type: 'real', unit: '', defaultValue: 0 });
updateEditing({ params }); updateEditing({ params });
}} }}
>+ 添加</button> >+ 添加</button>
...@@ -434,7 +434,7 @@ export default function ComponentEditor() { ...@@ -434,7 +434,7 @@ export default function ComponentEditor() {
)} )}
{(editingTemplate.params || []).map((p, i) => ( {(editingTemplate.params || []).map((p, i) => (
<div key={i} style={{ marginBottom: 8 }}> <div key={i} style={{ marginBottom: 8 }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr auto', gap: 4, alignItems: 'end' }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 80px 1fr 1fr auto', gap: 4, alignItems: 'end' }}>
<div> <div>
<label className={styles.propLabel} style={{ fontSize: 9 }}>英文key</label> <label className={styles.propLabel} style={{ fontSize: 9 }}>英文key</label>
<input <input
...@@ -449,6 +449,26 @@ export default function ComponentEditor() { ...@@ -449,6 +449,26 @@ export default function ComponentEditor() {
}} }}
/> />
</div> </div>
<div>
<label className={styles.propLabel} style={{ fontSize: 9 }}>类型</label>
<select
className={styles.propInput}
value={p.type || 'string'}
style={{ fontSize: 10 }}
onChange={(e) => {
const params = [...editingTemplate.params];
const newType = e.target.value;
const autoDefault = newType === 'bool' ? true : newType === 'int' ? 0 : newType === 'real' ? 0 : '';
params[i] = { ...params[i], type: newType, defaultValue: autoDefault };
updateEditing({ params });
}}
>
<option value="bool">bool</option>
<option value="int">int</option>
<option value="real">real</option>
<option value="string">string</option>
</select>
</div>
<div> <div>
<label className={styles.propLabel} style={{ fontSize: 9 }}>名称</label> <label className={styles.propLabel} style={{ fontSize: 9 }}>名称</label>
<input <input
...@@ -464,9 +484,24 @@ export default function ComponentEditor() { ...@@ -464,9 +484,24 @@ export default function ComponentEditor() {
</div> </div>
<div> <div>
<label className={styles.propLabel} style={{ fontSize: 9 }}>默认值</label> <label className={styles.propLabel} style={{ fontSize: 9 }}>默认值</label>
{(p.type === 'bool') ? (
<select
className={styles.propInput}
value={String(p.defaultValue ?? true)}
style={{ fontSize: 10 }}
onChange={(e) => {
const params = [...editingTemplate.params];
params[i] = { ...params[i], defaultValue: e.target.value === 'true' };
updateEditing({ params });
}}
>
<option value="true">true</option>
<option value="false">false</option>
</select>
) : (
<input <input
className={styles.propInput} className={styles.propInput}
value={p.defaultValue} value={String(p.defaultValue ?? '')}
style={{ fontSize: 10 }} style={{ fontSize: 10 }}
onChange={(e) => { onChange={(e) => {
const params = [...editingTemplate.params]; const params = [...editingTemplate.params];
...@@ -474,6 +509,7 @@ export default function ComponentEditor() { ...@@ -474,6 +509,7 @@ export default function ComponentEditor() {
updateEditing({ params }); updateEditing({ params });
}} }}
/> />
)}
</div> </div>
<button <button
style={{ background: 'none', border: 'none', color: '#666', cursor: 'pointer', fontSize: 12, padding: '2px 4px' }} style={{ background: 'none', border: 'none', color: '#666', cursor: 'pointer', fontSize: 12, padding: '2px 4px' }}
......
...@@ -81,7 +81,7 @@ function CustomDeviceNode({ data, selected }) { ...@@ -81,7 +81,7 @@ function CustomDeviceNode({ data, selected }) {
fontSize: 11, fontWeight: 600, color: '#fff', fontSize: 11, fontWeight: 600, color: '#fff',
letterSpacing: 0.5, flex: 1, letterSpacing: 0.5, flex: 1,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
}}>{name}</span> }}>{ data.label || name}</span>
{isHardware && ( {isHardware && (
<span style={{ <span style={{
fontSize: 8, fontWeight: 800, color: '#fff', fontSize: 8, fontWeight: 800, color: '#fff',
......
...@@ -6,8 +6,21 @@ ...@@ -6,8 +6,21 @@
import { useCallback, useState, useRef } from 'react'; import { useCallback, useState, useRef } from 'react';
import useFlowStore from '../../hooks/useFlowStore'; import useFlowStore from '../../hooks/useFlowStore';
import { getModelMapping } from '../../utils/modelMapping'; import { getModelMapping } from '../../utils/modelMapping';
import { coerceParamValue, DEVICE_CATEGORIES } from '../../utils/constants';
import styles from './PropertiesPanel.module.css'; import styles from './PropertiesPanel.module.css';
/** 从当前设备定义中查找参数类型(兼容旧节点没有 type 字段的情况) */
function getParamType(deviceType, paramKey) {
for (const cat of DEVICE_CATEGORIES) {
const item = cat.items.find(i => i.type === deviceType);
if (item?.params) {
const param = item.params.find(p => p.key === paramKey);
if (param?.type) return param.type;
}
}
return 'string';
}
export default function PropertiesPanel() { export default function PropertiesPanel() {
const { const {
selectedNode, selectedNode,
...@@ -151,16 +164,36 @@ export default function PropertiesPanel() { ...@@ -151,16 +164,36 @@ export default function PropertiesPanel() {
<span style={{ fontSize: 8 }}>{paramsCollapsed ? '\u25B6' : '\u25BC'}</span> <span style={{ fontSize: 8 }}>{paramsCollapsed ? '\u25B6' : '\u25BC'}</span>
模型参数 ({selectedNode.data.templateData.params.length}) 模型参数 ({selectedNode.data.templateData.params.length})
</div> </div>
{!paramsCollapsed && selectedNode.data.templateData.params.map(p => ( {!paramsCollapsed && selectedNode.data.templateData.params.map(p => {
const val = selectedNode.data.paramValues?.[p.key] ?? p.defaultValue;
const pType = p.type || getParamType(selectedNode.data.templateData?.type, p.key);
return (
<div key={p.key} style={{ marginBottom: 6 }}> <div key={p.key} style={{ marginBottom: 6 }}>
<label className={styles.label} style={{ marginBottom: 2 }}>{p.label}</label> <label className={styles.label} style={{ marginBottom: 2 }}>{p.label}</label>
{pType === 'bool' ? (
<select
className={styles.input}
value={String(val === true || val === 'true')}
onChange={(e) => {
updateNodeParam(selectedNode.id, p.key, e.target.value === 'true');
}}
>
<option value="true">true</option>
<option value="false">false</option>
</select>
) : (
<input <input
className={styles.input} className={styles.input}
value={selectedNode.data.paramValues?.[p.key] ?? p.defaultValue ?? ''} type={pType === 'int' || pType === 'real' ? 'number' : 'text'}
onChange={(e) => updateNodeParam(selectedNode.id, p.key, e.target.value)} step={pType === 'real' ? 'any' : undefined}
value={val ?? ''}
onChange={(e) => updateNodeParam(selectedNode.id, p.key, coerceParamValue(e.target.value, pType))}
/> />
)}
</div> </div>
))} );
})}
</> </>
)} )}
......
...@@ -93,7 +93,7 @@ export default function LiveChart({ rosBridgeUrl, sessionId, onClose, onDone }) ...@@ -93,7 +93,7 @@ export default function LiveChart({ rosBridgeUrl, sessionId, onClose, onDone })
const init = {}; const init = {};
p.forEach(port => { p.forEach(port => {
const name = port.instance_name || port.name; const name = port.instance_name || port.name;
init[name] = port.mode === 'switch' ? 0 : port.gain; init[name] = port.gain ?? (port.mode === 'switch' ? 0 : 1);
}); });
setHwValues(init); setHwValues(init);
} }
......
...@@ -5,7 +5,7 @@ import { create } from 'zustand'; ...@@ -5,7 +5,7 @@ import { create } from 'zustand';
import { applyNodeChanges, applyEdgeChanges, addEdge as rfAddEdge } from '@xyflow/react'; import { applyNodeChanges, applyEdgeChanges, addEdge as rfAddEdge } from '@xyflow/react';
import { parseConnectionSheet, toReactFlowData } from '../utils/xlsxParser'; import { parseConnectionSheet, toReactFlowData } from '../utils/xlsxParser';
import { getLayoutedNodes } from '../utils/layoutEngine'; import { getLayoutedNodes } from '../utils/layoutEngine';
import { getDeviceType, getDeviceDefinition, LAYOUT_DIRECTION, PORT_TYPES, getPortTypeByFunctionCode } from '../utils/constants'; import { getDeviceType, getDeviceDefinition, coerceParamValue, LAYOUT_DIRECTION, PORT_TYPES, getPortTypeByFunctionCode } from '../utils/constants';
/** 生成唯一节点/端口 ID(时间戳 + 随机后缀,不依赖计数器) */ /** 生成唯一节点/端口 ID(时间戳 + 随机后缀,不依赖计数器) */
function uid() { function uid() {
...@@ -111,16 +111,23 @@ const useFlowStore = create((set, get) => ({ ...@@ -111,16 +111,23 @@ const useFlowStore = create((set, get) => ({
params: deviceDef?.params || [], params: deviceDef?.params || [],
}; };
// 用 defaultValue 初始化参数值 // 用 defaultValue 初始化参数值(根据 type 强制转换)
const paramValues = {}; const paramValues = {};
(deviceDef?.params || []).forEach(p => { paramValues[p.key] = p.defaultValue; }); (deviceDef?.params || []).forEach(p => { paramValues[p.key] = coerceParamValue(p.defaultValue, p.type); });
// 自动递增编号: "SW 开关 1", "SW 开关 2", ...
const baseLabel = deviceType.label;
const existingLabels = new Set(get().nodes.map(n => n.data?.label).filter(Boolean));
let num = 1;
while (existingLabels.has(`${baseLabel}${num}`)) num++;
const autoLabel = `${baseLabel}${num}`;
const newNode = { const newNode = {
id, id,
type: 'customDeviceNode', type: 'customDeviceNode',
position, position,
data: { data: {
label: deviceType.label, label: autoLabel,
deviceId: id, deviceId: id,
color: deviceType.color, color: deviceType.color,
templateData, templateData,
...@@ -216,11 +223,13 @@ const useFlowStore = create((set, get) => ({ ...@@ -216,11 +223,13 @@ const useFlowStore = create((set, get) => ({
}, },
updateNodeData: (nodeId, newData) => { updateNodeData: (nodeId, newData) => {
set({ const nodes = get().nodes.map(n =>
nodes: get().nodes.map(n =>
n.id === nodeId ? { ...n, data: { ...n.data, ...newData } } : n n.id === nodeId ? { ...n, data: { ...n.data, ...newData } } : n
), );
}); const selectedNode = get().selectedNode;
const updatedSelected = selectedNode?.id === nodeId
? nodes.find(n => n.id === nodeId) : selectedNode;
set({ nodes, selectedNode: updatedSelected });
}, },
/** 切换节点的「实物」标记 */ /** 切换节点的「实物」标记 */
......
...@@ -83,8 +83,8 @@ export const DEVICE_CATEGORIES = [ ...@@ -83,8 +83,8 @@ export const DEVICE_CATEGORIES = [
{ id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' }, { id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
], ],
params: [ params: [
{ key: 'name', label: '参数名', unit: '', defaultValue: 'param1' }, { key: 'name', label: '参数名', type: 'string', unit: '', defaultValue: 'param1' },
{ key: 'value', label: '值', unit: '', defaultValue: '0' }, { key: 'value', label: '值', type: 'int', unit: '', defaultValue: 0 },
], ],
}, },
{ {
...@@ -94,8 +94,8 @@ export const DEVICE_CATEGORIES = [ ...@@ -94,8 +94,8 @@ export const DEVICE_CATEGORIES = [
{ id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' }, { id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
], ],
params: [ params: [
{ key: 'name', label: '参数名', unit: '', defaultValue: 'param2' }, { key: 'name', label: '参数名', type: 'string', unit: '', defaultValue: 'param2' },
{ key: 'value', label: '值', unit: '', defaultValue: '0.0' }, { key: 'value', label: '值', type: 'real', unit: '', defaultValue: 0.0 },
], ],
}, },
{ {
...@@ -105,8 +105,8 @@ export const DEVICE_CATEGORIES = [ ...@@ -105,8 +105,8 @@ export const DEVICE_CATEGORIES = [
{ id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' }, { id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
], ],
params: [ params: [
{ key: 'name', label: '参数名', unit: '', defaultValue: 'param3' }, { key: 'name', label: '参数名', type: 'string', unit: '', defaultValue: 'param3' },
{ key: 'value', label: '值', unit: '', defaultValue: '' }, { key: 'value', label: '值', type: 'string', unit: '', defaultValue: '' },
], ],
}, },
{ {
...@@ -116,8 +116,8 @@ export const DEVICE_CATEGORIES = [ ...@@ -116,8 +116,8 @@ export const DEVICE_CATEGORIES = [
{ id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' }, { id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
], ],
params: [ params: [
{ key: 'name', label: '参数名', unit: '', defaultValue: 'param4' }, { key: 'name', label: '参数名', type: 'string', unit: '', defaultValue: 'param4' },
{ key: 'value', label: '值', unit: '', defaultValue: 'true' }, { key: 'value', label: '值', type: 'bool', unit: '', defaultValue: true },
], ],
}, },
], ],
...@@ -134,7 +134,7 @@ export const DEVICE_CATEGORIES = [ ...@@ -134,7 +134,7 @@ export const DEVICE_CATEGORIES = [
{ id: 'p2', name: '2', side: 'right', position: 0.5, type: 'power', connector: 'n' }, { id: 'p2', name: '2', side: 'right', position: 0.5, type: 'power', connector: 'n' },
], ],
params: [ params: [
{ key: 'R', label: '阻值', unit: 'Ω', defaultValue: '10k' }, { key: 'R', label: '阻值', type: 'string', unit: 'Ω', defaultValue: '10k' },
], ],
}, },
{ {
...@@ -145,7 +145,7 @@ export const DEVICE_CATEGORIES = [ ...@@ -145,7 +145,7 @@ export const DEVICE_CATEGORIES = [
{ id: 'p2', name: '2', side: 'right', position: 0.5, type: 'power', connector: 'n' }, { id: 'p2', name: '2', side: 'right', position: 0.5, type: 'power', connector: 'n' },
], ],
params: [ params: [
{ key: 'C', label: '容值', unit: 'F', defaultValue: '100u' }, { key: 'C', label: '容值', type: 'string', unit: 'F', defaultValue: '100u' },
], ],
}, },
{ {
...@@ -156,7 +156,7 @@ export const DEVICE_CATEGORIES = [ ...@@ -156,7 +156,7 @@ export const DEVICE_CATEGORIES = [
{ id: 'p2', name: '2', side: 'right', position: 0.5, type: 'power', connector: 'n' }, { id: 'p2', name: '2', side: 'right', position: 0.5, type: 'power', connector: 'n' },
], ],
params: [ params: [
{ key: 'L', label: '感值', unit: 'H', defaultValue: '10m' }, { key: 'L', label: '感值', type: 'string', unit: 'H', defaultValue: '10m' },
], ],
}, },
{ {
...@@ -175,7 +175,7 @@ export const DEVICE_CATEGORIES = [ ...@@ -175,7 +175,7 @@ export const DEVICE_CATEGORIES = [
{ id: 'p-neg', name: 'V-', side: 'right', position: 0.5, type: 'power', connector: 'n' }, { id: 'p-neg', name: 'V-', side: 'right', position: 0.5, type: 'power', connector: 'n' },
], ],
params: [ params: [
{ key: 'V0', label: '电压', unit: 'V', defaultValue: '24' }, { key: 'V0', label: '电压', type: 'real', unit: 'V', defaultValue: 24 },
], ],
}, },
{ {
...@@ -193,7 +193,7 @@ export const DEVICE_CATEGORIES = [ ...@@ -193,7 +193,7 @@ export const DEVICE_CATEGORIES = [
{ id: 'p-out', name: 'OUT', side: 'right', position: 0.5, type: 'power', connector: 'n' }, { id: 'p-out', name: 'OUT', side: 'right', position: 0.5, type: 'power', connector: 'n' },
], ],
params: [ params: [
{ key: 'closed', label: '初始状态', unit: '', defaultValue: 'false' }, { key: 'closed', label: '初始状态', type: 'bool', unit: '', defaultValue: false },
], ],
}, },
], ],
...@@ -242,7 +242,7 @@ export const DEVICE_CATEGORIES = [ ...@@ -242,7 +242,7 @@ export const DEVICE_CATEGORIES = [
{ id: 'p-out', name: '出线', side: 'right', position: 0.5, type: 'power', connector: 'pOut' }, { id: 'p-out', name: '出线', side: 'right', position: 0.5, type: 'power', connector: 'pOut' },
], ],
params: [ params: [
{ key: 'In', label: '额定电流', unit: 'A', defaultValue: '16' }, { key: 'In', label: '额定电流', type: 'real', unit: 'A', defaultValue: 16 },
], ],
}, },
{ {
...@@ -264,8 +264,8 @@ export const DEVICE_CATEGORIES = [ ...@@ -264,8 +264,8 @@ export const DEVICE_CATEGORIES = [
{ id: 'p-pe',name: 'PE', side: 'bottom', position: 0.5, type: 'power', connector: 'pe' }, { id: 'p-pe',name: 'PE', side: 'bottom', position: 0.5, type: 'power', connector: 'pe' },
], ],
params: [ params: [
{ key: 'Pw', label: '功率', unit: 'kW', defaultValue: '2.2' }, { key: 'Pw', label: '功率', type: 'real', unit: 'kW', defaultValue: 2.2 },
{ key: 'Rpm', label: '转速', unit: 'rpm', defaultValue: '1450' }, { key: 'Rpm', label: '转速', type: 'int', unit: 'rpm', defaultValue: 1450 },
], ],
}, },
{ {
...@@ -573,3 +573,24 @@ export function getPortTypeByFunctionCode(functionCode) { ...@@ -573,3 +573,24 @@ export function getPortTypeByFunctionCode(functionCode) {
}; };
return map[functionCode] || 'generic'; return map[functionCode] || 'generic';
} }
/**
* 根据参数类型将值转换为正确的 JS 类型
* @param {*} value - 原始值(可能是字符串)
* @param {'bool'|'int'|'real'|'string'} type - 参数类型
* @returns {*} 转换后的值
*/
export function coerceParamValue(value, type) {
switch (type) {
case 'bool':
if (typeof value === 'boolean') return value;
return value === 'true' || value === true || value === 1;
case 'int':
return parseInt(value, 10) || 0;
case 'real':
return parseFloat(value) || 0;
case 'string':
default:
return String(value ?? '');
}
}
...@@ -397,7 +397,7 @@ export function exportToModelicaHIL(data, modelName = 'Circuit') { ...@@ -397,7 +397,7 @@ export function exportToModelicaHIL(data, modelName = 'Circuit') {
hardwarePorts.push({ hardwarePorts.push({
name: instanceName, name: instanceName,
hwNodeId: node.id, hwNodeId: node.id,
hwNodeLabel: node.data?.label || '', hwNodeLabel: (node.data?.label || type) + ' #' + instanceCounter[baseName],
instanceName, instanceName,
modelType: type, modelType: type,
simMode: simModeMap[type] || 'linear', simMode: simModeMap[type] || 'linear',
...@@ -405,6 +405,9 @@ export function exportToModelicaHIL(data, modelName = 'Circuit') { ...@@ -405,6 +405,9 @@ export function exportToModelicaHIL(data, modelName = 'Circuit') {
fmuVarPrefix: instanceName, fmuVarPrefix: instanceName,
controlVar: mapping?.controlVar ? `${instanceName}.${mapping.controlVar}` : '', controlVar: mapping?.controlVar ? `${instanceName}.${mapping.controlVar}` : '',
topic: `/${toMoId(modelName)}/${instanceName}`, topic: `/${toMoId(modelName)}/${instanceName}`,
gain: simModeMap[type] === 'switch'
? ((() => { const v = node.data?.paramValues?.closed; return v === true || v === 'true' ? 1 : 0; })())
: undefined,
}); });
} }
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment