支持图层处理

This commit is contained in:
2025-06-12 19:34:50 +08:00
parent 45565de5ef
commit 057ce85ff3
4 changed files with 254 additions and 296 deletions

View File

@@ -79,7 +79,8 @@ const handleComponentClick = (component) => {
type: component.type, type: component.type,
label: component.name, label: component.name,
position: { x: 100, y: 100 }, // 默认位置 position: { x: 100, y: 100 }, // 默认位置
data: { componentType: component.type } data: { componentType: component.type },
style: { background: '#fff', border: '2px solid black',width: '150px', height: '150px' },
}; };
// 发出添加节点事件 // 发出添加节点事件

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, shallowRef, markRaw } from 'vue'; import { ref, onMounted, shallowRef, markRaw, onUnmounted } from 'vue';
import { VueFlow, useVueFlow, Panel, NodeTypes } from '@vue-flow/core'; import { VueFlow, useVueFlow, Panel, NodeTypes } 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';
@@ -47,6 +47,14 @@ const { nodes, edges, onNodesChange, onEdgesChange, onConnect, addNodes, setTran
nodeTypes nodeTypes
}); });
// 右键菜单相关
const contextMenu = ref({
show: false,
x: 0,
y: 0,
nodeId: null
});
// 处理拖拽放置 // 处理拖拽放置
const handleDrop = (event) => { const handleDrop = (event) => {
event.preventDefault(); event.preventDefault();
@@ -179,8 +187,84 @@ const handleOpenPropertySelect = (node) => {
emit('open-property-select', 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(() => { onMounted(() => {
console.log('FlowEditor 组件已挂载'); console.log('FlowEditor 组件已挂载');
// 添加全局点击事件监听
document.addEventListener('click', handleClickOutside);
});
onUnmounted(() => {
// 移除事件监听
document.removeEventListener('click', handleClickOutside);
}); });
</script> </script>
@@ -195,14 +279,16 @@ onMounted(() => {
<!-- 中间流程图区域 --> <!-- 中间流程图区域 -->
<div class="flow-container"> <div class="flow-container">
<VueFlow <VueFlow
:nodes="nodes" :nodes="nodes"
:edges="edges" :edges="edges"
@nodes-change="onNodesChange" @nodes-change="onNodesChange"
@edges-change="onEdgesChange" @edges-change="onEdgesChange"
@connect="onConnect" @connect="onConnect"
fit-view-on-init fit-view-on-init
@drop="handleDrop" @drop="handleDrop"
@dragover="handleDragOver" @dragover="handleDragOver"
@node-context-menu="handleNodeContextMenu"
@pane-context-menu="handlePaneContextMenu"
> >
<Background pattern-color="#aaa" gap="8" /> <Background pattern-color="#aaa" gap="8" />
<Controls /> <Controls />
@@ -210,14 +296,27 @@ onMounted(() => {
<div>流程图编辑器 (模仿 draw.io)</div> <div>流程图编辑器 (模仿 draw.io)</div>
</Panel> </Panel>
</VueFlow> </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> </div>
<!-- 右侧属性面板 --> <!-- 右侧属性面板 -->
<PropertyPanel <PropertyPanel
:height="height" :height="height"
@open-shikigami-select="handleOpenShikigamiSelect" @open-shikigami-select="handleOpenShikigamiSelect"
@open-yuhun-select="handleOpenYuhunSelect" @open-yuhun-select="handleOpenYuhunSelect"
@open-property-select="handleOpenPropertySelect" @open-property-select="handleOpenPropertySelect"
/> />
</div> </div>
</div> </div>
@@ -268,4 +367,29 @@ onMounted(() => {
border-radius: 5px; border-radius: 5px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); 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> </style>

View File

@@ -60,156 +60,71 @@ onUnmounted(() => {
defineExpose({ defineExpose({
updateNodeShikigami updateNodeShikigami
}); });
// Add Position enum usage to fix type error
const position = Position;
</script> </script>
<template> <template>
<NodeResizer <NodeResizer
v-if="selected" v-if="selected"
:min-width="150"
:min-height="150"
:max-width="300"
:max-height="300"
/> />
<!-- 输入连接点 --> <Handle type="target" :position="position.Left" :id="`${id}-target`" />
<Handle type="target" position="left" :id="`${id}-target`" />
<div class="node-content"> <div class="node-content">
<div class="node-header"> <img
<div class="node-title">式神选择</div> v-if="currentShikigami.avatar"
</div> :src="currentShikigami.avatar"
:alt="currentShikigami.name"
<div class="node-body"> class="shikigami-image"
<div v-if="currentShikigami.avatar" class="shikigami-avatar"> />
<img :src="currentShikigami.avatar" alt="式神头像" /> <div v-else class="placeholder-text">点击选择式神</div>
</div> <div class="name-text">{{ currentShikigami.name }}</div>
<div v-else class="shikigami-placeholder">
点击选择式神
</div>
<div class="shikigami-name">{{ currentShikigami.name }}</div>
</div>
</div> </div>
<!-- 输出连接点 --> <Handle type="source" :position="position.Right" :id="`${id}-source`" />
<Handle type="source" position="right" :id="`${id}-source`" />
</template> </template>
<style scoped> <style scoped>
.shikigami-node { .node-content {
position: relative; display: flex;
width: 100%; flex-direction: column;
height: 100%; align-items: center;
min-width: 180px; justify-content: center;
min-height: 180px;
} }
.node-content { .shikigami-image {
position: relative; width: 85%;
background-color: white; height: 85%;
border: 1px solid #dcdfe6; object-fit: cover;
border-radius: 4px; }
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
width: 100%;
height: 100%;
min-width: 180px; .placeholder-text {
min-height: 180px; color: #909399;
font-size: 12px;
}
.name-text {
font-size: 14px;
text-align: center;
margin-top: 8px;
} }
:deep(.vue-flow__node-resizer) { :deep(.vue-flow__node-resizer) {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
top: 0; top: 0;
left: 0; left: 0;
pointer-events: none; pointer-events: none;
} }
:deep(.vue-flow__node-resizer-handle) { :deep(.vue-flow__node-resizer-handle) {
position: absolute; width: 8px;
width: 8px; height: 8px;
height: 8px; background-color: #409EFF;
background-color: #409EFF; border-radius: 50%;
border: 1px solid white; pointer-events: all;
border-radius: 50%;
pointer-events: all;
}
:deep(.vue-flow__node-resizer-handle.top-left) {
top: -4px;
left: -4px;
cursor: nwse-resize;
}
:deep(.vue-flow__node-resizer-handle.top-right) {
top: -4px;
right: -4px;
cursor: nesw-resize;
}
:deep(.vue-flow__node-resizer-handle.bottom-left) {
bottom: -4px;
left: -4px;
cursor: nesw-resize;
}
:deep(.vue-flow__node-resizer-handle.bottom-right) {
bottom: -4px;
right: -4px;
cursor: nwse-resize;
}
.node-header {
padding: 8px 10px;
background-color: #ecf5ff;
border-bottom: 1px solid #dcdfe6;
border-radius: 4px 4px 0 0;
}
.node-title {
font-weight: bold;
font-size: 14px;
}
.node-body {
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
height: calc(100% - 37px); /* 减去header的高度 */
box-sizing: border-box;
}
.shikigami-avatar {
width: 80%;
height: 80%;
margin-bottom: 8px;
transition: all 0.2s;
}
.shikigami-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.shikigami-placeholder {
width: 80%;
height: 80%;
border: 1px dashed #c0c4cc;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-size: 12px;
border-radius: 4px;
margin-bottom: 8px;
transition: all 0.2s;
}
.shikigami-name {
font-size: 14px;
margin-top: 5px;
} }
</style> </style>

View File

@@ -84,162 +84,80 @@ defineExpose({
</script> </script>
<template> <template>
<NodeResizer <NodeResizer
v-if="selected" v-if="selected"
:min-width="150" :min-width="150"
:min-height="150" :min-height="150"
:max-width="300" :max-width="300"
:max-height="300" :max-height="300"
/> />
<div class="yuhun-node" >
<!-- 输入连接点 --> <Handle type="target" :position="Position.Left" :id="`${id}-target`" />
<Handle type="target" position="left" :id="`${id}-target`"/>
<div class="node-content"> <div class="node-content">
<div class="node-header"> <img
<div class="node-title">御魂选择</div> v-if="currentYuhun.avatar"
</div> :src="currentYuhun.avatar"
:alt="currentYuhun.name"
<div class="node-body"> class="yuhun-image"
<div v-if="currentYuhun.avatar" class="yuhun-avatar"> />
<img :src="currentYuhun.avatar" alt="御魂图片"/> <div v-else class="placeholder-text">点击选择御魂</div>
</div> <div class="name-text">{{ currentYuhun.name }}</div>
<div v-else class="yuhun-placeholder"> <div v-if="currentYuhun.type" class="type-text">{{ currentYuhun.type }}</div>
点击选择御魂
</div>
<div class="yuhun-name">{{ currentYuhun.name }}</div>
<div v-if="currentYuhun.type" class="yuhun-type">{{ currentYuhun.type }}</div>
</div>
</div> </div>
<!-- 输出连接点 --> <Handle type="source" :position="Position.Right" :id="`${id}-source`" />
<Handle type="source" position="right" :id="`${id}-source`"/>
</div>
</template> </template>
<style scoped> <style scoped>
.yuhun-node { .node-content {
position: relative; width: 100%;
width: 100%; height: 100%;
height: 100%; min-width: 180px;
min-width: 180px; min-height: 180px;
min-height: 180px; display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
} }
.node-content { .yuhun-image {
position: relative; width: 85%;
background-color: white; height: 85%;
border: 1px solid #dcdfe6; object-fit: cover;
border-radius: 4px; }
padding: 0;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); .placeholder-text {
width: 100%; color: #909399;
height: 100%; font-size: 12px;
}
min-width: 180px; .name-text {
min-height: 180px; font-size: 14px;
text-align: center;
margin-top: 8px;
}
.type-text {
font-size: 12px;
color: #909399;
margin-top: 4px;
} }
:deep(.vue-flow__node-resizer) { :deep(.vue-flow__node-resizer) {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
top: 0; top: 0;
left: 0; left: 0;
pointer-events: none; pointer-events: none;
} }
:deep(.vue-flow__node-resizer-handle) { :deep(.vue-flow__node-resizer-handle) {
position: absolute; width: 8px;
width: 8px; height: 8px;
height: 8px; background-color: #409EFF;
background-color: #409EFF; border-radius: 50%;
border: 1px solid white; pointer-events: all;
border-radius: 50%;
pointer-events: all;
}
:deep(.vue-flow__node-resizer-handle.top-left) {
top: -4px;
left: -4px;
cursor: nwse-resize;
}
:deep(.vue-flow__node-resizer-handle.top-right) {
top: -4px;
right: -4px;
cursor: nesw-resize;
}
:deep(.vue-flow__node-resizer-handle.bottom-left) {
bottom: -4px;
left: -4px;
cursor: nesw-resize;
}
:deep(.vue-flow__node-resizer-handle.bottom-right) {
bottom: -4px;
right: -4px;
cursor: nwse-resize;
}
.node-header {
padding: 8px 10px;
background-color: #f0f7ff;
border-bottom: 1px solid #dcdfe6;
border-radius: 4px 4px 0 0;
}
.node-title {
font-weight: bold;
font-size: 14px;
}
.node-body {
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
}
.yuhun-avatar {
width: 80px;
height: 80px;
margin-bottom: 8px;
transition: width 0.2s, height 0.2s;
}
.yuhun-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.yuhun-placeholder {
width: 80px;
height: 80px;
border: 1px dashed #c0c4cc;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-size: 12px;
border-radius: 4px;
margin-bottom: 8px;
transition: width 0.2s, height 0.2s;
}
.yuhun-name {
font-size: 14px;
margin-top: 5px;
}
.yuhun-type {
font-size: 12px;
color: #909399;
margin-top: 3px;
} }
</style> </style>