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 }) { ...@@ -44,16 +44,27 @@ function CustomDeviceNode({ data, selected }) {
background: '#1e1e2e', background: '#1e1e2e',
borderRadius: 8, borderRadius: 8,
outline: isHardware outline: isHardware
? '2px dashed #fb923c' ? `${selected ? 3 : 2}px dashed ${selected ? '#fff' : color}`
: `2px solid ${selected ? '#fff' : color}`, : `2px solid ${selected ? '#fff' : color}`,
outlineOffset: -1, outlineOffset: -1,
boxShadow: isHardware 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)', : selected ? `0 0 12px ${color}88` : '0 4px 12px rgba(0,0,0,0.3)',
overflow: 'visible', overflow: 'visible',
transform: rotation ? `rotate(${rotation}deg)` : undefined, transform: rotation ? `rotate(${rotation}deg)` : undefined,
fontFamily: "'Segoe UI', sans-serif", 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 */} {/* 彩色 Header */}
<div style={{ <div style={{
background: color, background: color,
...@@ -72,7 +83,14 @@ function CustomDeviceNode({ data, selected }) { ...@@ -72,7 +83,14 @@ function CustomDeviceNode({ data, selected }) {
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
}}>{name}</span> }}>{name}</span>
{isHardware && ( {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> </div>
......
...@@ -492,3 +492,35 @@ ...@@ -492,3 +492,35 @@
background: rgba(34, 197, 94, 0.15); background: rgba(34, 197, 94, 0.15);
color: #86efac; 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() { ...@@ -104,10 +104,10 @@ export default function PropertiesPanel() {
))} ))}
</div> </div>
{/* 半实物仿真: 实物设备标记(流程节点不显示) */} {/* 实物标记(流程节点不显示) */}
{!getModelMapping(selectedNode.data?.templateData?.type)?.isFlowNode && ( {!getModelMapping(selectedNode.data?.templateData?.type)?.isFlowNode && (
<> <>
<label className={styles.label}>半实物仿真</label> <label className={styles.label}>实物标记</label>
<div <div
style={{ style={{
display: 'flex', alignItems: 'center', gap: 10, display: 'flex', alignItems: 'center', gap: 10,
...@@ -120,7 +120,7 @@ export default function PropertiesPanel() { ...@@ -120,7 +120,7 @@ export default function PropertiesPanel() {
> >
<div style={{ <div style={{
width: 36, height: 20, borderRadius: 10, position: 'relative', width: 36, height: 20, borderRadius: 10, position: 'relative',
background: selectedNode.data?.isHardware ? '#fb923c' : '#333', background: selectedNode.data?.isHardware ? '#fb923c' : '#3b4a5c',
transition: 'background 0.2s', transition: 'background 0.2s',
}}> }}>
<div style={{ <div style={{
...@@ -132,7 +132,7 @@ export default function PropertiesPanel() { ...@@ -132,7 +132,7 @@ export default function PropertiesPanel() {
</div> </div>
<span style={{ <span style={{
fontSize: 12, fontWeight: 600, fontSize: 12, fontWeight: 600,
color: selectedNode.data?.isHardware ? '#fb923c' : '#666', color: selectedNode.data?.isHardware ? '#fb923c' : '#8eafc5',
}}> }}>
{selectedNode.data?.isHardware ? '🔧 实物设备' : '软件仿真'} {selectedNode.data?.isHardware ? '🔧 实物设备' : '软件仿真'}
</span> </span>
......
...@@ -19,9 +19,26 @@ function loadProjects() { ...@@ -19,9 +19,26 @@ function loadProjects() {
} }
} }
/** 保存项目列表到 localStorage */ /** 保存项目列表到 localStorage(剔除大体积数据避免超限) */
function persistProjects(projects) { 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() { function generateId() {
...@@ -119,6 +136,7 @@ const useProjectStore = create((set, get) => ({ ...@@ -119,6 +136,7 @@ const useProjectStore = create((set, get) => ({
/** 打开项目,加载数据到画布 */ /** 打开项目,加载数据到画布 */
openProject: (id) => { openProject: (id) => {
if (!id) { set({ activeProjectId: null }); return; }
const project = get().projects.find(p => p.id === id); const project = get().projects.find(p => p.id === id);
if (!project) return; if (!project) return;
const flowStore = useFlowStore.getState(); const flowStore = useFlowStore.getState();
...@@ -297,10 +315,12 @@ const useProjectStore = create((set, get) => ({ ...@@ -297,10 +315,12 @@ const useProjectStore = create((set, get) => ({
return; return;
} }
// 调用后端启动 HIL // 调用后端启动 HIL(复用已有 FMU 跳过编译)
const cachedFmu = project.hilResult?.fmuPath || '';
const resp = await startHilSession({ const resp = await startHilSession({
moCode: exported.code, moCode: exported.code,
modelName, modelName,
fmuPath: cachedFmu,
hardwarePorts: exported.hardwarePorts, hardwarePorts: exported.hardwarePorts,
duration, duration,
stepSize, stepSize,
...@@ -335,6 +355,20 @@ const useProjectStore = create((set, get) => ({ ...@@ -335,6 +355,20 @@ const useProjectStore = create((set, get) => ({
const project = projects.find(p => p.id === id); const project = projects.find(p => p.id === id);
if (!project) return; 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 写入完成,然后获取结果 // 延迟 1s 等待 CSV 写入完成,然后获取结果
setTimeout(() => get().fetchHilResults(id), 1000); setTimeout(() => get().fetchHilResults(id), 1000);
}, },
...@@ -371,7 +405,7 @@ const useProjectStore = create((set, get) => ({ ...@@ -371,7 +405,7 @@ const useProjectStore = create((set, get) => ({
fetchHilResults: async (id) => { fetchHilResults: async (id) => {
const { projects } = get(); const { projects } = get();
const project = projects.find(p => p.id === id); const project = projects.find(p => p.id === id);
if (!project || !project.hilResult?.sessionId) return; if (!project) return;
const setStatus = (updates) => { const setStatus = (updates) => {
const updated = get().projects.map(p => const updated = get().projects.map(p =>
...@@ -381,8 +415,18 @@ const useProjectStore = create((set, get) => ({ ...@@ -381,8 +415,18 @@ const useProjectStore = create((set, get) => ({
set({ projects: updated }); set({ projects: updated });
}; };
const sid = project.hilResult?.sessionId;
if (!sid) {
console.warn('[fetchHilResults] sessionId 缺失', project.hilResult);
setStatus({
hilResult: { ...project.hilResult, errorDetail: '无法获取结果:缺少 sessionId' },
});
return;
}
try { 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) { if (resp.code === 0 && resp.data?.csv_data) {
setStatus({ setStatus({
hilStatus: 'done', hilStatus: 'done',
...@@ -390,11 +434,19 @@ const useProjectStore = create((set, get) => ({ ...@@ -390,11 +434,19 @@ const useProjectStore = create((set, get) => ({
...project.hilResult, ...project.hilResult,
message: '仿真完成', message: '仿真完成',
csvData: resp.data.csv_data, csvData: resp.data.csv_data,
errorDetail: null,
}, },
}); });
} else {
setStatus({
hilResult: { ...project.hilResult, errorDetail: resp.message || '结果为空,CSV 可能尚未写入' },
});
} }
} catch (err) { } 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