Files
school-developer/src/views/knowledge/aiFlow/NodePanel.vue
吴红兵 1f645dad3e init
2025-12-02 10:37:49 +08:00

204 lines
6.9 KiB
Vue

<template>
<el-drawer v-model="drawerVisible" :title="node.name" :size="520" :show-close="false" direction="rtl" @close="$emit('close')">
<template #header>
<div class="px-5 py-4 w-full bg-white border-b border-gray-200">
<div class="flex justify-between items-center mb-3">
<div class="flex gap-2 items-center text-gray-800">
<svg-icon :size="24" :class="['node-icon', 'node-icon--' + node.type]" :name="`local-${node.type}`" />
<input class="text-base font-bold bg-transparent border-none outline-none w-30" v-model="node.name" />
<small class="text-xs text-gray-500">{{ node.id }}</small>
</div>
<!-- 关闭按钮 -->
<div class="p-2 rounded transition-colors cursor-pointer hover:bg-gray-100" @click="$emit('close')">
<el-icon><Close /></el-icon>
</div>
</div>
<!-- 描述区域 -->
<div>
<input
v-model="node.description"
class="w-full min-h-[30px] px-2 py-1 text-sm bg-gray-50 border border-gray-200 rounded outline-none transition-all hover:border-gray-300 hover:bg-white focus:border-blue-500 focus:bg-white focus:shadow-[0_0_0_2px_rgba(59,130,246,0.1)]"
placeholder="添加描述..."
/>
</div>
</div>
</template>
<div class="box-border flex flex-col h-full">
<div class="overflow-y-auto flex-1 pb-4">
<component :is="nodeConfig" :key="node.id" :node="node" />
</div>
<!-- 下一个节点显示区域 -->
<div class="px-5 py-4 bg-white border-t border-gray-200" v-if="node.type !== 'end'">
<div class="flex justify-between mb-2">
<div class="mb-2 text-sm font-bold text-gray-700">下一个节点</div>
</div>
<div class="p-4 bg-gray-50 rounded-lg">
<div class="relative" v-if="nextNodes.length">
<div class="flex items-center relative min-h-[100px]">
<!-- 当前节点 -->
<div class="flex items-center gap-2 px-3 py-2 bg-gray-100 border border-gray-200 rounded min-w-[120px] h-10 relative mr-[80px]">
<svg-icon :size="24" :class="['node-icon', 'node-icon--' + node.type]" :name="`local-${node.type}`" />
<span class="text-sm text-gray-700 truncate">{{ node.name }}</span>
<!-- 连接线 -->
<div class="absolute top-1/2 right-[-80px] w-[80px] h-[2px] bg-gray-200"></div>
</div>
<!-- 分支连线和节点 -->
<div class="flex relative flex-col flex-1 gap-8 pl-10 min-h-full">
<!-- 垂直连接线 -->
<div class="absolute left-0 top-5 h-[calc(100%-20px)] w-[2px] bg-gray-200"></div>
<div v-for="branch in nextNodes" :key="branch.node?.id" class="relative flex items-center gap-4 min-h-[40px]">
<!-- 水平连接线 -->
<div class="absolute top-1/2 left-[-40px] w-[20px] h-[2px] bg-gray-200"></div>
<!-- 分支节点 -->
<template v-if="branch.node">
<div class="flex items-center gap-2 px-3 py-2 bg-white border border-gray-200 rounded min-w-[120px] h-10 ml-[-80px]">
<svg-icon :size="24" :class="['node-icon', 'node-icon--' + branch.node.type]" :name="`local-${branch.node.type}`" />
<span class="text-sm text-gray-700 truncate">{{ branch.node.name }}</span>
</div>
<el-button type="primary" size="small" link @click.stop="jumpToNextNode(branch.node)" class="flex gap-1 items-center">
<el-icon><Right /></el-icon>
跳转到节点
</el-button>
</template>
<template v-else>
<el-button type="primary" plain size="small" class="w-[180px] h-9 justify-center gap-1 border-dashed">
<el-icon><Plus /></el-icon>
选择下一个节点
</el-button>
</template>
</div>
</div>
</div>
</div>
<div v-else class="py-5 text-center text-gray-500 bg-white rounded-lg border border-gray-200 border-dashed">暂无下一个节点</div>
</div>
</div>
</div>
</el-drawer>
</template>
<script>
import { Close, Right, Plus } from '@element-plus/icons-vue';
// 使用 import.meta.glob 自动导入所有面板组件
const modules = import.meta.glob('./panels/*.vue', { eager: true });
export default {
name: 'NodePanel',
components: {
Close,
Right,
Plus,
},
inject: ['parent'],
props: {
visible: {
type: Boolean,
default: false,
},
node: {
type: Object,
required: true,
},
},
data() {
return {
highlightedConnection: null,
};
},
computed: {
// 添加抽屉显示状态的计算属性
drawerVisible: {
get() {
return this.visible;
},
set(value) {
this.$emit('update:visible', value);
},
},
nodeConfig() {
// 从文件名中提取组件类型
// 检查节点类型是否存在,并构建面板组件名称
const panelName = this.node?.type ? `${this.node.type.charAt(0).toUpperCase()}${this.node.type.slice(1)}Panel` : '';
// 查找对应的面板组件
const panel = panelName ? Object.values(modules).find((module) => module.default.name === panelName) : {};
return panel?.default;
},
// 获取下一个节点的逻辑
nextNodes() {
if (!this.parent?.connections || !this.node) {
return [];
}
// 获取所有从当前节点出发的连接
const nextConnections = this.parent.connections.filter((conn) => conn.sourceId === this.node.id);
// 将连接转换为节点信息
return nextConnections.map((conn) => {
const targetNode = this.parent.nodes.find((node) => node.id === conn.targetId);
return {
node: targetNode,
condition: conn.condition || '默认分支',
};
});
},
},
methods: {
jumpToNextNode(node) {
if (!node) return;
// 延迟执行以确保 DOM 已更新
this.$nextTick(() => {
// 获取工作流容器元素
const workflowContainer = document.querySelector('.workflow-container');
// 获取目标节点元素
const nextNodeElement = workflowContainer?.querySelector(`#${node.id}`);
if (nextNodeElement && workflowContainer) {
// 计算目标节点相对于容器的位置
const containerRect = workflowContainer.getBoundingClientRect();
const nodeRect = nextNodeElement.getBoundingClientRect();
// 计算滚动位置,使节点居中显示
const scrollLeft = nodeRect.left - containerRect.left - (containerRect.width - nodeRect.width) / 2;
const scrollTop = nodeRect.top - containerRect.top - (containerRect.height - nodeRect.height) / 2;
// 平滑滚动到目标位置
workflowContainer.scrollTo({
left: workflowContainer.scrollLeft + scrollLeft,
top: workflowContainer.scrollTop + scrollTop,
behavior: 'smooth',
});
}
// 选中节点
this.parent.selectNode(node);
});
},
},
};
</script>
<style scoped>
/* 保留节点图标样式,这部分可能需要单独处理 */
:deep(.highlight-connection) {
stroke: var(--el-color-primary) !important;
stroke-width: 2px !important;
animation: connectionPulse 2s infinite;
}
@keyframes connectionPulse {
0% {
stroke-opacity: 1;
}
50% {
stroke-opacity: 0.5;
}
100% {
stroke-opacity: 1;
}
}
</style>