Commit 9b79e64e authored by Developer's avatar Developer

feat: 优化项目面板三段式布局、实物节点样式和HIL结果获取

- ProjectPanel: 重构为模型编译/软件仿真/半实物仿真三段式卡片布局
- ProjectPanel: 编译成功后才显示仿真按钮,项目点击展开/折叠
- ProjectPanel: 合并获取+查看HIL结果为单按钮,HIL时长默认10秒
- ProjectPanel: 半实物仿真按钮重命名,去除冗余状态提示
- CustomDeviceNode: 实物节点虚线框和发光使用卡片主题色
- CustomDeviceNode: 每种颜色独立动画名,避免多节点颜色冲突
- CustomDeviceNode: 实物节点选中时白色虚线边框+白色光晕
- PropertiesPanel: toggle关闭态改为蓝灰色,文字改为实物标记
- useProjectStore: localStorage持久化剔除csvData避免超限
- useProjectStore: fetchHilResults增加错误反馈,成功后清除errorDetail
- useProjectStore: openProject支持null参数实现折叠
parent e722bb2d
......@@ -44,16 +44,27 @@ function CustomDeviceNode({ data, selected }) {
background: '#1e1e2e',
borderRadius: 8,
outline: isHardware
? '2px dashed #fb923c'
? `${selected ? 3 : 2}px dashed ${selected ? '#fff' : color}`
: `2px solid ${selected ? '#fff' : color}`,
outlineOffset: -1,
boxShadow: isHardware
? '0 0 14px rgba(251,146,60,0.35)'
? (selected ? `0 0 16px rgba(255,255,255,0.4), 0 0 14px ${color}55` : `0 0 14px ${color}55`)
: selected ? `0 0 12px ${color}88` : '0 4px 12px rgba(0,0,0,0.3)',
overflow: 'visible',
transform: rotation ? `rotate(${rotation}deg)` : undefined,
fontFamily: "'Segoe UI', sans-serif",
animation: isHardware ? `hwGlow_${color.replace('#','')} 2s ease-in-out infinite` : undefined,
}}>
{/* 发光动画关键帧 — 按主题色独立命名 */}
{isHardware && (
<style>{`
@keyframes hwGlow_${color.replace('#','')} {
0%, 100% { box-shadow: 0 0 12px ${color}66; }
50% { box-shadow: 0 0 28px ${color}bb, 0 0 56px ${color}44; }
}
`}</style>
)}
{/* 彩色 Header */}
<div style={{
background: color,
......@@ -72,7 +83,14 @@ function CustomDeviceNode({ data, selected }) {
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
}}>{name}</span>
{isHardware && (
<span style={{ fontSize: 12, flexShrink: 0 }} title="实物设备">🔧</span>
<span style={{
fontSize: 8, fontWeight: 800, color: '#fff',
background: '#fb923c',
border: '1.5px solid #fff',
padding: '1px 6px', borderRadius: 10,
letterSpacing: 1, lineHeight: '13px',
flexShrink: 0,
}}>HW</span>
)}
</div>
......
......@@ -57,7 +57,7 @@ export default function ProjectPanel() {
const [showExecuteLog, setShowExecuteLog] = useState(false);
const [showResultsMode, setShowResultsMode] = useState(null); // null | 'sim' | 'hil'
const [simDuration, setSimDuration] = useState(1);
const [hilDuration, setHilDuration] = useState(1);
const [hilDuration, setHilDuration] = useState(10);
const [toastMsg, setToastMsg] = useState(null);
const [toastFading, setToastFading] = useState(false);
const [showLiveChart, setShowLiveChart] = useState(false);
......@@ -73,6 +73,33 @@ export default function ProjectPanel() {
}, 2000);
}, []);
// ── 监听仿真完成 /hil/done ──
const hilRunningProject = projects.find(p => p.hilStatus === 'running');
useEffect(() => {
if (!hilRunningProject) return;
const projectId = hilRunningProject.id;
const ws = new WebSocket(`ws://${window.location.hostname}:9090`);
let done = false;
ws.onopen = () => {
ws.send(JSON.stringify({ op: 'subscribe', topic: '/hil/done', type: 'std_msgs/String' }));
};
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.topic === '/hil/done' && !done) {
done = true;
hilDone(projectId);
}
} catch { /* ignore */ }
};
return () => {
done = true;
if (ws.readyState <= 1) ws.close();
};
}, [hilRunningProject?.id, hilDone]);
const activeProject = projects.find(p => p.id === activeProjectId) || null;
// 执行成功后自动弹出结果页面(仅在 running → success 转换时触发)
......@@ -185,7 +212,7 @@ export default function ProjectPanel() {
return (
<div key={p.id} className={`${styles.projectItem} ${isActive ? styles.active : ''}`}>
{/* 项目标题行 */}
<div className={styles.projectHeader} onClick={() => openProject(p.id)}>
<div className={styles.projectHeader} onClick={() => isActive ? openProject(null) : openProject(p.id)}>
<span className={styles.projectIcon}>
{p.compileStatus === 'success' ? '✅' : p.compileStatus === 'error' ? '❌' : '📄'}
</span>
......@@ -227,230 +254,235 @@ export default function ProjectPanel() {
{/* 展开区域:编译 & 执行 */}
{isActive && (
<div className={styles.expandedSection}>
{/* 状态指示 */}
<div className={styles.statusRow}>
<span style={{ fontSize: 10, color: '#666' }}>编译:</span>
<StatusBadge
status={p.compileStatus}
labels={{ none: '未编译', success: '编译成功', error: '编译失败' }}
/>
<span style={{ fontSize: 10, color: '#666', marginLeft: 8 }}>执行:</span>
<StatusBadge
status={p.executeStatus}
labels={{ none: '未执行', success: '执行完成', error: '执行失败' }}
/>
</div>
{/* 模型名 */}
<div className={styles.modelNameRow}>
<span style={{ fontSize: 11, color: '#666', flexShrink: 0 }}>模型名:</span>
<input
className={styles.modelNameInput}
value={p.modelName || 'Circuit'}
onChange={(e) => updateModelName(p.id, e.target.value)}
placeholder="Circuit"
onClick={(e) => e.stopPropagation()}
/>
</div>
{/* ── 模块 1: 模型编译 ── */}
<div className={styles.sectionCard}>
<div className={styles.sectionTitle}>
<span className={styles.sectionTitleIcon}></span> 模型编译
</div>
{/* 编译按钮 */}
<button
className={`${styles.simBtn} ${styles.compile}`}
onClick={(e) => { e.stopPropagation(); compileProject(p.id); }}
disabled={p.compileStatus === 'compiling'}
style={{ width: '100%', marginBottom: 6 }}
>
{p.compileStatus === 'compiling' ? (
<><span className={styles.spinner}></span> 编译中…</>
) : (
<>▶ 编译模型</>
)}
</button>
<div className={styles.modelNameRow}>
<span style={{ fontSize: 11, color: '#666', flexShrink: 0 }}>模型名:</span>
<input
className={styles.modelNameInput}
value={p.modelName || 'Circuit'}
onChange={(e) => updateModelName(p.id, e.target.value)}
placeholder="Circuit"
onClick={(e) => e.stopPropagation()}
/>
</div>
{/* 普通仿真: 时长 + 执行按钮 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
<label style={{ fontSize: 11, color: '#888', whiteSpace: 'nowrap' }}>仿真时长:</label>
<input
type="number"
min="0.1" max="3600" step="0.1"
value={simDuration}
onChange={(e) => setSimDuration(e.target.value === '' ? '' : Number(e.target.value))}
onBlur={() => { if (!simDuration || simDuration <= 0) setSimDuration(1); }}
onClick={(e) => e.stopPropagation()}
disabled={p.compileStatus !== 'success'}
style={{
flex: 1, padding: '3px 6px', fontSize: 11,
background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.12)',
borderRadius: 4, color: '#ccc', outline: 'none',
opacity: p.compileStatus !== 'success' ? 0.4 : 1,
}}
/>
<span style={{ fontSize: 11, color: '#666' }}></span>
</div>
<button
className={`${styles.simBtn} ${styles.execute}`}
onClick={(e) => { e.stopPropagation(); executeProject(p.id, { stopTime: simDuration }); }}
disabled={p.compileStatus !== 'success' || p.executeStatus === 'running'}
title={p.compileStatus !== 'success' ? '请先编译成功' : '下发等效设备执行'}
style={{ width: '100%', marginBottom: 6 }}
>
{p.executeStatus === 'running' ? (
<><span className={styles.spinner}></span> 下发中…</>
) : (
<>⏵ 下发等效设备执行</>
)}
</button>
<button
className={`${styles.simBtn} ${styles.compile}`}
onClick={(e) => { e.stopPropagation(); compileProject(p.id); }}
disabled={p.compileStatus === 'compiling'}
style={{ width: '100%', marginBottom: 4 }}
>
{p.compileStatus === 'compiling' ? (
<><span className={styles.spinner}></span> 编译中…</>
) : (
<>▶ 编译模型</>
)}
</button>
<div className={styles.statusRow}>
<span style={{ fontSize: 10, color: '#666' }}>状态:</span>
<StatusBadge
status={p.compileStatus}
labels={{ none: '未编译', success: '编译成功', error: '编译失败' }}
/>
</div>
{/* 半实物仿真: 时长 + HIL 按钮 */}
{(() => {
const hwCount = (p.nodes || []).filter(n => n.data?.isHardware).length;
const hilDisabled = hwCount === 0 || p.compileStatus !== 'success' || p.hilStatus === 'starting' || p.hilStatus === 'running';
return (
<>
{hwCount > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
<label style={{ fontSize: 11, color: '#888', whiteSpace: 'nowrap' }}>HIL时长:</label>
<input
type="number"
min="1" max="3600" step="1"
value={hilDuration}
onChange={(e) => setHilDuration(Number(e.target.value) || 10)}
onClick={(e) => e.stopPropagation()}
disabled={hilDisabled}
style={{
flex: 1, padding: '3px 6px', fontSize: 11,
background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.12)',
borderRadius: 4, color: '#ccc', outline: 'none',
opacity: hilDisabled ? 0.4 : 1,
}}
/>
<span style={{ fontSize: 11, color: '#666' }}></span>
{/* 编译日志 */}
{p.compileResult && (
<div className={styles.logSection}>
<button className={styles.logToggle} onClick={(e) => { e.stopPropagation(); setShowCompileLog(v => !v); }}>
{showCompileLog ? '▼' : '▶'} 编译输出
</button>
{showCompileLog && (
<div className={`${styles.logContent} ${p.compileStatus === 'error' ? styles.logError : styles.logSuccess}`}>
{p.compileResult.errors || p.compileResult.output || '无输出'}
</div>
)}
<button
className={`${styles.simBtn} ${styles.execute}`}
</div>
)}
</div>
{/* ── 模块 2: 软件仿真(编译成功后显示) ── */}
{p.compileStatus === 'success' && (
<div className={styles.sectionCard}>
<div className={styles.sectionTitle}>
<span className={styles.sectionTitleIcon}></span> 软件仿真
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
<label style={{ fontSize: 11, color: '#888', whiteSpace: 'nowrap' }}>仿真时长:</label>
<input
type="number"
min="0.1" max="3600" step="0.1"
value={simDuration}
onChange={(e) => setSimDuration(e.target.value === '' ? '' : Number(e.target.value))}
onBlur={() => { if (!simDuration || simDuration <= 0) setSimDuration(1); }}
onClick={(e) => e.stopPropagation()}
style={{
width: '100%', marginBottom: 4,
background: hwCount > 0 ? 'rgba(251,146,60,0.08)' : undefined,
borderColor: hwCount > 0 ? 'rgba(251,146,60,0.3)' : undefined,
color: hwCount > 0 ? '#fb923c' : undefined,
flex: 1, padding: '3px 6px', fontSize: 11,
background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.12)',
borderRadius: 4, color: '#ccc', outline: 'none',
}}
disabled={hilDisabled}
title={p.compileStatus !== 'success' ? '请先编译成功' : hwCount === 0 ? '请先在属性面板中将节点标记为实物' : `${hwCount} 个实物节点`}
onClick={(e) => { e.stopPropagation(); startHil(p.id, { duration: hilDuration }); }}
>
{p.hilStatus === 'starting' ? (
<><span className={styles.spinner}></span> 启动中…</>
) : (
<>🔧 半实物仿真 {hwCount > 0 && <span style={{ opacity: 0.7 }}>({hwCount})</span>}</>
)}
</button>
</>
);
})()}
/>
<span style={{ fontSize: 11, color: '#666' }}></span>
</div>
<button
className={`${styles.simBtn} ${styles.execute}`}
onClick={(e) => { e.stopPropagation(); executeProject(p.id, { stopTime: simDuration }); }}
disabled={p.executeStatus === 'running'}
style={{ width: '100%', marginBottom: 4 }}
>
{p.executeStatus === 'running' ? (
<><span className={styles.spinner}></span> 下发中…</>
) : (
<>⏵ 下发等效设备执行</>
)}
</button>
{/* HIL 状态显示 */}
{p.hilStatus && p.hilStatus !== 'none' && (
<div className={styles.statusRow} style={{ flexDirection: 'column', alignItems: 'flex-start', gap: 2 }}>
<span style={{ fontSize: 11, color: '#888' }}>半实物仿真:</span>
<StatusBadge status={p.hilStatus === 'starting' ? 'compiling' : p.hilStatus === 'running' ? 'running' : p.hilStatus}
labels={{ none: '未启动', success: '就绪', error: '失败' }} />
{p.hilResult?.message && (
<span style={{ fontSize: 10, color: p.hilStatus === 'error' ? '#ef4444' : '#aaa' }}>
{p.hilResult.message}
</span>
)}
{p.hilResult?.fmuPath && (
<span style={{ fontSize: 9, color: '#666', wordBreak: 'break-all' }}>
FMU: {p.hilResult.fmuPath}
</span>
)}
{p.hilResult?.errorDetail && (
<span style={{ fontSize: 9, color: '#ef4444', wordBreak: 'break-all' }}>
{p.hilResult.errorDetail}
</span>
)}
{/* 停止按钮 */}
{p.hilStatus === 'running' && (
<div style={{ display: 'flex', gap: 4, width: '100%', marginTop: 4 }}>
<button
className={`${styles.simBtn}`}
style={{
flex: 1,
background: 'rgba(79,138,255,0.08)',
borderColor: 'rgba(79,138,255,0.3)',
color: '#4f8aff',
}}
onClick={(e) => { e.stopPropagation(); setShowLiveChart(true); }}
>📈 实时数据</button>
<button
className={`${styles.simBtn}`}
style={{
flex: 1,
background: 'rgba(239,68,68,0.08)',
borderColor: 'rgba(239,68,68,0.3)',
color: '#ef4444',
}}
onClick={(e) => { e.stopPropagation(); stopHil(p.id); }}
>⏹ 停止仿真</button>
{p.executeStatus && p.executeStatus !== 'none' && (
<div className={styles.statusRow} style={{ marginBottom: 4 }}>
<span style={{ fontSize: 10, color: '#666' }}>状态:</span>
<StatusBadge
status={p.executeStatus}
labels={{ none: '未执行', success: '执行完成', error: '执行失败' }}
/>
</div>
)}
</div>
)}
{/* 查看 HIL 结果 */}
{(p.hilStatus === 'done' || p.hilStatus === 'stopped') && p.hilResult?.csvData && (
<button
className={`${styles.simBtn} ${styles.viewResults}`}
onClick={(e) => { e.stopPropagation(); setShowResultsMode('hil'); }}
style={{ width: '100%', marginBottom: 4 }}
>📊 查看 HIL 结果</button>
)}
{/* 获取结果按钮(运行中/停止后可尝试获取) */}
{(p.hilStatus === 'running' || p.hilStatus === 'stopped') && !p.hilResult?.csvData && (
<button
className={`${styles.simBtn}`}
style={{ width: '100%', marginBottom: 4, color: '#60a5fa' }}
onClick={(e) => { e.stopPropagation(); fetchHilResults(p.id); }}
>📲 获取 HIL 结果</button>
)}
{/* 查看普通仿真结果 */}
{p.executeStatus === 'success' && p.executeResult?.csvData && (
<button
className={`${styles.simBtn} ${styles.viewResults}`}
onClick={(e) => { e.stopPropagation(); setShowResultsMode('sim'); }}
style={{ width: '100%', marginBottom: 4 }}
>📊 查看仿真结果</button>
)}
{/* 查看软件仿真结果 */}
{p.executeStatus === 'success' && p.executeResult?.csvData && (
<button
className={`${styles.simBtn} ${styles.viewResults}`}
onClick={(e) => { e.stopPropagation(); setShowResultsMode('sim'); }}
style={{ width: '100%', marginBottom: 4 }}
>📊 查看仿真结果</button>
)}
{/* 编译日志 */}
{p.compileResult && (
<div className={styles.logSection}>
<button className={styles.logToggle} onClick={(e) => { e.stopPropagation(); setShowCompileLog(v => !v); }}>
{showCompileLog ? '▼' : '▶'} 编译输出
</button>
{showCompileLog && (
<div className={`${styles.logContent} ${p.compileStatus === 'error' ? styles.logError : styles.logSuccess}`}>
{p.compileResult.errors || p.compileResult.output || '无输出'}
{/* 执行日志 */}
{p.executeResult && (
<div className={styles.logSection}>
<button className={styles.logToggle} onClick={(e) => { e.stopPropagation(); setShowExecuteLog(v => !v); }}>
{showExecuteLog ? '▼' : '▶'} 执行结果
</button>
{showExecuteLog && (
<div className={`${styles.logContent} ${p.executeStatus === 'error' ? styles.logError : ''}`}>
{p.executeResult.logs || p.executeResult.csvData || '无输出'}
</div>
)}
</div>
)}
</div>
)}
{/* 执行日志 */}
{p.executeResult && (
<div className={styles.logSection}>
<button className={styles.logToggle} onClick={(e) => { e.stopPropagation(); setShowExecuteLog(v => !v); }}>
{showExecuteLog ? '▼' : '▶'} 执行结果
</button>
{showExecuteLog && (
<div className={`${styles.logContent} ${p.executeStatus === 'error' ? styles.logError : ''}`}>
{p.executeResult.logs || p.executeResult.csvData || '无输出'}
{/* ── 模块 3: 半实物仿真(编译成功后显示) ── */}
{p.compileStatus === 'success' && (() => {
const hwCount = (p.nodes || []).filter(n => n.data?.isHardware).length;
const hilDisabled = hwCount === 0 || p.hilStatus === 'starting' || p.hilStatus === 'running';
return (
<div className={styles.sectionCardHil}>
<div className={styles.sectionTitle} style={{ color: '#fb923c' }}>
<span className={styles.sectionTitleIcon}>🔧</span> 半实物仿真
{hwCount > 0 && <span style={{ fontSize: 10, opacity: 0.7 }}>({hwCount} 个实物节点)</span>}
</div>
)}
</div>
)}
{hwCount === 0 ? (
<span style={{ fontSize: 10, color: '#666' }}>请先在属性面板中将节点标记为实物</span>
) : (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
<label style={{ fontSize: 11, color: '#888', whiteSpace: 'nowrap' }}>HIL时长:</label>
<input
type="number"
min="1" max="3600" step="1"
value={hilDuration}
onChange={(e) => setHilDuration(Number(e.target.value) || 10)}
onClick={(e) => e.stopPropagation()}
disabled={hilDisabled}
style={{
flex: 1, padding: '3px 6px', fontSize: 11,
background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.12)',
borderRadius: 4, color: '#ccc', outline: 'none',
opacity: hilDisabled ? 0.4 : 1,
}}
/>
<span style={{ fontSize: 11, color: '#666' }}></span>
</div>
<button
className={`${styles.simBtn} ${styles.execute}`}
style={{
width: '100%', marginBottom: 4,
background: 'rgba(251,146,60,0.08)',
borderColor: 'rgba(251,146,60,0.3)',
color: '#fb923c',
}}
disabled={hilDisabled}
onClick={(e) => { e.stopPropagation(); startHil(p.id, { duration: hilDuration }); }}
>
{p.hilStatus === 'starting' ? (
<><span className={styles.spinner}></span> 启动中…</>
) : (
<>🔧 半实物仿真</>
)}
</button>
{/* HIL 状态 + 运行中按钮 */}
{p.hilStatus && p.hilStatus !== 'none' && (
<>
<div className={styles.statusRow} style={{ marginBottom: 4 }}>
<span style={{ fontSize: 10, color: '#666' }}>状态:</span>
<StatusBadge status={p.hilStatus === 'starting' ? 'compiling' : p.hilStatus === 'running' ? 'running' : p.hilStatus}
labels={{ none: '未启动', success: '就绪', error: '失败', done: '已完成', stopped: '已停止' }} />
</div>
{p.hilResult?.errorDetail && (
<span style={{ fontSize: 9, color: '#ef4444', wordBreak: 'break-all', display: 'block', marginBottom: 4 }}>
{p.hilResult.errorDetail}
</span>
)}
</>
)}
{/* 运行中: 实时数据 + 停止 */}
{p.hilStatus === 'running' && (
<div style={{ display: 'flex', gap: 4, width: '100%', marginBottom: 4 }}>
<button
className={`${styles.simBtn}`}
style={{ flex: 1, background: 'rgba(79,138,255,0.08)', borderColor: 'rgba(79,138,255,0.3)', color: '#4f8aff' }}
onClick={(e) => { e.stopPropagation(); setShowLiveChart(true); }}
>📈 实时数据</button>
<button
className={`${styles.simBtn}`}
style={{ flex: 1, background: 'rgba(239,68,68,0.08)', borderColor: 'rgba(239,68,68,0.3)', color: '#ef4444' }}
onClick={(e) => { e.stopPropagation(); stopHil(p.id); }}
>⏹ 停止</button>
</div>
)}
{/* 查看 HIL 结果(自动获取+显示) */}
{(p.hilStatus === 'done' || p.hilStatus === 'stopped' || p.hilStatus === 'running') && (
<button
className={`${styles.simBtn} ${styles.viewResults}`}
onClick={async (e) => {
e.stopPropagation();
if (!p.hilResult?.csvData) await fetchHilResults(p.id);
setShowResultsMode('hil');
}}
style={{ width: '100%', marginBottom: 4 }}
>📊 查看 HIL 结果</button>
)}
</>
)}
</div>
);
})()}
</div>
)}
</div>
......@@ -458,7 +490,7 @@ export default function ProjectPanel() {
})
)}
</div>
</div>
</div>
{/* 仿真结果弹窗 */}
{showResultsMode && (
......@@ -477,6 +509,7 @@ export default function ProjectPanel() {
{showLiveChart && (
<LiveChart
rosBridgeUrl={`ws://${window.location.hostname}:9090`}
sessionId={activeProject?.hilResult?.sessionId}
onClose={() => setShowLiveChart(false)}
onDone={() => {
const ap = activeProject;
......
......@@ -492,3 +492,35 @@
background: rgba(34, 197, 94, 0.15);
color: #86efac;
}
/* ===== 三段式模块卡片 ===== */
.sectionCard {
background: rgba(255,255,255,0.02);
border: 1px solid #2a2a3a;
border-radius: 6px;
padding: 10px;
margin-bottom: 8px;
}
.sectionCardHil {
border: 1px solid rgba(251,146,60,0.18);
border-radius: 6px;
padding: 10px;
margin-bottom: 8px;
background: rgba(251,146,60,0.04);
}
.sectionTitle {
font-size: 11px;
font-weight: 700;
color: #999;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 5px;
letter-spacing: 0.5px;
}
.sectionTitleIcon {
font-size: 12px;
}
......@@ -104,10 +104,10 @@ export default function PropertiesPanel() {
))}
</div>
{/* 半实物仿真: 实物设备标记(流程节点不显示) */}
{/* 实物标记(流程节点不显示) */}
{!getModelMapping(selectedNode.data?.templateData?.type)?.isFlowNode && (
<>
<label className={styles.label}>半实物仿真</label>
<label className={styles.label}>实物标记</label>
<div
style={{
display: 'flex', alignItems: 'center', gap: 10,
......@@ -120,7 +120,7 @@ export default function PropertiesPanel() {
>
<div style={{
width: 36, height: 20, borderRadius: 10, position: 'relative',
background: selectedNode.data?.isHardware ? '#fb923c' : '#333',
background: selectedNode.data?.isHardware ? '#fb923c' : '#3b4a5c',
transition: 'background 0.2s',
}}>
<div style={{
......@@ -132,7 +132,7 @@ export default function PropertiesPanel() {
</div>
<span style={{
fontSize: 12, fontWeight: 600,
color: selectedNode.data?.isHardware ? '#fb923c' : '#666',
color: selectedNode.data?.isHardware ? '#fb923c' : '#8eafc5',
}}>
{selectedNode.data?.isHardware ? '🔧 实物设备' : '软件仿真'}
</span>
......
......@@ -19,9 +19,26 @@ function loadProjects() {
}
}
/** 保存项目列表到 localStorage */
/** 保存项目列表到 localStorage(剔除大体积数据避免超限) */
function persistProjects(projects) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(projects));
const lite = projects.map(p => {
const clean = { ...p };
// csvData 体积大,仅保留在内存不持久化
if (clean.executeResult) {
clean.executeResult = { ...clean.executeResult };
delete clean.executeResult.csvData;
}
if (clean.hilResult) {
clean.hilResult = { ...clean.hilResult };
delete clean.hilResult.csvData;
}
return clean;
});
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(lite));
} catch (e) {
console.warn('[persistProjects] localStorage 写入失败:', e.message);
}
}
function generateId() {
......@@ -119,6 +136,7 @@ const useProjectStore = create((set, get) => ({
/** 打开项目,加载数据到画布 */
openProject: (id) => {
if (!id) { set({ activeProjectId: null }); return; }
const project = get().projects.find(p => p.id === id);
if (!project) return;
const flowStore = useFlowStore.getState();
......@@ -297,10 +315,12 @@ const useProjectStore = create((set, get) => ({
return;
}
// 调用后端启动 HIL
// 调用后端启动 HIL(复用已有 FMU 跳过编译)
const cachedFmu = project.hilResult?.fmuPath || '';
const resp = await startHilSession({
moCode: exported.code,
modelName,
fmuPath: cachedFmu,
hardwarePorts: exported.hardwarePorts,
duration,
stepSize,
......@@ -335,6 +355,20 @@ const useProjectStore = create((set, get) => ({
const project = projects.find(p => p.id === id);
if (!project) return;
// 立即更新 UI 状态为完成
const setStatus = (updates) => {
const updated = get().projects.map(p =>
p.id === id ? { ...p, ...updates } : p
);
persistProjects(updated);
set({ projects: updated });
};
setStatus({
hilStatus: 'done',
hilResult: { ...project.hilResult, message: '仿真已完成' },
});
// 延迟 1s 等待 CSV 写入完成,然后获取结果
setTimeout(() => get().fetchHilResults(id), 1000);
},
......@@ -371,7 +405,7 @@ const useProjectStore = create((set, get) => ({
fetchHilResults: async (id) => {
const { projects } = get();
const project = projects.find(p => p.id === id);
if (!project || !project.hilResult?.sessionId) return;
if (!project) return;
const setStatus = (updates) => {
const updated = get().projects.map(p =>
......@@ -381,8 +415,18 @@ const useProjectStore = create((set, get) => ({
set({ projects: updated });
};
const sid = project.hilResult?.sessionId;
if (!sid) {
console.warn('[fetchHilResults] sessionId 缺失', project.hilResult);
setStatus({
hilResult: { ...project.hilResult, errorDetail: '无法获取结果:缺少 sessionId' },
});
return;
}
try {
const resp = await getHilResults(project.hilResult.sessionId);
const resp = await getHilResults(sid);
console.log('[fetchHilResults] resp:', resp);
if (resp.code === 0 && resp.data?.csv_data) {
setStatus({
hilStatus: 'done',
......@@ -390,11 +434,19 @@ const useProjectStore = create((set, get) => ({
...project.hilResult,
message: '仿真完成',
csvData: resp.data.csv_data,
errorDetail: null,
},
});
} else {
setStatus({
hilResult: { ...project.hilResult, errorDetail: resp.message || '结果为空,CSV 可能尚未写入' },
});
}
} catch (err) {
// 结果未就绪,静默失败
console.error('[fetchHilResults] error:', err);
setStatus({
hilResult: { ...project.hilResult, errorDetail: `获取失败: ${err.message}` },
});
}
},
......
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