Files
yys-editor/src/components/flow/FlowEditor.vue
2025-06-12 19:34:50 +08:00

395 lines
9.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { ref, onMounted, shallowRef, markRaw, onUnmounted } from 'vue';
import { VueFlow, useVueFlow, Panel, NodeTypes } 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 ComponentsPanel from './ComponentsPanel.vue';
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';
const props = defineProps({
height: {
type: String,
default: '100%'
}
});
const emit = defineEmits(['open-shikigami-select', 'open-yuhun-select', 'open-property-select']);
// 设置节点类型
const nodeTypes = shallowRef({
shikigamiSelect: markRaw(ShikigamiSelectNode),
yuhunSelect: markRaw(YuhunSelectNode),
propertySelect: markRaw(PropertySelectNode),
imageNode: markRaw(ImageNode),
textNode: markRaw(TextNode)
});
// 初始化流程图节点 - 使用普通数组而非ref
const initialNodes = [
{ id: '1', label: '开始', position: { x: 100, y: 100 }, type: 'input' }
];
// 初始化流程图连线 - 使用普通数组而非ref
const initialEdges = [];
// 使用VueFlow的API传入普通数组而非ref
const { nodes, edges, onNodesChange, onEdgesChange, onConnect, addNodes, setTransform, getViewport, updateNode } = useVueFlow({
defaultNodes: initialNodes,
defaultEdges: initialEdges,
nodeTypes
});
// 右键菜单相关
const contextMenu = ref({
show: false,
x: 0,
y: 0,
nodeId: null
});
// 处理拖拽放置
const handleDrop = (event) => {
event.preventDefault();
try {
// 获取拖拽数据
const nodeData = JSON.parse(event.dataTransfer.getData('application/json'));
// 获取画布元素
const flowContainer = event.currentTarget;
const bounds = flowContainer.getBoundingClientRect();
// 获取画布的缩放和偏移信息
const { x: viewportX, y: viewportY, zoom } = getViewport();
// 计算相对于画布的位置,并考虑缩放和偏移
const position = {
x: (event.clientX - bounds.left - viewportX) / zoom,
y: (event.clientY - bounds.top - viewportY) / zoom
};
// 创建新节点
const newNode = {
...nodeData,
position,
selected: true // 设置节点为选中状态
};
// 添加节点
handleAddNode(newNode);
} catch (error) {
console.error('拖拽放置处理失败:', error);
}
};
// 处理拖拽悬停
const handleDragOver = (event) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
};
// 处理添加节点
const handleAddNode = (newNode) => {
// 取消所有现有节点的选中状态
nodes.value.forEach(node => {
if (node.selected) {
updateNode(node.id, { selected: false });
}
});
// 根据节点类型设置初始数据
let initialData = {};
switch (newNode.type) {
case 'shikigamiSelect':
initialData = {
shikigami: { name: '未选择式神', avatar: '', rarity: '' }
};
break;
case 'yuhunSelect':
initialData = {
yuhun: { name: '未选择御魂', avatar: '', type: '' }
};
break;
case 'propertySelect':
initialData = {
property: {
type: '未选择',
priority: 'optional',
description: '',
value: 0,
valueType: '',
levelRequired: "40",
skillRequiredMode: "all",
skillRequired: ["5", "5", "5"],
yuhun: {
yuhunSetEffect: [],
target: "1",
property2: ["Attack"],
property4: ["Attack"],
property6: ["Crit", "CritDamage"],
},
expectedDamage: 0,
survivalRate: 50,
damageType: "balanced"
}
};
break;
case 'imageNode':
initialData = {
url: '',
width: 180,
height: 120
};
break;
case 'textNode':
initialData = {
html: '<div>双击右侧可编辑文字</div>',
width: 200,
height: 120
};
break;
}
// 添加新节点,并设置为选中状态
addNodes([{
...newNode,
selected: true,
data: {
...newNode.data,
...initialData
}
}]);
// 重新设置视图,使新节点可见
setTransform({ x: 0, y: 0, zoom: 1 });
};
// 处理从属性面板打开式神选择
const handleOpenShikigamiSelect = (node) => {
emit('open-shikigami-select', node);
};
// 处理从属性面板打开御魂选择
const handleOpenYuhunSelect = (node) => {
emit('open-yuhun-select', node);
};
// 处理从属性面板打开属性选择
const handleOpenPropertySelect = (node) => {
emit('open-property-select', node);
};
// 处理节点右键点击
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 nodeIndex = nodes.value.findIndex(n => n.id === nodeId);
if (nodeIndex === -1) return;
const node = nodes.value[nodeIndex];
const newNodes = [...nodes.value];
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;
}
// 更新节点顺序
nodes.value = newNodes;
contextMenu.value.show = false;
};
onMounted(() => {
console.log('FlowEditor 组件已挂载');
// 添加全局点击事件监听
document.addEventListener('click', handleClickOutside);
});
onUnmounted(() => {
// 移除事件监听
document.removeEventListener('click', handleClickOutside);
});
</script>
<template>
<div class="flow-editor" :style="{ height }">
<div class="editor-layout">
<!-- 左侧组件面板 -->
<div class="components-sidebar">
<ComponentsPanel @add-node="handleAddNode" />
</div>
<!-- 中间流程图区域 -->
<div class="flow-container">
<VueFlow
:nodes="nodes"
:edges="edges"
@nodes-change="onNodesChange"
@edges-change="onEdgesChange"
@connect="onConnect"
fit-view-on-init
@drop="handleDrop"
@dragover="handleDragOver"
@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"
@open-shikigami-select="handleOpenShikigamiSelect"
@open-yuhun-select="handleOpenYuhunSelect"
@open-property-select="handleOpenPropertySelect"
/>
</div>
</div>
</template>
<style scoped>
.flow-editor {
display: flex;
flex-direction: column;
width: 100%;
}
.editor-layout {
display: flex;
height: 100%;
}
.components-sidebar {
width: 230px;
background-color: #f5f7fa;
border-right: 1px solid #e4e7ed;
overflow-y: auto;
}
.flow-container {
flex: 1;
position: relative;
overflow: hidden;
}
:deep(.vue-flow__node) {
padding: 10px;
border-radius: 5px;
background-color: white;
border: 1px solid #1a192b;
color: #333;
text-align: center;
}
:deep(.vue-flow__node-input) {
background-color: #f6fafd;
border: 1px solid #66B1FF;
}
:deep(.flow-panel) {
background-color: white;
padding: 10px;
border-radius: 5px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
}
.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>