|
|
@@ -72,6 +72,30 @@
|
|
|
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
|
|
|
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#e0e0e0" stroke-width="0.5"/>
|
|
|
</pattern>
|
|
|
+ <!-- 箭头标记 -->
|
|
|
+ <marker
|
|
|
+ id="arrowhead"
|
|
|
+ markerWidth="10"
|
|
|
+ markerHeight="10"
|
|
|
+ refX="9"
|
|
|
+ refY="3"
|
|
|
+ orient="auto"
|
|
|
+ markerUnits="strokeWidth"
|
|
|
+ >
|
|
|
+ <path d="M0,0 L0,6 L9,3 z" fill="#999" />
|
|
|
+ </marker>
|
|
|
+ <!-- 选中状态的箭头 -->
|
|
|
+ <marker
|
|
|
+ id="arrowhead-selected"
|
|
|
+ markerWidth="10"
|
|
|
+ markerHeight="10"
|
|
|
+ refX="9"
|
|
|
+ refY="3"
|
|
|
+ orient="auto"
|
|
|
+ markerUnits="strokeWidth"
|
|
|
+ >
|
|
|
+ <path d="M0,0 L0,6 L9,3 z" fill="#1890ff" />
|
|
|
+ </marker>
|
|
|
</defs>
|
|
|
<rect :width="canvasSize.width" :height="canvasSize.height" fill="url(#grid)" />
|
|
|
|
|
|
@@ -88,6 +112,7 @@
|
|
|
:stroke="edge.edgeColor || '#999'"
|
|
|
stroke-width="2"
|
|
|
fill="none"
|
|
|
+ :marker-end="selectedEdge === edge ? 'url(#arrowhead-selected)' : 'url(#arrowhead)'"
|
|
|
:class="{ selected: selectedEdge === edge }"
|
|
|
/>
|
|
|
<text
|
|
|
@@ -110,6 +135,7 @@
|
|
|
stroke-width="2"
|
|
|
fill="none"
|
|
|
stroke-dasharray="5,5"
|
|
|
+ marker-end="url(#arrowhead-selected)"
|
|
|
/>
|
|
|
</g>
|
|
|
|
|
|
@@ -141,13 +167,13 @@
|
|
|
</foreignObject>
|
|
|
<!-- 连接点 -->
|
|
|
<circle :cx="nodeWidth / 2" cy="0" r="5" fill="#fff" stroke="#1890ff" stroke-width="2"
|
|
|
- class="anchor top" @mousedown.stop="startConnect($event, node, 'top')" />
|
|
|
+ class="anchor top" data-type="top" @mousedown.stop="startConnect($event, node, 'top')" />
|
|
|
<circle :cx="nodeWidth / 2" :cy="node.height || 36" r="5" fill="#fff" stroke="#1890ff" stroke-width="2"
|
|
|
- class="anchor bottom" @mousedown.stop="startConnect($event, node, 'bottom')" />
|
|
|
+ class="anchor bottom" data-type="bottom" @mousedown.stop="startConnect($event, node, 'bottom')" />
|
|
|
<circle cx="0" :cy="(node.height || 36) / 2" r="5" fill="#fff" stroke="#1890ff" stroke-width="2"
|
|
|
- class="anchor left" @mousedown.stop="startConnect($event, node, 'left')" />
|
|
|
+ class="anchor left" data-type="left" @mousedown.stop="startConnect($event, node, 'left')" />
|
|
|
<circle :cx="nodeWidth" :cy="(node.height || 36) / 2" r="5" fill="#fff" stroke="#1890ff" stroke-width="2"
|
|
|
- class="anchor right" @mousedown.stop="startConnect($event, node, 'right')" />
|
|
|
+ class="anchor right" data-type="right" @mousedown.stop="startConnect($event, node, 'right')" />
|
|
|
</g>
|
|
|
</g>
|
|
|
</svg>
|
|
|
@@ -193,15 +219,59 @@
|
|
|
|
|
|
<!-- 连线属性 -->
|
|
|
<el-form v-if="selectedEdge" label-width="80px" size="small">
|
|
|
- <el-form-item label="连线标签">
|
|
|
+ <el-form-item label="备注">
|
|
|
<el-input v-model="selectedEdge.edgeLabel" />
|
|
|
</el-form-item>
|
|
|
<el-form-item label="连线颜色">
|
|
|
<el-color-picker v-model="selectedEdge.edgeColor" />
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="条件表达式">
|
|
|
- <el-input v-model="selectedEdge.conditionExpr" type="textarea" :rows="3" />
|
|
|
+ <el-form-item v-if="edgeSourceNode.nodeType == 'AI_CALL_TASK' || edgeSourceNode.nodeType == 'AI_SEND_MSG_TASK' || edgeSourceNode.nodeType == 'AI_ADD_WX_TASK'">
|
|
|
+ <el-button type="success" @click="addCondition">新增条件判断</el-button>
|
|
|
</el-form-item>
|
|
|
+ <div v-if="edgeSourceNode.nodeType == 'AI_CALL_TASK'">
|
|
|
+ <div v-for="(item, index) in selectedEdge.conditionExprObj">
|
|
|
+ <el-form-item label="是否拨通">
|
|
|
+ <div style="display: flex;justify-content: space-between;">
|
|
|
+ <el-select v-model="item.callConnected">
|
|
|
+ <el-option :value="false" label="否" />
|
|
|
+ <el-option :value="true" label="是" />
|
|
|
+ </el-select>
|
|
|
+ <el-button type="danger" size="mini" round @click="removeCondition(index)">删除</el-button>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="延迟时间" v-if="item.callConnected == false">
|
|
|
+ <el-input v-model="item.callTime" style="width: 180px">
|
|
|
+ <template slot="append">分钟</template>
|
|
|
+ </el-input>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="意向度">
|
|
|
+ <el-select v-model="item.intention" placeholder="意向等级" filterable clearable>
|
|
|
+ <el-option v-for="item in levelList" :label="item.dictLabel" :value="item.dictValue"/>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="edgeSourceNode.nodeType == 'AI_SEND_MSG_TASK'">
|
|
|
+ <div v-for="(item, index) in selectedEdge.conditionExprObj">
|
|
|
+ <el-form-item label="添加状态">
|
|
|
+ <div style="display: flex;justify-content: space-between;">
|
|
|
+ <el-select v-model="item.isAdd">
|
|
|
+ <el-option :value="false" label="未同意" />
|
|
|
+ <el-option :value="true" label="已同意" />
|
|
|
+ </el-select>
|
|
|
+ <el-button type="danger" size="mini" round @click="removeCondition(index)">删除</el-button>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="超时时间" v-if="item.isAdd == false">
|
|
|
+ <el-input v-model="item.addTime" style="width: 180px">
|
|
|
+ <template slot="append">分钟</template>
|
|
|
+ </el-input>
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="edgeSourceNode.nodeType == 'AI_ADD_WX_TASK'">
|
|
|
+
|
|
|
+ </div>
|
|
|
<el-form-item>
|
|
|
<el-button type="danger" size="mini" @click="deleteEdge">删除连线</el-button>
|
|
|
</el-form-item>
|
|
|
@@ -213,6 +283,7 @@
|
|
|
|
|
|
<script>
|
|
|
import { getWorkflow, addWorkflow, updateWorkflow, getNodeTypes } from '@/api/company/companyWorkflow'
|
|
|
+import {getDicts} from "@/api/system/dict/data";
|
|
|
|
|
|
export default {
|
|
|
name: 'WorkflowDesign',
|
|
|
@@ -233,12 +304,14 @@ export default {
|
|
|
edges: [],
|
|
|
// 节点类型列表
|
|
|
nodeTypes: [],
|
|
|
+ levelList: [],
|
|
|
// 节点分类
|
|
|
nodeCategories: [],
|
|
|
// 选中的节点
|
|
|
selectedNode: null,
|
|
|
// 选中的连线
|
|
|
selectedEdge: null,
|
|
|
+ edgeSourceNode: null,
|
|
|
// 缩放比例
|
|
|
scale: 1,
|
|
|
// 画布偏移
|
|
|
@@ -266,7 +339,9 @@ export default {
|
|
|
{ dictValue: '1', dictLabel: '对话流程' },
|
|
|
{ dictValue: '2', dictLabel: '任务流程' },
|
|
|
{ dictValue: '3', dictLabel: '审批流程' }
|
|
|
- ]
|
|
|
+ ],
|
|
|
+ conditionExprTemp:{
|
|
|
+ },
|
|
|
}
|
|
|
},
|
|
|
created() {
|
|
|
@@ -307,14 +382,23 @@ export default {
|
|
|
// 点击节点时聚焦
|
|
|
selectNode(node) {
|
|
|
this.selectedNode = node
|
|
|
- this.selectedNode.nodeConfig = JSON.parse(node.nodeConfig)
|
|
|
+ // this.selectedNode.nodeConfig = JSON.parse(node.nodeConfig)
|
|
|
this.selectedEdge = null
|
|
|
+ this.edgeSourceNode = null
|
|
|
this.focusCanvasContainer()
|
|
|
},
|
|
|
|
|
|
// 点击连线时聚焦
|
|
|
selectEdge(edge) {
|
|
|
this.selectedEdge = edge
|
|
|
+ this.edgeSourceNode = this.nodes.filter(e => e.nodeKey == this.selectedEdge.sourceNodeKey)[0]
|
|
|
+ if(this.edgeSourceNode.nodeType == "AI_CALL_TASK"){
|
|
|
+ if(this.levelList.length == 0) {
|
|
|
+ getDicts("customer_intention_level").then(e => {
|
|
|
+ this.levelList = e.data;
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
this.selectedNode = null
|
|
|
this.focusCanvasContainer()
|
|
|
},
|
|
|
@@ -396,11 +480,6 @@ export default {
|
|
|
{ typeCode: 'start', typeName: '开始', typeCategory: 'basic', typeIcon: 'el-icon-video-play', typeColor: '#52c41a' },
|
|
|
{ typeCode: 'end', typeName: '结束', typeCategory: 'basic', typeIcon: 'el-icon-video-pause', typeColor: '#ff4d4f' },
|
|
|
{ typeCode: 'condition', typeName: '条件判断', typeCategory: 'logic', typeIcon: 'el-icon-question', typeColor: '#faad14' },
|
|
|
- { typeCode: 'parallel', typeName: '并行网关', typeCategory: 'logic', typeIcon: 'el-icon-share', typeColor: '#722ed1' },
|
|
|
- { typeCode: 'ai_chat', typeName: 'AI对话', typeCategory: 'ai', typeIcon: 'el-icon-chat-dot-round', typeColor: '#1890ff' },
|
|
|
- { typeCode: 'ai_analysis', typeName: 'AI分析', typeCategory: 'ai', typeIcon: 'el-icon-data-analysis', typeColor: '#13c2c2' },
|
|
|
- { typeCode: 'http', typeName: 'HTTP请求', typeCategory: 'integration', typeIcon: 'el-icon-link', typeColor: '#eb2f96' },
|
|
|
- { typeCode: 'database', typeName: '数据库', typeCategory: 'integration', typeIcon: 'el-icon-coin', typeColor: '#fa8c16' }
|
|
|
]
|
|
|
this.groupNodeTypes()
|
|
|
})
|
|
|
@@ -410,9 +489,7 @@ export default {
|
|
|
const categoryMap = {
|
|
|
basic: { key: 'basic', name: '基础节点', types: [] },
|
|
|
logic: { key: 'logic', name: '逻辑节点', types: [] },
|
|
|
- ai: { key: 'ai', name: 'AI节点', types: [] },
|
|
|
- ai: { key: 'ai-cell', name: '外呼几点', types: [] },
|
|
|
- integration: { key: 'integration', name: '集成节点', types: [] }
|
|
|
+ aiCell: { key: 'aiCell', name: '外呼节点', types: [] },
|
|
|
}
|
|
|
this.nodeTypes.forEach(t => {
|
|
|
const cat = categoryMap[t.typeCategory] || categoryMap.basic
|
|
|
@@ -430,6 +507,17 @@ export default {
|
|
|
workflowType: String(data.workflowType),
|
|
|
canvasData: data.canvasData
|
|
|
}
|
|
|
+ if(data.edges){
|
|
|
+ data.edges.forEach(edge => {
|
|
|
+ const con = edge.conditionExpr;
|
|
|
+ if(con == null){
|
|
|
+ edge.conditionExprObj = [];
|
|
|
+ }else{
|
|
|
+ edge.conditionExprObj = JSON.parse(edge.conditionExpr);
|
|
|
+ }
|
|
|
+ console.info(edge)
|
|
|
+ })
|
|
|
+ }
|
|
|
this.nodes = data.nodes || []
|
|
|
this.edges = data.edges || []
|
|
|
})
|
|
|
@@ -586,7 +674,7 @@ export default {
|
|
|
const targetNode = this.findNodeAtPoint(e.clientX, e.clientY)
|
|
|
if (targetNode && targetNode.nodeKey !== this.connectStart.nodeKey) {
|
|
|
this.createEdge(this.connectStart.nodeKey, targetNode.nodeKey,
|
|
|
- this.connectStart.anchor, 'top')
|
|
|
+ this.connectStart.anchor, e.target.dataset.type || 'top')
|
|
|
}
|
|
|
this.connecting = false
|
|
|
this.connectStart = null
|
|
|
@@ -644,6 +732,7 @@ export default {
|
|
|
sourceAnchor: sourceAnchor,
|
|
|
targetAnchor: targetAnchor,
|
|
|
edgeType: 'smoothstep',
|
|
|
+ conditionExprObj: [],
|
|
|
edgeColor: '#999999'
|
|
|
})
|
|
|
},
|
|
|
@@ -655,12 +744,57 @@ export default {
|
|
|
|
|
|
const start = this.getAnchorPos(sourceNode, edge.sourceAnchor || 'bottom')
|
|
|
const end = this.getAnchorPos(targetNode, edge.targetAnchor || 'top')
|
|
|
- return this.calcPath(start.x, start.y, end.x, end.y)
|
|
|
+ return this.calcPath(start.x, start.y, end.x, end.y, edge.sourceAnchor || 'bottom', edge.targetAnchor || 'top')
|
|
|
},
|
|
|
/** 计算贝塞尔曲线路径 */
|
|
|
- calcPath(x1, y1, x2, y2) {
|
|
|
- const midY = (y1 + y2) / 2
|
|
|
- return `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`
|
|
|
+ calcPath(x1, y1, x2, y2, sourceAnchor, targetAnchor) {
|
|
|
+ const dx = x2 - x1
|
|
|
+ const dy = y2 - y1
|
|
|
+ const distance = Math.sqrt(dx * dx + dy * dy)
|
|
|
+
|
|
|
+ // 根据锚点位置计算控制点偏移量
|
|
|
+ let offset = Math.min(distance * 0.5, 100)
|
|
|
+
|
|
|
+ let cp1x = x1, cp1y = y1
|
|
|
+ let cp2x = x2, cp2y = y2
|
|
|
+
|
|
|
+ // 根据源节点锚点方向设置第一个控制点
|
|
|
+ switch(sourceAnchor) {
|
|
|
+ case 'top':
|
|
|
+ cp1y = y1 - offset
|
|
|
+ break
|
|
|
+ case 'bottom':
|
|
|
+ cp1y = y1 + offset
|
|
|
+ break
|
|
|
+ case 'left':
|
|
|
+ cp1x = x1 - offset
|
|
|
+ break
|
|
|
+ case 'right':
|
|
|
+ cp1x = x1 + offset
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ cp1y = y1 + offset
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据目标节点锚点方向设置第二个控制点
|
|
|
+ switch(targetAnchor) {
|
|
|
+ case 'top':
|
|
|
+ cp2y = y2 - offset
|
|
|
+ break
|
|
|
+ case 'bottom':
|
|
|
+ cp2y = y2 + offset
|
|
|
+ break
|
|
|
+ case 'left':
|
|
|
+ cp2x = x2 - offset
|
|
|
+ break
|
|
|
+ case 'right':
|
|
|
+ cp2x = x2 + offset
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ cp2y = y2 - offset
|
|
|
+ }
|
|
|
+
|
|
|
+ return `M ${x1} ${y1} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${x2} ${y2}`
|
|
|
},
|
|
|
/** 获取连线标签位置 */
|
|
|
getEdgeLabelPos(edge) {
|
|
|
@@ -687,6 +821,13 @@ export default {
|
|
|
this.deleteSelectedNode()
|
|
|
},
|
|
|
/** 删除连线(按钮触发) */
|
|
|
+ addCondition() {
|
|
|
+ this.selectedEdge.conditionExprObj.push(JSON.parse(JSON.stringify(this.conditionExprTemp)));
|
|
|
+ },
|
|
|
+ removeCondition(index) {
|
|
|
+ this.selectedEdge.conditionExprObj.splice(index, 1);
|
|
|
+ },
|
|
|
+ /** 删除连线(按钮触发) */
|
|
|
deleteEdge() {
|
|
|
this.deleteSelectedEdge()
|
|
|
},
|
|
|
@@ -697,8 +838,10 @@ export default {
|
|
|
return
|
|
|
}
|
|
|
const nodes = JSON.parse(JSON.stringify(this.nodes))
|
|
|
+ this.edges.forEach((edges) => {
|
|
|
+ edges.conditionExpr = JSON.stringify(edges.conditionExprObj);
|
|
|
+ })
|
|
|
nodes.filter(node => typeof node.nodeConfig == 'object').forEach(node => {
|
|
|
- console.log(typeof node.nodeConfig)
|
|
|
node.nodeConfig = JSON.stringify(node.nodeConfig);
|
|
|
})
|
|
|
const data = {
|