From 47dac9f33b667b21bbbbec612c4be82efbd8b5a5 Mon Sep 17 00:00:00 2001
From: rjb <263303411@qq.com>
Date: Tue, 20 Jan 2026 18:28:32 +0800
Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81=E6=A8=A1=E6=9D=BF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../WorkflowEditor/WorkflowEditor.vue | 316 +++++++++++++++++-
1 file changed, 315 insertions(+), 1 deletion(-)
diff --git a/frontend/src/components/WorkflowEditor/WorkflowEditor.vue b/frontend/src/components/WorkflowEditor/WorkflowEditor.vue
index f469924..a77e2af 100644
--- a/frontend/src/components/WorkflowEditor/WorkflowEditor.vue
+++ b/frontend/src/components/WorkflowEditor/WorkflowEditor.vue
@@ -69,6 +69,10 @@
自动布局
+
+
+ 应用模板
+
@@ -1284,6 +1288,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ template.description || '无描述' }}
+
+
+ 节点数: {{ getTemplateNodeCount(template) }}
+ 使用次数: {{ template.use_count || 0 }}
+ 评分: {{ template.rating_avg.toFixed(1) }} ⭐
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+
{
router.push({ name: 'node-templates' })
}
+// 工作流模板相关
+const templateDialogVisible = ref(false)
+const workflowTemplates = ref([])
+const templateSearchKeyword = ref('')
+const loadingWorkflowTemplates = ref(false)
+
+// 加载工作流模板列表
+const loadWorkflowTemplates = async () => {
+ loadingWorkflowTemplates.value = true
+ try {
+ // 优先从模板市场获取
+ try {
+ const response = await api.get('/api/v1/template-market', {
+ params: {
+ limit: 50,
+ sort_by: 'use_count',
+ sort_order: 'desc'
+ }
+ })
+ workflowTemplates.value = response.data || []
+ } catch (e) {
+ // 如果模板市场失败,尝试从工作流模板API获取
+ const response = await api.get('/api/v1/workflows/templates')
+ workflowTemplates.value = response.data || []
+ }
+ } catch (error: any) {
+ console.error('加载工作流模板失败:', error)
+ ElMessage.error('加载模板列表失败')
+ workflowTemplates.value = []
+ } finally {
+ loadingWorkflowTemplates.value = false
+ }
+}
+
+// 过滤模板
+const filteredTemplates = computed(() => {
+ if (!templateSearchKeyword.value) {
+ return workflowTemplates.value
+ }
+ const keyword = templateSearchKeyword.value.toLowerCase()
+ return workflowTemplates.value.filter(template =>
+ template.name?.toLowerCase().includes(keyword) ||
+ template.description?.toLowerCase().includes(keyword) ||
+ template.category?.toLowerCase().includes(keyword)
+ )
+})
+
+// 获取模板节点数
+const getTemplateNodeCount = (template: any) => {
+ if (template.nodes && Array.isArray(template.nodes)) {
+ return template.nodes.length
+ }
+ if (template.workflow_config?.nodes) {
+ return template.workflow_config.nodes.length
+ }
+ return 0
+}
+
+// 打开模板对话框
+const handleApplyTemplate = async () => {
+ templateDialogVisible.value = true
+ if (workflowTemplates.value.length === 0) {
+ await loadWorkflowTemplates()
+ }
+}
+
+// 选择并应用模板
+const handleSelectTemplate = async (template: any) => {
+ try {
+ // 获取模板详情
+ let templateData: any
+ try {
+ const response = await api.get(`/api/v1/template-market/${template.id}`)
+ templateData = response.data
+ } catch (e) {
+ // 如果模板市场API失败,尝试从工作流模板API获取
+ const response = await api.get(`/api/v1/workflows/templates/${template.id}`)
+ templateData = response.data
+ }
+
+ if (!templateData) {
+ ElMessage.error('获取模板详情失败')
+ return
+ }
+
+ // 获取模板的节点和边
+ let templateNodes = templateData.nodes || templateData.workflow_config?.nodes || []
+ let templateEdges = templateData.edges || templateData.workflow_config?.edges || []
+
+ if (!templateNodes || templateNodes.length === 0) {
+ ElMessage.warning('模板中没有节点')
+ return
+ }
+
+ // 生成节点ID映射(避免ID冲突)
+ const nodeIdMapping: Record = {}
+ const timestamp = Date.now()
+
+ templateNodes.forEach((node: any, index: number) => {
+ const oldId = node.id
+ const newId = `${node.type || 'node'}_${timestamp}_${index}`
+ nodeIdMapping[oldId] = newId
+ node.id = newId
+ })
+
+ // 更新边的源节点和目标节点ID
+ templateEdges.forEach((edge: any) => {
+ if (edge.source && nodeIdMapping[edge.source]) {
+ edge.source = nodeIdMapping[edge.source]
+ }
+ if (edge.target && nodeIdMapping[edge.target]) {
+ edge.target = nodeIdMapping[edge.target]
+ }
+ // 生成新的边ID
+ edge.id = `edge_${edge.source}_${edge.target}_${timestamp}`
+ })
+
+ // 计算偏移量,将模板节点添加到画布右侧
+ const existingNodes = nodes.value
+ let offsetX = 100
+ let offsetY = 100
+
+ if (existingNodes.length > 0) {
+ // 找到现有节点的最大X坐标
+ const maxX = Math.max(...existingNodes.map(n => n.position.x))
+ offsetX = maxX + 400 // 在现有节点右侧400px处开始
+
+ // 找到现有节点的最小Y坐标
+ const minY = Math.min(...existingNodes.map(n => n.position.y))
+ offsetY = minY
+ }
+
+ // 调整节点位置
+ templateNodes.forEach((node: any) => {
+ node.position = {
+ x: (node.position?.x || 0) + offsetX,
+ y: (node.position?.y || 0) + offsetY
+ }
+ })
+
+ // 转换为Vue Flow节点格式
+ const vueFlowNodes = templateNodes.map((node: any) => ({
+ id: node.id,
+ type: node.type || 'default',
+ position: node.position || { x: 0, y: 0 },
+ data: node.data || { label: node.label || node.type }
+ }))
+
+ // 转换为Vue Flow边格式
+ const vueFlowEdges = templateEdges.map((edge: any) => ({
+ id: edge.id,
+ source: edge.source,
+ target: edge.target,
+ sourceHandle: edge.sourceHandle || 'right',
+ targetHandle: edge.targetHandle || 'left',
+ type: 'bezier',
+ animated: true,
+ selectable: true,
+ deletable: true,
+ focusable: true,
+ style: {
+ stroke: '#409eff',
+ strokeWidth: 2.5,
+ strokeDasharray: '0'
+ },
+ markerEnd: {
+ type: 'arrowclosed',
+ color: '#409eff',
+ width: 20,
+ height: 20
+ }
+ }))
+
+ // 添加到画布
+ addNodes(vueFlowNodes)
+ await nextTick()
+ addEdges(vueFlowEdges)
+
+ // 关闭对话框
+ templateDialogVisible.value = false
+ ElMessage.success(`已应用模板: ${template.name} (${templateNodes.length}个节点)`)
+
+ // 标记有变更
+ hasChanges.value = true
+
+ // 可选:自动布局新添加的节点
+ await nextTick()
+ setTimeout(() => {
+ handleAutoLayout()
+ }, 300)
+
+ } catch (error: any) {
+ console.error('应用模板失败:', error)
+ ElMessage.error(error.response?.data?.detail || '应用模板失败')
+ }
+}
+
// 执行状态相关的节点高亮
const executedNodeIds = ref>(new Set())
const runningNodeId = ref(null)
@@ -3885,4 +4137,66 @@ onUnmounted(() => {
stroke-width: 2.5 !important;
stroke-dasharray: 5,5 !important;
}
+
+/* 工作流模板对话框样式 */
+.template-list {
+ max-height: 500px;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.template-item {
+ padding: 15px;
+ border: 1px solid #e4e7ed;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.3s;
+ background: #fff;
+}
+
+.template-item:hover {
+ border-color: #409eff;
+ box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
+ transform: translateY(-2px);
+}
+
+.template-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 8px;
+}
+
+.template-header h4 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+}
+
+.template-description {
+ color: #606266;
+ font-size: 14px;
+ line-height: 1.5;
+ margin-bottom: 10px;
+}
+
+.template-meta {
+ display: flex;
+ gap: 15px;
+ font-size: 12px;
+ color: #909399;
+}
+
+.template-meta span {
+ display: flex;
+ align-items: center;
+}
+
+.empty-templates {
+ padding: 40px 0;
+ text-align: center;
+}