This commit is contained in:
吴红兵
2025-12-02 10:37:49 +08:00
commit 1f645dad3e
1183 changed files with 147673 additions and 0 deletions

View File

@@ -0,0 +1,313 @@
<template>
<div>
<div v-if="visible"
class="more-menu"
:style="menuStyle">
<!-- 拷贝 -->
<div class="menu-item"
@click="copyNode">
<span>复制</span>
</div>
<!-- 新增子节点 -->
<div class="menu-item"
@click="handleShowAddChild"
v-if="canAddChild">
<span>新增子节点</span>
<i class="el-icon-arrow-right"></i>
</div>
<!-- 更换类型 -->
<div class="menu-item"
@click="handleShowChangeType">
<span>更换类型</span>
<i class="el-icon-arrow-right"></i>
</div>
<!-- 删除 -->
<div class="menu-item"
@click="deleteNode" v-if="node.type !== 'start'">
<span>删除</span>
</div>
</div>
<!-- 使用 NodeContextMenu 组件 - 更换类型 -->
<NodeContextMenu v-model:visible="showChangeType"
:position="changeTypeMenuPosition"
:node="node"
add-position="replace"
@add="handleChangeType" />
<!-- 使用 NodeContextMenu 组件 - 新增子节点 -->
<NodeContextMenu v-model:visible="showAddChild"
:position="addChildMenuPosition"
:node="node"
add-position="right"
@add="handleAddChild" />
</div>
</template>
<script>
import NodeContextMenu from './NodeContextMenu.vue'
import { getNodeConfig } from './nodes/nodeTypes.ts'
export default {
name: 'NodeMoreMenu',
inject: ['parent'],
components: {
NodeContextMenu
},
props: {
visible: {
type: Boolean,
default: false
},
position: {
type: Object,
default: () => ({ x: 0, y: 0 })
},
node: {
type: Object,
default: null
}
},
emits: ['update:visible'],
data () {
return {
showChangeType: false,
changeTypeMenuPosition: { x: 0, y: 0 },
showAddChild: false,
addChildMenuPosition: { x: 0, y: 0 }
}
},
computed: {
menuStyle () {
return {
left: `${this.position.x}px`,
top: `${this.position.y}px`
}
},
// 判断是否可以添加子节点
canAddChild () {
if (!this.node || !this.parent) return false
// 结束节点不能添加子节点
if (this.node.type === 'end') {
return false
}
// 获取当前节点已有的连接数量
const existingConnections = this.parent.connections?.filter((conn) => conn.sourceId === this.node.id) || []
// 如果是分支节点switch 或 question可以添加子节点
if (['switch', 'question'].includes(this.node.type)) {
return true
}
// 如果不是分支节点且已经有连接,则不能添加子节点
if (existingConnections.length > 0) {
return false
}
return true
}
},
methods: {
// 复制节点
copyNode () {
if (!this.node) return
// 创建节点的深拷贝
const nodeCopy = JSON.parse(JSON.stringify(this.node))
// 生成新的唯一ID
nodeCopy.id = `node_${Date.now()}`
// 设置新节点的位置(在原节点右下方20px处)
nodeCopy.x = this.node.x + 20
nodeCopy.y = this.node.y + 20
// 更新节点列表
this.parent.nodes = [...this.parent.nodes, nodeCopy]
// 关闭菜单
this.$emit('update:visible', false)
},
// 删除节点
deleteNode () {
if (!this.node) return
// 删除节点的所有端点
this.parent.jsPlumbInstance.removeAllEndpoints(this.node.id)
// 从节点列表中删除节点
this.parent.nodes= this.parent.nodes.filter(n => n.id !== this.node.id)
// 关闭菜单
this.$emit('update:visible', false)
},
// 更换节点类型
changeNodeType (newType) {
if (!this.node || !newType) return
// 获取新节点类型的配置
const nodeConfig = getNodeConfig(newType)
// 找到当前节点的索引
const nodeIndex = this.parent.nodes.findIndex(n => n.id === this.node.id)
if (nodeIndex === -1) return
// 保存原节点的位置和ID
const { x, y, id } = this.node
// 创建新节点保持原有的位置和ID
const newNode = {
...nodeConfig,
id,
x,
y,
}
// 更新节点列表
const newNodes = [...this.parent.nodes]
newNodes[nodeIndex] = newNode
this.parent.nodes = newNodes
// 关闭菜单
this.$emit('update:visible', false)
this.showChangeType = false
},
handleShowChangeType (event) {
// 计算子菜单位置,在父菜单右侧显示
const menuEl = this.$el.querySelector('.more-menu')
if (menuEl) {
const rect = menuEl.getBoundingClientRect()
// 检查右侧空间是否足够
const rightSpace = window.innerWidth - rect.right
const leftSpace = rect.left
// 如果右侧空间不足,且左侧空间足够,则显示在左侧
if (rightSpace < 200 && leftSpace > 200) {
this.changeTypeMenuPosition = {
x: rect.left - 5, // 左侧偏移5px
y: rect.top
}
} else {
this.changeTypeMenuPosition = {
x: rect.right + 5, // 右侧偏移5px
y: rect.top
}
}
}
this.showChangeType = true
// 阻止事件冒泡,防止触发外部点击事件
event?.stopPropagation()
},
handleChangeType (type) {
this.changeNodeType(type)
},
// 显示新增子节点菜单
handleShowAddChild (event) {
// 计算子菜单位置,在父菜单右侧显示
const menuEl = this.$el.querySelector('.more-menu')
if (menuEl) {
const rect = menuEl.getBoundingClientRect()
// 检查右侧空间是否足够
const rightSpace = window.innerWidth - rect.right
const leftSpace = rect.left
// 如果右侧空间不足,且左侧空间足够,则显示在左侧
if (rightSpace < 200 && leftSpace > 200) {
this.addChildMenuPosition = {
x: rect.left - 5, // 左侧偏移5px
y: rect.top
}
} else {
this.addChildMenuPosition = {
x: rect.right + 5, // 右侧偏移5px
y: rect.top
}
}
}
this.showAddChild = true
// 阻止事件冒泡,防止触发外部点击事件
event?.stopPropagation()
},
// 处理新增子节点
handleAddChild (type) {
if (!this.node || !this.parent || typeof this.parent.addNode !== 'function') return
// 模拟右键添加节点的流程
this.parent.contextMenuNode = this.node
this.parent.contextMenuAddPosition = 'right'
this.parent.contextMenuPortIndex = 0
// 调用父组件的 addNode 方法
this.parent.addNode(type)
// 关闭菜单
this.$emit('update:visible', false)
this.showAddChild = false
}
},
mounted () {
// 点击外部关闭菜单
const handleClickOutside = (e) => {
if (!this.$el.contains(e.target)) {
this.$emit('update:visible', false)
this.showChangeType = false
this.showAddChild = false
}
}
document.addEventListener('click', handleClickOutside)
// 保存引用以便在组件销毁时移除
this.handleClickOutside = handleClickOutside
},
beforeUnmount () {
// 移除事件监听器
document.removeEventListener('click', this.handleClickOutside)
}
}
</script>
<style scoped>
.more-menu {
opacity: 0.9;
position: fixed;
background: white;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
padding: 4px 0;
min-width: 160px;
z-index: 1000;
}
.menu-item {
padding: 8px 16px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
font-size: 13px;
color: rgb(71 84 103 / 1);
transition: all 0.2s;
position: relative;
}
.menu-item:hover {
background-color: #f5f5f5;
}
.el-icon-arrow-right {
font-size: 12px;
color: #909399;
}
</style>