固定useStore核心功能,调用解耦,优化代码

This commit is contained in:
2025-07-30 17:04:36 +08:00
parent b904b257e5
commit 7a87ca6c03
7 changed files with 386 additions and 816 deletions

View File

@@ -2,16 +2,17 @@
import Toolbar from './components/Toolbar.vue'; import Toolbar from './components/Toolbar.vue';
import ProjectExplorer from './components/ProjectExplorer.vue'; import ProjectExplorer from './components/ProjectExplorer.vue';
import ComponentsPanel from './components/flow/ComponentsPanel.vue'; import ComponentsPanel from './components/flow/ComponentsPanel.vue';
import { computed, ref, onMounted, onUnmounted, onBeforeUpdate, reactive, provide, inject, watch } from "vue"; import {computed, ref, onMounted, onUnmounted, onBeforeUpdate, reactive, provide, inject, watch} from "vue";
import { useFilesStore } from "@/ts/useStore"; import {useFilesStore} from "@/ts/useStore";
import Vue3DraggableResizable from 'vue3-draggable-resizable'; 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 FlowEditor from './components/flow/FlowEditor.vue';
import ShikigamiSelect from './components/flow/nodes/yys/ShikigamiSelect.vue'; import ShikigamiSelect from './components/flow/nodes/yys/ShikigamiSelect.vue';
import YuhunSelect from './components/flow/nodes/yys/YuhunSelect.vue'; import YuhunSelect from './components/flow/nodes/yys/YuhunSelect.vue';
import PropertySelect from './components/flow/nodes/yys/PropertySelect.vue'; import PropertySelect from './components/flow/nodes/yys/PropertySelect.vue';
// import { useVueFlow } from '@vue-flow/core'; // import { useVueFlow } from '@vue-flow/core';
import DialogManager from './components/DialogManager.vue'; import DialogManager from './components/DialogManager.vue';
import {getLogicFlowInstance} from "@/ts/useLogicFlow";
const filesStore = useFilesStore(); const filesStore = useFilesStore();
// const { updateNode,toObject,fromObject } = useVueFlow(); // const { updateNode,toObject,fromObject } = useVueFlow();
@@ -22,122 +23,53 @@ const toolbarHeight = 48; // 工具栏的高度
const windowHeight = ref(window.innerHeight); const windowHeight = ref(window.innerHeight);
const contentHeight = computed(() => `${windowHeight.value - toolbarHeight}px`); const contentHeight = computed(() => `${windowHeight.value - toolbarHeight}px`);
const flowEditorRef = ref(null);
const flowEditorRefs = ref({});
const lastActiveFile = ref(filesStore.activeFile);
const handleTabsEdit = ( const handleTabsEdit = (
targetName: string | undefined, targetName: string | undefined,
action: 'remove' | 'add' action: 'remove' | 'add'
) => { ) => {
if (action === 'remove') { if (action === 'remove') {
filesStore.closeTab(targetName); filesStore.removeTab(targetName);
} else if (action === 'add') { } else if (action === 'add') {
const newFileName = `File ${filesStore.fileList.length + 1}`; filesStore.addTab();
filesStore.addFile({
label: newFileName,
name: newFileName,
visible: true,
type: 'FLOW',
groups: [
{
shortDescription: " ",
groupInfo: [{}, {}, {}, {}, {}],
details: ''
},
{
shortDescription: '',
groupInfo: [{}, {}, {}, {}, {}],
details: ''
}
]
});
} }
}; };
onMounted(() => { onMounted(() => {
window.addEventListener('resize', () => {
windowHeight.value = window.innerHeight;
});
// 初始化自动保存功能 // 初始化自动保存功能
filesStore.initializeWithPrompt(); filesStore.initializeWithPrompt();
filesStore.setupAutoSave(); filesStore.setupAutoSave();
}); });
onUnmounted(() => { 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( watch(
() => filesStore.activeFile, () => filesStore.activeFile,
async (newVal, oldVal) => { async (newVal, oldVal) => {
// 切换前保存旧 tab 数据和视口 // 保存旧 tab 数据
if (oldVal && flowEditorRef.value) { if (oldVal) {
if (flowEditorRef.value.getGraphRawData) { filesStore.updateTab(oldVal);
const rawData = flowEditorRef.value.getGraphRawData();
filesStore.updateFileFlowData(oldVal, rawData);
} }
if (flowEditorRef.value.getViewport) {
const viewport = flowEditorRef.value.getViewport();
console.log(`[Tab切换] 切换前保存 tab "${oldVal}" 的视口信息:`, viewport);
filesStore.updateFileViewport(oldVal, viewport);
}
}
lastActiveFile.value = newVal;
// 切换后恢复新 tab 数据和视口 // 渲染新 tab 数据
if (newVal && flowEditorRef.value) { if (newVal) {
if (flowEditorRef.value.renderRawData) { const logicFlowInstance = getLogicFlowInstance();
const newRawData = filesStore.getFileFlowData(newVal); const currentTab = filesStore.getTab(newVal);
if (newRawData) flowEditorRef.value.renderRawData(newRawData);
} if (logicFlowInstance && currentTab?.graphRawData) {
if (flowEditorRef.value.setViewport) { try {
const newViewport = filesStore.getFileViewport(newVal); logicFlowInstance.render(currentTab.graphRawData);
console.log(`[Tab切换] 切换后恢复 tab "${newVal}" 的视口信息:`, newViewport); logicFlowInstance.zoom(currentTab.transform.SCALE_X, [currentTab.transform.TRANSLATE_X, currentTab.transform.TRANSLATE_Y]);
requestAnimationFrame(() => { } catch (error) {
flowEditorRef.value.setViewport(newViewport); 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();
};
</script> </script>
<template> <template>
@@ -147,7 +79,7 @@ const handleDragOverOnCanvas = (event: DragEvent) => {
<!-- 侧边栏和工作区 --> <!-- 侧边栏和工作区 -->
<div class="main-content"> <div class="main-content">
<!-- 侧边栏 --> <!-- 侧边栏 -->
<ComponentsPanel @add-node="handleAddNode" /> <ComponentsPanel/>
<!-- 工作区 --> <!-- 工作区 -->
<div class="workspace"> <div class="workspace">
<el-tabs <el-tabs
@@ -164,22 +96,14 @@ const handleDragOverOnCanvas = (event: DragEvent) => {
:name="file.name.toString()" :name="file.name.toString()"
/> />
</el-tabs> </el-tabs>
<div id="main-container" :style="{ height: contentHeight, overflow: 'auto' }" <div id="main-container" :style="{ height: contentHeight, overflow: 'auto' }">
@dragover="handleDragOverOnCanvas"
@drop="handleDropOnCanvas"
>
<FlowEditor <FlowEditor
ref="flowEditorRef" :height="contentHeight"
:height="contentHeight"
:nodes="filesStore.getFileFlowData(filesStore.activeFile)?.nodes || []"
:edges="filesStore.getFileFlowData(filesStore.activeFile)?.edges || []"
:viewport="filesStore.getFileFlowData(filesStore.activeFile)?.viewport || { x: 0, y: 0, zoom: 1 }"
:key="filesStore.activeFile"
/> />
</div> </div>
</div> </div>
</div> </div>
<DialogManager /> <DialogManager/>
</div> </div>
</template> </template>

View File

@@ -82,6 +82,7 @@ import updateLogs from "../data/updateLog.json"
import {useFilesStore} from "@/ts/useStore"; import {useFilesStore} from "@/ts/useStore";
import {ElMessageBox} from "element-plus"; import {ElMessageBox} from "element-plus";
import {useGlobalMessage} from "@/ts/useGlobalMessage"; import {useGlobalMessage} from "@/ts/useGlobalMessage";
import { getLogicFlowInstance } from "@/ts/useLogicFlow";
// import { useScreenshot } from '@/ts/useScreenshot'; // import { useScreenshot } from '@/ts/useScreenshot';
import { getCurrentInstance } from 'vue'; import { getCurrentInstance } from 'vue';
@@ -100,6 +101,23 @@ const state = reactive({
showFeedbackFormDialog: false, // 控制反馈表单对话框的显示状态 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 = () => { const loadExample = () => {
ElMessageBox.confirm( ElMessageBox.confirm(
'加载样例会覆盖当前数据,是否覆盖?', '加载样例会覆盖当前数据,是否覆盖?',
@@ -133,6 +151,7 @@ const loadExample = () => {
activeFile: "example" activeFile: "example"
}; };
filesStore.importData(defaultState); filesStore.importData(defaultState);
refreshLogicFlowCanvas('LogicFlow 画布已重新渲染(示例数据)');
showMessage('success', '数据已恢复'); showMessage('success', '数据已恢复');
}).catch(() => { }).catch(() => {
showMessage('info', '选择了不恢复旧数据'); showMessage('info', '选择了不恢复旧数据');
@@ -161,7 +180,13 @@ const showFeedbackForm = () => {
}; };
const handleExport = () => { const handleExport = () => {
filesStore.exportData(); // 导出前先更新当前数据,确保不丢失最新修改
filesStore.updateTab();
// 延迟一点确保更新完成后再导出
setTimeout(() => {
filesStore.exportData();
}, 2000);
}; };
const handleImport = () => { const handleImport = () => {
@@ -178,6 +203,7 @@ const handleImport = () => {
const target = e.target as FileReader; const target = e.target as FileReader;
const data = JSON.parse(target.result as string); const data = JSON.parse(target.result as string);
filesStore.importData(data); filesStore.importData(data);
// refreshLogicFlowCanvas('LogicFlow 画布已重新渲染(导入数据)');
} catch (error) { } catch (error) {
console.error('Failed to import file', error); console.error('Failed to import file', error);
showMessage('error', '文件格式错误'); showMessage('error', '文件格式错误');

View File

@@ -36,13 +36,9 @@ import { useFilesStore } from "@/ts/useStore";
import { setLogicFlowInstance, destroyLogicFlowInstance } from '@/ts/useLogicFlow'; import { setLogicFlowInstance, destroyLogicFlowInstance } from '@/ts/useLogicFlow';
const props = defineProps<{ const props = defineProps<{
nodes: any[];
edges: any[];
viewport?: { x: number; y: number; zoom: number };
height?: string; height?: string;
}>(); }>();
const filesStore = useFilesStore();
const containerRef = ref<HTMLElement | null>(null); const containerRef = ref<HTMLElement | null>(null);
const lf = ref<LogicFlow | null>(null); const lf = ref<LogicFlow | null>(null);
@@ -70,13 +66,17 @@ function registerNodes(lfInstance: LogicFlow) {
// 初始化 LogicFlow // 初始化 LogicFlow
onMounted(() => { onMounted(() => {
lf.value = new LogicFlow({ lf.value = new LogicFlow({
container: containerRef.value as HTMLElement, container: containerRef.value,
// container: document.querySelector('#container'),
grid: true, grid: true,
allowResize: true,
allowRotate : true
}); });
registerNodes(lf.value); registerNodes(lf.value);
renderFlow();
setLogicFlowInstance(lf.value); setLogicFlowInstance(lf.value);
lf.value.render({
// 渲染的数据
})
// 监听节点点击事件,更新 selectedNode // 监听节点点击事件,更新 selectedNode
lf.value.on(EventType.NODE_CLICK, ({ data }) => { lf.value.on(EventType.NODE_CLICK, ({ data }) => {
selectedNode.value = data; selectedNode.value = data;
@@ -112,46 +112,8 @@ onBeforeUnmount(() => {
destroyLogicFlowInstance(); 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 }) { function handleNodeContextMenu({ data, e }: { data: any; e: MouseEvent }) {
@@ -174,23 +136,6 @@ function handleLayerOrder(action: string) {
contextMenu.value.show = false; 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,
});
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,65 +1,65 @@
<script setup lang="ts"> <!--<script setup lang="ts">-->
import {ref, watch} from 'vue'; <!--import {ref, watch} from 'vue';-->
import {Handle, useVueFlow} from '@vue-flow/core'; <!--import {Handle, useVueFlow} from '@vue-flow/core';-->
import {NodeResizer} from '@vue-flow/node-resizer'; <!--import {NodeResizer} from '@vue-flow/node-resizer';-->
import '@vue-flow/node-resizer/dist/style.css'; <!--import '@vue-flow/node-resizer/dist/style.css';-->
const props = defineProps({ <!--const props = defineProps({-->
data: Object, <!-- data: Object,-->
id: String, <!-- id: String,-->
selected: Boolean <!-- selected: Boolean-->
}); <!--});-->
const nodeWidth = ref(180); <!--const nodeWidth = ref(180);-->
const nodeHeight = ref(120); <!--const nodeHeight = ref(120);-->
// 监听props.data变化支持外部更新图片 <!--// 监听props.data变化支持外部更新图片-->
watch(() => props.data, (newData) => { <!--watch(() => props.data, (newData) => {-->
if (newData && newData.width) nodeWidth.value = newData.width; <!-- if (newData && newData.width) nodeWidth.value = newData.width;-->
if (newData && newData.height) nodeHeight.value = newData.height; <!-- if (newData && newData.height) nodeHeight.value = newData.height;-->
}, {immediate: true}); <!--}, {immediate: true});-->
</script> <!--</script>-->
<template> <!--<template>-->
<NodeResizer v-if="selected" :min-width="60" :min-height="60" :max-width="400" :max-height="400"/> <!-- <NodeResizer v-if="selected" :min-width="60" :min-height="60" :max-width="400" :max-height="400"/>-->
<div class="image-node"> <!-- <div class="image-node">-->
<Handle type="target" position="left" :id="`${id}-target`"/> <!-- <Handle type="target" position="left" :id="`${id}-target`"/>-->
<div class="image-content"> <!-- <div class="image-content">-->
<img v-if="props.data && props.data.url" :src="props.data.url" alt="图片节点" <!-- <img v-if="props.data && props.data.url" :src="props.data.url" alt="图片节点"-->
style="width:100%;height:100%;object-fit:contain;"/> <!-- style="width:100%;height:100%;object-fit:contain;"/>-->
<div v-else class="image-placeholder">未上传图片</div> <!-- <div v-else class="image-placeholder">未上传图片</div>-->
</div> <!-- </div>-->
<Handle type="source" position="right" :id="`${id}-source`"/> <!-- <Handle type="source" position="right" :id="`${id}-source`"/>-->
</div> <!-- </div>-->
</template> <!--</template>-->
<style scoped> <!--<style scoped>-->
.image-node { <!--.image-node {-->
background: #fff; <!-- background: #fff;-->
border: 1px solid #dcdfe6; <!-- border: 1px solid #dcdfe6;-->
border-radius: 4px; <!-- border-radius: 4px;-->
display: flex; <!-- display: flex;-->
flex-direction: column; <!-- flex-direction: column;-->
align-items: center; <!-- align-items: center;-->
justify-content: center; <!-- justify-content: center;-->
overflow: hidden; <!-- overflow: hidden;-->
position: relative; <!-- position: relative;-->
width: 100%; <!-- width: 100%;-->
height: 100%; <!-- height: 100%;-->
min-width: 180px; <!-- min-width: 180px;-->
min-height: 180px; <!-- min-height: 180px;-->
} <!--}-->
.image-content { <!--.image-content {-->
position: relative; <!-- position: relative;-->
width: 100%; <!-- width: 100%;-->
height: 100%; <!-- height: 100%;-->
display: flex; <!-- display: flex;-->
align-items: center; <!-- align-items: center;-->
justify-content: center; <!-- justify-content: center;-->
} <!--}-->
.image-placeholder { <!--.image-placeholder {-->
color: #bbb; <!-- color: #bbb;-->
font-size: 14px; <!-- font-size: 14px;-->
} <!--}-->
</style> <!--</style>-->

View File

@@ -1,51 +1,51 @@
<script setup lang="ts"> <!--<script setup lang="ts">-->
import { ref, watch } from 'vue'; <!--import { ref, watch } from 'vue';-->
import { Handle, useVueFlow } from '@vue-flow/core'; <!--import { Handle, useVueFlow } from '@vue-flow/core';-->
import { NodeResizer } from '@vue-flow/node-resizer'; <!--import { NodeResizer } from '@vue-flow/node-resizer';-->
import '@vue-flow/node-resizer/dist/style.css'; <!--import '@vue-flow/node-resizer/dist/style.css';-->
const props = defineProps({ <!--const props = defineProps({-->
data: Object, <!-- data: Object,-->
id: String, <!-- id: String,-->
selected: Boolean <!-- selected: Boolean-->
}); <!--});-->
const nodeWidth = ref(200); <!--const nodeWidth = ref(200);-->
const nodeHeight = ref(120); <!--const nodeHeight = ref(120);-->
const html = ref(''); <!--const html = ref('');-->
watch(() => props.data, (newData) => { <!--watch(() => props.data, (newData) => {-->
if (newData && newData.html !== undefined) html.value = newData.html; <!-- if (newData && newData.html !== undefined) html.value = newData.html;-->
if (newData && newData.width) nodeWidth.value = newData.width; <!-- if (newData && newData.width) nodeWidth.value = newData.width;-->
if (newData && newData.height) nodeHeight.value = newData.height; <!-- if (newData && newData.height) nodeHeight.value = newData.height;-->
}, { immediate: true }); <!--}, { immediate: true });-->
</script> <!--</script>-->
<template> <!--<template>-->
<div class="text-node" :style="{ width: `${nodeWidth}px`, height: `${nodeHeight}px` }"> <!-- <div class="text-node" :style="{ width: `${nodeWidth}px`, height: `${nodeHeight}px` }">-->
<NodeResizer v-if="selected" :min-width="80" :min-height="40" :max-width="400" :max-height="400" /> <!-- <NodeResizer v-if="selected" :min-width="80" :min-height="40" :max-width="400" :max-height="400" />-->
<Handle type="target" position="left" :id="`${id}-target`" /> <!-- <Handle type="target" position="left" :id="`${id}-target`" />-->
<div class="text-content" v-html="html"></div> <!-- <div class="text-content" v-html="html"></div>-->
<Handle type="source" position="right" :id="`${id}-source`" /> <!-- <Handle type="source" position="right" :id="`${id}-source`" />-->
</div> <!-- </div>-->
</template> <!--</template>-->
<style scoped> <!--<style scoped>-->
.text-node { <!--.text-node {-->
background: #fff; <!-- background: #fff;-->
border: 1px solid #dcdfe6; <!-- border: 1px solid #dcdfe6;-->
border-radius: 4px; <!-- border-radius: 4px;-->
display: flex; <!-- display: flex;-->
flex-direction: column; <!-- flex-direction: column;-->
align-items: center; <!-- align-items: center;-->
justify-content: center; <!-- justify-content: center;-->
overflow: hidden; <!-- overflow: hidden;-->
} <!--}-->
.text-content { <!--.text-content {-->
width: 100%; <!-- width: 100%;-->
height: 100%; <!-- height: 100%;-->
padding: 8px; <!-- padding: 8px;-->
font-size: 15px; <!-- font-size: 15px;-->
color: #333; <!-- color: #333;-->
word-break: break-all; <!-- word-break: break-all;-->
overflow: auto; <!-- overflow: auto;-->
} <!--}-->
</style> <!--</style>-->

View File

@@ -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;
}
},
},
});

View File

@@ -1,183 +1,54 @@
import { defineStore } from 'pinia'; import {defineStore} from 'pinia';
import { ref, computed } from 'vue'; import {ref, computed} from 'vue';
// import type { Edge, Node, ViewportTransform } from '@vue-flow/core'; // import type { Edge, Node, ViewportTransform } from '@vue-flow/core';
import { ElMessageBox } from "element-plus"; import {ElMessageBox} from "element-plus";
import { useGlobalMessage } from "./useGlobalMessage"; import {useGlobalMessage} from "./useGlobalMessage";
import {getLogicFlowInstance} from "./useLogicFlow";
const { showMessage } = useGlobalMessage(); const {showMessage} = useGlobalMessage();
// LogicFlow 实例全局引用 // localStorage 防抖定时器
let logicFlowInstance: any = null; let localStorageDebounceTimer: NodeJS.Timeout | null = null;
function setLogicFlowInstance(lf: any) { const LOCALSTORAGE_DEBOUNCE_DELAY = 1000; // 1秒防抖
logicFlowInstance = lf;
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() { function getDefaultState() {
return { return {
fileList: [ "fileList": [
{ {
label: "File 1", "label": "File 1",
name: "1", "name": "File 1",
visible: true, "visible": true,
type: "FLOW", "type": "FLOW",
groups: [ "graphRawData": {
{ "nodes": [],
shortDescription: "File 1 Group", "edges": []
groupInfo: [{}, {}, {}, {}, {}], },
details: "File 1 详情" "transform": {
} "SCALE_X": 1,
], "SCALE_Y": 1,
flowData: { "TRANSLATE_X": 0,
nodes: [ "TRANSLATE_Y": 0
{
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 }
} }
} }
], ],
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() { function clearFilesStoreLocalStorage() {
localStorage.removeItem('filesStore'); localStorage.removeItem('filesStore');
} }
@@ -192,34 +63,30 @@ function loadStateFromLocalStorage() {
} }
} }
// 文件相关的类型定义 function saveStateToLocalStorage(state: any) {
interface FileGroup { // 清除之前的防抖定时器
shortDescription: string; if (localStorageDebounceTimer) {
groupInfo: Record<string, any>[]; clearTimeout(localStorageDebounceTimer);
details: string; }
// 设置新的防抖定时器
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<string, any>;
}
interface FlowFile {
label: string;
name: string;
visible: boolean;
type: string;
groups: FileGroup[];
flowData?: {
nodes: LogicFlowNode[];
edges: any[];
viewport: any;
};
}
export const useFilesStore = defineStore('files', () => { export const useFilesStore = defineStore('files', () => {
// 文件列表状态 // 文件列表状态
@@ -231,149 +98,67 @@ export const useFilesStore = defineStore('files', () => {
return fileList.value.filter(file => file.visible); return fileList.value.filter(file => file.visible);
}); });
// 获取当前活动文件的节点和边 // 导入数据
const activeFileNodes = computed(() => { const importData = (data: any) => {
const file = fileList.value.find(f => f.name === activeFile.value); try {
return file?.flowData?.nodes || []; if (data.fileList && Array.isArray(data.fileList)) {
}); // 新版本格式:包含 fileList 和 activeFile
fileList.value = data.fileList;
const activeFileEdges = computed(() => { activeFile.value = data.activeFile || data[0]?.name;
const file = fileList.value.find(f => f.name === activeFile.value); showMessage('success', '数据导入成功');
return file?.flowData?.edges || []; } else if (Array.isArray(data) && data[0]?.visible === true) {
}); // 兼容旧版本格式:直接是 fileList 数组
fileList.value = data;
// 添加新文件 activeFile.value = data[0]?.name || "1";
const addFile = (file: FlowFile) => { showMessage('success', '数据导入成功');
const newFile = { } else {
...file, // 兼容更旧版本格式:仅包含 groups 数组
flowData: { const newFile = {
nodes: [], label: `File ${fileList.value.length + 1}`,
edges: [], name: String(fileList.value.length + 1),
viewport: { x: 0, y: 0, zoom: 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', '数据导入成功');
} }
}; } catch (error) {
fileList.value.push(newFile); console.error('Failed to import file', error);
activeFile.value = file.name; showMessage('error', '数据导入失败');
};
// 关闭文件标签
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 || '';
} }
}; };
// 添加节点 // 导出数据
const addNode = (node: LogicFlowNode) => { const exportData = () => {
const file = fileList.value.find(f => f.name === activeFile.value); try {
if (!file) return; const dataStr = JSON.stringify({
if (!file.flowData) file.flowData = { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } }; fileList: fileList.value,
file.flowData.nodes.push(node); activeFile: activeFile.value
}; }, null, 2);
const blob = new Blob([dataStr], {type: 'application/json;charset=utf-8'});
// 更新节点 const url = URL.createObjectURL(blob);
const updateNode = (nodeId: string, updateData: Partial<LogicFlowNode>) => { const link = document.createElement('a');
const file = fileList.value.find(f => f.name === activeFile.value); link.href = url;
if (!file || !file.flowData || !file.flowData.nodes) return; link.download = 'yys-editor-files.json';
const nodeIndex = file.flowData.nodes.findIndex((n: LogicFlowNode) => n.id === nodeId); link.click();
if (nodeIndex === -1) return; URL.revokeObjectURL(url);
showMessage('success', '数据导出成功');
const oldNode = file.flowData.nodes[nodeIndex]; } catch (error) {
const mergedNode = { ...oldNode, ...updateData }; console.error('导出数据失败:', error);
showMessage('error', '数据导出失败');
// Deep merge properties
if (updateData.properties && oldNode.properties) {
mergedNode.properties = { ...oldNode.properties, ...updateData.properties };
} }
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 = () => { const setupAutoSave = () => {
console.log('自动保存功能已启动每30秒保存一次'); console.log('自动更新功能已启动每30秒更新一次');
setInterval(() => { setInterval(() => {
try { updateTab(); // 使用统一的更新方法
saveStateToLocalStorage({
fileList: fileList.value,
activeFile: activeFile.value
});
console.log('数据已自动保存到 localStorage');
} catch (error) {
console.error('自动保存失败:', error);
}
}, 30000); // 设置间隔时间为30秒 }, 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 { try {
const dataStr = JSON.stringify({ const targetFile = fileName || activeFile.value;
// 先同步 LogicFlow 数据到内存
syncLogicFlowDataToStore(targetFile);
// 再保存到 localStorage带防抖
const state = {
fileList: fileList.value, fileList: fileList.value,
activeFile: activeFile.value activeFile: activeFile.value
}, null, 2); };
const blob = new Blob([dataStr], {type: 'application/json;charset=utf-8'}); saveStateToLocalStorage(state);
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) { } catch (error) {
console.error('导出数据失败:', error); console.error('更新 Tab 失败:', error);
showMessage('error', '数据导出失败'); showMessage('error', '数据更新失败');
} }
}; };
// 导入数据 // 获取当前 Tab 数据
const importData = (data: any) => { const getTab = (fileName?: string) => {
try { const targetFile = fileName || activeFile.value;
if (data.fileList && Array.isArray(data.fileList)) { return fileList.value.find(f => f.name === targetFile);
// 新版本格式:包含 fileList 和 activeFile };
fileList.value = data.fileList;
activeFile.value = data.activeFile || "1"; // 同步 LogicFlow 画布数据到 store 的内部方法
showMessage('success', '数据导入成功'); const syncLogicFlowDataToStore = (fileName?: string) => {
} else if (Array.isArray(data) && data[0]?.visible === true) { const logicFlowInstance = getLogicFlowInstance();
// 兼容旧版本格式:直接是 fileList 数组 const targetFile = fileName || activeFile.value;
fileList.value = data;
activeFile.value = data[0]?.name || "1"; if (logicFlowInstance && targetFile) {
showMessage('success', '数据导入成功'); try {
} else { // 获取画布最新数据
// 兼容更旧版本格式:仅包含 groups 数组 const graphData = logicFlowInstance.getGraphRawData();
const newFile = { const transform = logicFlowInstance.getTransform();
label: `File ${fileList.value.length + 1}`,
name: String(fileList.value.length + 1), if (graphData) {
visible: true, // 直接保存原始数据到 GraphRawData
type: "FLOW", const file = fileList.value.find(f => f.name === targetFile);
groups: data, if (file) {
flowData: { file.graphRawData = graphData;
nodes: [], file.transform = transform;
edges: [], console.log(`已同步画布数据到文件 "${targetFile}"`);
viewport: { x: 0, y: 0, zoom: 1 }
} }
}; }
addFile(newFile); } catch (error) {
showMessage('success', '数据导入成功'); console.warn('同步画布数据失败:', error);
} }
// 导入后立即保存到 localStorage
saveStateToLocalStorage({
fileList: fileList.value,
activeFile: activeFile.value
});
} catch (error) {
console.error('Failed to import file', error);
showMessage('error', '数据导入失败');
} }
}; };
return { return {
importData,
exportData,
initializeWithPrompt,
setupAutoSave,
addTab,
removeTab,
updateTab,
getTab,
fileList, fileList,
activeFile, activeFile,
visibleFiles, visibleFiles,
activeFileNodes,
activeFileEdges,
addFile,
closeTab,
addNode,
updateNode,
removeNode,
addEdge,
removeEdge,
updateNodePosition,
updateNodesOrder,
updateFileViewport,
getFileViewport,
updateFileFlowData,
getFileFlowData,
initializeWithPrompt,
setupAutoSave,
exportData,
importData,
setLogicFlowInstance,
}; };
}); });