185 lines
3.7 KiB
Vue
185 lines
3.7 KiB
Vue
<template>
|
|
<div v-if="visible" class="context-menu" :style="menuStyle">
|
|
<div v-if="!canAddRight && addPosition === 'right'" class="menu-disabled-item">
|
|
<el-icon class="menu-icon"><WarningFilled /></el-icon>
|
|
该节点已有子节点
|
|
</div>
|
|
<div v-else>
|
|
<div class="menu-item" v-for="item in nodeTypes" :key="item.type" @click="handleAddNode(item.type)">
|
|
<svg-icon :size="24" :class="['menu-icon', 'node-icon', 'node-icon--' + item.type]" :name="`local-${item.type}`" />
|
|
{{ item.name }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { nodeTypes } from './nodes/nodeTypes.ts';
|
|
import { WarningFilled } from '@element-plus/icons-vue';
|
|
|
|
export default {
|
|
name: 'NodeContextMenu',
|
|
components: {
|
|
WarningFilled,
|
|
},
|
|
props: {
|
|
visible: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
position: {
|
|
type: Object,
|
|
default: () => ({ x: 0, y: 0 }),
|
|
},
|
|
node: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
addPosition: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
portIndex: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
},
|
|
inject: ['parent'],
|
|
computed: {
|
|
menuStyle() {
|
|
return {
|
|
left: `${this.position.x}px`,
|
|
top: `${this.position.y}px`,
|
|
};
|
|
},
|
|
canAddNode() {
|
|
return this.addPosition && this.node;
|
|
},
|
|
// 检查是否可以向右添加节点
|
|
canAddRight() {
|
|
if (this.addPosition !== 'right' || !this.node || !this.parent) {
|
|
return true;
|
|
}
|
|
|
|
// 获取当前节点已有的连接数量
|
|
const existingConnections = this.parent.connections?.filter((conn) => conn.sourceId === this.node.id) || [];
|
|
|
|
// 如果不是分支节点且已经有连接,则不能向右添加
|
|
if (!['switch', 'question'].includes(this.node.type) && existingConnections.length > 0) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
nodeTypes() {
|
|
// 如果不能向右添加,返回空数组
|
|
if (!this.canAddRight) {
|
|
return [];
|
|
}
|
|
|
|
let types = nodeTypes.map((config) => ({
|
|
type: config.type,
|
|
name: config.name,
|
|
}));
|
|
|
|
if (this.addPosition === 'replace') {
|
|
types = types.filter((node) => node.type !== 'start' && node.type !== this.node?.type);
|
|
} else {
|
|
types = types.filter((node) => node.type !== 'start');
|
|
}
|
|
|
|
return types;
|
|
},
|
|
},
|
|
methods: {
|
|
handleAddNode(type) {
|
|
this.$emit('add', type);
|
|
this.$emit('update:visible', false);
|
|
},
|
|
handleClickOutside(e) {
|
|
if (!this.$el.contains(e.target)) {
|
|
this.$emit('update:visible', false);
|
|
}
|
|
},
|
|
},
|
|
watch: {
|
|
visible(val) {
|
|
if (!val) {
|
|
this.$emit('update:visible', false);
|
|
}
|
|
},
|
|
},
|
|
mounted() {
|
|
// 点击外部关闭菜单
|
|
document.addEventListener('click', this.handleClickOutside);
|
|
},
|
|
beforeUnmount() {
|
|
document.removeEventListener('click', this.handleClickOutside);
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.context-menu {
|
|
opacity: 0.9;
|
|
position: fixed;
|
|
background: #ffffff;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
padding: 6px 0;
|
|
min-width: 180px;
|
|
z-index: 1000;
|
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
animation: menuFadeIn 0.15s ease-out;
|
|
}
|
|
|
|
.menu-item {
|
|
padding: 8px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
color: rgb(71 84 103 / 1);
|
|
font-size: 13px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.menu-item:hover {
|
|
background-color: #f8fafc;
|
|
transform: translateX(2px);
|
|
}
|
|
|
|
.menu-disabled-item {
|
|
padding: 8px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
color: #9ca3af;
|
|
font-size: 13px;
|
|
white-space: nowrap;
|
|
background-color: #f9f9f9;
|
|
}
|
|
|
|
.menu-icon {
|
|
padding: 3px;
|
|
width: 12px;
|
|
height: 12px;
|
|
}
|
|
|
|
.menu-divider {
|
|
height: 1px;
|
|
background-color: #e5e7eb;
|
|
margin: 6px 0;
|
|
}
|
|
|
|
@keyframes menuFadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.95);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
</style>
|