工作流模板
This commit is contained in:
@@ -69,6 +69,10 @@
|
|||||||
<el-icon><Operation /></el-icon>
|
<el-icon><Operation /></el-icon>
|
||||||
自动布局
|
自动布局
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button @click="handleApplyTemplate" title="应用工作流模板">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
应用模板
|
||||||
|
</el-button>
|
||||||
<div class="toolbar-spacer"></div>
|
<div class="toolbar-spacer"></div>
|
||||||
<div class="zoom-controls">
|
<div class="zoom-controls">
|
||||||
<el-button size="small" @click="zoomIn" title="放大 (Ctrl +)">
|
<el-button size="small" @click="zoomIn" title="放大 (Ctrl +)">
|
||||||
@@ -1284,6 +1288,57 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</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
|
<NodeExecutionDetail
|
||||||
v-model:visible="nodeDetailVisible"
|
v-model:visible="nodeDetailVisible"
|
||||||
@@ -1304,7 +1359,7 @@ import { Controls } from '@vue-flow/controls'
|
|||||||
import { MiniMap } from '@vue-flow/minimap'
|
import { MiniMap } from '@vue-flow/minimap'
|
||||||
import type { Node, Edge, NodeClickEvent, EdgeClickEvent, Connection, Viewport } from '@vue-flow/core'
|
import type { Node, Edge, NodeClickEvent, EdgeClickEvent, Connection, Viewport } from '@vue-flow/core'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
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 { useWorkflowStore } from '@/stores/workflow'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { WorkflowNode, WorkflowEdge } from '@/types'
|
import type { WorkflowNode, WorkflowEdge } from '@/types'
|
||||||
@@ -1407,6 +1462,203 @@ const handleManageTemplates = () => {
|
|||||||
router.push({ name: 'node-templates' })
|
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 executedNodeIds = ref<Set<string>>(new Set())
|
||||||
const runningNodeId = ref<string | null>(null)
|
const runningNodeId = ref<string | null>(null)
|
||||||
@@ -3885,4 +4137,66 @@ onUnmounted(() => {
|
|||||||
stroke-width: 2.5 !important;
|
stroke-width: 2.5 !important;
|
||||||
stroke-dasharray: 5,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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user