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() {
style={{ background: 'none', border: '1px solid #333', borderRadius: 3, color: '#6366f1', fontSize: 10, cursor: 'pointer', padding: '1px 6px' }}
onClick={() => {
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 });
}}
>+ 添加</button>
......@@ -434,7 +434,7 @@ export default function ComponentEditor() {
)}
{(editingTemplate.params || []).map((p, i) => (
<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>
<label className={styles.propLabel} style={{ fontSize: 9 }}>英文key</label>
<input
......@@ -450,31 +450,67 @@ export default function ComponentEditor() {
/>
</div>
<div>
<label className={styles.propLabel} style={{ fontSize: 9 }}>名称</label>
<input
<label className={styles.propLabel} style={{ fontSize: 9 }}>类型</label>
<select
className={styles.propInput}
value={p.label}
value={p.type || 'string'}
style={{ fontSize: 10 }}
onChange={(e) => {
const params = [...editingTemplate.params];
params[i] = { ...params[i], label: e.target.value };
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>
<label className={styles.propLabel} style={{ fontSize: 9 }}>默认值</label>
<label className={styles.propLabel} style={{ fontSize: 9 }}>名称</label>
<input
className={styles.propInput}
value={p.defaultValue}
value={p.label}
style={{ fontSize: 10 }}
onChange={(e) => {
const params = [...editingTemplate.params];
params[i] = { ...params[i], defaultValue: e.target.value };
params[i] = { ...params[i], label: e.target.value };
updateEditing({ params });
}}
/>
</div>
<div>
<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
className={styles.propInput}
value={String(p.defaultValue ?? '')}
style={{ fontSize: 10 }}
onChange={(e) => {
const params = [...editingTemplate.params];
params[i] = { ...params[i], defaultValue: e.target.value };
updateEditing({ params });
}}
/>
)}
</div>
<button
style={{ background: 'none', border: 'none', color: '#666', cursor: 'pointer', fontSize: 12, padding: '2px 4px' }}
onClick={() => {
......
......@@ -81,7 +81,7 @@ function CustomDeviceNode({ data, selected }) {
fontSize: 11, fontWeight: 600, color: '#fff',
letterSpacing: 0.5, flex: 1,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
}}>{name}</span>
}}>{ data.label || name}</span>
{isHardware && (
<span style={{
fontSize: 8, fontWeight: 800, color: '#fff',
......
......@@ -6,8 +6,21 @@
import { useCallback, useState, useRef } from 'react';
import useFlowStore from '../../hooks/useFlowStore';
import { getModelMapping } from '../../utils/modelMapping';
import { coerceParamValue, DEVICE_CATEGORIES } from '../../utils/constants';
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() {
const {
selectedNode,
......@@ -151,16 +164,36 @@ export default function PropertiesPanel() {
<span style={{ fontSize: 8 }}>{paramsCollapsed ? '\u25B6' : '\u25BC'}</span>
模型参数 ({selectedNode.data.templateData.params.length})
</div>
{!paramsCollapsed && selectedNode.data.templateData.params.map(p => (
<div key={p.key} style={{ marginBottom: 6 }}>
<label className={styles.label} style={{ marginBottom: 2 }}>{p.label}</label>
<input
className={styles.input}
value={selectedNode.data.paramValues?.[p.key] ?? p.defaultValue ?? ''}
onChange={(e) => updateNodeParam(selectedNode.id, p.key, e.target.value)}
/>
</div>
))}
{!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 }}>
<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
className={styles.input}
type={pType === 'int' || pType === 'real' ? 'number' : 'text'}
step={pType === 'real' ? 'any' : undefined}
value={val ?? ''}
onChange={(e) => updateNodeParam(selectedNode.id, p.key, coerceParamValue(e.target.value, pType))}
/>
)}
</div>
);
})}
</>
)}
......
......@@ -93,7 +93,7 @@ export default function LiveChart({ rosBridgeUrl, sessionId, onClose, onDone })
const init = {};
p.forEach(port => {
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);
}
......
......@@ -5,7 +5,7 @@ import { create } from 'zustand';
import { applyNodeChanges, applyEdgeChanges, addEdge as rfAddEdge } from '@xyflow/react';
import { parseConnectionSheet, toReactFlowData } from '../utils/xlsxParser';
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(时间戳 + 随机后缀,不依赖计数器) */
function uid() {
......@@ -111,16 +111,23 @@ const useFlowStore = create((set, get) => ({
params: deviceDef?.params || [],
};
// 用 defaultValue 初始化参数值
// 用 defaultValue 初始化参数值(根据 type 强制转换)
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 = {
id,
type: 'customDeviceNode',
position,
data: {
label: deviceType.label,
label: autoLabel,
deviceId: id,
color: deviceType.color,
templateData,
......@@ -216,11 +223,13 @@ const useFlowStore = create((set, get) => ({
},
updateNodeData: (nodeId, newData) => {
set({
nodes: get().nodes.map(n =>
n.id === nodeId ? { ...n, data: { ...n.data, ...newData } } : n
),
});
const nodes = get().nodes.map(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 = [
{ id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
],
params: [
{ key: 'name', label: '参数名', unit: '', defaultValue: 'param1' },
{ key: 'value', label: '值', unit: '', defaultValue: '0' },
{ key: 'name', label: '参数名', type: 'string', unit: '', defaultValue: 'param1' },
{ key: 'value', label: '值', type: 'int', unit: '', defaultValue: 0 },
],
},
{
......@@ -94,8 +94,8 @@ export const DEVICE_CATEGORIES = [
{ id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
],
params: [
{ key: 'name', label: '参数名', unit: '', defaultValue: 'param2' },
{ key: 'value', label: '值', unit: '', defaultValue: '0.0' },
{ key: 'name', label: '参数名', type: 'string', unit: '', defaultValue: 'param2' },
{ key: 'value', label: '值', type: 'real', unit: '', defaultValue: 0.0 },
],
},
{
......@@ -105,8 +105,8 @@ export const DEVICE_CATEGORIES = [
{ id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
],
params: [
{ key: 'name', label: '参数名', unit: '', defaultValue: 'param3' },
{ key: 'value', label: '值', unit: '', defaultValue: '' },
{ key: 'name', label: '参数名', type: 'string', unit: '', defaultValue: 'param3' },
{ key: 'value', label: '值', type: 'string', unit: '', defaultValue: '' },
],
},
{
......@@ -116,8 +116,8 @@ export const DEVICE_CATEGORIES = [
{ id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
],
params: [
{ key: 'name', label: '参数名', unit: '', defaultValue: 'param4' },
{ key: 'value', label: '值', unit: '', defaultValue: 'true' },
{ key: 'name', label: '参数名', type: 'string', unit: '', defaultValue: 'param4' },
{ key: 'value', label: '值', type: 'bool', unit: '', defaultValue: true },
],
},
],
......@@ -134,7 +134,7 @@ export const DEVICE_CATEGORIES = [
{ id: 'p2', name: '2', side: 'right', position: 0.5, type: 'power', connector: 'n' },
],
params: [
{ key: 'R', label: '阻值', unit: 'Ω', defaultValue: '10k' },
{ key: 'R', label: '阻值', type: 'string', unit: 'Ω', defaultValue: '10k' },
],
},
{
......@@ -145,7 +145,7 @@ export const DEVICE_CATEGORIES = [
{ id: 'p2', name: '2', side: 'right', position: 0.5, type: 'power', connector: 'n' },
],
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 = [
{ id: 'p2', name: '2', side: 'right', position: 0.5, type: 'power', connector: 'n' },
],
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 = [
{ id: 'p-neg', name: 'V-', side: 'right', position: 0.5, type: 'power', connector: 'n' },
],
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 = [
{ id: 'p-out', name: 'OUT', side: 'right', position: 0.5, type: 'power', connector: 'n' },
],
params: [
{ key: 'closed', label: '初始状态', unit: '', defaultValue: 'false' },
{ key: 'closed', label: '初始状态', type: 'bool', unit: '', defaultValue: false },
],
},
],
......@@ -242,7 +242,7 @@ export const DEVICE_CATEGORIES = [
{ id: 'p-out', name: '出线', side: 'right', position: 0.5, type: 'power', connector: 'pOut' },
],
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 = [
{ id: 'p-pe',name: 'PE', side: 'bottom', position: 0.5, type: 'power', connector: 'pe' },
],
params: [
{ key: 'Pw', label: '功率', unit: 'kW', defaultValue: '2.2' },
{ key: 'Rpm', label: '转速', unit: 'rpm', defaultValue: '1450' },
{ key: 'Pw', label: '功率', type: 'real', unit: 'kW', defaultValue: 2.2 },
{ key: 'Rpm', label: '转速', type: 'int', unit: 'rpm', defaultValue: 1450 },
],
},
{
......@@ -573,3 +573,24 @@ export function getPortTypeByFunctionCode(functionCode) {
};
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') {
hardwarePorts.push({
name: instanceName,
hwNodeId: node.id,
hwNodeLabel: node.data?.label || '',
hwNodeLabel: (node.data?.label || type) + ' #' + instanceCounter[baseName],
instanceName,
modelType: type,
simMode: simModeMap[type] || 'linear',
......@@ -405,6 +405,9 @@ export function exportToModelicaHIL(data, modelName = 'Circuit') {
fmuVarPrefix: instanceName,
controlVar: mapping?.controlVar ? `${instanceName}.${mapping.controlVar}` : '',
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