mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2025-08-23 08:04:50 +00:00
FlowEditor.vue重写,数据结构调整,重新实现数据持久化和画布缩放保存
This commit is contained in:
40
src/App.vue
40
src/App.vue
@@ -10,11 +10,11 @@ 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';
|
||||||
|
|
||||||
const filesStore = useFilesStore();
|
const filesStore = useFilesStore();
|
||||||
const { updateNode,toObject,fromObject } = useVueFlow();
|
// const { updateNode,toObject,fromObject } = useVueFlow();
|
||||||
|
|
||||||
const width = ref('100%');
|
const width = ref('100%');
|
||||||
const height = ref('100vh');
|
const height = ref('100vh');
|
||||||
@@ -89,23 +89,37 @@ const handleAddNode = (nodeData) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveViewport = (viewport) => {
|
|
||||||
filesStore.updateFileViewport(filesStore.activeFile, viewport);
|
|
||||||
};
|
|
||||||
const handleRequestViewport = () => {
|
|
||||||
return filesStore.getFileViewport(filesStore.activeFile);
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => filesStore.activeFile,
|
() => filesStore.activeFile,
|
||||||
(newVal, oldVal) => {
|
async (newVal, oldVal) => {
|
||||||
// 切换前保存旧 tab 的 viewport
|
// 切换前保存旧 tab 的数据和视口
|
||||||
if (oldVal && flowEditorRef.value && flowEditorRef.value.getViewport) {
|
if (oldVal && flowEditorRef.value) {
|
||||||
|
if (flowEditorRef.value.getGraphRawData) {
|
||||||
|
const rawData = flowEditorRef.value.getGraphRawData();
|
||||||
|
filesStore.updateFileFlowData(oldVal, rawData);
|
||||||
|
}
|
||||||
|
if (flowEditorRef.value.getViewport) {
|
||||||
const viewport = flowEditorRef.value.getViewport();
|
const viewport = flowEditorRef.value.getViewport();
|
||||||
|
console.log(`[Tab切换] 切换前保存 tab "${oldVal}" 的视口信息:`, viewport);
|
||||||
filesStore.updateFileViewport(oldVal, viewport);
|
filesStore.updateFileViewport(oldVal, viewport);
|
||||||
filesStore.updateFileFlowData(oldVal, toObject());
|
}
|
||||||
}
|
}
|
||||||
lastActiveFile.value = newVal;
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -1,283 +1,137 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted, shallowRef, markRaw, onUnmounted, watch, nextTick } from 'vue';
|
|
||||||
import { VueFlow, useVueFlow, Panel } from '@vue-flow/core';
|
|
||||||
import { Background } from '@vue-flow/background';
|
|
||||||
import { Controls } from '@vue-flow/controls';
|
|
||||||
import '@vue-flow/core/dist/style.css';
|
|
||||||
import '@vue-flow/core/dist/theme-default.css';
|
|
||||||
import '@vue-flow/controls/dist/style.css';
|
|
||||||
import PropertyPanel from './PropertyPanel.vue';
|
|
||||||
import ShikigamiSelectNode from './nodes/yys/ShikigamiSelectNode.vue';
|
|
||||||
import YuhunSelectNode from './nodes/yys/YuhunSelectNode.vue';
|
|
||||||
import PropertySelectNode from './nodes/yys/PropertySelectNode.vue';
|
|
||||||
import ImageNode from './nodes/common/ImageNode.vue';
|
|
||||||
import TextNode from './nodes/common/TextNode.vue';
|
|
||||||
import useDragAndDrop from '@/ts/useDnD';
|
|
||||||
import { useFilesStore } from '@/ts/useStore';
|
|
||||||
import type { Node, Edge, ViewportTransform } from '@vue-flow/core';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
height: string;
|
|
||||||
nodes: Node[];
|
|
||||||
edges: Edge[];
|
|
||||||
viewport: ViewportTransform;
|
|
||||||
}>();
|
|
||||||
const emit = defineEmits(['save-viewport', 'request-viewport']);
|
|
||||||
|
|
||||||
// 获取文件 store
|
|
||||||
const filesStore = useFilesStore();
|
|
||||||
|
|
||||||
// 设置节点类型
|
|
||||||
const nodeTypes = shallowRef({
|
|
||||||
shikigamiSelect: markRaw(ShikigamiSelectNode),
|
|
||||||
yuhunSelect: markRaw(YuhunSelectNode),
|
|
||||||
propertySelect: markRaw(PropertySelectNode),
|
|
||||||
imageNode: markRaw(ImageNode),
|
|
||||||
textNode: markRaw(TextNode)
|
|
||||||
});
|
|
||||||
|
|
||||||
// 使用VueFlow的API
|
|
||||||
const { nodes, edges, setNodes, setEdges, setTransform, getViewport, onNodesChange, onEdgesChange, onConnect, addNodes, updateNode } = useVueFlow({
|
|
||||||
nodes: props.nodes,
|
|
||||||
edges: props.edges,
|
|
||||||
nodeTypes: nodeTypes.value
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听 viewport 变化,重绘视图
|
|
||||||
watch(
|
|
||||||
() => props.viewport,
|
|
||||||
(newViewport) => {
|
|
||||||
setTransform(newViewport);
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
setTransform(props.viewport);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听节点变化
|
|
||||||
const handleNodesChange = (changes) => {
|
|
||||||
// 更新 store 中的节点
|
|
||||||
changes.forEach(change => {
|
|
||||||
if (change.type === 'position' && change.position) {
|
|
||||||
filesStore.updateNodePosition(change.id, change.position);
|
|
||||||
} else if (change.type === 'remove') {
|
|
||||||
filesStore.removeNode(change.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
onNodesChange(changes);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 监听边变化
|
|
||||||
const handleEdgesChange = (changes) => {
|
|
||||||
// 更新 store 中的边
|
|
||||||
changes.forEach(change => {
|
|
||||||
if (change.type === 'remove') {
|
|
||||||
filesStore.removeEdge(change.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
onEdgesChange(changes);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 监听连接
|
|
||||||
const handleConnect = (connection) => {
|
|
||||||
// 添加新边到 store
|
|
||||||
filesStore.addEdge({
|
|
||||||
id: `e${connection.source}-${connection.target}`,
|
|
||||||
source: connection.source,
|
|
||||||
target: connection.target
|
|
||||||
});
|
|
||||||
onConnect(connection);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 使用拖拽功能
|
|
||||||
const { onDragOver, onDrop } = useDragAndDrop();
|
|
||||||
|
|
||||||
// 右键菜单相关
|
|
||||||
const contextMenu = ref({
|
|
||||||
show: false,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
nodeId: null
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理节点右键点击
|
|
||||||
const handleNodeContextMenu = (event) => {
|
|
||||||
const { event: mouseEvent, node } = event;
|
|
||||||
mouseEvent.preventDefault();
|
|
||||||
mouseEvent.stopPropagation();
|
|
||||||
|
|
||||||
contextMenu.value = {
|
|
||||||
show: true,
|
|
||||||
x: mouseEvent.clientX,
|
|
||||||
y: mouseEvent.clientY,
|
|
||||||
nodeId: node.id
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理画布右键点击
|
|
||||||
const handlePaneContextMenu = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
contextMenu.value.show = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 点击其他地方时关闭菜单
|
|
||||||
const handleClickOutside = (event) => {
|
|
||||||
if (!event.target.closest('.context-menu')) {
|
|
||||||
contextMenu.value.show = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理图层顺序调整
|
|
||||||
const handleLayerOrder = (action) => {
|
|
||||||
if (!contextMenu.value.nodeId) return;
|
|
||||||
|
|
||||||
const nodeId = contextMenu.value.nodeId;
|
|
||||||
const currentNodes = filesStore.activeFileNodes;
|
|
||||||
const nodeIndex = currentNodes.findIndex(n => n.id === nodeId);
|
|
||||||
if (nodeIndex === -1) return;
|
|
||||||
|
|
||||||
const node = currentNodes[nodeIndex];
|
|
||||||
const newNodes = [...currentNodes];
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case 'bringToFront':
|
|
||||||
// 移至最前
|
|
||||||
newNodes.splice(nodeIndex, 1);
|
|
||||||
newNodes.push(node);
|
|
||||||
break;
|
|
||||||
case 'sendToBack':
|
|
||||||
// 移至最后
|
|
||||||
newNodes.splice(nodeIndex, 1);
|
|
||||||
newNodes.unshift(node);
|
|
||||||
break;
|
|
||||||
case 'bringForward':
|
|
||||||
// 上移一层
|
|
||||||
if (nodeIndex < newNodes.length - 1) {
|
|
||||||
[newNodes[nodeIndex], newNodes[nodeIndex + 1]] = [newNodes[nodeIndex + 1], newNodes[nodeIndex]];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'sendBackward':
|
|
||||||
// 下移一层
|
|
||||||
if (nodeIndex > 0) {
|
|
||||||
[newNodes[nodeIndex], newNodes[nodeIndex - 1]] = [newNodes[nodeIndex - 1], newNodes[nodeIndex]];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新 store 中的节点顺序
|
|
||||||
filesStore.updateNodesOrder(newNodes);
|
|
||||||
contextMenu.value.show = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
handleAddNode: addNodes,
|
|
||||||
getViewport
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
console.log('FlowEditor 组件已挂载');
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
// 移除事件监听
|
|
||||||
// document.removeEventListener('click', handleClickOutside);
|
|
||||||
});
|
|
||||||
|
|
||||||
const lastActiveFile = ref(filesStore.activeFile);
|
|
||||||
|
|
||||||
const flowEditorRef = ref();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flow-editor" :style="{ height }">
|
<div class="container" ref="containerRef" :style="{ height }"></div>
|
||||||
<div class="editor-layout">
|
|
||||||
<!-- 中间流程图区域 -->
|
|
||||||
<div class="flow-container">
|
|
||||||
<VueFlow
|
|
||||||
:nodes="props.nodes"
|
|
||||||
:edges="props.edges"
|
|
||||||
@nodes-change="handleNodesChange"
|
|
||||||
@edges-change="handleEdgesChange"
|
|
||||||
@connect="handleConnect"
|
|
||||||
fit-view-on-init
|
|
||||||
@drop="onDrop"
|
|
||||||
@dragover="onDragOver"
|
|
||||||
@node-context-menu="handleNodeContextMenu"
|
|
||||||
@pane-context-menu="handlePaneContextMenu"
|
|
||||||
>
|
|
||||||
<Background :pattern-color="'#aaa'" :gap="8" />
|
|
||||||
<Controls />
|
|
||||||
<Panel position="top-right" class="flow-panel">
|
|
||||||
<div>流程图编辑器 (模仿 draw.io)</div>
|
|
||||||
</Panel>
|
|
||||||
</VueFlow>
|
|
||||||
|
|
||||||
<!-- 右键菜单 -->
|
|
||||||
<Teleport to="body">
|
|
||||||
<div v-if="contextMenu.show"
|
|
||||||
class="context-menu"
|
|
||||||
:style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
|
|
||||||
@click.stop>
|
|
||||||
<div class="menu-item" @click="handleLayerOrder('bringToFront')">移至最前</div>
|
|
||||||
<div class="menu-item" @click="handleLayerOrder('sendToBack')">移至最后</div>
|
|
||||||
<div class="menu-item" @click="handleLayerOrder('bringForward')">上移一层</div>
|
|
||||||
<div class="menu-item" @click="handleLayerOrder('sendBackward')">下移一层</div>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 右侧属性面板 -->
|
|
||||||
<PropertyPanel
|
|
||||||
:height="height"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, onMounted, onBeforeUnmount, defineExpose } from 'vue';
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
import LogicFlow from '@logicflow/core';
|
||||||
|
import '@logicflow/core/lib/style/index.css';
|
||||||
|
|
||||||
|
// 类型定义放在 import 之后,避免顶层 await 错误
|
||||||
|
|
||||||
|
type NodeData = {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
text?: string;
|
||||||
|
properties?: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EdgeData = {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
sourceNodeId: string;
|
||||||
|
targetNodeId: string;
|
||||||
|
properties?: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Viewport = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
zoom: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
nodes: NodeData[];
|
||||||
|
edges: EdgeData[];
|
||||||
|
viewport?: Viewport;
|
||||||
|
height?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const containerRef = ref<HTMLElement | null>(null);
|
||||||
|
let lf: LogicFlow | null = null;
|
||||||
|
|
||||||
|
// 初始化 LogicFlow
|
||||||
|
onMounted(() => {
|
||||||
|
lf = new LogicFlow({
|
||||||
|
container: containerRef.value as HTMLElement,
|
||||||
|
grid: true,
|
||||||
|
});
|
||||||
|
// lf.zoom(2);
|
||||||
|
// zoom(2);
|
||||||
|
renderFlow();
|
||||||
|
|
||||||
|
|
||||||
|
// if (props.viewport) setViewport(props.viewport);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 销毁 LogicFlow
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
lf?.destroy();
|
||||||
|
lf = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 响应式更新 nodes/edges
|
||||||
|
watch(
|
||||||
|
() => [props.nodes, props.edges],
|
||||||
|
() => {
|
||||||
|
renderFlow();
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 响应式更新 viewport
|
||||||
|
watch(
|
||||||
|
() => props.viewport,
|
||||||
|
(val) => {
|
||||||
|
if (val) setViewport(val);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function renderFlow() {
|
||||||
|
if (!lf) return;
|
||||||
|
lf.render({
|
||||||
|
nodes: props.nodes,
|
||||||
|
edges: props.edges,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setViewport(viewport: Viewport) {
|
||||||
|
if (!lf || !viewport) return;
|
||||||
|
lf.zoom(viewport.zoom);
|
||||||
|
// lf.focusOn({ x: viewport.x, y: viewport.y });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getViewport(): Viewport {
|
||||||
|
if (!lf) return { x: 0, y: 0, zoom: 1 };
|
||||||
|
const t = lf.getTransform();
|
||||||
|
return {
|
||||||
|
x: t.TRANSLATE_X,
|
||||||
|
y: t.TRANSLATE_Y,
|
||||||
|
zoom: t.SCALE_X
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddNode(node: NodeData) {
|
||||||
|
if (!lf) return;
|
||||||
|
lf.addNode(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGraphRawData() {
|
||||||
|
if (!lf) return null;
|
||||||
|
return lf.getGraphRawData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRawData(data: any) {
|
||||||
|
if (!lf) return;
|
||||||
|
lf.renderRawData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getViewport,
|
||||||
|
setViewport, // 新增暴露
|
||||||
|
handleAddNode,
|
||||||
|
getGraphRawData,
|
||||||
|
renderRawData,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.flow-editor {
|
.container {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
min-height: 300px;
|
||||||
|
background: #fff;
|
||||||
.editor-layout {
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flow-container {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.context-menu {
|
|
||||||
position: fixed;
|
|
||||||
background: white;
|
|
||||||
border: 1px solid #dcdfe6;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 5px 0;
|
|
||||||
z-index: 9999;
|
|
||||||
min-width: 120px;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item {
|
|
||||||
padding: 8px 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #606266;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item:hover {
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
color: #409eff;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@@ -1,6 +1,6 @@
|
|||||||
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";
|
||||||
|
|
||||||
@@ -8,24 +8,88 @@ const { showMessage } = useGlobalMessage();
|
|||||||
|
|
||||||
function getDefaultState() {
|
function getDefaultState() {
|
||||||
return {
|
return {
|
||||||
fileList: [{
|
fileList: [
|
||||||
"label": "File 1",
|
|
||||||
"name": "1",
|
|
||||||
"visible": true,
|
|
||||||
"type": "FLOW",
|
|
||||||
"groups": [
|
|
||||||
{
|
{
|
||||||
"shortDescription": "",
|
label: "File 1",
|
||||||
"groupInfo": [{}, {}, {}, {}, {}],
|
name: "1",
|
||||||
"details": ""
|
visible: true,
|
||||||
|
type: "FLOW",
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
shortDescription: "File 1 Group",
|
||||||
|
groupInfo: [{}, {}, {}, {}, {}],
|
||||||
|
details: "File 1 详情"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"flowData": {
|
flowData: {
|
||||||
"nodes": [],
|
nodes: [
|
||||||
"edges": [],
|
{
|
||||||
"viewport": { "x": 0, "y": 0, "zoom": 1 }
|
id: "node-1",
|
||||||
|
type: "rect",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
text: "File1-矩形节点"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "node-2",
|
||||||
|
type: "ellipse",
|
||||||
|
x: 350,
|
||||||
|
y: 120,
|
||||||
|
text: "File1-圆形节点"
|
||||||
}
|
}
|
||||||
}],
|
],
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
id: "edge-1",
|
||||||
|
type: "polyline",
|
||||||
|
sourceNodeId: "node-1",
|
||||||
|
targetNodeId: "node-2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
viewport: { x: 0, y: 0, zoom: 1 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "File 2",
|
||||||
|
name: "2",
|
||||||
|
visible: true,
|
||||||
|
type: "FLOW",
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
shortDescription: "File 2 Group",
|
||||||
|
groupInfo: [{}, {}, {}, {}, {}],
|
||||||
|
details: "File 2 详情"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
flowData: {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: "node-1",
|
||||||
|
type: "rect",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
text: "File2-矩形节点"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "node-2",
|
||||||
|
type: "ellipse",
|
||||||
|
x: 350,
|
||||||
|
y: 120,
|
||||||
|
text: "File2222-圆形节点"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
id: "edge-1",
|
||||||
|
type: "polyline",
|
||||||
|
sourceNodeId: "node-1",
|
||||||
|
targetNodeId: "node-2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
viewport: { x: 0, y: 0, zoom: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
activeFile: "1",
|
activeFile: "1",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -157,7 +221,7 @@ export const useFilesStore = defineStore('files', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 添加边
|
// 添加边
|
||||||
const addEdge = (edge: Edge) => {
|
const addEdge = (edge) => {
|
||||||
const file = fileList.value.find(f => f.name === activeFile.value);
|
const file = fileList.value.find(f => f.name === activeFile.value);
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
if (!file.flowData) file.flowData = { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } };
|
if (!file.flowData) file.flowData = { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } };
|
||||||
@@ -191,14 +255,17 @@ export const useFilesStore = defineStore('files', () => {
|
|||||||
// 更新文件的 viewport
|
// 更新文件的 viewport
|
||||||
const updateFileViewport = (fileName: string, viewport: { x: number; y: number; zoom: number }) => {
|
const updateFileViewport = (fileName: string, viewport: { x: number; y: number; zoom: number }) => {
|
||||||
const file = fileList.value.find(f => f.name === fileName);
|
const file = fileList.value.find(f => f.name === fileName);
|
||||||
if (file && file.flowData) file.flowData.viewport = viewport;
|
if (file && file.flowData) {
|
||||||
|
console.log(`[updateFileViewport] 保存 tab "${fileName}" 的视口信息:`, viewport);
|
||||||
|
file.flowData.viewport = viewport;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFileViewport = (fileName: string): ViewportTransform => {
|
const getFileViewport = (fileName: string) => {
|
||||||
const file = fileList.value.find(f => f.name === fileName);
|
const file = fileList.value.find(f => f.name === fileName);
|
||||||
const v = file?.flowData?.viewport;
|
const v = file?.flowData?.viewport;
|
||||||
if (v && typeof v.x === 'number' && typeof v.y === 'number' && typeof v.zoom === 'number') {
|
if (v && typeof v.x === 'number' && typeof v.y === 'number' && typeof v.zoom === 'number') {
|
||||||
return v as ViewportTransform;
|
return v ;
|
||||||
}
|
}
|
||||||
return { x: 0, y: 0, zoom: 1 };
|
return { x: 0, y: 0, zoom: 1 };
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user