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:
134
src/App.vue
134
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();
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -147,7 +79,7 @@ const handleDragOverOnCanvas = (event: DragEvent) => {
|
||||
<!-- 侧边栏和工作区 -->
|
||||
<div class="main-content">
|
||||
<!-- 侧边栏 -->
|
||||
<ComponentsPanel @add-node="handleAddNode" />
|
||||
<ComponentsPanel/>
|
||||
<!-- 工作区 -->
|
||||
<div class="workspace">
|
||||
<el-tabs
|
||||
@@ -164,22 +96,14 @@ const handleDragOverOnCanvas = (event: DragEvent) => {
|
||||
:name="file.name.toString()"
|
||||
/>
|
||||
</el-tabs>
|
||||
<div id="main-container" :style="{ height: contentHeight, overflow: 'auto' }"
|
||||
@dragover="handleDragOverOnCanvas"
|
||||
@drop="handleDropOnCanvas"
|
||||
>
|
||||
<div id="main-container" :style="{ height: contentHeight, overflow: 'auto' }">
|
||||
<FlowEditor
|
||||
ref="flowEditorRef"
|
||||
: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"
|
||||
:height="contentHeight"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogManager />
|
||||
<DialogManager/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@@ -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', '文件格式错误');
|
||||
|
@@ -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<HTMLElement | null>(null);
|
||||
const lf = ref<LogicFlow | null>(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,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@@ -1,65 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, watch} from 'vue';
|
||||
import {Handle, useVueFlow} from '@vue-flow/core';
|
||||
import {NodeResizer} from '@vue-flow/node-resizer';
|
||||
import '@vue-flow/node-resizer/dist/style.css';
|
||||
<!--<script setup lang="ts">-->
|
||||
<!--import {ref, watch} from 'vue';-->
|
||||
<!--import {Handle, useVueFlow} from '@vue-flow/core';-->
|
||||
<!--import {NodeResizer} from '@vue-flow/node-resizer';-->
|
||||
<!--import '@vue-flow/node-resizer/dist/style.css';-->
|
||||
|
||||
const props = defineProps({
|
||||
data: Object,
|
||||
id: String,
|
||||
selected: Boolean
|
||||
});
|
||||
<!--const props = defineProps({-->
|
||||
<!-- data: Object,-->
|
||||
<!-- id: String,-->
|
||||
<!-- selected: Boolean-->
|
||||
<!--});-->
|
||||
|
||||
const nodeWidth = ref(180);
|
||||
const nodeHeight = ref(120);
|
||||
<!--const nodeWidth = ref(180);-->
|
||||
<!--const nodeHeight = ref(120);-->
|
||||
|
||||
// 监听props.data变化,支持外部更新图片
|
||||
watch(() => props.data, (newData) => {
|
||||
if (newData && newData.width) nodeWidth.value = newData.width;
|
||||
if (newData && newData.height) nodeHeight.value = newData.height;
|
||||
}, {immediate: true});
|
||||
<!--// 监听props.data变化,支持外部更新图片-->
|
||||
<!--watch(() => props.data, (newData) => {-->
|
||||
<!-- if (newData && newData.width) nodeWidth.value = newData.width;-->
|
||||
<!-- if (newData && newData.height) nodeHeight.value = newData.height;-->
|
||||
<!--}, {immediate: true});-->
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<NodeResizer v-if="selected" :min-width="60" :min-height="60" :max-width="400" :max-height="400"/>
|
||||
<div class="image-node">
|
||||
<Handle type="target" position="left" :id="`${id}-target`"/>
|
||||
<div class="image-content">
|
||||
<img v-if="props.data && props.data.url" :src="props.data.url" alt="图片节点"
|
||||
style="width:100%;height:100%;object-fit:contain;"/>
|
||||
<div v-else class="image-placeholder">未上传图片</div>
|
||||
</div>
|
||||
<Handle type="source" position="right" :id="`${id}-source`"/>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.image-node {
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 180px;
|
||||
min-height: 180px;
|
||||
}
|
||||
<!--</script>-->
|
||||
<!--<template>-->
|
||||
<!-- <NodeResizer v-if="selected" :min-width="60" :min-height="60" :max-width="400" :max-height="400"/>-->
|
||||
<!-- <div class="image-node">-->
|
||||
<!-- <Handle type="target" position="left" :id="`${id}-target`"/>-->
|
||||
<!-- <div class="image-content">-->
|
||||
<!-- <img v-if="props.data && props.data.url" :src="props.data.url" alt="图片节点"-->
|
||||
<!-- style="width:100%;height:100%;object-fit:contain;"/>-->
|
||||
<!-- <div v-else class="image-placeholder">未上传图片</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <Handle type="source" position="right" :id="`${id}-source`"/>-->
|
||||
<!-- </div>-->
|
||||
<!--</template>-->
|
||||
<!--<style scoped>-->
|
||||
<!--.image-node {-->
|
||||
<!-- background: #fff;-->
|
||||
<!-- border: 1px solid #dcdfe6;-->
|
||||
<!-- border-radius: 4px;-->
|
||||
<!-- display: flex;-->
|
||||
<!-- flex-direction: column;-->
|
||||
<!-- align-items: center;-->
|
||||
<!-- justify-content: center;-->
|
||||
<!-- overflow: hidden;-->
|
||||
<!-- position: relative;-->
|
||||
<!-- width: 100%;-->
|
||||
<!-- height: 100%;-->
|
||||
<!-- min-width: 180px;-->
|
||||
<!-- min-height: 180px;-->
|
||||
<!--}-->
|
||||
|
||||
.image-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
<!--.image-content {-->
|
||||
<!-- position: relative;-->
|
||||
<!-- width: 100%;-->
|
||||
<!-- height: 100%;-->
|
||||
<!-- display: flex;-->
|
||||
<!-- align-items: center;-->
|
||||
<!-- justify-content: center;-->
|
||||
<!--}-->
|
||||
|
||||
.image-placeholder {
|
||||
color: #bbb;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
<!--.image-placeholder {-->
|
||||
<!-- color: #bbb;-->
|
||||
<!-- font-size: 14px;-->
|
||||
<!--}-->
|
||||
<!--</style>-->
|
@@ -1,51 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { Handle, useVueFlow } from '@vue-flow/core';
|
||||
import { NodeResizer } from '@vue-flow/node-resizer';
|
||||
import '@vue-flow/node-resizer/dist/style.css';
|
||||
<!--<script setup lang="ts">-->
|
||||
<!--import { ref, watch } from 'vue';-->
|
||||
<!--import { Handle, useVueFlow } from '@vue-flow/core';-->
|
||||
<!--import { NodeResizer } from '@vue-flow/node-resizer';-->
|
||||
<!--import '@vue-flow/node-resizer/dist/style.css';-->
|
||||
|
||||
const props = defineProps({
|
||||
data: Object,
|
||||
id: String,
|
||||
selected: Boolean
|
||||
});
|
||||
<!--const props = defineProps({-->
|
||||
<!-- data: Object,-->
|
||||
<!-- id: String,-->
|
||||
<!-- selected: Boolean-->
|
||||
<!--});-->
|
||||
|
||||
const nodeWidth = ref(200);
|
||||
const nodeHeight = ref(120);
|
||||
const html = ref('');
|
||||
<!--const nodeWidth = ref(200);-->
|
||||
<!--const nodeHeight = ref(120);-->
|
||||
<!--const html = ref('');-->
|
||||
|
||||
watch(() => props.data, (newData) => {
|
||||
if (newData && newData.html !== undefined) html.value = newData.html;
|
||||
if (newData && newData.width) nodeWidth.value = newData.width;
|
||||
if (newData && newData.height) nodeHeight.value = newData.height;
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
<template>
|
||||
<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" />
|
||||
<Handle type="target" position="left" :id="`${id}-target`" />
|
||||
<div class="text-content" v-html="html"></div>
|
||||
<Handle type="source" position="right" :id="`${id}-source`" />
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.text-node {
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.text-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
word-break: break-all;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
<!--watch(() => props.data, (newData) => {-->
|
||||
<!-- if (newData && newData.html !== undefined) html.value = newData.html;-->
|
||||
<!-- if (newData && newData.width) nodeWidth.value = newData.width;-->
|
||||
<!-- if (newData && newData.height) nodeHeight.value = newData.height;-->
|
||||
<!--}, { immediate: true });-->
|
||||
<!--</script>-->
|
||||
<!--<template>-->
|
||||
<!-- <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" />-->
|
||||
<!-- <Handle type="target" position="left" :id="`${id}-target`" />-->
|
||||
<!-- <div class="text-content" v-html="html"></div>-->
|
||||
<!-- <Handle type="source" position="right" :id="`${id}-source`" />-->
|
||||
<!-- </div>-->
|
||||
<!--</template>-->
|
||||
<!--<style scoped>-->
|
||||
<!--.text-node {-->
|
||||
<!-- background: #fff;-->
|
||||
<!-- border: 1px solid #dcdfe6;-->
|
||||
<!-- border-radius: 4px;-->
|
||||
<!-- display: flex;-->
|
||||
<!-- flex-direction: column;-->
|
||||
<!-- align-items: center;-->
|
||||
<!-- justify-content: center;-->
|
||||
<!-- overflow: hidden;-->
|
||||
<!--}-->
|
||||
<!--.text-content {-->
|
||||
<!-- width: 100%;-->
|
||||
<!-- height: 100%;-->
|
||||
<!-- padding: 8px;-->
|
||||
<!-- font-size: 15px;-->
|
||||
<!-- color: #333;-->
|
||||
<!-- word-break: break-all;-->
|
||||
<!-- overflow: auto;-->
|
||||
<!--}-->
|
||||
<!--</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 { 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<string, any>[];
|
||||
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<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', () => {
|
||||
// 文件列表状态
|
||||
@@ -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<LogicFlowNode>) => {
|
||||
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,
|
||||
};
|
||||
});
|
Reference in New Issue
Block a user