支持多标签编辑

This commit is contained in:
2025-07-02 17:38:11 +08:00
parent 51af096f2e
commit ed2050c5c7
7 changed files with 227 additions and 38 deletions

View File

@@ -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';

View File

@@ -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();

View File

@@ -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";

View File

@@ -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>

View File

@@ -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)
@@ -63,5 +63,3 @@ app.use(pinia) // 使用 Pinia
.mount('#app') .mount('#app')
const filesStore = useFilesStore(); const filesStore = useFilesStore();
filesStore.setupAutoSave();
filesStore.initializeWithPrompt();

View File

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