mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2025-08-23 08:04:50 +00:00
固定useStore核心功能,调用解耦,优化代码
This commit is contained in:
124
src/App.vue
124
src/App.vue
@@ -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) {
|
||||||
|
try {
|
||||||
|
logicFlowInstance.render(currentTab.graphRawData);
|
||||||
|
logicFlowInstance.zoom(currentTab.transform.SCALE_X, [currentTab.transform.TRANSLATE_X, currentTab.transform.TRANSLATE_Y]);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('渲染画布数据失败:', error);
|
||||||
}
|
}
|
||||||
if (flowEditorRef.value.setViewport) {
|
|
||||||
const newViewport = filesStore.getFileViewport(newVal);
|
|
||||||
console.log(`[Tab切换] 切换后恢复 tab "${newVal}" 的视口信息:`, newViewport);
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
flowEditorRef.value.setViewport(newViewport);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
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>
|
||||||
|
|
||||||
|
@@ -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.updateTab();
|
||||||
|
|
||||||
|
// 延迟一点确保更新完成后再导出
|
||||||
|
setTimeout(() => {
|
||||||
filesStore.exportData();
|
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', '文件格式错误');
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>-->
|
@@ -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>-->
|
130
src/ts/files.ts
130
src/ts/files.ts
@@ -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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
@@ -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 详情"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
flowData: {
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
id: "node-1",
|
|
||||||
type: "rect",
|
|
||||||
x: 100,
|
|
||||||
y: 100,
|
|
||||||
text: "File1-矩形节点"
|
|
||||||
},
|
},
|
||||||
{
|
"transform": {
|
||||||
id: "node-2",
|
"SCALE_X": 1,
|
||||||
type: "ellipse",
|
"SCALE_Y": 1,
|
||||||
x: 350,
|
"TRANSLATE_X": 0,
|
||||||
y: 120,
|
"TRANSLATE_Y": 0
|
||||||
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: [
|
"activeFile": "File 1"
|
||||||
{
|
|
||||||
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",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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', '数据导入成功');
|
||||||
|
} else {
|
||||||
|
// 兼容更旧版本格式:仅包含 groups 数组
|
||||||
const newFile = {
|
const newFile = {
|
||||||
...file,
|
label: `File ${fileList.value.length + 1}`,
|
||||||
flowData: {
|
name: String(fileList.value.length + 1),
|
||||||
|
visible: true,
|
||||||
|
type: "FLOW",
|
||||||
|
groups: data,
|
||||||
|
graphRawData: {
|
||||||
nodes: [],
|
nodes: [],
|
||||||
edges: [],
|
edges: []
|
||||||
viewport: { x: 0, y: 0, zoom: 1 }
|
},
|
||||||
|
transform: {
|
||||||
|
SCALE_X: 1,
|
||||||
|
SCALE_Y: 1,
|
||||||
|
TRANSLATE_X: 0,
|
||||||
|
TRANSLATE_Y: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fileList.value.push(newFile);
|
fileList.value.push(newFile);
|
||||||
activeFile.value = file.name;
|
activeFile.value = newFile.name;
|
||||||
};
|
showMessage('success', '数据导入成功');
|
||||||
|
}
|
||||||
// 关闭文件标签
|
} catch (error) {
|
||||||
const closeTab = (fileName: string | undefined) => {
|
console.error('Failed to import file', error);
|
||||||
if (!fileName) return;
|
showMessage('error', '数据导入失败');
|
||||||
|
|
||||||
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) => {
|
||||||
|
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 {
|
try {
|
||||||
if (data.fileList && Array.isArray(data.fileList)) {
|
// 获取画布最新数据
|
||||||
// 新版本格式:包含 fileList 和 activeFile
|
const graphData = logicFlowInstance.getGraphRawData();
|
||||||
fileList.value = data.fileList;
|
const transform = logicFlowInstance.getTransform();
|
||||||
activeFile.value = data.activeFile || "1";
|
|
||||||
showMessage('success', '数据导入成功');
|
if (graphData) {
|
||||||
} else if (Array.isArray(data) && data[0]?.visible === true) {
|
// 直接保存原始数据到 GraphRawData
|
||||||
// 兼容旧版本格式:直接是 fileList 数组
|
const file = fileList.value.find(f => f.name === targetFile);
|
||||||
fileList.value = data;
|
if (file) {
|
||||||
activeFile.value = data[0]?.name || "1";
|
file.graphRawData = graphData;
|
||||||
showMessage('success', '数据导入成功');
|
file.transform = transform;
|
||||||
} else {
|
console.log(`已同步画布数据到文件 "${targetFile}"`);
|
||||||
// 兼容更旧版本格式:仅包含 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 }
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
addFile(newFile);
|
|
||||||
showMessage('success', '数据导入成功');
|
|
||||||
}
|
}
|
||||||
// 导入后立即保存到 localStorage
|
|
||||||
saveStateToLocalStorage({
|
|
||||||
fileList: fileList.value,
|
|
||||||
activeFile: activeFile.value
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to import file', error);
|
console.warn('同步画布数据失败:', 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,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
Reference in New Issue
Block a user