工作流模板
This commit is contained in:
@@ -69,6 +69,10 @@
|
||||
<el-icon><Operation /></el-icon>
|
||||
自动布局
|
||||
</el-button>
|
||||
<el-button @click="handleApplyTemplate" title="应用工作流模板">
|
||||
<el-icon><Document /></el-icon>
|
||||
应用模板
|
||||
</el-button>
|
||||
<div class="toolbar-spacer"></div>
|
||||
<div class="zoom-controls">
|
||||
<el-button size="small" @click="zoomIn" title="放大 (Ctrl +)">
|
||||
@@ -1284,6 +1288,57 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 工作流模板应用对话框 -->
|
||||
<el-dialog
|
||||
v-model="templateDialogVisible"
|
||||
title="应用工作流模板"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div v-loading="loadingWorkflowTemplates">
|
||||
<el-input
|
||||
v-model="templateSearchKeyword"
|
||||
placeholder="搜索模板..."
|
||||
clearable
|
||||
style="margin-bottom: 15px;"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<div class="template-list">
|
||||
<div
|
||||
v-for="template in filteredTemplates"
|
||||
:key="template.id"
|
||||
class="template-item"
|
||||
@click="handleSelectTemplate(template)"
|
||||
>
|
||||
<div class="template-header">
|
||||
<h4>{{ template.name }}</h4>
|
||||
<el-tag v-if="template.is_featured" type="warning" size="small">精选</el-tag>
|
||||
</div>
|
||||
<div class="template-description">
|
||||
{{ template.description || '无描述' }}
|
||||
</div>
|
||||
<div class="template-meta">
|
||||
<span>节点数: {{ getTemplateNodeCount(template) }}</span>
|
||||
<span>使用次数: {{ template.use_count || 0 }}</span>
|
||||
<span v-if="template.rating_avg">评分: {{ template.rating_avg.toFixed(1) }} ⭐</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredTemplates.length === 0" class="empty-templates">
|
||||
<el-empty description="暂无可用模板" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="templateDialogVisible = false">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 节点执行详情面板 -->
|
||||
<NodeExecutionDetail
|
||||
v-model:visible="nodeDetailVisible"
|
||||
@@ -1304,7 +1359,7 @@ import { Controls } from '@vue-flow/controls'
|
||||
import { MiniMap } from '@vue-flow/minimap'
|
||||
import type { Node, Edge, NodeClickEvent, EdgeClickEvent, Connection, Viewport } from '@vue-flow/core'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Check, Warning, ZoomIn, ZoomOut, FullScreen, DocumentCopy, User, VideoPlay, InfoFilled, WarningFilled, Rank, ArrowDown, Sort, Grid, Operation } from '@element-plus/icons-vue'
|
||||
import { Check, Warning, ZoomIn, ZoomOut, FullScreen, DocumentCopy, User, VideoPlay, InfoFilled, WarningFilled, Rank, ArrowDown, Sort, Grid, Operation, Document, Search } from '@element-plus/icons-vue'
|
||||
import { useWorkflowStore } from '@/stores/workflow'
|
||||
import api from '@/api'
|
||||
import type { WorkflowNode, WorkflowEdge } from '@/types'
|
||||
@@ -1407,6 +1462,203 @@ const handleManageTemplates = () => {
|
||||
router.push({ name: 'node-templates' })
|
||||
}
|
||||
|
||||
// 工作流模板相关
|
||||
const templateDialogVisible = ref(false)
|
||||
const workflowTemplates = ref<any[]>([])
|
||||
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<string, string> = {}
|
||||
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<Set<string>>(new Set())
|
||||
const runningNodeId = ref<string | null>(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;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user