From 7a87ca6c03f0d603160761f066b4fc99b746aceb Mon Sep 17 00:00:00 2001 From: rookie4show Date: Wed, 30 Jul 2025 17:04:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=BA=E5=AE=9AuseStore=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E8=B0=83=E7=94=A8=E8=A7=A3=E8=80=A6?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 134 +--- src/components/Toolbar.vue | 28 +- src/components/flow/FlowEditor.vue | 69 +- .../flow/nodes/common/ImageNode.vue | 118 ++-- src/components/flow/nodes/common/TextNode.vue | 96 +-- src/ts/files.ts | 130 ---- src/ts/useStore.ts | 627 ++++++------------ 7 files changed, 386 insertions(+), 816 deletions(-) delete mode 100644 src/ts/files.ts diff --git a/src/App.vue b/src/App.vue index 870be6c..e5d6569 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,16 +2,17 @@ import Toolbar from './components/Toolbar.vue'; import ProjectExplorer from './components/ProjectExplorer.vue'; import ComponentsPanel from './components/flow/ComponentsPanel.vue'; -import { computed, ref, onMounted, onUnmounted, onBeforeUpdate, reactive, provide, inject, watch } from "vue"; -import { useFilesStore } from "@/ts/useStore"; +import {computed, ref, onMounted, onUnmounted, onBeforeUpdate, reactive, provide, inject, watch} from "vue"; +import {useFilesStore} from "@/ts/useStore"; import Vue3DraggableResizable from 'vue3-draggable-resizable'; -import { TabPaneName, TabsPaneContext } from "element-plus"; +import {TabPaneName, TabsPaneContext} from "element-plus"; import FlowEditor from './components/flow/FlowEditor.vue'; import ShikigamiSelect from './components/flow/nodes/yys/ShikigamiSelect.vue'; import YuhunSelect from './components/flow/nodes/yys/YuhunSelect.vue'; import PropertySelect from './components/flow/nodes/yys/PropertySelect.vue'; // import { useVueFlow } from '@vue-flow/core'; import DialogManager from './components/DialogManager.vue'; +import {getLogicFlowInstance} from "@/ts/useLogicFlow"; const filesStore = useFilesStore(); // const { updateNode,toObject,fromObject } = useVueFlow(); @@ -22,122 +23,53 @@ const toolbarHeight = 48; // 工具栏的高度 const windowHeight = ref(window.innerHeight); const contentHeight = computed(() => `${windowHeight.value - toolbarHeight}px`); -const flowEditorRef = ref(null); -const flowEditorRefs = ref({}); -const lastActiveFile = ref(filesStore.activeFile); - const handleTabsEdit = ( targetName: string | undefined, action: 'remove' | 'add' ) => { if (action === 'remove') { - filesStore.closeTab(targetName); + filesStore.removeTab(targetName); } else if (action === 'add') { - const newFileName = `File ${filesStore.fileList.length + 1}`; - - filesStore.addFile({ - label: newFileName, - name: newFileName, - visible: true, - type: 'FLOW', - groups: [ - { - shortDescription: " ", - groupInfo: [{}, {}, {}, {}, {}], - details: '' - }, - { - shortDescription: '', - groupInfo: [{}, {}, {}, {}, {}], - details: '' - } - ] - }); + filesStore.addTab(); } }; onMounted(() => { - window.addEventListener('resize', () => { - windowHeight.value = window.innerHeight; - }); // 初始化自动保存功能 filesStore.initializeWithPrompt(); filesStore.setupAutoSave(); }); onUnmounted(() => { - window.removeEventListener('resize', () => { - windowHeight.value = window.innerHeight; - }); + }); -const activeFileGroups = computed(() => { - const activeFile = filesStore.fileList.find(file => file.name === filesStore.activeFile); - return activeFile ? activeFile.groups : []; -}); - -onBeforeUpdate(() => { - flowEditorRefs.value = {}; -}); - -const handleAddNode = (nodeData) => { - const activeEditor = flowEditorRefs.value[filesStore.activeFile]; - if (activeEditor) { - const { x, y, zoom } = activeEditor.getViewport(); - const position = { x: -x / zoom + 150, y: -y / zoom + 150 }; - activeEditor.handleAddNode({ ...nodeData, position }); - } -}; watch( - () => filesStore.activeFile, - async (newVal, oldVal) => { - // 切换前保存旧 tab 的数据和视口 - if (oldVal && flowEditorRef.value) { - if (flowEditorRef.value.getGraphRawData) { - const rawData = flowEditorRef.value.getGraphRawData(); - filesStore.updateFileFlowData(oldVal, rawData); + () => filesStore.activeFile, + async (newVal, oldVal) => { + // 保存旧 tab 数据 + if (oldVal) { + filesStore.updateTab(oldVal); } - if (flowEditorRef.value.getViewport) { - const viewport = flowEditorRef.value.getViewport(); - console.log(`[Tab切换] 切换前保存 tab "${oldVal}" 的视口信息:`, viewport); - filesStore.updateFileViewport(oldVal, viewport); - } - } - lastActiveFile.value = newVal; - // 切换后恢复新 tab 的数据和视口 - if (newVal && flowEditorRef.value) { - if (flowEditorRef.value.renderRawData) { - const newRawData = filesStore.getFileFlowData(newVal); - if (newRawData) flowEditorRef.value.renderRawData(newRawData); - } - if (flowEditorRef.value.setViewport) { - const newViewport = filesStore.getFileViewport(newVal); - console.log(`[Tab切换] 切换后恢复 tab "${newVal}" 的视口信息:`, newViewport); - requestAnimationFrame(() => { - flowEditorRef.value.setViewport(newViewport); - }); + // 渲染新 tab 数据 + if (newVal) { + const logicFlowInstance = getLogicFlowInstance(); + const currentTab = filesStore.getTab(newVal); + + if (logicFlowInstance && currentTab?.graphRawData) { + try { + logicFlowInstance.render(currentTab.graphRawData); + logicFlowInstance.zoom(currentTab.transform.SCALE_X, [currentTab.transform.TRANSLATE_X, currentTab.transform.TRANSLATE_Y]); + } catch (error) { + console.warn('渲染画布数据失败:', error); + } + } } } - } ); -const handleDropOnCanvas = (event: DragEvent) => { - event.preventDefault(); - const nodeData = JSON.parse(event.dataTransfer?.getData('application/json') || '{}'); - // 计算画布坐标(这里简单用鼠标坐标,后续可结合 LogicFlow 视口变换优化) - const rect = (event.target as HTMLElement).getBoundingClientRect(); - const x = event.clientX - rect.left; - const y = event.clientY - rect.top; - const id = `node-${Date.now()}-${Math.floor(Math.random() * 10000)}`; - filesStore.addNode({ id, type: nodeData.type, x, y, ...nodeData.data }); -}; - -const handleDragOverOnCanvas = (event: DragEvent) => { - event.preventDefault(); -}; - diff --git a/src/components/Toolbar.vue b/src/components/Toolbar.vue index 401b871..7a4fc31 100644 --- a/src/components/Toolbar.vue +++ b/src/components/Toolbar.vue @@ -82,6 +82,7 @@ import updateLogs from "../data/updateLog.json" import {useFilesStore} from "@/ts/useStore"; import {ElMessageBox} from "element-plus"; import {useGlobalMessage} from "@/ts/useGlobalMessage"; +import { getLogicFlowInstance } from "@/ts/useLogicFlow"; // import { useScreenshot } from '@/ts/useScreenshot'; import { getCurrentInstance } from 'vue'; @@ -100,6 +101,23 @@ const state = reactive({ showFeedbackFormDialog: false, // 控制反馈表单对话框的显示状态 }); +// 重新渲染 LogicFlow 画布的通用方法 +const refreshLogicFlowCanvas = (message?: string) => { + setTimeout(() => { + const logicFlowInstance = getLogicFlowInstance(); + if (logicFlowInstance) { + // 获取当前活动文件的数据 + const currentFileData = filesStore.getTab(filesStore.activeFile); + if (currentFileData) { + // 清空画布并重新渲染 + logicFlowInstance.clearData(); + logicFlowInstance.render(currentFileData); + console.log(message || 'LogicFlow 画布已重新渲染'); + } + } + }, 100); // 延迟一点确保数据更新完成 +}; + const loadExample = () => { ElMessageBox.confirm( '加载样例会覆盖当前数据,是否覆盖?', @@ -133,6 +151,7 @@ const loadExample = () => { activeFile: "example" }; filesStore.importData(defaultState); + refreshLogicFlowCanvas('LogicFlow 画布已重新渲染(示例数据)'); showMessage('success', '数据已恢复'); }).catch(() => { showMessage('info', '选择了不恢复旧数据'); @@ -161,7 +180,13 @@ const showFeedbackForm = () => { }; const handleExport = () => { - filesStore.exportData(); + // 导出前先更新当前数据,确保不丢失最新修改 + filesStore.updateTab(); + + // 延迟一点确保更新完成后再导出 + setTimeout(() => { + filesStore.exportData(); + }, 2000); }; const handleImport = () => { @@ -178,6 +203,7 @@ const handleImport = () => { const target = e.target as FileReader; const data = JSON.parse(target.result as string); filesStore.importData(data); + // refreshLogicFlowCanvas('LogicFlow 画布已重新渲染(导入数据)'); } catch (error) { console.error('Failed to import file', error); showMessage('error', '文件格式错误'); diff --git a/src/components/flow/FlowEditor.vue b/src/components/flow/FlowEditor.vue index 705f4f9..d266eaa 100644 --- a/src/components/flow/FlowEditor.vue +++ b/src/components/flow/FlowEditor.vue @@ -36,13 +36,9 @@ import { useFilesStore } from "@/ts/useStore"; import { setLogicFlowInstance, destroyLogicFlowInstance } from '@/ts/useLogicFlow'; const props = defineProps<{ - nodes: any[]; - edges: any[]; - viewport?: { x: number; y: number; zoom: number }; height?: string; }>(); -const filesStore = useFilesStore(); const containerRef = ref(null); const lf = ref(null); @@ -70,13 +66,17 @@ function registerNodes(lfInstance: LogicFlow) { // 初始化 LogicFlow onMounted(() => { lf.value = new LogicFlow({ - container: containerRef.value as HTMLElement, + container: containerRef.value, + // container: document.querySelector('#container'), grid: true, + allowResize: true, + allowRotate : true }); registerNodes(lf.value); - renderFlow(); setLogicFlowInstance(lf.value); - + lf.value.render({ + // 渲染的数据 + }) // 监听节点点击事件,更新 selectedNode lf.value.on(EventType.NODE_CLICK, ({ data }) => { selectedNode.value = data; @@ -112,46 +112,8 @@ onBeforeUnmount(() => { destroyLogicFlowInstance(); }); -// 响应式更新 nodes/edges -// watch( -// () => [props.nodes, props.edges], -// () => { -// renderFlow(); -// }, -// { deep: true } -// ); -// 响应式更新 viewport -watch( - () => props.viewport, - (val) => { - if (val) setViewport(val); - } -); -function renderFlow() { - if (!lf.value) return; - lf.value.render({ - nodes: props.nodes, - edges: props.edges, - }); -} - -function setViewport(viewport?: { x: number; y: number; zoom: number }) { - if (!lf.value || !viewport) return; - lf.value.zoom(viewport.zoom); - // lf.value.focusOn({ x: viewport.x, y: viewport.y }); -} - -function getViewport() { - if (!lf.value) return { x: 0, y: 0, zoom: 1 }; - const t = lf.value.getTransform(); - return { - x: t.TRANSLATE_X, - y: t.TRANSLATE_Y, - zoom: t.SCALE_X - }; -} // 右键菜单相关 function handleNodeContextMenu({ data, e }: { data: any; e: MouseEvent }) { @@ -174,23 +136,6 @@ function handleLayerOrder(action: string) { contextMenu.value.show = false; } -function getGraphRawData() { - if (!lf) return null; - return lf.value.getGraphRawData(); -} - -function renderRawData(data: any) { - if (!lf) return; - lf.value.renderRawData(data); -} - -defineExpose({ - getViewport, - setViewport, - renderFlow, - getGraphRawData, - renderRawData, -}); \ No newline at end of file + + + + + \ No newline at end of file diff --git a/src/components/flow/nodes/common/TextNode.vue b/src/components/flow/nodes/common/TextNode.vue index d9d9a49..c2d1703 100644 --- a/src/components/flow/nodes/common/TextNode.vue +++ b/src/components/flow/nodes/common/TextNode.vue @@ -1,51 +1,51 @@ - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ts/files.ts b/src/ts/files.ts deleted file mode 100644 index c5ec2f7..0000000 --- a/src/ts/files.ts +++ /dev/null @@ -1,130 +0,0 @@ -import {defineStore} from 'pinia'; -import {ElMessageBox} from "element-plus"; -import {useGlobalMessage} from "./useGlobalMessage"; - -const { showMessage } = useGlobalMessage(); - -function getDefaultState() { - return { - fileList: [{ - "label": "File 1", - "name": "1", - "visible": true, - "type":"PVE", - "groups": [ - { - "shortDescription": "", - "groupInfo": [ - {}, {}, {}, {}, {} - ], - "details": "" - } - ] - }], - activeFile: "1", - }; -} - -function saveStateToLocalStorage(state) { - localStorage.setItem('filesStore', JSON.stringify(state)); -} - -function clearFilesStoreLocalStorage() { - localStorage.removeItem('filesStore') -} - -function loadStateFromLocalStorage() { - return JSON.parse(localStorage.getItem('filesStore')); -} - -export const useFilesStore = defineStore('files', { - state: () => getDefaultState(), - getters: { - visibleFiles: (state) => state.fileList.filter(file => file.visible), - }, - actions: { - initializeWithPrompt() { - const savedState = loadStateFromLocalStorage(); - const defaultState = getDefaultState(); - - const isSame = JSON.stringify(savedState) === JSON.stringify(defaultState); - if (savedState && !isSame) { - ElMessageBox.confirm( - '检测到有未保存的旧数据,是否恢复?', - '提示', - { - confirmButtonText: '恢复', - cancelButtonText: '不恢复', - type: 'warning', - } - ).then(() => { - this.fileList = savedState.fileList || []; - this.activeFile = savedState.activeFile || "1"; - showMessage('success', '数据已恢复'); - }).catch(() => { - clearFilesStoreLocalStorage(); - showMessage('info', '选择了不恢复旧数据'); - }); - } - }, - setupAutoSave() { - setInterval(() => { - saveStateToLocalStorage(this.$state); - }, 30000); // 设置间隔时间为30秒 - }, - addFile(file) { - this.fileList.push({...file, visible: true}); - this.activeFile = file.name; - }, - setActiveFile(fileId: number) { - this.activeFile = fileId; - }, - setVisible(fileId: number, visibility: boolean) { - const file = this.fileList.find(file => file.name === fileId); - if (file) { - file.visible = visibility; - } - }, - closeTab(fileName: String) { - const file = this.fileList.find(file => file.name === fileName); - if (file) { - file.visible = false; - if (this.activeFile === fileName) { - const nextVisibleFile = this.visibleFiles[0]; - this.activeFile = nextVisibleFile ? nextVisibleFile.name : -1; - } - } - }, - async deleteFile(fileId: string) { - try { - if (this.fileList.length === 1) { - showMessage('warning', '无法删除'); - return; - } - await ElMessageBox.confirm('确定要删除此文件吗?', '提示', { - confirmButtonText: '确定', - cancelButtonText: '取消', - type: 'warning', - }); - - const index = this.fileList.findIndex(file => file.name === fileId); - if (index > -1) { - this.fileList.splice(index, 1); - if (this.activeFile === fileId) { - const nextVisibleFile = this.visibleFiles[0]; - this.activeFile = nextVisibleFile ? nextVisibleFile.name : "-1"; - } - } - showMessage('success', '删除成功!'); - } catch (error) { - showMessage('info', '已取消删除'); - } - }, - renameFile(fileId, newName) { - const file = this.fileList.find(file => file.name === fileId); - if (file) { - file.label = newName; - } - }, - }, -}); \ No newline at end of file diff --git a/src/ts/useStore.ts b/src/ts/useStore.ts index 41d1a9c..9c058a1 100644 --- a/src/ts/useStore.ts +++ b/src/ts/useStore.ts @@ -1,183 +1,54 @@ -import { defineStore } from 'pinia'; -import { ref, computed } from 'vue'; +import {defineStore} from 'pinia'; +import {ref, computed} from 'vue'; // import type { Edge, Node, ViewportTransform } from '@vue-flow/core'; -import { ElMessageBox } from "element-plus"; -import { useGlobalMessage } from "./useGlobalMessage"; +import {ElMessageBox} from "element-plus"; +import {useGlobalMessage} from "./useGlobalMessage"; +import {getLogicFlowInstance} from "./useLogicFlow"; -const { showMessage } = useGlobalMessage(); +const {showMessage} = useGlobalMessage(); -// LogicFlow 实例全局引用 -let logicFlowInstance: any = null; -function setLogicFlowInstance(lf: any) { - logicFlowInstance = lf; +// localStorage 防抖定时器 +let localStorageDebounceTimer: NodeJS.Timeout | null = null; +const LOCALSTORAGE_DEBOUNCE_DELAY = 1000; // 1秒防抖 + +interface FlowFile { + label: string; + name: string; + visible: boolean; + type: string; + graphRawData?: object; + transform?: { + "SCALE_X": number, + "SCALE_Y": number, + "TRANSLATE_X": number, + "TRANSLATE_Y": number + }; } function getDefaultState() { return { - fileList: [ + "fileList": [ { - label: "File 1", - name: "1", - visible: true, - type: "FLOW", - groups: [ - { - shortDescription: "File 1 Group", - groupInfo: [{}, {}, {}, {}, {}], - details: "File 1 详情" - } - ], - flowData: { - nodes: [ - { - id: "node-1", - type: "rect", - x: 100, - y: 100, - text: "File1-矩形节点" - }, - { - id: "node-2", - type: "ellipse", - x: 350, - y: 120, - text: "File1-圆形节点" - }, - { - id: "node-3", - type: "shikigamiSelect", - x: 200, - y: 300, - properties: { - shikigami: { - name: "时曜泷夜叉姬", - avatar: "/assets/Shikigami/sp/584.png", - rarity: "SP" - }, - width: 100, - height: 80 - } - }, - { - id: "node-yuhun-1", - type: "yuhunSelect", - x: 300, - y: 200, - properties: { - yuhun: { - name: "针女", - avatar: "/assets/Yuhun/针女.png", - type: "攻击类" - }, - width: 100, - height: 80 - } - }, - { - id: "node-property-1", - type: "propertySelect", - x: 500, - y: 300, - properties: { - property: { - type: "attack", - priority: "required", - attackType: "fixed", - attackValue: 3000, - description: "主输出式神,需高攻击", - levelRequired: "40", - skillRequiredMode: "all", - skillRequired: ["5", "5", "5"], - yuhun: { - yuhunSetEffect: [ - { name: "破势", avatar: "/assets/Yuhun/破势.png" }, - { name: "荒骷髅", avatar: "/assets/Yuhun/荒骷髅.png" } - ], - target: "1", - property2: ["Attack"], - property4: ["Attack"], - property6: ["Crit", "CritDamage"] - }, - expectedDamage: 10000, - survivalRate: 50, - damageType: "balanced" - }, - width: 120, - height: 100 - } - } - ], - edges: [ - { - id: "edge-1", - type: "polyline", - sourceNodeId: "node-1", - targetNodeId: "node-2" - } - ], - viewport: { x: 0, y: 0, zoom: 1 } - } - }, - { - label: "File 2", - name: "2", - visible: true, - type: "FLOW", - groups: [ - { - shortDescription: "File 2 Group", - groupInfo: [{}, {}, {}, {}, {}], - details: "File 2 详情" - } - ], - flowData: { - nodes: [ - { - id: "node-1", - type: "rect", - x: 100, - y: 100, - text: "File2-矩形节点" - }, - { - id: "node-2", - type: "ellipse", - x: 350, - y: 120, - text: "File2222-圆形节点" - } - ], - edges: [ - { - id: "edge-1", - type: "polyline", - sourceNodeId: "node-1", - targetNodeId: "node-2" - } - ], - viewport: { x: 0, y: 0, zoom: 1 } + "label": "File 1", + "name": "File 1", + "visible": true, + "type": "FLOW", + "graphRawData": { + "nodes": [], + "edges": [] + }, + "transform": { + "SCALE_X": 1, + "SCALE_Y": 1, + "TRANSLATE_X": 0, + "TRANSLATE_Y": 0 } } ], - activeFile: "1", + "activeFile": "File 1" }; } -function saveStateToLocalStorage(state: any) { - try { - localStorage.setItem('filesStore', JSON.stringify(state)); - } catch (error) { - console.error('保存到 localStorage 失败:', error); - // 如果 localStorage 满了,尝试清理一些数据 - try { - localStorage.clear(); - localStorage.setItem('filesStore', JSON.stringify(state)); - } catch (clearError) { - console.error('清理 localStorage 后仍无法保存:', clearError); - } - } -} - function clearFilesStoreLocalStorage() { localStorage.removeItem('filesStore'); } @@ -192,34 +63,30 @@ function loadStateFromLocalStorage() { } } -// 文件相关的类型定义 -interface FileGroup { - shortDescription: string; - groupInfo: Record[]; - details: string; +function saveStateToLocalStorage(state: any) { + // 清除之前的防抖定时器 + if (localStorageDebounceTimer) { + clearTimeout(localStorageDebounceTimer); + } + + // 设置新的防抖定时器 + localStorageDebounceTimer = setTimeout(() => { + try { + localStorage.setItem('filesStore', JSON.stringify(state)); + console.log('数据已防抖保存到 localStorage'); + } catch (error) { + console.error('保存到 localStorage 失败:', error); + // 如果 localStorage 满了,尝试清理一些数据 + try { + localStorage.clear(); + localStorage.setItem('filesStore', JSON.stringify(state)); + } catch (clearError) { + console.error('清理 localStorage 后仍无法保存:', clearError); + } + } + }, LOCALSTORAGE_DEBOUNCE_DELAY); } -interface LogicFlowNode { - id: string; - type: string; - x: number; - y: number; - text?: string | object; - properties?: Record; -} - -interface FlowFile { - label: string; - name: string; - visible: boolean; - type: string; - groups: FileGroup[]; - flowData?: { - nodes: LogicFlowNode[]; - edges: any[]; - viewport: any; - }; -} export const useFilesStore = defineStore('files', () => { // 文件列表状态 @@ -231,149 +98,67 @@ export const useFilesStore = defineStore('files', () => { return fileList.value.filter(file => file.visible); }); - // 获取当前活动文件的节点和边 - const activeFileNodes = computed(() => { - const file = fileList.value.find(f => f.name === activeFile.value); - return file?.flowData?.nodes || []; - }); - - const activeFileEdges = computed(() => { - const file = fileList.value.find(f => f.name === activeFile.value); - return file?.flowData?.edges || []; - }); - - // 添加新文件 - const addFile = (file: FlowFile) => { - const newFile = { - ...file, - flowData: { - nodes: [], - edges: [], - viewport: { x: 0, y: 0, zoom: 1 } + // 导入数据 + const importData = (data: any) => { + try { + if (data.fileList && Array.isArray(data.fileList)) { + // 新版本格式:包含 fileList 和 activeFile + fileList.value = data.fileList; + activeFile.value = data.activeFile || data[0]?.name; + showMessage('success', '数据导入成功'); + } else if (Array.isArray(data) && data[0]?.visible === true) { + // 兼容旧版本格式:直接是 fileList 数组 + fileList.value = data; + activeFile.value = data[0]?.name || "1"; + showMessage('success', '数据导入成功'); + } else { + // 兼容更旧版本格式:仅包含 groups 数组 + const newFile = { + label: `File ${fileList.value.length + 1}`, + name: String(fileList.value.length + 1), + visible: true, + type: "FLOW", + groups: data, + graphRawData: { + nodes: [], + edges: [] + }, + transform: { + SCALE_X: 1, + SCALE_Y: 1, + TRANSLATE_X: 0, + TRANSLATE_Y: 0 + } + }; + fileList.value.push(newFile); + activeFile.value = newFile.name; + showMessage('success', '数据导入成功'); } - }; - fileList.value.push(newFile); - activeFile.value = file.name; - }; - - // 关闭文件标签 - const closeTab = (fileName: string | undefined) => { - if (!fileName) return; - - const index = fileList.value.findIndex(file => file.name === fileName); - if (index === -1) return; - - fileList.value.splice(index, 1); - - // 如果关闭的是当前活动文件,则切换到其他文件 - if (activeFile.value === fileName) { - activeFile.value = fileList.value[Math.max(0, index - 1)]?.name || ''; + } catch (error) { + console.error('Failed to import file', error); + showMessage('error', '数据导入失败'); } }; - // 添加节点 - const addNode = (node: LogicFlowNode) => { - const file = fileList.value.find(f => f.name === activeFile.value); - if (!file) return; - if (!file.flowData) file.flowData = { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } }; - file.flowData.nodes.push(node); - }; - - // 更新节点 - const updateNode = (nodeId: string, updateData: Partial) => { - const file = fileList.value.find(f => f.name === activeFile.value); - if (!file || !file.flowData || !file.flowData.nodes) return; - const nodeIndex = file.flowData.nodes.findIndex((n: LogicFlowNode) => n.id === nodeId); - if (nodeIndex === -1) return; - - const oldNode = file.flowData.nodes[nodeIndex]; - const mergedNode = { ...oldNode, ...updateData }; - - // Deep merge properties - if (updateData.properties && oldNode.properties) { - mergedNode.properties = { ...oldNode.properties, ...updateData.properties }; + // 导出数据 + const exportData = () => { + try { + const dataStr = JSON.stringify({ + fileList: fileList.value, + activeFile: activeFile.value + }, null, 2); + const blob = new Blob([dataStr], {type: 'application/json;charset=utf-8'}); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = 'yys-editor-files.json'; + link.click(); + URL.revokeObjectURL(url); + showMessage('success', '数据导出成功'); + } catch (error) { + console.error('导出数据失败:', error); + showMessage('error', '数据导出失败'); } - file.flowData.nodes[nodeIndex] = mergedNode; - - // 同步 LogicFlow 画布 - if (logicFlowInstance && mergedNode.properties) { - // setProperties overwrites, so we pass the fully merged properties object - logicFlowInstance.setProperties(nodeId, mergedNode.properties); - } - }; - - // 删除节点 - const removeNode = (nodeId: string) => { - const file = fileList.value.find(f => f.name === activeFile.value); - if (!file || !file.flowData || !file.flowData.nodes) return; - file.flowData.nodes = file.flowData.nodes.filter(n => n.id !== nodeId); - // 同时删除相关的边 - if (file.flowData.edges) { - file.flowData.edges = file.flowData.edges.filter(e => e.source !== nodeId && e.target !== nodeId); - } - }; - - // 添加边 - const addEdge = (edge) => { - const file = fileList.value.find(f => f.name === activeFile.value); - if (!file) return; - if (!file.flowData) file.flowData = { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } }; - file.flowData.edges.push(edge); - }; - - // 删除边 - const removeEdge = (edgeId: string) => { - const file = fileList.value.find(f => f.name === activeFile.value); - if (!file || !file.flowData || !file.flowData.edges) return; - file.flowData.edges = file.flowData.edges.filter(e => e.id !== edgeId); - }; - - // 更新节点位置 - const updateNodePosition = (nodeId: string, position: { x: number; y: number }) => { - const file = fileList.value.find(f => f.name === activeFile.value); - if (!file || !file.flowData || !file.flowData.nodes) return; - const node = file.flowData.nodes.find((n: LogicFlowNode) => n.id === nodeId); - if (node) { - node.x = position.x; - node.y = position.y; - } - }; - - // 更新节点顺序 - const updateNodesOrder = (nodes: LogicFlowNode[]) => { - const file = fileList.value.find(f => f.name === activeFile.value); - if (!file || !file.flowData) return; - file.flowData.nodes = nodes; - }; - - // 更新文件的 viewport - const updateFileViewport = (fileName: string, viewport: { x: number; y: number; zoom: number }) => { - const file = fileList.value.find(f => f.name === fileName); - if (file && file.flowData) { - console.log(`[updateFileViewport] 保存 tab "${fileName}" 的视口信息:`, viewport); - file.flowData.viewport = viewport; - } - }; - - const getFileViewport = (fileName: string) => { - const file = fileList.value.find(f => f.name === fileName); - const v = file?.flowData?.viewport; - if (v && typeof v.x === 'number' && typeof v.y === 'number' && typeof v.zoom === 'number') { - return v ; - } - return { x: 0, y: 0, zoom: 1 }; - }; - - // 更新文件的 flowData - const updateFileFlowData = (fileName: string, flowData: any) => { - const file = fileList.value.find(f => f.name === fileName); - if (file) file.flowData = flowData; - }; - - // 获取文件的 flowData - const getFileFlowData = (fileName: string): any => { - const file = fileList.value.find(f => f.name === fileName); - return file?.flowData; }; // 初始化时检查是否有未保存的数据 @@ -415,107 +200,127 @@ export const useFilesStore = defineStore('files', () => { } }; - // 设置自动保存 + // 设置自动更新 const setupAutoSave = () => { - console.log('自动保存功能已启动,每30秒保存一次'); + console.log('自动更新功能已启动,每30秒更新一次'); setInterval(() => { - try { - saveStateToLocalStorage({ - fileList: fileList.value, - activeFile: activeFile.value - }); - console.log('数据已自动保存到 localStorage'); - } catch (error) { - console.error('自动保存失败:', error); - } + updateTab(); // 使用统一的更新方法 }, 30000); // 设置间隔时间为30秒 }; - // 导出数据 - const exportData = () => { + // 添加新文件 + const addTab = () => { + // 添加文件前先保存 + updateTab(); + + requestAnimationFrame(() => { + const newFileName = `File ${fileList.value.length + 1}`; + const newFile = { + label: newFileName, + name: newFileName, + visible: true, + type: 'FLOW', + graphRawData: {}, + transform: { + SCALE_X: 1, + SCALE_Y: 1, + TRANSLATE_X: 0, + TRANSLATE_Y: 0 + } + }; + fileList.value.push(newFile); + activeFile.value = newFileName; + }); + }; + + // 关闭文件标签 + const removeTab = (fileName: string | undefined) => { + if (!fileName) return; + + const index = fileList.value.findIndex(file => file.name === fileName); + if (index === -1) return; + + fileList.value.splice(index, 1); + + // 如果关闭的是当前活动文件,则切换到其他文件 + if (activeFile.value === fileName) { + activeFile.value = fileList.value[Math.max(0, index - 1)]?.name || ''; + } + + // 关闭文件后立即更新 + updateTab(); + }; + + // 更新指定 Tab - 内存操作即时,localStorage 操作防抖 + const updateTab = (fileName?: string) => { try { - const dataStr = JSON.stringify({ + const targetFile = fileName || activeFile.value; + + // 先同步 LogicFlow 数据到内存 + syncLogicFlowDataToStore(targetFile); + + // 再保存到 localStorage(带防抖) + const state = { fileList: fileList.value, activeFile: activeFile.value - }, null, 2); - const blob = new Blob([dataStr], {type: 'application/json;charset=utf-8'}); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = 'yys-editor-files.json'; - link.click(); - URL.revokeObjectURL(url); - showMessage('success', '数据导出成功'); + }; + saveStateToLocalStorage(state); } catch (error) { - console.error('导出数据失败:', error); - showMessage('error', '数据导出失败'); + console.error('更新 Tab 失败:', error); + showMessage('error', '数据更新失败'); } }; - // 导入数据 - const importData = (data: any) => { - try { - if (data.fileList && Array.isArray(data.fileList)) { - // 新版本格式:包含 fileList 和 activeFile - fileList.value = data.fileList; - activeFile.value = data.activeFile || "1"; - showMessage('success', '数据导入成功'); - } else if (Array.isArray(data) && data[0]?.visible === true) { - // 兼容旧版本格式:直接是 fileList 数组 - fileList.value = data; - activeFile.value = data[0]?.name || "1"; - showMessage('success', '数据导入成功'); - } else { - // 兼容更旧版本格式:仅包含 groups 数组 - const newFile = { - label: `File ${fileList.value.length + 1}`, - name: String(fileList.value.length + 1), - visible: true, - type: "FLOW", - groups: data, - flowData: { - nodes: [], - edges: [], - viewport: { x: 0, y: 0, zoom: 1 } + // 获取当前 Tab 数据 + const getTab = (fileName?: string) => { + const targetFile = fileName || activeFile.value; + return fileList.value.find(f => f.name === targetFile); + }; + + // 同步 LogicFlow 画布数据到 store 的内部方法 + const syncLogicFlowDataToStore = (fileName?: string) => { + const logicFlowInstance = getLogicFlowInstance(); + const targetFile = fileName || activeFile.value; + + if (logicFlowInstance && targetFile) { + try { + // 获取画布最新数据 + const graphData = logicFlowInstance.getGraphRawData(); + const transform = logicFlowInstance.getTransform(); + + if (graphData) { + // 直接保存原始数据到 GraphRawData + const file = fileList.value.find(f => f.name === targetFile); + if (file) { + file.graphRawData = graphData; + file.transform = transform; + console.log(`已同步画布数据到文件 "${targetFile}"`); } - }; - addFile(newFile); - showMessage('success', '数据导入成功'); + } + } catch (error) { + console.warn('同步画布数据失败:', error); } - // 导入后立即保存到 localStorage - saveStateToLocalStorage({ - fileList: fileList.value, - activeFile: activeFile.value - }); - } catch (error) { - console.error('Failed to import file', error); - showMessage('error', '数据导入失败'); } }; + + + + return { + importData, + exportData, + + initializeWithPrompt, + setupAutoSave, + + addTab, + removeTab, + updateTab, + getTab, + fileList, activeFile, visibleFiles, - activeFileNodes, - activeFileEdges, - addFile, - closeTab, - addNode, - updateNode, - removeNode, - addEdge, - removeEdge, - updateNodePosition, - updateNodesOrder, - updateFileViewport, - getFileViewport, - updateFileFlowData, - getFileFlowData, - initializeWithPrompt, - setupAutoSave, - exportData, - importData, - setLogicFlowInstance, }; }); \ No newline at end of file