mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2025-08-23 08:04:50 +00:00
支持多标签编辑
This commit is contained in:
@@ -3,7 +3,7 @@ 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 } from "vue";
|
import { computed, ref, onMounted, onUnmounted, onBeforeUpdate, reactive, provide, inject } from "vue";
|
||||||
import { useFilesStore } from "@/ts/files";
|
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';
|
||||||
|
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {defineProps, defineEmits, ref} from 'vue';
|
import {defineProps, defineEmits, ref} from 'vue';
|
||||||
import {useFilesStore} from "@/ts/files";
|
import {useFilesStore} from "@/ts/useStore";
|
||||||
import {ElTree, ElButton, ElDropdownMenu, ElDropdownItem} from 'element-plus';
|
import {ElTree, ElButton, ElDropdownMenu, ElDropdownItem} from 'element-plus';
|
||||||
|
|
||||||
const filesStore = useFilesStore();
|
const filesStore = useFilesStore();
|
||||||
|
@@ -81,7 +81,7 @@ import html2canvas from "html2canvas";
|
|||||||
import {useI18n} from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import updateLogs from "../data/updateLog.json"
|
import updateLogs from "../data/updateLog.json"
|
||||||
import filesStoreExample from "../data/filesStoreExample.json"
|
import filesStoreExample from "../data/filesStoreExample.json"
|
||||||
import {useFilesStore} from "@/ts/files";
|
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";
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, shallowRef, markRaw, onUnmounted } from 'vue';
|
import { ref, onMounted, shallowRef, markRaw, onUnmounted } from 'vue';
|
||||||
import { VueFlow, useVueFlow, Panel, NodeTypes } from '@vue-flow/core';
|
import { VueFlow, useVueFlow, Panel } from '@vue-flow/core';
|
||||||
import { Background } from '@vue-flow/background';
|
import { Background } from '@vue-flow/background';
|
||||||
import { Controls } from '@vue-flow/controls';
|
import { Controls } from '@vue-flow/controls';
|
||||||
import '@vue-flow/core/dist/style.css';
|
import '@vue-flow/core/dist/style.css';
|
||||||
@@ -13,6 +13,7 @@ import PropertySelectNode from './nodes/yys/PropertySelectNode.vue';
|
|||||||
import ImageNode from './nodes/common/ImageNode.vue';
|
import ImageNode from './nodes/common/ImageNode.vue';
|
||||||
import TextNode from './nodes/common/TextNode.vue';
|
import TextNode from './nodes/common/TextNode.vue';
|
||||||
import useDragAndDrop from '@/ts/useDnD';
|
import useDragAndDrop from '@/ts/useDnD';
|
||||||
|
import { useFilesStore } from '@/ts/useStore';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
height: {
|
height: {
|
||||||
@@ -21,6 +22,9 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取文件 store
|
||||||
|
const filesStore = useFilesStore();
|
||||||
|
|
||||||
// 设置节点类型
|
// 设置节点类型
|
||||||
const nodeTypes = shallowRef({
|
const nodeTypes = shallowRef({
|
||||||
shikigamiSelect: markRaw(ShikigamiSelectNode),
|
shikigamiSelect: markRaw(ShikigamiSelectNode),
|
||||||
@@ -30,21 +34,48 @@ const nodeTypes = shallowRef({
|
|||||||
textNode: markRaw(TextNode)
|
textNode: markRaw(TextNode)
|
||||||
});
|
});
|
||||||
|
|
||||||
// 初始化流程图节点 - 使用普通数组而非ref
|
// 使用VueFlow的API
|
||||||
const initialNodes = [
|
const { onNodesChange, onEdgesChange, onConnect, addNodes, setTransform, getViewport, updateNode } = useVueFlow({
|
||||||
{ id: '1', label: '开始', position: { x: 100, y: 100 }, type: 'input' }
|
nodes: filesStore.activeFileNodes,
|
||||||
];
|
edges: filesStore.activeFileEdges,
|
||||||
|
nodeTypes: nodeTypes.value
|
||||||
// 初始化流程图连线 - 使用普通数组而非ref
|
|
||||||
const initialEdges = [];
|
|
||||||
|
|
||||||
// 使用VueFlow的API,传入普通数组而非ref
|
|
||||||
const { nodes, edges, onNodesChange, onEdgesChange, onConnect, addNodes, setTransform, getViewport, updateNode } = useVueFlow({
|
|
||||||
defaultNodes: initialNodes,
|
|
||||||
defaultEdges: initialEdges,
|
|
||||||
nodeTypes
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 监听节点变化
|
||||||
|
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 { onDragOver, onDrop } = useDragAndDrop();
|
||||||
|
|
||||||
@@ -89,11 +120,12 @@ const handleLayerOrder = (action) => {
|
|||||||
if (!contextMenu.value.nodeId) return;
|
if (!contextMenu.value.nodeId) return;
|
||||||
|
|
||||||
const nodeId = contextMenu.value.nodeId;
|
const nodeId = contextMenu.value.nodeId;
|
||||||
const nodeIndex = nodes.value.findIndex(n => n.id === nodeId);
|
const currentNodes = filesStore.activeFileNodes;
|
||||||
|
const nodeIndex = currentNodes.findIndex(n => n.id === nodeId);
|
||||||
if (nodeIndex === -1) return;
|
if (nodeIndex === -1) return;
|
||||||
|
|
||||||
const node = nodes.value[nodeIndex];
|
const node = currentNodes[nodeIndex];
|
||||||
const newNodes = [...nodes.value];
|
const newNodes = [...currentNodes];
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'bringToFront':
|
case 'bringToFront':
|
||||||
@@ -120,8 +152,8 @@ const handleLayerOrder = (action) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新节点顺序
|
// 更新 store 中的节点顺序
|
||||||
nodes.value = newNodes;
|
filesStore.updateNodesOrder(newNodes);
|
||||||
contextMenu.value.show = false;
|
contextMenu.value.show = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -146,18 +178,18 @@ onUnmounted(() => {
|
|||||||
<!-- 中间流程图区域 -->
|
<!-- 中间流程图区域 -->
|
||||||
<div class="flow-container">
|
<div class="flow-container">
|
||||||
<VueFlow
|
<VueFlow
|
||||||
:nodes="nodes"
|
:nodes="filesStore.activeFileNodes"
|
||||||
:edges="edges"
|
:edges="filesStore.activeFileEdges"
|
||||||
@nodes-change="onNodesChange"
|
@nodes-change="handleNodesChange"
|
||||||
@edges-change="onEdgesChange"
|
@edges-change="handleEdgesChange"
|
||||||
@connect="onConnect"
|
@connect="handleConnect"
|
||||||
fit-view-on-init
|
fit-view-on-init
|
||||||
@drop="onDrop"
|
@drop="onDrop"
|
||||||
@dragover="onDragOver"
|
@dragover="onDragOver"
|
||||||
@node-context-menu="handleNodeContextMenu"
|
@node-context-menu="handleNodeContextMenu"
|
||||||
@pane-context-menu="handlePaneContextMenu"
|
@pane-context-menu="handlePaneContextMenu"
|
||||||
>
|
>
|
||||||
<Background pattern-color="#aaa" gap="8" />
|
<Background :pattern-color="'#aaa'" :gap="8" />
|
||||||
<Controls />
|
<Controls />
|
||||||
<Panel position="top-right" class="flow-panel">
|
<Panel position="top-right" class="flow-panel">
|
||||||
<div>流程图编辑器 (模仿 draw.io)</div>
|
<div>流程图编辑器 (模仿 draw.io)</div>
|
||||||
|
@@ -16,7 +16,7 @@ import zh from './locales/zh.json'
|
|||||||
import ja from './locales/ja.json'
|
import ja from './locales/ja.json'
|
||||||
|
|
||||||
import { createPinia } from 'pinia' // 导入 Pinia
|
import { createPinia } from 'pinia' // 导入 Pinia
|
||||||
import {useFilesStore} from "@/ts/files";
|
import { useFilesStore } from './ts/useStore';
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
@@ -62,6 +62,4 @@ app.use(pinia) // 使用 Pinia
|
|||||||
.use(Vue3DraggableResizable)
|
.use(Vue3DraggableResizable)
|
||||||
.mount('#app')
|
.mount('#app')
|
||||||
|
|
||||||
const filesStore = useFilesStore();
|
const filesStore = useFilesStore();
|
||||||
filesStore.setupAutoSave();
|
|
||||||
filesStore.initializeWithPrompt();
|
|
@@ -1,5 +1,6 @@
|
|||||||
import { useVueFlow } from '@vue-flow/core'
|
import { useVueFlow } from '@vue-flow/core'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
import { useFilesStore } from './useStore'
|
||||||
|
|
||||||
let id = 0
|
let id = 0
|
||||||
|
|
||||||
@@ -15,7 +16,8 @@ const state = {
|
|||||||
|
|
||||||
export default function useDragAndDrop() {
|
export default function useDragAndDrop() {
|
||||||
const { draggedType, isDragOver, isDragging } = state
|
const { draggedType, isDragOver, isDragging } = state
|
||||||
const { addNodes, screenToFlowCoordinate, onNodesInitialized, updateNode } = useVueFlow()
|
const { screenToFlowCoordinate, onNodesInitialized } = useVueFlow()
|
||||||
|
const filesStore = useFilesStore()
|
||||||
|
|
||||||
watch(isDragging, (dragging) => {
|
watch(isDragging, (dragging) => {
|
||||||
document.body.style.userSelect = dragging ? 'none' : ''
|
document.body.style.userSelect = dragging ? 'none' : ''
|
||||||
@@ -72,17 +74,18 @@ export default function useDragAndDrop() {
|
|||||||
position,
|
position,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filesStore.addNode(newNode)
|
||||||
|
|
||||||
const { off } = onNodesInitialized(() => {
|
const { off } = onNodesInitialized(() => {
|
||||||
updateNode(nodeId, (node) => ({
|
filesStore.updateNode(nodeId, {
|
||||||
position: {
|
position: {
|
||||||
x: node.position.x - node.dimensions.width / 2,
|
x: position.x - newNode.dimensions?.width / 2 || position.x,
|
||||||
y: node.position.y - node.dimensions.height / 2
|
y: position.y - newNode.dimensions?.height / 2 || position.y
|
||||||
},
|
},
|
||||||
}))
|
})
|
||||||
off()
|
off()
|
||||||
})
|
})
|
||||||
|
|
||||||
addNodes(newNode)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('拖拽放置处理失败:', error)
|
console.error('拖拽放置处理失败:', error)
|
||||||
}
|
}
|
||||||
|
156
src/ts/useStore.ts
Normal file
156
src/ts/useStore.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import type { Edge, Node } from '@vue-flow/core';
|
||||||
|
|
||||||
|
// 文件相关的类型定义
|
||||||
|
interface FileGroup {
|
||||||
|
shortDescription: string;
|
||||||
|
groupInfo: Record<string, any>[];
|
||||||
|
details: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FlowFile {
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
visible: boolean;
|
||||||
|
type: string;
|
||||||
|
groups: FileGroup[];
|
||||||
|
nodes?: Node[];
|
||||||
|
edges?: Edge[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFilesStore = defineStore('files', () => {
|
||||||
|
// 文件列表状态
|
||||||
|
const fileList = ref<FlowFile[]>([]);
|
||||||
|
const activeFile = ref<string>('');
|
||||||
|
|
||||||
|
// 计算属性:获取可见的文件
|
||||||
|
const visibleFiles = computed(() => {
|
||||||
|
return fileList.value.filter(file => file.visible);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取当前活动文件的节点和边
|
||||||
|
const activeFileNodes = computed(() => {
|
||||||
|
const file = fileList.value.find(f => f.name === activeFile.value);
|
||||||
|
return file?.nodes || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeFileEdges = computed(() => {
|
||||||
|
const file = fileList.value.find(f => f.name === activeFile.value);
|
||||||
|
return file?.edges || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加新文件
|
||||||
|
const addFile = (file: FlowFile) => {
|
||||||
|
// 确保新文件包含空的节点和边数组
|
||||||
|
const newFile = {
|
||||||
|
...file,
|
||||||
|
nodes: [],
|
||||||
|
edges: []
|
||||||
|
};
|
||||||
|
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 || '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加节点
|
||||||
|
const addNode = (node: Node) => {
|
||||||
|
const file = fileList.value.find(f => f.name === activeFile.value);
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
if (!file.nodes) file.nodes = [];
|
||||||
|
file.nodes.push(node);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新节点
|
||||||
|
const updateNode = (nodeId: string, updateData: Partial<Node>) => {
|
||||||
|
const file = fileList.value.find(f => f.name === activeFile.value);
|
||||||
|
if (!file || !file.nodes) return;
|
||||||
|
|
||||||
|
const nodeIndex = file.nodes.findIndex(n => n.id === nodeId);
|
||||||
|
if (nodeIndex === -1) return;
|
||||||
|
|
||||||
|
file.nodes[nodeIndex] = {
|
||||||
|
...file.nodes[nodeIndex],
|
||||||
|
...updateData,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除节点
|
||||||
|
const removeNode = (nodeId: string) => {
|
||||||
|
const file = fileList.value.find(f => f.name === activeFile.value);
|
||||||
|
if (!file || !file.nodes) return;
|
||||||
|
|
||||||
|
file.nodes = file.nodes.filter(n => n.id !== nodeId);
|
||||||
|
// 同时删除相关的边
|
||||||
|
if (file.edges) {
|
||||||
|
file.edges = file.edges.filter(e => e.source !== nodeId && e.target !== nodeId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加边
|
||||||
|
const addEdge = (edge: Edge) => {
|
||||||
|
const file = fileList.value.find(f => f.name === activeFile.value);
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
if (!file.edges) file.edges = [];
|
||||||
|
file.edges.push(edge);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除边
|
||||||
|
const removeEdge = (edgeId: string) => {
|
||||||
|
const file = fileList.value.find(f => f.name === activeFile.value);
|
||||||
|
if (!file || !file.edges) return;
|
||||||
|
|
||||||
|
file.edges = file.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.nodes) return;
|
||||||
|
|
||||||
|
const node = file.nodes.find(n => n.id === nodeId);
|
||||||
|
if (node) {
|
||||||
|
node.position = position;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新节点顺序
|
||||||
|
const updateNodesOrder = (nodes: Node[]) => {
|
||||||
|
const file = fileList.value.find(f => f.name === activeFile.value);
|
||||||
|
if (!file) return;
|
||||||
|
file.nodes = nodes;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
fileList,
|
||||||
|
activeFile,
|
||||||
|
visibleFiles,
|
||||||
|
activeFileNodes,
|
||||||
|
activeFileEdges,
|
||||||
|
addFile,
|
||||||
|
closeTab,
|
||||||
|
addNode,
|
||||||
|
updateNode,
|
||||||
|
removeNode,
|
||||||
|
addEdge,
|
||||||
|
removeEdge,
|
||||||
|
updateNodePosition,
|
||||||
|
updateNodesOrder,
|
||||||
|
};
|
||||||
|
});
|
Reference in New Issue
Block a user