| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732 |
- <template>
- <div class="workflow-designer">
- <!-- 顶部工具栏 -->
- <div class="toolbar">
- <div class="toolbar-left">
- <el-button icon="el-icon-back" size="small" @click="goBack">返回</el-button>
- <el-divider direction="vertical" />
- <el-input v-model="form.workflowName" placeholder="工作流名称" size="small" style="width: 200px" />
- <el-select v-model="form.workflowType" placeholder="类型" size="small" style="width: 120px; margin-left: 10px">
- <el-option
- v-for="dict in workflowTypeOptions"
- :key="dict.dictValue"
- :label="dict.dictLabel"
- :value="dict.dictValue"
- />
- </el-select>
- </div>
- <div class="toolbar-right">
- <el-input
- v-model="form.workflowDesc"
- placeholder="工作流描述"
- size="small"
- style="width: 200px; margin-right: 10px"
- />
- <el-divider direction="vertical" />
- <el-button icon="el-icon-zoom-in" size="small" @click="zoomIn">放大</el-button>
- <el-button icon="el-icon-zoom-out" size="small" @click="zoomOut">缩小</el-button>
- <el-button icon="el-icon-rank" size="small" @click="fitView">适应</el-button>
- <el-divider direction="vertical" />
- <el-button type="primary" icon="el-icon-check" size="small" @click="handleSave">保存</el-button>
- </div>
- </div>
- <div class="main-content">
- <!-- 左侧节点面板 -->
- <div class="node-panel">
- <div class="panel-title">节点类型</div>
- <div class="node-category" v-for="category in nodeCategories" :key="category.key">
- <div class="category-title">{{ category.name }}</div>
- <div class="node-list">
- <div
- class="node-item"
- v-for="nodeType in category.types"
- :key="nodeType.typeCode"
- draggable="true"
- @dragstart="onDragStart($event, nodeType)"
- :style="{ borderColor: nodeType.typeColor }"
- >
- <i :class="nodeType.typeIcon" :style="{ color: nodeType.typeColor }"></i>
- <span>{{ nodeType.typeName }}</span>
- </div>
- </div>
- </div>
- </div>
- <!-- 中间画布区域 -->
- <div
- class="canvas-container"
- ref="canvasContainer"
- tabindex="0"
- @drop="onDrop"
- @dragover.prevent
- @click="onCanvasClick"
- @mousedown="onCanvasMouseDown"
- @keydown.delete="handleDelete"
- @keydown.backspace="handleDelete"
- >
- <svg class="canvas-svg" ref="canvasSvg" :width="canvasSize.width" :height="canvasSize.height"
- :class="{ 'dragging-canvas': isDraggingCanvas }">
- <!-- 网格背景 -->
- <defs>
- <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>
- </defs>
- <rect :width="canvasSize.width" :height="canvasSize.height" fill="url(#grid)" />
- <!-- 连线 -->
- <g class="edges-layer" :transform="`translate(${canvasOffset.x}, ${canvasOffset.y}) scale(${scale})`">
- <g
- v-for="edge in edges"
- :key="edge.edgeKey"
- class="edge-group"
- @click.stop="selectEdge(edge)"
- >
- <path
- :d="getEdgePath(edge)"
- :stroke="edge.edgeColor || '#999'"
- stroke-width="2"
- fill="none"
- :class="{ selected: selectedEdge === edge }"
- />
- <text
- v-if="edge.edgeLabel"
- :x="getEdgeLabelPos(edge).x"
- :y="getEdgeLabelPos(edge).y"
- text-anchor="middle"
- font-size="12"
- fill="#666"
- >{{ edge.edgeLabel }}</text>
- </g>
- </g>
- <!-- 临时连线 -->
- <g :transform="`translate(${canvasOffset.x}, ${canvasOffset.y}) scale(${scale})`">
- <path
- v-if="tempEdge"
- :d="tempEdge.path"
- stroke="#1890ff"
- stroke-width="2"
- fill="none"
- stroke-dasharray="5,5"
- />
- </g>
- <!-- 节点 -->
- <g class="nodes-layer" :transform="`translate(${canvasOffset.x}, ${canvasOffset.y}) scale(${scale})`">
- <g
- v-for="node in nodes"
- :key="node.nodeKey"
- class="node-group"
- :transform="`translate(${node.posX}, ${node.posY})`"
- @mousedown="onNodeMouseDown($event, node)"
- @click.stop="selectNode(node)"
- >
- <rect
- :width="nodeWidth"
- :height="node.height || 36"
- rx="6"
- ry="6"
- :fill="getNodeBgColor(node)"
- :stroke="node.nodeColor || '#1890ff'"
- stroke-width="2"
- :class="{ selected: selectedNode === node }"
- />
- <foreignObject :width="nodeWidth" :height="node.height || 36">
- <div class="node-content" xmlns="http://www.w3.org/1999/xhtml" :title="node.nodeName">
- <i :class="node.nodeIcon" :style="{ color: node.nodeColor }"></i>
- <span class="node-name">{{ node.nodeName }}</span>
- </div>
- </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')" />
- <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')" />
- <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')" />
- <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')" />
- </g>
- </g>
- </svg>
- </div>
- <!-- 右侧属性面板 -->
- <div class="property-panel" v-if="selectedNode || selectedEdge">
- <div class="panel-title">
- {{ selectedNode ? '节点属性' : '连线属性' }}
- <i class="el-icon-close" @click="clearSelection"></i>
- </div>
- <!-- 节点属性 -->
- <el-form v-if="selectedNode" label-width="80px" size="small">
- <el-form-item label="节点内容">
- <el-input
- v-model="selectedNode.nodeName"
- type="textarea"
- :autosize="{ minRows: 1, maxRows: 6 }"
- />
- </el-form-item>
- <div v-if="selectedNode.nodeType == 'http'">
- <el-form-item label="URL地址">
- <el-input v-model="selectedNode.nodeConfig.url" />
- </el-form-item>
- </div>
- <el-form-item label="节点类型">
- <el-input :value="getNodeTypeName(selectedNode.nodeType)" disabled />
- </el-form-item>
- <el-form-item label="节点颜色">
- <el-color-picker v-model="selectedNode.nodeColor" />
- </el-form-item>
- <el-form-item label="X坐标">
- <el-input-number v-model="selectedNode.posX" :min="0" />
- </el-form-item>
- <el-form-item label="Y坐标">
- <el-input-number v-model="selectedNode.posY" :min="0" />
- </el-form-item>
- <el-form-item>
- <el-button type="danger" size="mini" @click="deleteNode">删除节点</el-button>
- </el-form-item>
- </el-form>
- <!-- 连线属性 -->
- <el-form v-if="selectedEdge" label-width="80px" size="small">
- <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>
- <el-form-item>
- <el-button type="danger" size="mini" @click="deleteEdge">删除连线</el-button>
- </el-form-item>
- </el-form>
- </div>
- </div>
- </div>
- </template>
- <script>
- import { getWorkflow, addWorkflow, updateWorkflow, getNodeTypes } from '@/api/his/aiWorkflow'
- export default {
- name: 'WorkflowDesign',
- data() {
- return {
- // 工作流ID
- workflowId: null,
- // 表单数据
- form: {
- workflowName: '新建工作流',
- workflowDesc: '',
- workflowType: '1',
- canvasData: ''
- },
- // 节点列表
- nodes: [],
- // 连线列表
- edges: [],
- // 节点类型列表
- nodeTypes: [],
- // 节点分类
- nodeCategories: [],
- // 选中的节点
- selectedNode: null,
- // 选中的连线
- selectedEdge: null,
- // 缩放比例
- scale: 1,
- // 画布偏移
- canvasOffset: { x: 0, y: 0 },
- // 拖拽中的节点
- draggingNode: null,
- // 拖拽偏移
- dragOffset: { x: 0, y: 0 },
- // 是否正在连线
- connecting: false,
- // 连线起点
- connectStart: null,
- // 临时连线
- tempEdge: null,
- // 画布尺寸
- canvasSize: { width: 2000, height: 2000 },
- // 节点固定宽度
- nodeWidth: 120,
- // 是否正在拖动画布
- isDraggingCanvas: false,
- // 画布拖动起始点
- canvasDragStart: { x: 0, y: 0 },
- // 工作流类型字典
- workflowTypeOptions: [
- { dictValue: '1', dictLabel: '对话流程' },
- { dictValue: '2', dictLabel: '任务流程' },
- { dictValue: '3', dictLabel: '审批流程' }
- ]
- }
- },
- created() {
- this.workflowId = this.$route.params.id
- this.loadNodeTypes()
- if (this.workflowId) {
- this.loadWorkflow()
- }
- },
- mounted() {
- // 确保容器可获取焦点
- this.$nextTick(() => {
- this.$refs.canvasContainer.focus() // 这里应该是 canvasContainer 不是 container
- })
- // 添加全局键盘事件监听
- // window.addEventListener('keydown', this.handleGlobalKeydown)
- document.addEventListener('mousemove', this.onMouseMove)
- document.addEventListener('mouseup', this.onMouseUp)
- },
- beforeDestroy() {
- // window.removeEventListener('keydown', this.handleGlobalKeydown)
- document.removeEventListener('mousemove', this.onMouseMove)
- document.removeEventListener('mouseup', this.onMouseUp)
- },
- methods: {
- // 聚焦画布容器
- focusCanvasContainer() {
- this.$refs.canvasContainer.focus()
- },
- // 点击画布时聚焦
- onCanvasClick() {
- this.clearSelection()
- this.focusCanvasContainer()
- },
- // 点击节点时聚焦
- selectNode(node) {
- this.selectedNode = node
- this.selectedNode.nodeConfig = JSON.parse(node.nodeConfig)
- this.selectedEdge = null
- this.focusCanvasContainer()
- },
- // 点击连线时聚焦
- selectEdge(edge) {
- this.selectedEdge = edge
- this.selectedNode = null
- this.focusCanvasContainer()
- },
- // 局部键盘事件
- handleDelete(event) {
- event.preventDefault() // 阻止默认行为(如浏览器后退)
- this.deleteSelected()
- },
- // 全局键盘事件
- // handleGlobalKeydown(event) {
- // // 检查是否按下了退格键或删除键
- // if ((event.key === 'Backspace' || event.key === 'Delete') && !this.isInputFocused()) {
- // event.preventDefault()
- // this.deleteSelected()
- // }
- // },
- // 删除选中项的逻辑
- deleteSelected() {
- if (this.selectedNode) {
- this.deleteSelectedNode()
- } else if (this.selectedEdge) {
- this.deleteSelectedEdge()
- }
- },
- // 删除选中节点
- deleteSelectedNode() {
- if (!this.selectedNode) return
- // 确认对话框
- this.$confirm('是否确认删除该节点?', '警告', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- }).then(() => {
- const key = this.selectedNode.nodeKey
- this.nodes = this.nodes.filter(n => n.nodeKey !== key)
- // 同时删除与该节点相关的连线
- this.edges = this.edges.filter(e =>
- e.sourceNodeKey !== key && e.targetNodeKey !== key)
- this.selectedNode = null
- }).catch(() => {})
- },
- // 删除选中连线
- deleteSelectedEdge() {
- if (!this.selectedEdge) return
- this.$confirm('是否确认删除该连线?', '警告', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- }).then(() => {
- this.edges = this.edges.filter(e => e.edgeKey !== this.selectedEdge.edgeKey)
- this.selectedEdge = null
- }).catch(() => {})
- },
- // 检查是否有输入框获得焦点
- isInputFocused() {
- const activeElement = document.activeElement
- // 注意:节点名称输入框也需要排除
- const isEditable = ['INPUT', 'TEXTAREA', 'SELECT'].includes(activeElement.tagName)
- // 额外检查是否是节点名称输入框
- const isNodeInput = activeElement.classList && activeElement.classList.contains('node-name-input')
- return isEditable || isNodeInput
- },
- /** 加载节点类型 */
- loadNodeTypes() {
- getNodeTypes().then(res => {
- this.nodeTypes = res.data || []
- this.groupNodeTypes()
- }).catch(() => {
- // 如果接口未实现,使用默认节点类型
- this.nodeTypes = [
- { 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()
- })
- },
- /** 节点类型分组 */
- groupNodeTypes() {
- 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: [] }
- }
- this.nodeTypes.forEach(t => {
- const cat = categoryMap[t.typeCategory] || categoryMap.basic
- cat.types.push(t)
- })
- this.nodeCategories = Object.values(categoryMap).filter(c => c.types.length > 0)
- },
- /** 加载工作流 */
- loadWorkflow() {
- getWorkflow(this.workflowId).then(res => {
- const data = res.data
- this.form = {
- workflowName: data.workflowName,
- workflowDesc: data.workflowDesc,
- workflowType: String(data.workflowType),
- canvasData: data.canvasData
- }
- this.nodes = data.nodes || []
- this.edges = data.edges || []
- })
- },
- /** 返回列表 */
- goBack() {
- this.$router.push('/his/aiWorkflow')
- },
- /** 放大 */
- zoomIn() {
- this.scale = Math.min(2, this.scale + 0.1)
- },
- /** 缩小 */
- zoomOut() {
- this.scale = Math.max(0.5, this.scale - 0.1)
- },
- /** 适应画布 */
- fitView() {
- this.scale = 1
- this.canvasOffset = { x: 0, y: 0 }
- this.focusCanvasContainer()
- },
- /** 生成唯一Key */
- generateKey() {
- return 'node_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
- },
- /** 拖拽开始 */
- onDragStart(e, nodeType) {
- e.dataTransfer.setData('nodeType', JSON.stringify(nodeType))
- },
- /** 放置节点 */
- onDrop(e) {
- const nodeTypeStr = e.dataTransfer.getData('nodeType')
- if (!nodeTypeStr) return
- const nodeType = JSON.parse(nodeTypeStr)
- const rect = this.$refs.canvasContainer.getBoundingClientRect()
- const x = (e.clientX - rect.left - this.canvasOffset.x) / this.scale
- const y = (e.clientY - rect.top - this.canvasOffset.y) / this.scale
- const newNode = {
- nodeKey: this.generateKey(),
- nodeName: nodeType.typeName,
- nodeType: nodeType.typeCode,
- nodeIcon: nodeType.typeIcon,
- nodeColor: nodeType.typeColor,
- posX: Math.round(x - 50),
- posY: Math.round(y - 20),
- height: 40,
- nodeConfig: nodeType.defaultConfig || '{}'
- }
- this.nodes.push(newNode)
- this.selectNode(newNode)
- this.focusCanvasContainer()
- // 检测并扩展画布
- this.checkAndExpandCanvas(newNode)
- },
- /** 画布鼠标按下 */
- onCanvasMouseDown(e) {
- // 只响应左键且点击在空白区域
- if (e.button !== 0) return
- if (e.target.tagName === 'rect' && e.target.getAttribute('fill') === 'url(#grid)') {
- this.isDraggingCanvas = true
- this.canvasDragStart = {
- x: e.clientX,
- y: e.clientY,
- scrollLeft: this.$refs.canvasContainer.scrollLeft,
- scrollTop: this.$refs.canvasContainer.scrollTop
- }
- e.preventDefault()
- }
- },
- /** 清除选中 */
- clearSelection() {
- this.selectedNode = null
- this.selectedEdge = null
- },
- /** 节点鼠标按下 */
- onNodeMouseDown(e, node) {
- if (e.target.classList.contains('anchor')) return
- this.draggingNode = node
- const rect = this.$refs.canvasContainer.getBoundingClientRect()
- const scrollLeft = this.$refs.canvasContainer.scrollLeft
- const scrollTop = this.$refs.canvasContainer.scrollTop
- this.dragOffset = {
- x: (e.clientX - rect.left + scrollLeft - this.canvasOffset.x) / this.scale - node.posX,
- y: (e.clientY - rect.top + scrollTop - this.canvasOffset.y) / this.scale - node.posY
- }
- this.selectNode(node)
- },
- /** 鼠标移动 */
- onMouseMove(e) {
- // 拖动画布
- if (this.isDraggingCanvas) {
- const dx = e.clientX - this.canvasDragStart.x
- const dy = e.clientY - this.canvasDragStart.y
- this.$refs.canvasContainer.scrollLeft = this.canvasDragStart.scrollLeft - dx
- this.$refs.canvasContainer.scrollTop = this.canvasDragStart.scrollTop - dy
- return
- }
- // 拖动节点
- if (this.draggingNode) {
- const rect = this.$refs.canvasContainer.getBoundingClientRect()
- const scrollLeft = this.$refs.canvasContainer.scrollLeft
- const scrollTop = this.$refs.canvasContainer.scrollTop
- const x = (e.clientX - rect.left + scrollLeft - this.canvasOffset.x) / this.scale - this.dragOffset.x
- const y = (e.clientY - rect.top + scrollTop - this.canvasOffset.y) / this.scale - this.dragOffset.y
- this.draggingNode.posX = Math.max(0, Math.round(x))
- this.draggingNode.posY = Math.max(0, Math.round(y))
- // 检测并扩展画布
- this.checkAndExpandCanvas(this.draggingNode)
- }
- // 连线
- if (this.connecting && this.connectStart) {
- const rect = this.$refs.canvasContainer.getBoundingClientRect()
- const scrollLeft = this.$refs.canvasContainer.scrollLeft
- const scrollTop = this.$refs.canvasContainer.scrollTop
- const endX = (e.clientX - rect.left + scrollLeft - this.canvasOffset.x) / this.scale
- const endY = (e.clientY - rect.top + scrollTop - this.canvasOffset.y) / this.scale
- this.tempEdge = {
- path: this.calcPath(this.connectStart.x, this.connectStart.y, endX, endY)
- }
- }
- },
- /** 检测并扩展画布 */
- checkAndExpandCanvas(node) {
- const nodeHeight = node.height || 36
- const padding = 200 // 边缘预留空间
- const expandStep = 500 // 每次扩展的大小
- const nodeRight = node.posX + this.nodeWidth
- const nodeBottom = node.posY + nodeHeight
- // 检测右边缘
- if (nodeRight + padding > this.canvasSize.width) {
- this.canvasSize.width += expandStep
- }
- // 检测下边缘
- if (nodeBottom + padding > this.canvasSize.height) {
- this.canvasSize.height += expandStep
- }
- },
- /** 鼠标松开 */
- onMouseUp(e) {
- // 停止拖动画布
- if (this.isDraggingCanvas) {
- this.isDraggingCanvas = false
- this.focusCanvasContainer()
- return
- }
- // 停止连线
- if (this.connecting) {
- 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.connecting = false
- this.connectStart = null
- this.tempEdge = null
- }
- this.draggingNode = null
- },
- /** 开始连线 */
- startConnect(e, node, anchor) {
- this.connecting = true
- const anchorPos = this.getAnchorPos(node, anchor)
- this.connectStart = {
- nodeKey: node.nodeKey,
- anchor: anchor,
- x: anchorPos.x,
- y: anchorPos.y
- }
- this.selectNode(node)
- },
- /** 获取锚点位置 */
- getAnchorPos(node, anchor) {
- const w = this.nodeWidth
- const h = node.height || 36
- switch (anchor) {
- case 'top': return { x: node.posX + w / 2, y: node.posY }
- case 'bottom': return { x: node.posX + w / 2, y: node.posY + h }
- case 'left': return { x: node.posX, y: node.posY + h / 2 }
- case 'right': return { x: node.posX + w, y: node.posY + h / 2 }
- default: return { x: node.posX + w / 2, y: node.posY + h }
- }
- },
- /** 查找点击位置的节点 */
- findNodeAtPoint(clientX, clientY) {
- const rect = this.$refs.canvasContainer.getBoundingClientRect()
- const scrollLeft = this.$refs.canvasContainer.scrollLeft
- const scrollTop = this.$refs.canvasContainer.scrollTop
- const x = (clientX - rect.left + scrollLeft - this.canvasOffset.x) / this.scale
- const y = (clientY - rect.top + scrollTop - this.canvasOffset.y) / this.scale
- return this.nodes.find(n => {
- const w = this.nodeWidth
- const h = n.height || 36
- return x >= n.posX && x <= n.posX + w && y >= n.posY && y <= n.posY + h
- })
- },
- /** 创建连线 */
- createEdge(sourceKey, targetKey, sourceAnchor, targetAnchor) {
- const exists = this.edges.find(e =>
- e.sourceNodeKey === sourceKey && e.targetNodeKey === targetKey)
- if (exists) return
- this.edges.push({
- edgeKey: 'edge_' + Date.now(),
- sourceNodeKey: sourceKey,
- targetNodeKey: targetKey,
- sourceAnchor: sourceAnchor,
- targetAnchor: targetAnchor,
- edgeType: 'smoothstep',
- edgeColor: '#999999'
- })
- },
- /** 获取连线路径 */
- getEdgePath(edge) {
- const sourceNode = this.nodes.find(n => n.nodeKey === edge.sourceNodeKey)
- const targetNode = this.nodes.find(n => n.nodeKey === edge.targetNodeKey)
- if (!sourceNode || !targetNode) return ''
- 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)
- },
- /** 计算贝塞尔曲线路径 */
- calcPath(x1, y1, x2, y2) {
- const midY = (y1 + y2) / 2
- return `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`
- },
- /** 获取连线标签位置 */
- getEdgeLabelPos(edge) {
- const sourceNode = this.nodes.find(n => n.nodeKey === edge.sourceNodeKey)
- const targetNode = this.nodes.find(n => n.nodeKey === edge.targetNodeKey)
- if (!sourceNode || !targetNode) return { x: 0, y: 0 }
- const start = this.getAnchorPos(sourceNode, edge.sourceAnchor || 'bottom')
- const end = this.getAnchorPos(targetNode, edge.targetAnchor || 'top')
- return { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 }
- },
- /** 获取节点背景色 */
- getNodeBgColor(node) {
- const color = node.nodeColor || '#1890ff'
- return color + '15'
- },
- /** 获取节点类型名称 */
- getNodeTypeName(typeCode) {
- const t = this.nodeTypes.find(n => n.typeCode === typeCode)
- return t ? t.typeName : typeCode
- },
- /** 删除节点(按钮触发) */
- deleteNode() {
- this.deleteSelectedNode()
- },
- /** 删除连线(按钮触发) */
- deleteEdge() {
- this.deleteSelectedEdge()
- },
- /** 保存工作流 */
- handleSave() {
- if (!this.form.workflowName) {
- this.msgWarning('请输入工作流名称')
- return
- }
- const nodes = JSON.parse(JSON.stringify(this.nodes))
- nodes.filter(node => typeof node.nodeConfig == 'object').forEach(node => {
- console.log(typeof node.nodeConfig)
- node.nodeConfig = JSON.stringify(node.nodeConfig);
- })
- const data = {
- workflowId: this.workflowId,
- workflowName: this.form.workflowName,
- workflowDesc: this.form.workflowDesc,
- workflowType: this.form.workflowType,
- canvasData: JSON.stringify({ scale: this.scale, offset: this.canvasOffset }),
- nodes: nodes,
- edges: this.edges
- }
- if (this.workflowId) {
- updateWorkflow(data).then(() => {
- this.msgSuccess('保存成功')
- })
- } else {
- addWorkflow(data).then(res => {
- this.msgSuccess('保存成功')
- this.workflowId = res.data
- this.$router.replace('/his/aiWorkflow/design/' + this.workflowId)
- })
- }
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- @import './design.scss';
- </style>
|