|
|
@@ -4,7 +4,7 @@
|
|
|
<div class="header-left">
|
|
|
<el-button @click="goBack" icon="el-icon-back" plain size="small">返回模板库</el-button>
|
|
|
<span class="header-title">工作流可视化编辑</span>
|
|
|
- <el-tag v-if="templateData" size="small">{{ templateData.prompt_name || templateData.templateName }}</el-tag>
|
|
|
+ <el-tag v-if="templateData" size="small">{{ templateData.template_name || templateData.templateName }}</el-tag>
|
|
|
</div>
|
|
|
<div class="header-right">
|
|
|
<el-radio-group v-model="viewMode" size="small">
|
|
|
@@ -17,7 +17,7 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <div v-if="!templateId" class="no-template">
|
|
|
+ <div v-if="!workflowId" class="no-template">
|
|
|
<el-empty description="请从模板库选择一个模板进行编辑">
|
|
|
<el-button type="primary" @click="goBack">返回模板库</el-button>
|
|
|
</el-empty>
|
|
|
@@ -26,23 +26,31 @@
|
|
|
<template v-else>
|
|
|
<el-card class="info-card" shadow="never" v-if="templateData">
|
|
|
<el-descriptions :column="4" size="small" border>
|
|
|
- <el-descriptions-item label="模板名称">{{ templateData.prompt_name || templateData.templateName }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="行业类型">{{ templateData.industry_type || templateData.industryType || '-' }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="模板编码">{{ templateData.prompt_key || templateData.templateCode || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="模板名称">
|
|
|
+ <el-input v-model="templateName" size="mini" style="width:200px" />
|
|
|
+
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="行业类型">{{ templateField('industry_type') || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="模板编码">{{ templateField('template_code') || '-' }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="节点数量">{{ nodes.length }} 个</el-descriptions-item>
|
|
|
</el-descriptions>
|
|
|
</el-card>
|
|
|
|
|
|
+ <!-- ── 画布视图 ── -->
|
|
|
<div v-show="viewMode === 'canvas'" class="canvas-container" ref="canvasContainer">
|
|
|
<div class="canvas-toolbar">
|
|
|
- <el-button-group>
|
|
|
- <el-button size="mini" @click="addNode(1)" type="success" plain>开始节点</el-button>
|
|
|
- <el-button size="mini" @click="addNode(2)" plain>消息节点</el-button>
|
|
|
- <el-button size="mini" @click="addNode(3)" type="warning" plain>判断节点</el-button>
|
|
|
- <el-button size="mini" @click="addNode(4)" type="info" plain>等待节点</el-button>
|
|
|
- <el-button size="mini" @click="addNode(6)" plain>API节点</el-button>
|
|
|
- <el-button size="mini" @click="addNode(5)" type="danger" plain>结束节点</el-button>
|
|
|
- </el-button-group>
|
|
|
+ <el-button size="mini" @click="addNode(1)" type="success" plain>开始</el-button>
|
|
|
+ <el-button size="mini" @click="addNode(2)" plain>消息</el-button>
|
|
|
+ <el-button size="mini" @click="addNode(3)" type="warning" plain>判断</el-button>
|
|
|
+ <el-button size="mini" @click="addNode(4)" type="info" plain>等待</el-button>
|
|
|
+ <el-button size="mini" @click="addNode(5)" type="danger" plain>结束</el-button>
|
|
|
+ <el-button size="mini" @click="addNode(8)" plain>优惠券</el-button>
|
|
|
+ <el-button size="mini" @click="addNode(9)" plain>标签</el-button>
|
|
|
+ <el-button size="mini" @click="addNode(10)" plain>关怀</el-button>
|
|
|
+ <el-button size="mini" @click="addNode(11)" plain>调研</el-button>
|
|
|
+ <el-button size="mini" @click="addNode(12)" type="info" plain>画像</el-button>
|
|
|
+ <el-button size="mini" @click="addNode(13)" plain>复购</el-button>
|
|
|
+ <el-button size="mini" @click="addNode(14)" plain>智能API</el-button>
|
|
|
<el-button size="mini" @click="autoLayout" icon="el-icon-sort" style="margin-left:8px">自动排列</el-button>
|
|
|
</div>
|
|
|
<div class="canvas-area" ref="canvasArea"
|
|
|
@@ -54,22 +62,32 @@
|
|
|
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
|
|
|
<polygon points="0 0, 10 3.5, 0 7" fill="#409EFF" />
|
|
|
</marker>
|
|
|
+ <marker id="arrowhead-green" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
|
|
|
+ <polygon points="0 0, 10 3.5, 0 7" fill="#67C23A" />
|
|
|
+ </marker>
|
|
|
+ <marker id="arrowhead-red" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
|
|
|
+ <polygon points="0 0, 10 3.5, 0 7" fill="#F56C6C" />
|
|
|
+ </marker>
|
|
|
</defs>
|
|
|
<line v-for="(edge, idx) in edges" :key="'e'+idx"
|
|
|
:x1="edge.x1" :y1="edge.y1" :x2="edge.x2" :y2="edge.y2"
|
|
|
- stroke="#409EFF" stroke-width="2" marker-end="url(#arrowhead)" />
|
|
|
+ :stroke="edge.color || '#409EFF'" stroke-width="2"
|
|
|
+ :marker-end="edge.marker || 'url(#arrowhead)'" />
|
|
|
</svg>
|
|
|
<div v-for="(node, index) in nodes" :key="'n'+index"
|
|
|
class="canvas-node"
|
|
|
- :class="'node-type-' + node.nodeType"
|
|
|
+ :class="'node-type-' + (NODE_COLORS[node.nodeType] ? node.nodeType : 'default')"
|
|
|
:style="{ left: node.x + 'px', top: node.y + 'px' }"
|
|
|
@mousedown.stop="onNodeMouseDown($event, index)">
|
|
|
<div class="node-title">
|
|
|
<span>{{ node.nodeName }}</span>
|
|
|
- <el-tag size="mini" :type="getNodeTypeColor(node.nodeType)">{{ getNodeTypeText(node.nodeType) }}</el-tag>
|
|
|
+ <el-tag size="mini" :type="getNodeTagType(node.nodeType)">{{ NODE_NAMES[node.nodeType] || node.nodeType }}</el-tag>
|
|
|
</div>
|
|
|
<div class="node-body" v-if="node.messageTemplate">
|
|
|
- <div class="node-msg">{{ node.messageTemplate.substring(0, 50) }}{{ node.messageTemplate.length > 50 ? '...' : '' }}</div>
|
|
|
+ <div class="node-msg">{{ truncate(node.messageTemplate, 50) }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="node-body" v-if="node.nodeType === 3 && node.conditionExpr">
|
|
|
+ <el-tag size="mini" type="warning">条件分支</el-tag>
|
|
|
</div>
|
|
|
<div class="node-actions">
|
|
|
<el-button size="mini" type="text" icon="el-icon-edit" @click.stop="editNode(index)"></el-button>
|
|
|
@@ -79,6 +97,7 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- ── 列表视图 ── -->
|
|
|
<div v-show="viewMode === 'list'" class="list-container">
|
|
|
<el-card shadow="never">
|
|
|
<div slot="header">
|
|
|
@@ -90,16 +109,13 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
<el-timeline>
|
|
|
- <el-timeline-item
|
|
|
- v-for="(node, index) in nodes"
|
|
|
- :key="index"
|
|
|
- :type="getNodeTypeColor(node.nodeType)"
|
|
|
- >
|
|
|
+ <el-timeline-item v-for="(node, index) in nodes" :key="index" :type="getNodeTagType(node.nodeType)">
|
|
|
<el-card shadow="hover" class="timeline-node-card">
|
|
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
|
<div>
|
|
|
<span style="font-weight:bold">{{ node.nodeName }}</span>
|
|
|
- <el-tag size="small" :type="getNodeTypeColor(node.nodeType)" style="margin-left:8px">{{ getNodeTypeText(node.nodeType) }}</el-tag>
|
|
|
+ <el-tag size="small" :type="getNodeTagType(node.nodeType)" style="margin-left:8px">{{ NODE_NAMES[node.nodeType] || node.nodeType }}</el-tag>
|
|
|
+ <span v-if="node.nextNodeCode" style="font-size:12px;color:#909399;margin-left:8px">→ {{ node.nextNodeCode }}</span>
|
|
|
</div>
|
|
|
<div>
|
|
|
<el-button size="mini" type="text" icon="el-icon-edit" @click="editNode(index)">编辑</el-button>
|
|
|
@@ -107,10 +123,6 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
<div v-if="node.messageTemplate" style="margin-top:8px;font-size:12px;color:#606266;white-space:pre-wrap;background:#f5f7fa;padding:8px;border-radius:4px">{{ node.messageTemplate }}</div>
|
|
|
- <div v-if="node.nodeType === 3 && node.conditionExpr" style="margin-top:8px">
|
|
|
- <el-tag size="small" type="warning">条件分支</el-tag>
|
|
|
- <span style="font-size:12px;color:#909399;margin-left:4px">{{ node.conditionExpr }}</span>
|
|
|
- </div>
|
|
|
</el-card>
|
|
|
</el-timeline-item>
|
|
|
</el-timeline>
|
|
|
@@ -118,38 +130,64 @@
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
- <el-dialog :title="editingNodeIndex >= 0 ? '编辑节点' : '添加节点'" :visible.sync="nodeDialogVisible" width="600px" append-to-body>
|
|
|
+ <!-- 节点编辑弹窗 -->
|
|
|
+ <el-dialog :title="editingNodeIndex >= 0 ? '编辑节点' : '添加节点'" :visible.sync="nodeDialogVisible" width="650px" append-to-body>
|
|
|
<el-form :model="nodeForm" label-width="100px" size="small">
|
|
|
- <el-form-item label="节点名称">
|
|
|
- <el-input v-model="nodeForm.nodeName" placeholder="节点名称" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="节点类型">
|
|
|
- <el-select v-model="nodeForm.nodeType" style="width:100%">
|
|
|
- <el-option label="开始节点" :value="1" />
|
|
|
- <el-option label="消息节点" :value="2" />
|
|
|
- <el-option label="判断节点" :value="3" />
|
|
|
- <el-option label="等待节点" :value="4" />
|
|
|
- <el-option label="结束节点" :value="5" />
|
|
|
- <el-option label="API调用节点" :value="6" />
|
|
|
+ <el-row :gutter="12">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="节点名称"><el-input v-model="nodeForm.nodeName" /></el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="节点编码"><el-input v-model="nodeForm.nodeCode" /></el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <el-row :gutter="12">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="节点类型">
|
|
|
+ <el-select v-model="nodeForm.nodeType" style="width:100%">
|
|
|
+ <el-option v-for="(name, type) in NODE_NAMES" :key="type" :label="name" :value="Number(type)" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="下一节点"><el-input v-model="nodeForm.nextNodeCode" placeholder="下一节点编码" /></el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <el-row :gutter="12">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="排序号"><el-input-number v-model="nodeForm.sortNo" :min="0" style="width:100%" /></el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="最大轮次"><el-input-number v-model="nodeForm.maxRound" :min="0" style="width:100%" /></el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <el-row :gutter="12">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="模型"><el-input v-model="nodeForm.modelName" placeholder="如 gpt-4o-mini" /></el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="场景编码"><el-input v-model="nodeForm.sceneCode" placeholder="如 sale" /></el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <el-form-item label="Prompt模板" v-if="[2,7,10,11,13].includes(nodeForm.nodeType)">
|
|
|
+ <el-select v-model="nodeForm.promptId" style="width:100%" placeholder="选择Prompt模板(可选)" clearable filterable :loading="promptLoading">
|
|
|
+ <el-option v-for="p in promptList" :key="p.id" :label="p.promptName || p.title" :value="p.id" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="节点编码">
|
|
|
- <el-input v-model="nodeForm.nodeCode" placeholder="节点编码" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="下一节点编码">
|
|
|
- <el-input v-model="nodeForm.nextNodeCode" placeholder="下一节点编码" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item v-if="nodeForm.nodeType === 2" label="消息模板">
|
|
|
- <el-input v-model="nodeForm.messageTemplate" type="textarea" :rows="4" placeholder="消息模板,支持{变量}占位符" />
|
|
|
+ <el-form-item v-if="nodeForm.nodeType === 2 || nodeForm.nodeType === 10 || nodeForm.nodeType === 11" label="消息模板">
|
|
|
+ <el-input v-model="nodeForm.messageTemplate" type="textarea" :rows="4" />
|
|
|
</el-form-item>
|
|
|
<el-form-item v-if="nodeForm.nodeType === 3" label="条件表达式">
|
|
|
- <el-input v-model="nodeForm.conditionExpr" type="textarea" :rows="4" placeholder='条件分支JSON' />
|
|
|
+ <el-input v-model="nodeForm.conditionExpr" type="textarea" :rows="3" placeholder='{"branches":[{"nextNode":"A","condition":"...","desc":"描述"}]}' />
|
|
|
</el-form-item>
|
|
|
<el-form-item v-if="nodeForm.nodeType === 4" label="等待配置">
|
|
|
- <el-input v-model="nodeForm.nodeConfig" type="textarea" :rows="3" placeholder='等待配置JSON' />
|
|
|
+ <el-input v-model="nodeForm.nodeConfig" type="textarea" :rows="3" placeholder='{"waitType":"daily","sendTime":"08:00:00"}' />
|
|
|
</el-form-item>
|
|
|
- <el-form-item v-if="nodeForm.nodeType === 6" label="API配置">
|
|
|
- <el-input v-model="nodeForm.nodeConfig" type="textarea" :rows="3" placeholder='API调用配置JSON' />
|
|
|
+ <el-form-item v-if="[8,9,12,14].includes(nodeForm.nodeType)" label="节点配置JSON">
|
|
|
+ <el-input v-model="nodeForm.nodeConfig" type="textarea" :rows="3" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="发送时间" v-if="nodeForm.nodeType === 4">
|
|
|
+ <el-time-picker v-model="nodeForm._sendTime" value-format="HH:mm:ss" placeholder="选择时间" />
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
<div slot="footer">
|
|
|
@@ -158,175 +196,219 @@
|
|
|
</div>
|
|
|
</el-dialog>
|
|
|
|
|
|
+ <!-- 快速添加节点 -->
|
|
|
<el-dialog title="添加节点" :visible.sync="showAddNodeDialog" width="400px" append-to-body>
|
|
|
- <div style="display:flex;flex-direction:column;gap:8px">
|
|
|
- <el-button @click="addNode(1); showAddNodeDialog=false" type="success" plain>开始节点</el-button>
|
|
|
- <el-button @click="addNode(2); showAddNodeDialog=false" plain>消息节点</el-button>
|
|
|
- <el-button @click="addNode(3); showAddNodeDialog=false" type="warning" plain>判断节点</el-button>
|
|
|
- <el-button @click="addNode(4); showAddNodeDialog=false" type="info" plain>等待节点</el-button>
|
|
|
- <el-button @click="addNode(6); showAddNodeDialog=false" plain>API节点</el-button>
|
|
|
- <el-button @click="addNode(5); showAddNodeDialog=false" type="danger" plain>结束节点</el-button>
|
|
|
+ <div style="display:flex;flex-direction:column;gap:6px">
|
|
|
+ <el-button v-for="(name, type) in NODE_NAMES" :key="type" @click="addNode(Number(type)); showAddNodeDialog=false" :type="getNodeTagType(Number(type))" plain>{{ name }}</el-button>
|
|
|
</div>
|
|
|
</el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import { getPrompt, listPrompts, updatePrompt } from '@/api/workflow/lobster'
|
|
|
+import { getWorkflowNodes, saveWorkflowNodes, listPrompts } from '@/api/workflow/lobster'
|
|
|
+
|
|
|
+const NODE_NAMES = {
|
|
|
+ 1: '开始', 2: '消息', 3: '判断', 4: '等待', 5: '结束',
|
|
|
+ 7: '成单', 8: '优惠券', 9: '标签', 10: '关怀', 11: '调研',
|
|
|
+ 12: '画像', 13: '复购', 14: '智能API',
|
|
|
+ 20: '意图识别', 21: '转人工检测', 22: '质检评分',
|
|
|
+ 23: '知识库检索', 30: '企微消息', 31: '个微消息',
|
|
|
+ 40: '变量赋值', 41: '打标签', 42: 'Webhook',
|
|
|
+ 50: 'SOP执行', 51: 'CID任务', 52: '商品推送', 53: '物流推送', 100: '外部API'
|
|
|
+}
|
|
|
+const NODE_COLORS = {
|
|
|
+ 1: 'success', 2: '', 3: 'warning', 4: 'info', 5: 'danger',
|
|
|
+ 7: '', 8: 'warning', 9: 'info', 10: '', 11: '', 12: 'info', 13: '', 14: '',
|
|
|
+ 20: '', 21: 'warning', 22: 'info'
|
|
|
+}
|
|
|
|
|
|
export default {
|
|
|
name: 'LobsterCanvas',
|
|
|
data() {
|
|
|
return {
|
|
|
- templateId: null,
|
|
|
+ workflowId: null,
|
|
|
templateData: null,
|
|
|
+ templateName: '',
|
|
|
nodes: [],
|
|
|
viewMode: 'canvas',
|
|
|
- saving: false,
|
|
|
nodeDialogVisible: false,
|
|
|
showAddNodeDialog: false,
|
|
|
editingNodeIndex: -1,
|
|
|
nodeForm: {},
|
|
|
- dragging: false,
|
|
|
- dragIndex: -1,
|
|
|
- dragOffsetX: 0,
|
|
|
- dragOffsetY: 0
|
|
|
+ promptLoading: false,
|
|
|
+ promptList: [],
|
|
|
+ dragging: false, dragIndex: -1, dragOffsetX: 0, dragOffsetY: 0,
|
|
|
+ saving: false,
|
|
|
+ NODE_NAMES, NODE_COLORS
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
edges() {
|
|
|
const result = []
|
|
|
- for (let i = 0; i < this.nodes.length - 1; i++) {
|
|
|
- const from = this.nodes[i]
|
|
|
- const to = this.nodes[i + 1]
|
|
|
- if (from && to && from.x !== undefined && to.x !== undefined) {
|
|
|
- result.push({
|
|
|
- x1: from.x + 90,
|
|
|
- y1: from.y + 40,
|
|
|
- x2: to.x + 90,
|
|
|
- y2: to.y + 40
|
|
|
- })
|
|
|
+ const nodeMap = {}
|
|
|
+ this.nodes.forEach((n, i) => { nodeMap[n.nodeCode] = i })
|
|
|
+ this.nodes.forEach((n, i) => {
|
|
|
+ if (n.nodeType === 3 && n.conditionExpr) {
|
|
|
+ // 判断节点:解析 branches
|
|
|
+ try {
|
|
|
+ const cfg = typeof n.conditionExpr === 'string' ? JSON.parse(n.conditionExpr) : n.conditionExpr
|
|
|
+ if (cfg && cfg.branches) {
|
|
|
+ cfg.branches.forEach(b => {
|
|
|
+ const ti = nodeMap[b.nextNode]
|
|
|
+ if (ti !== undefined && this.nodes[ti]) {
|
|
|
+ const color = b.nextNode === (cfg.branches[0] && cfg.branches[0].nextNode) ? '#67C23A' : '#F56C6C'
|
|
|
+ result.push({
|
|
|
+ x1: n.x + 90, y1: n.y + 40, x2: this.nodes[ti].x + 90, y2: this.nodes[ti].y + 40,
|
|
|
+ color, marker: color === '#67C23A' ? 'url(#arrowhead-green)' : 'url(#arrowhead-red)'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } catch (e) {}
|
|
|
}
|
|
|
- }
|
|
|
+ if (n.nextNodeCode) {
|
|
|
+ const ti = nodeMap[n.nextNodeCode]
|
|
|
+ if (ti !== undefined && this.nodes[ti]) {
|
|
|
+ result.push({ x1: n.x + 90, y1: n.y + 40, x2: this.nodes[ti].x + 90, y2: this.nodes[ti].y + 40, color: '#409EFF', marker: 'url(#arrowhead)' })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
return result
|
|
|
}
|
|
|
},
|
|
|
created() {
|
|
|
- this.templateId = this.$route.query.templateId
|
|
|
- if (this.templateId) {
|
|
|
- this.loadTemplate()
|
|
|
- }
|
|
|
+ this.workflowId = this.$route.query.workflowId
|
|
|
+ if (this.workflowId) this.loadNodes()
|
|
|
},
|
|
|
methods: {
|
|
|
- loadTemplate() {
|
|
|
- getPrompt(this.templateId).then(res => {
|
|
|
- this.templateData = res.data || {}
|
|
|
- if (this.templateData.prompt_content) {
|
|
|
- try {
|
|
|
- const parsed = JSON.parse(this.templateData.prompt_content)
|
|
|
- this.nodes = (parsed.nodes || []).map((n, i) => ({
|
|
|
- ...n,
|
|
|
- x: n.x || 100 + (i % 4) * 220,
|
|
|
- y: n.y || 80 + Math.floor(i / 4) * 120
|
|
|
- }))
|
|
|
- } catch (e) {
|
|
|
- this.nodes = []
|
|
|
- }
|
|
|
- }
|
|
|
- }).catch(() => {
|
|
|
- this.$message.error('加载模板失败')
|
|
|
- })
|
|
|
+ templateField(key) {
|
|
|
+ return this.templateData ? (this.templateData[key] || this.templateData[key.replace(/_/g, '')]) : null
|
|
|
},
|
|
|
- goBack() {
|
|
|
- this.$router.push('/lobster/production-workflow/template')
|
|
|
+ truncate(s, max) { return s && s.length > max ? s.substring(0, max) + '...' : s },
|
|
|
+ loadNodes() {
|
|
|
+ getWorkflowNodes(this.workflowId).then(res => {
|
|
|
+ const data = res.data || {}
|
|
|
+ this.templateData = data.template || {}
|
|
|
+ this.templateName = this.templateData.template_name || this.templateData.templateName || ''
|
|
|
+ this.nodes = (data.nodes || []).map((n, i) => ({
|
|
|
+ ...n,
|
|
|
+ x: n.x || 100 + (i % 5) * 200,
|
|
|
+ y: n.y || 80 + Math.floor(i / 5) * 130,
|
|
|
+ maxRound: n.max_round || n.maxRound || 0,
|
|
|
+ nodeType: Number(n.node_type || n.nodeType),
|
|
|
+ nodeCode: n.node_code || n.nodeCode,
|
|
|
+ nodeName: n.node_name || n.nodeName,
|
|
|
+ nextNodeCode: n.next_node_code || n.nextNodeCode || '',
|
|
|
+ messageTemplate: n.message_template || n.messageTemplate || '',
|
|
|
+ conditionExpr: n.condition_expr || n.conditionExpr || '',
|
|
|
+ nodeConfig: n.node_config || n.nodeConfig || '',
|
|
|
+ sceneCode: n.scene_code || n.sceneCode || '',
|
|
|
+ modelName: n.model_name || n.modelName || '',
|
|
|
+ sendTime: n.send_time || n.sendTime || '',
|
|
|
+ sortNo: n.sort_no || n.sortNo || i + 1,
|
|
|
+ promptId: extractPromptId(n.node_config || n.nodeConfig)
|
|
|
+ }))
|
|
|
+ }).catch(() => this.$message.error('加载工作流失败'))
|
|
|
+ },
|
|
|
+ goBack() { this.$router.push('/lobster/production-workflow/template') },
|
|
|
+ loadPrompts() {
|
|
|
+ if (this.promptList.length > 0) return
|
|
|
+ this.promptLoading = true
|
|
|
+ listPrompts({ page: 1, size: 200 }).then(res => {
|
|
|
+ this.promptList = (res.data && res.data.records) ? res.data.records : (res.data || [])
|
|
|
+ }).finally(() => { this.promptLoading = false })
|
|
|
},
|
|
|
addNode(type) {
|
|
|
- const names = { 1: '开始', 2: '消息', 3: '判断', 4: '等待', 5: '结束', 6: 'API调用' }
|
|
|
- const codes = { 1: 'START', 2: 'MSG', 3: 'DECISION', 4: 'WAIT', 5: 'END', 6: 'API' }
|
|
|
const idx = this.nodes.length + 1
|
|
|
+ const defaultName = NODE_NAMES[type] || '节点'
|
|
|
this.nodes.push({
|
|
|
- nodeType: type,
|
|
|
- nodeName: names[type] + '_' + idx,
|
|
|
- nodeCode: codes[type] + '_' + idx,
|
|
|
- nextNodeCode: '',
|
|
|
- messageTemplate: '',
|
|
|
- conditionExpr: '',
|
|
|
- nodeConfig: '',
|
|
|
- x: 100 + (idx % 4) * 220,
|
|
|
- y: 80 + Math.floor(idx / 4) * 120
|
|
|
+ nodeType: type, nodeName: defaultName + '_' + idx, nodeCode: 'NODE_' + idx,
|
|
|
+ nextNodeCode: '', messageTemplate: '', conditionExpr: '', nodeConfig: '',
|
|
|
+ sceneCode: '', modelName: '', sendTime: '', sortNo: idx, maxRound: 0,
|
|
|
+ x: 100 + (idx % 5) * 200, y: 80 + Math.floor(idx / 5) * 130
|
|
|
})
|
|
|
},
|
|
|
editNode(index) {
|
|
|
this.editingNodeIndex = index
|
|
|
- this.nodeForm = { ...this.nodes[index] }
|
|
|
+ const n = this.nodes[index]
|
|
|
+ this.nodeForm = { ...n, _sendTime: n.sendTime || null }
|
|
|
this.nodeDialogVisible = true
|
|
|
+ this.loadPrompts()
|
|
|
},
|
|
|
confirmNode() {
|
|
|
+ if (this.nodeForm._sendTime) this.nodeForm.sendTime = this.nodeForm._sendTime
|
|
|
+ delete this.nodeForm._sendTime
|
|
|
if (this.editingNodeIndex >= 0) {
|
|
|
const pos = { x: this.nodes[this.editingNodeIndex].x, y: this.nodes[this.editingNodeIndex].y }
|
|
|
this.$set(this.nodes, this.editingNodeIndex, { ...this.nodeForm, ...pos })
|
|
|
}
|
|
|
- this.nodeDialogVisible = false
|
|
|
- this.editingNodeIndex = -1
|
|
|
+ this.nodeDialogVisible = false; this.editingNodeIndex = -1
|
|
|
},
|
|
|
removeNode(index) {
|
|
|
- this.$confirm('确定删除该节点吗?', '提示', { type: 'warning' }).then(() => {
|
|
|
- this.nodes.splice(index, 1)
|
|
|
- })
|
|
|
+ this.$confirm('确定删除该节点吗?', '提示', { type: 'warning' }).then(() => this.nodes.splice(index, 1))
|
|
|
},
|
|
|
autoLayout() {
|
|
|
- const cols = 4
|
|
|
- this.nodes.forEach((node, i) => {
|
|
|
- this.$set(node, 'x', 100 + (i % cols) * 220)
|
|
|
- this.$set(node, 'y', 80 + Math.floor(i / cols) * 120)
|
|
|
+ const cols = 5
|
|
|
+ this.nodes.forEach((n, i) => {
|
|
|
+ this.$set(n, 'x', 100 + (i % cols) * 200)
|
|
|
+ this.$set(n, 'y', 80 + Math.floor(i / cols) * 130)
|
|
|
})
|
|
|
},
|
|
|
saveWorkflow() {
|
|
|
this.saving = true
|
|
|
- const content = JSON.stringify({ nodes: this.nodes.map(n => {
|
|
|
- const { x, y, ...rest } = n
|
|
|
- return rest
|
|
|
- }) })
|
|
|
- updatePrompt(this.templateId, {
|
|
|
- promptContent: content
|
|
|
- }).then(res => {
|
|
|
- if (res.code === 200) {
|
|
|
- this.$message.success('保存成功')
|
|
|
- } else {
|
|
|
- this.$message.error(res.msg || '保存失败')
|
|
|
- }
|
|
|
+ const payload = {
|
|
|
+ workflowId: Number(this.workflowId),
|
|
|
+ templateName: this.templateName,
|
|
|
+ industryType: this.templateField('industry_type'),
|
|
|
+ description: this.templateField('description'),
|
|
|
+ nodes: this.nodes.map(n => {
|
|
|
+ let nodeConfig = n.nodeConfig || null
|
|
|
+ // promptId 合并到 nodeConfig JSON
|
|
|
+ if (n.promptId) {
|
|
|
+ try {
|
|
|
+ const cfg = nodeConfig ? JSON.parse(nodeConfig) : {}
|
|
|
+ cfg.promptId = n.promptId
|
|
|
+ nodeConfig = JSON.stringify(cfg)
|
|
|
+ } catch (e) { nodeConfig = JSON.stringify({ promptId: n.promptId }) }
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ nodeCode: n.nodeCode, nodeName: n.nodeName, nodeType: n.nodeType,
|
|
|
+ sortNo: n.sortNo, nextNodeCode: n.nextNodeCode || null,
|
|
|
+ messageTemplate: n.messageTemplate || null,
|
|
|
+ conditionExpr: n.conditionExpr || null,
|
|
|
+ nodeConfig: nodeConfig,
|
|
|
+ sceneCode: n.sceneCode || null, modelName: n.modelName || null,
|
|
|
+ sendTime: n.sendTime || null, maxRound: n.maxRound || 0
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ saveWorkflowNodes(payload).then(res => {
|
|
|
+ this.$message.success(res.msg || '保存成功')
|
|
|
}).catch(err => {
|
|
|
this.$message.error('保存失败:' + (err.message || ''))
|
|
|
- }).finally(() => {
|
|
|
- this.saving = false
|
|
|
- })
|
|
|
+ }).finally(() => { this.saving = false })
|
|
|
},
|
|
|
onNodeMouseDown(e, index) {
|
|
|
- this.dragging = true
|
|
|
- this.dragIndex = index
|
|
|
+ this.dragging = true; this.dragIndex = index
|
|
|
this.dragOffsetX = e.clientX - this.nodes[index].x
|
|
|
this.dragOffsetY = e.clientY - this.nodes[index].y
|
|
|
},
|
|
|
onCanvasMouseMove(e) {
|
|
|
if (!this.dragging || this.dragIndex < 0) return
|
|
|
const rect = this.$refs.canvasArea.getBoundingClientRect()
|
|
|
- const x = e.clientX - this.dragOffsetX
|
|
|
- const y = e.clientY - this.dragOffsetY
|
|
|
- this.$set(this.nodes[this.dragIndex], 'x', Math.max(0, x - rect.left))
|
|
|
- this.$set(this.nodes[this.dragIndex], 'y', Math.max(0, y - rect.top))
|
|
|
- },
|
|
|
- onCanvasMouseUp() {
|
|
|
- this.dragging = false
|
|
|
- this.dragIndex = -1
|
|
|
+ this.$set(this.nodes[this.dragIndex], 'x', Math.max(0, e.clientX - this.dragOffsetX - rect.left))
|
|
|
+ this.$set(this.nodes[this.dragIndex], 'y', Math.max(0, e.clientY - this.dragOffsetY - rect.top))
|
|
|
},
|
|
|
+ onCanvasMouseUp() { this.dragging = false; this.dragIndex = -1 },
|
|
|
onCanvasMouseDown() {},
|
|
|
- getNodeTypeText(type) {
|
|
|
- const map = { 1: '开始', 2: '消息', 3: '判断', 4: '等待', 5: '结束', 6: 'API' }
|
|
|
- return map[type] || '未知'
|
|
|
+ getNodeTagType(type) {
|
|
|
+ const m = { 1: 'success', 3: 'warning', 4: 'info', 5: 'danger', 8: 'warning', 9: 'info', 12: 'info', 21: 'warning', 22: 'info' }
|
|
|
+ return m[type] || ''
|
|
|
},
|
|
|
- getNodeTypeColor(type) {
|
|
|
- const map = { 1: 'success', 2: '', 3: 'warning', 4: 'info', 5: 'danger', 6: '' }
|
|
|
- return map[type] || ''
|
|
|
+ extractPromptId(nodeConfig) {
|
|
|
+ if (!nodeConfig) return null
|
|
|
+ try { return JSON.parse(nodeConfig).promptId || null } catch (e) { return null }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -340,18 +422,21 @@ export default {
|
|
|
.header-right { display: flex; align-items: center; }
|
|
|
.info-card { margin-bottom: 16px; }
|
|
|
.no-template { text-align: center; padding: 100px 0; }
|
|
|
-.canvas-container { position: relative; }
|
|
|
-.canvas-toolbar { margin-bottom: 12px; }
|
|
|
-.canvas-area { position: relative; width: 100%; height: 600px; border: 1px solid #dcdfe6; border-radius: 4px; background: #fafafa; overflow: hidden; }
|
|
|
-.canvas-lines { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
|
|
|
-.canvas-node { position: absolute; width: 180px; background: #fff; border: 2px solid #dcdfe6; border-radius: 8px; cursor: move; user-select: none; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
|
|
-.canvas-node:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.15); }
|
|
|
+.canvas-toolbar { margin-bottom: 12px; display:flex; flex-wrap:wrap; gap:4px; }
|
|
|
+.canvas-area { position: relative; width: 100%; height: 700px; border: 1px solid #dcdfe6; border-radius: 4px; background: #fafafa; overflow: auto; }
|
|
|
+.canvas-lines { position: absolute; top: 0; left: 0; width: 3000px; height: 3000px; pointer-events: none; }
|
|
|
+.canvas-node { position: absolute; width: 180px; background: #fff; border: 2px solid #dcdfe6; border-radius: 8px; cursor: move; user-select: none; box-shadow: 0 2px 8px rgba(0,0,0,0.1); z-index: 10; }
|
|
|
+.canvas-node:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.15); z-index: 20; }
|
|
|
.canvas-node.node-type-1 { border-color: #67C23A; }
|
|
|
.canvas-node.node-type-2 { border-color: #409EFF; }
|
|
|
.canvas-node.node-type-3 { border-color: #E6A23C; }
|
|
|
.canvas-node.node-type-4 { border-color: #909399; }
|
|
|
.canvas-node.node-type-5 { border-color: #F56C6C; }
|
|
|
-.canvas-node.node-type-6 { border-color: #409EFF; }
|
|
|
+.canvas-node.node-type-8 { border-color: #E6A23C; }
|
|
|
+.canvas-node.node-type-9 { border-color: #909399; }
|
|
|
+.canvas-node.node-type-12 { border-color: #909399; }
|
|
|
+.canvas-node.node-type-14 { border-color: #3b82f6; }
|
|
|
+.canvas-node.node-type-21 { border-color: #E6A23C; }
|
|
|
.node-title { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-bottom: 1px solid #ebeef5; font-size: 13px; font-weight: bold; }
|
|
|
.node-body { padding: 8px 12px; }
|
|
|
.node-msg { font-size: 12px; color: #606266; white-space: pre-wrap; }
|