Commit 231f4574 authored by Cloud's avatar Cloud

Merge branch 'master' into current and resolve conflicts in modelicaExporter.js

parent 2d8936ef
...@@ -74,6 +74,7 @@ npm run build ...@@ -74,6 +74,7 @@ npm run build
| `data-import` | xlsx 导入、JSON 导入导出、自动布局 | | `data-import` | xlsx 导入、JSON 导入导出、自动布局 |
| `ui-styling` | 暗色主题、CSS Modules、工具栏/侧栏/属性面板 | | `ui-styling` | 暗色主题、CSS Modules、工具栏/侧栏/属性面板 |
| `runtime-setup` | nvm 环境搭建、项目启动、端口转发 | | `runtime-setup` | nvm 环境搭建、项目启动、端口转发 |
| `backend-integration` | 后端项目路径、接口文档位置 |
## 建模理念 ## 建模理念
......
/** /**
* App - 主入口组件 * App - 主入口组件
* 支持两个视图:流程编辑器 / 符号编辑器 * 支持两个视图:流程编辑器 / 符号编辑器
* 右侧集成项目管理面板
*/ */
import { ReactFlowProvider } from '@xyflow/react'; import { ReactFlowProvider } from '@xyflow/react';
import Toolbar from './components/Toolbar/Toolbar'; import Toolbar from './components/Toolbar/Toolbar';
...@@ -8,6 +9,7 @@ import Sidebar from './components/Sidebar/Sidebar'; ...@@ -8,6 +9,7 @@ import Sidebar from './components/Sidebar/Sidebar';
import FlowCanvas from './components/Canvas/FlowCanvas'; import FlowCanvas from './components/Canvas/FlowCanvas';
import PropertiesPanel from './components/PropertiesPanel/PropertiesPanel'; import PropertiesPanel from './components/PropertiesPanel/PropertiesPanel';
import ComponentEditor from './components/ComponentEditor/ComponentEditor'; import ComponentEditor from './components/ComponentEditor/ComponentEditor';
import ProjectPanel from './components/ProjectPanel/ProjectPanel';
import useComponentLibrary from './hooks/useComponentLibrary'; import useComponentLibrary from './hooks/useComponentLibrary';
import './App.css'; import './App.css';
...@@ -23,6 +25,7 @@ export default function App() { ...@@ -23,6 +25,7 @@ export default function App() {
<Sidebar /> <Sidebar />
<FlowCanvas /> <FlowCanvas />
<PropertiesPanel /> <PropertiesPanel />
<ProjectPanel />
</div> </div>
) : ( ) : (
<ComponentEditor /> <ComponentEditor />
......
/** /**
* Toolbar - 顶部工具栏 * Toolbar - 顶部工具栏
* 提供布局切换、导入导出、清空等操作 * 提供布局切换、导入导出、清空、项目管理等操作
*/ */
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import useFlowStore from '../../hooks/useFlowStore'; import useFlowStore from '../../hooks/useFlowStore';
import useProjectStore from '../../hooks/useProjectStore';
import { downloadModelicaFile } from '../../utils/modelicaExporter'; import { downloadModelicaFile } from '../../utils/modelicaExporter';
import styles from './Toolbar.module.css'; import styles from './Toolbar.module.css';
import useComponentLibrary from '../../hooks/useComponentLibrary'; import useComponentLibrary from '../../hooks/useComponentLibrary';
...@@ -21,6 +22,17 @@ export default function Toolbar() { ...@@ -21,6 +22,17 @@ export default function Toolbar() {
nodes, nodes,
} = useFlowStore(); } = useFlowStore();
const { panelOpen, togglePanel, saveProject, projects, activeProjectId } = useProjectStore();
const activeProject = useProjectStore(s => s.projects.find(p => p.id === s.activeProjectId));
const handleSave = useCallback(() => {
if (activeProjectId) {
saveProject();
} else {
const name = prompt('请输入原理图名称', `原理图 ${projects.length + 1}`);
if (name) saveProject(name);
}
}, [activeProjectId, saveProject, projects.length]);
const handleExportJSON = useCallback(() => { const handleExportJSON = useCallback(() => {
const json = exportToJSON(); const json = exportToJSON();
...@@ -43,7 +55,7 @@ export default function Toolbar() { ...@@ -43,7 +55,7 @@ export default function Toolbar() {
if (!name) return; if (!name) return;
const { errors, warnings } = downloadModelicaFile({ nodes, edges }, name); const { errors, warnings } = downloadModelicaFile({ nodes, edges }, name);
if (errors && errors.length > 0) { if (errors && errors.length > 0) {
alert(`导出失败,以下符号未建立模型映射:\n\n` + errors.join('\n') + '\n\n请先在侧边栏“模型映射”中配置对应关系。'); alert(`导出失败,以下符号未建立模型映射:\n\n` + errors.join('\n') + '\n\n请先在侧边栏"模型映射"中配置对应关系。');
return; return;
} }
if (warnings.length > 0) { if (warnings.length > 0) {
...@@ -81,9 +93,17 @@ export default function Toolbar() { ...@@ -81,9 +93,17 @@ export default function Toolbar() {
<div className={styles.toolbar}> <div className={styles.toolbar}>
<div className={styles.brand}> <div className={styles.brand}>
<span className={styles.logo}>&#9883;</span> <span className={styles.logo}>&#9883;</span>
<span className={styles.title}>SimuFlow</span> <span className={styles.title}>仿真系统</span>
</div> </div>
{/* 当前项目名 */}
{activeProject && (
<>
<div className={styles.divider} />
<span className={styles.projectName}>{activeProject.name}</span>
</>
)}
<div className={styles.divider} /> <div className={styles.divider} />
<div className={styles.group}> <div className={styles.group}>
...@@ -101,6 +121,21 @@ export default function Toolbar() { ...@@ -101,6 +121,21 @@ export default function Toolbar() {
<div className={styles.spacer} /> <div className={styles.spacer} />
<div className={styles.group}>
<button className={`${styles.btn} ${styles.save}`} onClick={handleSave} title="保存原理图 (Ctrl+S)">
💾 保存
</button>
<button
className={`${styles.btn} ${panelOpen ? styles.active : ''}`}
onClick={togglePanel}
title="打开/关闭项目管理面板"
>
📁 项目
</button>
</div>
<div className={styles.divider} />
<div className={styles.group}> <div className={styles.group}>
<button className={styles.btn} onClick={handleExportJSON} title="导出 JSON 文件"> <button className={styles.btn} onClick={handleExportJSON} title="导出 JSON 文件">
&#8615; JSON &#8615; JSON
......
...@@ -94,3 +94,25 @@ ...@@ -94,3 +94,25 @@
border-color: #f44336; border-color: #f44336;
color: #f44336; color: #f44336;
} }
.btn.save {
background: #1a2a1a;
border-color: #2a5a2a;
color: #6ec66e;
}
.btn.save:hover:not(:disabled) {
background: #1e3a1e;
border-color: #4a8a4a;
color: #8afe8a;
}
.projectName {
font-size: 12px;
color: #8888f0;
font-weight: 600;
max-width: 180px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
...@@ -67,6 +67,15 @@ export function exportToModelica(data, modelName = 'Circuit') { ...@@ -67,6 +67,15 @@ export function exportToModelica(data, modelName = 'Circuit') {
mappingOverrides = JSON.parse(localStorage.getItem('eplan_model_mapping_overrides') || '{}'); mappingOverrides = JSON.parse(localStorage.getItem('eplan_model_mapping_overrides') || '{}');
} catch { /* ignore */ } } catch { /* ignore */ }
const typeCounters = {}; // 每种类型的计数器
const usedModelNames = new Set(); // 收集使用到的完整模型名(用于 import)
/** 将 type 转为 camelCase 实例名前缀: voltage_source → voltageSource */
function typeToCamelCase(type) {
if (!type) return 'comp';
return type.replace(/[-_]([a-zA-Z])/g, (_, c) => c.toUpperCase());
}
lines.push(`model ${toMoId(modelName)}`); lines.push(`model ${toMoId(modelName)}`);
lines.push(''); lines.push('');
...@@ -78,8 +87,11 @@ export function exportToModelica(data, modelName = 'Circuit') { ...@@ -78,8 +87,11 @@ export function exportToModelica(data, modelName = 'Circuit') {
} }
}); });
const usedPackages = new Set(); const usedPackages = new Set();
// 占位 — 后续回填 import 语句
const importInsertIdx = lines.length;
// ===== 1. 组件声明(跳过流程节点) ===== // ===== 1. 组件声明(跳过流程节点) =====
const declLines = [];
nodes.forEach((node, idx) => { nodes.forEach((node, idx) => {
const td = node.data?.templateData; const td = node.data?.templateData;
const type = td?.type; const type = td?.type;
...@@ -113,11 +125,9 @@ export function exportToModelica(data, modelName = 'Circuit') { ...@@ -113,11 +125,9 @@ export function exportToModelica(data, modelName = 'Circuit') {
if (mapping?.isFlowNode) return; if (mapping?.isFlowNode) return;
if (!mapping) { if (!mapping) {
// 检查用户自定义映射(自定义符号用 custom_${id} 作为 key)
const customKey = td?.id ? `custom_${td.id}` : type; const customKey = td?.id ? `custom_${td.id}` : type;
const customOverride = customKey ? mappingOverrides[customKey] : null; const customOverride = customKey ? mappingOverrides[customKey] : null;
if (customOverride && customOverride.modelName) { if (customOverride && customOverride.modelName) {
// 有自定义映射:使用用户配置的模型名
const paramParts = []; const paramParts = [];
(td?.params || []).forEach(p => { (td?.params || []).forEach(p => {
const val = node.data?.paramValues?.[p.key] ?? p.defaultValue; const val = node.data?.paramValues?.[p.key] ?? p.defaultValue;
...@@ -127,7 +137,8 @@ export function exportToModelica(data, modelName = 'Circuit') { ...@@ -127,7 +137,8 @@ export function exportToModelica(data, modelName = 'Circuit') {
} }
}); });
const paramStr = paramParts.length > 0 ? `(${paramParts.join(', ')})` : ''; const paramStr = paramParts.length > 0 ? `(${paramParts.join(', ')})` : '';
lines.push(` ${customOverride.modelName} ${instanceName}${paramStr};`); usedModelNames.add(customOverride.modelName);
declLines.push(` ${customOverride.modelName} ${instanceName}${paramStr};`);
return; return;
} }
errors.push(`"${label}" (type: ${type || 'custom'}) 未建立模型映射,请先在"模型映射"中配置`); errors.push(`"${label}" (type: ${type || 'custom'}) 未建立模型映射,请先在"模型映射"中配置`);
...@@ -151,18 +162,37 @@ export function exportToModelica(data, modelName = 'Circuit') { ...@@ -151,18 +162,37 @@ export function exportToModelica(data, modelName = 'Circuit') {
}); });
const paramStr = paramParts.length > 0 ? `(${paramParts.join(', ')})` : ''; const paramStr = paramParts.length > 0 ? `(${paramParts.join(', ')})` : '';
// 优先使用用户覆盖的模型名
const finalModelName = mappingOverrides[type]?.modelName || mapping.modelName; const finalModelName = mappingOverrides[type]?.modelName || mapping.modelName;
lines.push(` ${finalModelName} ${instanceName}${paramStr};`); usedModelNames.add(finalModelName);
declLines.push(` ${finalModelName} ${instanceName}${paramStr};`);
}); });
// 插入 import 语句(在 model 声明与组件声明之间) // 回填 import 语句(在 model 声明与组件声明之间)
if (usedPackages.size > 0) { // Upstream used `usedPackages` (which relied on `typeToPackage` map), we use `usedModelNames` directly.
const importLines = [...usedPackages].sort().map(pkg => ` import ${pkg}.*;`); if (usedModelNames.size > 0 || typeof usedPackages !== 'undefined' && usedPackages.size > 0) {
const packages = new Set();
// 包含我们通过模型名解析出来的包
usedModelNames.forEach(name => {
const dot = name.indexOf('.');
if (dot > 0) {
packages.add(name.substring(0, dot));
} else {
packages.add('Modelica.Electrical.Analog.Basic'); // Fallback to basic electrical
}
});
// 包含上游直接指定的包 (如果上游代码在别处添加了)
if (typeof usedPackages !== 'undefined') {
usedPackages.forEach(pkg => packages.add(pkg));
}
const importLines = [...packages].sort().map(pkg => ` import ${pkg}.*;`);
// lines[0] = 'model XXX', lines[1] = '', 组件声明从 lines[2] 开始 // lines[0] = 'model XXX', lines[1] = '', 组件声明从 lines[2] 开始
lines.splice(2, 0, ...importLines, ''); lines.splice(2, 0, ...importLines, '');
} }
// 插入组件声明
declLines.forEach(l => lines.push(l));
lines.push(''); lines.push('');
// ===== 2. 构建流程节点端口到连接源的映射 ===== // ===== 2. 构建流程节点端口到连接源的映射 =====
......
...@@ -4,4 +4,12 @@ import react from '@vitejs/plugin-react' ...@@ -4,4 +4,12 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8888',
changeOrigin: true,
},
},
},
}) })
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