Commit 1683fc76 authored by Developer's avatar Developer

feat: 优化工具栏按钮与项目管理面板

- 工具栏按钮重命名: 项目→已有, JSON→导出原理图, .mo→导出模型, 导入→导入原理图
- 新增「新建」按钮,直接创建空白原理图
- 项目面板改为可展开式设计,点击项目展开显示编译/执行状态
- 自动生成不重复的模型名称 (Circuit_1, Circuit_2, ...)
- 画布底部添加保存状态指示器:编辑中常驻显示,保存后淡出
- 工具栏保存按钮根据dirty状态切换显示
- 移除项目面板中的保存/另存为按钮
parent 641c56ec
......@@ -2,7 +2,7 @@
* FlowCanvas - React Flow 画布容器
* 承载所有节点和边的渲染及交互
*/
import { useCallback, useRef, useEffect, useState } from 'react';
import { useCallback, useRef, useEffect, useState, useMemo } from 'react';
import {
ReactFlow,
Controls,
......@@ -20,6 +20,7 @@ import GradientBezierEdge from '../Edges/GradientBezierEdge';
import CustomConnectionLine from '../Edges/CustomConnectionLine';
import useFlowStore from '../../hooks/useFlowStore';
import useComponentLibrary from '../../hooks/useComponentLibrary';
import useProjectStore from '../../hooks/useProjectStore';
import styles from './FlowCanvas.module.css';
......@@ -48,6 +49,29 @@ export default function FlowCanvas() {
connectionError,
} = useFlowStore();
// 保存状态检测
const activeProject = useProjectStore(s => s.projects.find(p => p.id === s.activeProjectId));
const isDirty = useMemo(() => {
if (!activeProject) return nodes.length > 0;
const savedNodes = activeProject.nodes || [];
const savedEdges = activeProject.edges || [];
if (nodes.length !== savedNodes.length || edges.length !== savedEdges.length) return true;
return JSON.stringify(nodes) !== JSON.stringify(savedNodes)
|| JSON.stringify(edges) !== JSON.stringify(savedEdges);
}, [activeProject, nodes, edges]);
// 保存后短暂显示"已保存"然后淡出
const [justSaved, setJustSaved] = useState(false);
const prevDirtyRef = useRef(isDirty);
useEffect(() => {
if (prevDirtyRef.current && !isDirty) {
setJustSaved(true);
const timer = setTimeout(() => setJustSaved(false), 2000);
return () => clearTimeout(timer);
}
prevDirtyRef.current = isDirty;
}, [isDirty]);
/** 连接错误 Toast 状态 */
const [showError, setShowError] = useState(null);
......@@ -235,6 +259,15 @@ export default function FlowCanvas() {
{showError}
</div>
)}
{/* 保存状态指示器:未保存时常驻,保存后短暂显示再淡出 */}
{(isDirty || justSaved) && (
<div className={`${styles.saveStatus} ${isDirty ? styles.saveStatusDirty : styles.saveStatusFadeOut}`}>
<span className={styles.saveStatusIcon}>{isDirty ? '✏️' : '✅'}</span>
{activeProject && <span className={styles.saveStatusName}>{activeProject.name}</span>}
<span className={styles.saveStatusLabel}>{isDirty ? '编辑中' : '已保存'}</span>
</div>
)}
</div>
);
}
......
......@@ -57,3 +57,62 @@
pointer-events: none;
animation: toastFadeIn 0.2s ease-out, toastShake 0.5s ease-in-out 0.2s;
}
/* ===== 右上角保存状态指示器 ===== */
.saveStatus {
position: absolute;
bottom: 12px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
pointer-events: none;
z-index: 10;
transition: all 0.3s ease;
backdrop-filter: blur(8px);
}
.saveStatusFadeOut {
background: rgba(34, 197, 94, 0.12);
border: 1px solid rgba(34, 197, 94, 0.3);
color: #22c55e;
animation: fadeOut 2s ease-out forwards;
}
@keyframes fadeOut {
0%, 70% { opacity: 1; }
100% { opacity: 0; }
}
.saveStatusDirty {
background: rgba(251, 146, 60, 0.15);
border: 1px solid rgba(251, 146, 60, 0.4);
color: #fb923c;
animation: pulseDirty 2s ease-in-out infinite;
}
@keyframes pulseDirty {
0%, 100% { box-shadow: 0 0 0 0 rgba(251, 146, 60, 0); }
50% { box-shadow: 0 0 12px 2px rgba(251, 146, 60, 0.25); }
}
.saveStatusIcon {
font-size: 14px;
}
.saveStatusName {
max-width: 120px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.saveStatusLabel {
opacity: 0.8;
font-size: 11px;
}
......@@ -107,15 +107,13 @@
.projectItem {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
flex-direction: column;
border: 1px solid #2a2a3a;
border-radius: 8px;
margin-bottom: 6px;
cursor: pointer;
transition: all 0.15s ease;
background: rgba(255, 255, 255, 0.02);
overflow: hidden;
}
.projectItem:hover {
......@@ -128,6 +126,15 @@
background: rgba(99, 102, 241, 0.08);
}
/* 项目标题行(可点击) */
.projectHeader {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
cursor: pointer;
}
.projectIcon {
font-size: 18px;
color: #555;
......@@ -138,6 +145,20 @@
color: #6366f1;
}
/* 展开区域 */
.expandedSection {
padding: 8px 12px 12px;
border-top: 1px solid rgba(99, 102, 241, 0.15);
background: rgba(99, 102, 241, 0.03);
}
.statusRow {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
.projectInfo {
flex: 1;
min-width: 0;
......
......@@ -2,7 +2,7 @@
* Toolbar - 顶部工具栏
* 提供布局切换、导入导出、清空、项目管理等操作
*/
import { useCallback, useRef } from 'react';
import { useCallback, useRef, useMemo } from 'react';
import useFlowStore from '../../hooks/useFlowStore';
import useProjectStore from '../../hooks/useProjectStore';
import { downloadModelicaFile } from '../../utils/modelicaExporter';
......@@ -20,11 +20,22 @@ export default function Toolbar() {
selectedNode,
selectedEdge,
nodes,
edges,
} = useFlowStore();
const { panelOpen, togglePanel, saveProject, projects, activeProjectId } = useProjectStore();
const activeProject = useProjectStore(s => s.projects.find(p => p.id === s.activeProjectId));
// 检测画布是否有未保存的修改
const isDirty = useMemo(() => {
if (!activeProject) return nodes.length > 0; // 无项目时,有内容就算脏
const savedNodes = activeProject.nodes || [];
const savedEdges = activeProject.edges || [];
if (nodes.length !== savedNodes.length || edges.length !== savedEdges.length) return true;
return JSON.stringify(nodes) !== JSON.stringify(savedNodes)
|| JSON.stringify(edges) !== JSON.stringify(savedEdges);
}, [activeProject, nodes, edges]);
const handleSave = useCallback(() => {
if (activeProjectId) {
saveProject();
......@@ -100,7 +111,7 @@ export default function Toolbar() {
{activeProject && (
<>
<div className={styles.divider} />
<span className={styles.projectName}>{activeProject.name}</span>
<span className={styles.projectName}>{activeProject.name}{isDirty ? ' ●' : ''}</span>
</>
)}
......@@ -122,29 +133,32 @@ export default function Toolbar() {
<div className={styles.spacer} />
<div className={styles.group}>
<button className={`${styles.btn} ${styles.save}`} onClick={handleSave} title="保存原理图 (Ctrl+S)">
💾 保存
<button className={styles.btn} onClick={() => {
clearAll();
useProjectStore.getState().closeProject();
}} title="新建空白原理图">
✚ 新建
</button>
<button className={`${styles.btn} ${isDirty ? styles.save : styles.saved}`} onClick={handleSave} title="保存原理图 (Ctrl+S)">
{isDirty ? '💾 保存' : '✅ 已保存'}
</button>
<button
className={`${styles.btn} ${panelOpen ? styles.active : ''}`}
onClick={togglePanel}
title="打开/关闭项目管理面板"
>
📁 项目
📁 已有
</button>
</div>
<div className={styles.divider} />
<div className={styles.group}>
<button className={styles.btn} onClick={handleExportJSON} title="导出 JSON 文件">
&#8615; JSON
<button className={styles.btn} onClick={handleExportJSON} title="导出原理图 JSON">
&#8615; 导出原理图
</button>
<button className={styles.btn} onClick={handleExportMo} title="导出 OpenModelica .mo 模型">
&#8615; .mo
</button>
<button className={styles.btn} onClick={handleImportJSON} title="导入 JSON 文件">
&#8613; 导入
<button className={styles.btn} onClick={handleImportJSON} title="导入原理图 JSON">
&#8613; 导入原理图
</button>
<input
ref={jsonInputRef}
......@@ -153,6 +167,9 @@ export default function Toolbar() {
style={{ display: 'none' }}
onChange={handleJSONFileChange}
/>
<button className={styles.btn} onClick={handleExportMo} title="导出 Modelica 模型">
&#8615; 导出模型
</button>
<button className={`${styles.btn} ${styles.danger}`} onClick={handleClear} title="清空画布">
&#9746; 清空
</button>
......
......@@ -107,6 +107,13 @@
color: #8afe8a;
}
.btn.saved {
background: #1e1e2e;
border-color: #333;
color: #666;
cursor: default;
}
.projectName {
font-size: 12px;
color: #8888f0;
......
......@@ -28,6 +28,14 @@ function generateId() {
return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
}
/** 生成不重复的模型名称 */
function nextModelName(projects) {
const existing = new Set(projects.map(p => p.modelName).filter(Boolean));
let i = 1;
while (existing.has(`Circuit_${i}`)) i++;
return `Circuit_${i}`;
}
const useProjectStore = create((set, get) => ({
projects: loadProjects(),
activeProjectId: null,
......@@ -64,7 +72,7 @@ const useProjectStore = create((set, get) => ({
edges,
createdAt: now,
updatedAt: now,
modelName: 'Circuit',
modelName: nextModelName(projects),
compileStatus: 'none',
compileResult: null,
executeStatus: 'none',
......@@ -88,7 +96,7 @@ const useProjectStore = create((set, get) => ({
edges,
createdAt: now,
updatedAt: now,
modelName: 'Circuit',
modelName: nextModelName(projects),
compileStatus: 'none',
compileResult: null,
executeStatus: 'none',
......
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