design.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. <template>
  2. <div class="workflow-designer">
  3. <!-- 顶部工具栏 -->
  4. <div class="toolbar">
  5. <div class="toolbar-left">
  6. <el-button icon="el-icon-back" size="small" @click="goBack">返回</el-button>
  7. <el-divider direction="vertical" />
  8. <el-input v-model="form.workflowName" placeholder="工作流名称" size="small" style="width: 200px" />
  9. <el-select v-model="form.workflowType" placeholder="类型" size="small" style="width: 120px; margin-left: 10px">
  10. <el-option
  11. v-for="dict in workflowTypeOptions"
  12. :key="dict.dictValue"
  13. :label="dict.dictLabel"
  14. :value="dict.dictValue"
  15. />
  16. </el-select>
  17. </div>
  18. <div class="toolbar-right">
  19. <el-input
  20. v-model="form.workflowDesc"
  21. placeholder="工作流描述"
  22. size="small"
  23. style="width: 200px; margin-right: 10px"
  24. />
  25. <el-divider direction="vertical" />
  26. <el-button icon="el-icon-zoom-in" size="small" @click="zoomIn">放大</el-button>
  27. <el-button icon="el-icon-zoom-out" size="small" @click="zoomOut">缩小</el-button>
  28. <el-button icon="el-icon-rank" size="small" @click="fitView">适应</el-button>
  29. <el-divider direction="vertical" />
  30. <el-button type="primary" icon="el-icon-check" size="small" @click="handleSave">保存</el-button>
  31. </div>
  32. </div>
  33. <div class="main-content">
  34. <!-- 左侧节点面板 -->
  35. <div class="node-panel">
  36. <div class="panel-title">节点类型</div>
  37. <div class="node-category" v-for="category in nodeCategories" :key="category.key">
  38. <div class="category-title">{{ category.name }}</div>
  39. <div class="node-list">
  40. <div
  41. class="node-item"
  42. v-for="nodeType in category.types"
  43. :key="nodeType.typeCode"
  44. draggable="true"
  45. @dragstart="onDragStart($event, nodeType)"
  46. :style="{ borderColor: nodeType.typeColor }"
  47. >
  48. <i :class="nodeType.typeIcon" :style="{ color: nodeType.typeColor }"></i>
  49. <span>{{ nodeType.typeName }}</span>
  50. </div>
  51. </div>
  52. </div>
  53. </div>
  54. <!-- 中间画布区域 -->
  55. <div
  56. class="canvas-container"
  57. ref="canvasContainer"
  58. @drop="onDrop"
  59. @dragover.prevent
  60. @click="onCanvasClick"
  61. @mousedown="onCanvasMouseDown"
  62. >
  63. <svg class="canvas-svg" ref="canvasSvg" :width="canvasSize.width" :height="canvasSize.height"
  64. :class="{ 'dragging-canvas': isDraggingCanvas }">
  65. <!-- 网格背景 -->
  66. <defs>
  67. <pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
  68. <path d="M 20 0 L 0 0 0 20" fill="none" stroke="#e0e0e0" stroke-width="0.5"/>
  69. </pattern>
  70. </defs>
  71. <rect :width="canvasSize.width" :height="canvasSize.height" fill="url(#grid)" />
  72. <!-- 连线 -->
  73. <g class="edges-layer" :transform="`translate(${canvasOffset.x}, ${canvasOffset.y}) scale(${scale})`">
  74. <g
  75. v-for="edge in edges"
  76. :key="edge.edgeKey"
  77. class="edge-group"
  78. @click.stop="selectEdge(edge)"
  79. >
  80. <path
  81. :d="getEdgePath(edge)"
  82. :stroke="edge.edgeColor || '#999'"
  83. stroke-width="2"
  84. fill="none"
  85. :class="{ selected: selectedEdge === edge }"
  86. />
  87. <text
  88. v-if="edge.edgeLabel"
  89. :x="getEdgeLabelPos(edge).x"
  90. :y="getEdgeLabelPos(edge).y"
  91. text-anchor="middle"
  92. font-size="12"
  93. fill="#666"
  94. >{{ edge.edgeLabel }}</text>
  95. </g>
  96. </g>
  97. <!-- 临时连线 -->
  98. <g :transform="`translate(${canvasOffset.x}, ${canvasOffset.y}) scale(${scale})`">
  99. <path
  100. v-if="tempEdge"
  101. :d="tempEdge.path"
  102. stroke="#1890ff"
  103. stroke-width="2"
  104. fill="none"
  105. stroke-dasharray="5,5"
  106. />
  107. </g>
  108. <!-- 节点 -->
  109. <g class="nodes-layer" :transform="`translate(${canvasOffset.x}, ${canvasOffset.y}) scale(${scale})`">
  110. <g
  111. v-for="node in nodes"
  112. :key="node.nodeKey"
  113. class="node-group"
  114. :transform="`translate(${node.posX}, ${node.posY})`"
  115. @mousedown="onNodeMouseDown($event, node)"
  116. @click.stop="selectNode(node)"
  117. >
  118. <rect
  119. :width="getNodeWidth(node)"
  120. :height="node.height || 40"
  121. rx="6"
  122. ry="6"
  123. :fill="getNodeBgColor(node)"
  124. :stroke="node.nodeColor || '#1890ff'"
  125. stroke-width="2"
  126. :class="{ selected: selectedNode === node }"
  127. />
  128. <foreignObject :width="getNodeWidth(node)" :height="node.height || 40">
  129. <div class="node-content" xmlns="http://www.w3.org/1999/xhtml">
  130. <i :class="node.nodeIcon" :style="{ color: node.nodeColor }"></i>
  131. <input
  132. type="text"
  133. class="node-name-input"
  134. v-model="node.nodeName"
  135. @mousedown.stop
  136. @input="onNodeNameInput(node)"
  137. :style="{ width: getInputWidth(node) + 'px' }"
  138. />
  139. </div>
  140. </foreignObject>
  141. <!-- 连接点 -->
  142. <circle :cx="getNodeWidth(node) / 2" cy="0" r="6" fill="#fff" stroke="#1890ff" stroke-width="2"
  143. class="anchor top" @mousedown.stop="startConnect($event, node, 'top')" />
  144. <circle :cx="getNodeWidth(node) / 2" :cy="node.height || 40" r="6" fill="#fff" stroke="#1890ff" stroke-width="2"
  145. class="anchor bottom" @mousedown.stop="startConnect($event, node, 'bottom')" />
  146. <circle cx="0" :cy="(node.height || 40) / 2" r="6" fill="#fff" stroke="#1890ff" stroke-width="2"
  147. class="anchor left" @mousedown.stop="startConnect($event, node, 'left')" />
  148. <circle :cx="getNodeWidth(node)" :cy="(node.height || 40) / 2" r="6" fill="#fff" stroke="#1890ff" stroke-width="2"
  149. class="anchor right" @mousedown.stop="startConnect($event, node, 'right')" />
  150. </g>
  151. </g>
  152. </svg>
  153. </div>
  154. <!-- 右侧属性面板 -->
  155. <div class="property-panel" v-if="selectedNode || selectedEdge">
  156. <div class="panel-title">
  157. {{ selectedNode ? '节点属性' : '连线属性' }}
  158. <i class="el-icon-close" @click="clearSelection"></i>
  159. </div>
  160. <!-- 节点属性 -->
  161. <el-form v-if="selectedNode" label-width="80px" size="small">
  162. <el-form-item label="节点名称">
  163. <el-input v-model="selectedNode.nodeName" />
  164. </el-form-item>
  165. <el-form-item label="节点类型">
  166. <el-input :value="getNodeTypeName(selectedNode.nodeType)" disabled />
  167. </el-form-item>
  168. <el-form-item label="节点颜色">
  169. <el-color-picker v-model="selectedNode.nodeColor" />
  170. </el-form-item>
  171. <el-form-item label="X坐标">
  172. <el-input-number v-model="selectedNode.posX" :min="0" />
  173. </el-form-item>
  174. <el-form-item label="Y坐标">
  175. <el-input-number v-model="selectedNode.posY" :min="0" />
  176. </el-form-item>
  177. <el-form-item>
  178. <el-button type="danger" size="mini" @click="deleteNode">删除节点</el-button>
  179. </el-form-item>
  180. </el-form>
  181. <!-- 连线属性 -->
  182. <el-form v-if="selectedEdge" label-width="80px" size="small">
  183. <el-form-item label="连线标签">
  184. <el-input v-model="selectedEdge.edgeLabel" />
  185. </el-form-item>
  186. <el-form-item label="连线颜色">
  187. <el-color-picker v-model="selectedEdge.edgeColor" />
  188. </el-form-item>
  189. <el-form-item label="条件表达式">
  190. <el-input v-model="selectedEdge.conditionExpr" type="textarea" :rows="3" />
  191. </el-form-item>
  192. <el-form-item>
  193. <el-button type="danger" size="mini" @click="deleteEdge">删除连线</el-button>
  194. </el-form-item>
  195. </el-form>
  196. </div>
  197. </div>
  198. </div>
  199. </template>
  200. <script>
  201. import { getWorkflow, addWorkflow, updateWorkflow, getNodeTypes } from '@/api/his/aiWorkflow'
  202. export default {
  203. name: 'WorkflowDesign',
  204. data() {
  205. return {
  206. // 工作流ID
  207. workflowId: null,
  208. // 表单数据
  209. form: {
  210. workflowName: '新建工作流',
  211. workflowDesc: '',
  212. workflowType: '1',
  213. canvasData: ''
  214. },
  215. // 节点列表
  216. nodes: [],
  217. // 连线列表
  218. edges: [],
  219. // 节点类型列表
  220. nodeTypes: [],
  221. // 节点分类
  222. nodeCategories: [],
  223. // 选中的节点
  224. selectedNode: null,
  225. // 选中的连线
  226. selectedEdge: null,
  227. // 缩放比例
  228. scale: 1,
  229. // 画布偏移
  230. canvasOffset: { x: 0, y: 0 },
  231. // 拖拽中的节点
  232. draggingNode: null,
  233. // 拖拽偏移
  234. dragOffset: { x: 0, y: 0 },
  235. // 是否正在连线
  236. connecting: false,
  237. // 连线起点
  238. connectStart: null,
  239. // 临时连线
  240. tempEdge: null,
  241. // 画布尺寸
  242. canvasSize: { width: 2000, height: 2000 },
  243. // 是否正在拖动画布
  244. isDraggingCanvas: false,
  245. // 画布拖动起始点
  246. canvasDragStart: { x: 0, y: 0 },
  247. // 工作流类型字典
  248. workflowTypeOptions: [
  249. { dictValue: '1', dictLabel: '对话流程' },
  250. { dictValue: '2', dictLabel: '任务流程' },
  251. { dictValue: '3', dictLabel: '审批流程' }
  252. ]
  253. }
  254. },
  255. created() {
  256. this.workflowId = this.$route.params.id
  257. this.loadNodeTypes()
  258. if (this.workflowId) {
  259. this.loadWorkflow()
  260. }
  261. },
  262. mounted() {
  263. document.addEventListener('mousemove', this.onMouseMove)
  264. document.addEventListener('mouseup', this.onMouseUp)
  265. },
  266. beforeDestroy() {
  267. document.removeEventListener('mousemove', this.onMouseMove)
  268. document.removeEventListener('mouseup', this.onMouseUp)
  269. },
  270. methods: {
  271. /** 加载节点类型 */
  272. loadNodeTypes() {
  273. getNodeTypes().then(res => {
  274. this.nodeTypes = res.data || []
  275. this.groupNodeTypes()
  276. }).catch(() => {
  277. // 如果接口未实现,使用默认节点类型
  278. this.nodeTypes = [
  279. { typeCode: 'start', typeName: '开始', typeCategory: 'basic', typeIcon: 'el-icon-video-play', typeColor: '#52c41a' },
  280. { typeCode: 'end', typeName: '结束', typeCategory: 'basic', typeIcon: 'el-icon-video-pause', typeColor: '#ff4d4f' },
  281. { typeCode: 'condition', typeName: '条件判断', typeCategory: 'logic', typeIcon: 'el-icon-question', typeColor: '#faad14' },
  282. { typeCode: 'parallel', typeName: '并行网关', typeCategory: 'logic', typeIcon: 'el-icon-share', typeColor: '#722ed1' },
  283. { typeCode: 'ai_chat', typeName: 'AI对话', typeCategory: 'ai', typeIcon: 'el-icon-chat-dot-round', typeColor: '#1890ff' },
  284. { typeCode: 'ai_analysis', typeName: 'AI分析', typeCategory: 'ai', typeIcon: 'el-icon-data-analysis', typeColor: '#13c2c2' },
  285. { typeCode: 'http', typeName: 'HTTP请求', typeCategory: 'integration', typeIcon: 'el-icon-link', typeColor: '#eb2f96' },
  286. { typeCode: 'database', typeName: '数据库', typeCategory: 'integration', typeIcon: 'el-icon-coin', typeColor: '#fa8c16' }
  287. ]
  288. this.groupNodeTypes()
  289. })
  290. },
  291. /** 节点类型分组 */
  292. groupNodeTypes() {
  293. const categoryMap = {
  294. basic: { key: 'basic', name: '基础节点', types: [] },
  295. logic: { key: 'logic', name: '逻辑节点', types: [] },
  296. ai: { key: 'ai', name: 'AI节点', types: [] },
  297. integration: { key: 'integration', name: '集成节点', types: [] }
  298. }
  299. this.nodeTypes.forEach(t => {
  300. const cat = categoryMap[t.typeCategory] || categoryMap.basic
  301. cat.types.push(t)
  302. })
  303. this.nodeCategories = Object.values(categoryMap).filter(c => c.types.length > 0)
  304. },
  305. /** 加载工作流 */
  306. loadWorkflow() {
  307. getWorkflow(this.workflowId).then(res => {
  308. const data = res.data
  309. this.form = {
  310. workflowName: data.workflowName,
  311. workflowDesc: data.workflowDesc,
  312. workflowType: String(data.workflowType),
  313. canvasData: data.canvasData
  314. }
  315. this.nodes = data.nodes || []
  316. this.edges = data.edges || []
  317. })
  318. },
  319. /** 返回列表 */
  320. goBack() {
  321. this.$router.push('/his/aiWorkflow')
  322. },
  323. /** 放大 */
  324. zoomIn() {
  325. this.scale = Math.min(2, this.scale + 0.1)
  326. },
  327. /** 缩小 */
  328. zoomOut() {
  329. this.scale = Math.max(0.5, this.scale - 0.1)
  330. },
  331. /** 适应画布 */
  332. fitView() {
  333. this.scale = 1
  334. this.canvasOffset = { x: 0, y: 0 }
  335. },
  336. /** 生成唯一Key */
  337. generateKey() {
  338. return 'node_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
  339. },
  340. /** 拖拽开始 */
  341. onDragStart(e, nodeType) {
  342. e.dataTransfer.setData('nodeType', JSON.stringify(nodeType))
  343. },
  344. /** 放置节点 */
  345. onDrop(e) {
  346. const nodeTypeStr = e.dataTransfer.getData('nodeType')
  347. if (!nodeTypeStr) return
  348. const nodeType = JSON.parse(nodeTypeStr)
  349. const rect = this.$refs.canvasContainer.getBoundingClientRect()
  350. const x = (e.clientX - rect.left - this.canvasOffset.x) / this.scale
  351. const y = (e.clientY - rect.top - this.canvasOffset.y) / this.scale
  352. const newNode = {
  353. nodeKey: this.generateKey(),
  354. nodeName: nodeType.typeName,
  355. nodeType: nodeType.typeCode,
  356. nodeIcon: nodeType.typeIcon,
  357. nodeColor: nodeType.typeColor,
  358. posX: Math.round(x - 50),
  359. posY: Math.round(y - 20),
  360. height: 40,
  361. nodeConfig: nodeType.defaultConfig || '{}'
  362. }
  363. this.nodes.push(newNode)
  364. this.selectNode(newNode)
  365. // 检测并扩展画布
  366. this.checkAndExpandCanvas(newNode)
  367. },
  368. /** 点击画布 */
  369. onCanvasClick() {
  370. this.clearSelection()
  371. },
  372. /** 画布鼠标按下 */
  373. onCanvasMouseDown(e) {
  374. // 只响应左键且点击在空白区域
  375. if (e.button !== 0) return
  376. if (e.target.tagName === 'rect' && e.target.getAttribute('fill') === 'url(#grid)') {
  377. this.isDraggingCanvas = true
  378. this.canvasDragStart = {
  379. x: e.clientX,
  380. y: e.clientY,
  381. scrollLeft: this.$refs.canvasContainer.scrollLeft,
  382. scrollTop: this.$refs.canvasContainer.scrollTop
  383. }
  384. e.preventDefault()
  385. }
  386. },
  387. /** 选中节点 */
  388. selectNode(node) {
  389. this.selectedNode = node
  390. this.selectedEdge = null
  391. },
  392. /** 选中连线 */
  393. selectEdge(edge) {
  394. this.selectedEdge = edge
  395. this.selectedNode = null
  396. },
  397. /** 清除选中 */
  398. clearSelection() {
  399. this.selectedNode = null
  400. this.selectedEdge = null
  401. },
  402. /** 节点鼠标按下 */
  403. onNodeMouseDown(e, node) {
  404. if (e.target.classList.contains('anchor')) return
  405. this.draggingNode = node
  406. const rect = this.$refs.canvasContainer.getBoundingClientRect()
  407. const scrollLeft = this.$refs.canvasContainer.scrollLeft
  408. const scrollTop = this.$refs.canvasContainer.scrollTop
  409. this.dragOffset = {
  410. x: (e.clientX - rect.left + scrollLeft - this.canvasOffset.x) / this.scale - node.posX,
  411. y: (e.clientY - rect.top + scrollTop - this.canvasOffset.y) / this.scale - node.posY
  412. }
  413. },
  414. /** 鼠标移动 */
  415. onMouseMove(e) {
  416. // 拖动画布
  417. if (this.isDraggingCanvas) {
  418. const dx = e.clientX - this.canvasDragStart.x
  419. const dy = e.clientY - this.canvasDragStart.y
  420. this.$refs.canvasContainer.scrollLeft = this.canvasDragStart.scrollLeft - dx
  421. this.$refs.canvasContainer.scrollTop = this.canvasDragStart.scrollTop - dy
  422. return
  423. }
  424. // 拖动节点
  425. if (this.draggingNode) {
  426. const rect = this.$refs.canvasContainer.getBoundingClientRect()
  427. const scrollLeft = this.$refs.canvasContainer.scrollLeft
  428. const scrollTop = this.$refs.canvasContainer.scrollTop
  429. const x = (e.clientX - rect.left + scrollLeft - this.canvasOffset.x) / this.scale - this.dragOffset.x
  430. const y = (e.clientY - rect.top + scrollTop - this.canvasOffset.y) / this.scale - this.dragOffset.y
  431. this.draggingNode.posX = Math.max(0, Math.round(x))
  432. this.draggingNode.posY = Math.max(0, Math.round(y))
  433. // 检测并扩展画布
  434. this.checkAndExpandCanvas(this.draggingNode)
  435. }
  436. // 连线
  437. if (this.connecting && this.connectStart) {
  438. const rect = this.$refs.canvasContainer.getBoundingClientRect()
  439. const scrollLeft = this.$refs.canvasContainer.scrollLeft
  440. const scrollTop = this.$refs.canvasContainer.scrollTop
  441. const endX = (e.clientX - rect.left + scrollLeft - this.canvasOffset.x) / this.scale
  442. const endY = (e.clientY - rect.top + scrollTop - this.canvasOffset.y) / this.scale
  443. this.tempEdge = {
  444. path: this.calcPath(this.connectStart.x, this.connectStart.y, endX, endY)
  445. }
  446. }
  447. },
  448. /** 检测并扩展画布 */
  449. checkAndExpandCanvas(node) {
  450. const nodeWidth = this.getNodeWidth(node)
  451. const nodeHeight = node.height || 40
  452. const padding = 200 // 边缘预留空间
  453. const expandStep = 500 // 每次扩展的大小
  454. const nodeRight = node.posX + nodeWidth
  455. const nodeBottom = node.posY + nodeHeight
  456. // 检测右边缘
  457. if (nodeRight + padding > this.canvasSize.width) {
  458. this.canvasSize.width += expandStep
  459. }
  460. // 检测下边缘
  461. if (nodeBottom + padding > this.canvasSize.height) {
  462. this.canvasSize.height += expandStep
  463. }
  464. },
  465. /** 鼠标松开 */
  466. onMouseUp(e) {
  467. // 停止拖动画布
  468. if (this.isDraggingCanvas) {
  469. this.isDraggingCanvas = false
  470. return
  471. }
  472. // 停止连线
  473. if (this.connecting) {
  474. const targetNode = this.findNodeAtPoint(e.clientX, e.clientY)
  475. if (targetNode && targetNode.nodeKey !== this.connectStart.nodeKey) {
  476. this.createEdge(this.connectStart.nodeKey, targetNode.nodeKey,
  477. this.connectStart.anchor, 'top')
  478. }
  479. this.connecting = false
  480. this.connectStart = null
  481. this.tempEdge = null
  482. }
  483. this.draggingNode = null
  484. },
  485. /** 开始连线 */
  486. startConnect(e, node, anchor) {
  487. this.connecting = true
  488. const anchorPos = this.getAnchorPos(node, anchor)
  489. this.connectStart = {
  490. nodeKey: node.nodeKey,
  491. anchor: anchor,
  492. x: anchorPos.x,
  493. y: anchorPos.y
  494. }
  495. },
  496. /** 获取锚点位置 */
  497. getAnchorPos(node, anchor) {
  498. const w = this.getNodeWidth(node)
  499. const h = node.height || 40
  500. switch (anchor) {
  501. case 'top': return { x: node.posX + w / 2, y: node.posY }
  502. case 'bottom': return { x: node.posX + w / 2, y: node.posY + h }
  503. case 'left': return { x: node.posX, y: node.posY + h / 2 }
  504. case 'right': return { x: node.posX + w, y: node.posY + h / 2 }
  505. default: return { x: node.posX + w / 2, y: node.posY + h }
  506. }
  507. },
  508. /** 查找点击位置的节点 */
  509. findNodeAtPoint(clientX, clientY) {
  510. const rect = this.$refs.canvasContainer.getBoundingClientRect()
  511. const scrollLeft = this.$refs.canvasContainer.scrollLeft
  512. const scrollTop = this.$refs.canvasContainer.scrollTop
  513. const x = (clientX - rect.left + scrollLeft - this.canvasOffset.x) / this.scale
  514. const y = (clientY - rect.top + scrollTop - this.canvasOffset.y) / this.scale
  515. return this.nodes.find(n => {
  516. const w = this.getNodeWidth(n)
  517. const h = n.height || 40
  518. return x >= n.posX && x <= n.posX + w && y >= n.posY && y <= n.posY + h
  519. })
  520. },
  521. /** 创建连线 */
  522. createEdge(sourceKey, targetKey, sourceAnchor, targetAnchor) {
  523. const exists = this.edges.find(e =>
  524. e.sourceNodeKey === sourceKey && e.targetNodeKey === targetKey)
  525. if (exists) return
  526. this.edges.push({
  527. edgeKey: 'edge_' + Date.now(),
  528. sourceNodeKey: sourceKey,
  529. targetNodeKey: targetKey,
  530. sourceAnchor: sourceAnchor,
  531. targetAnchor: targetAnchor,
  532. edgeType: 'smoothstep',
  533. edgeColor: '#999999'
  534. })
  535. },
  536. /** 获取连线路径 */
  537. getEdgePath(edge) {
  538. const sourceNode = this.nodes.find(n => n.nodeKey === edge.sourceNodeKey)
  539. const targetNode = this.nodes.find(n => n.nodeKey === edge.targetNodeKey)
  540. if (!sourceNode || !targetNode) return ''
  541. const start = this.getAnchorPos(sourceNode, edge.sourceAnchor || 'bottom')
  542. const end = this.getAnchorPos(targetNode, edge.targetAnchor || 'top')
  543. return this.calcPath(start.x, start.y, end.x, end.y)
  544. },
  545. /** 计算贝塞尔曲线路径 */
  546. calcPath(x1, y1, x2, y2) {
  547. const midY = (y1 + y2) / 2
  548. return `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`
  549. },
  550. /** 获取连线标签位置 */
  551. getEdgeLabelPos(edge) {
  552. const sourceNode = this.nodes.find(n => n.nodeKey === edge.sourceNodeKey)
  553. const targetNode = this.nodes.find(n => n.nodeKey === edge.targetNodeKey)
  554. if (!sourceNode || !targetNode) return { x: 0, y: 0 }
  555. const start = this.getAnchorPos(sourceNode, edge.sourceAnchor || 'bottom')
  556. const end = this.getAnchorPos(targetNode, edge.targetAnchor || 'top')
  557. return { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 }
  558. },
  559. /** 获取节点背景色 */
  560. getNodeBgColor(node) {
  561. const color = node.nodeColor || '#1890ff'
  562. return color + '15'
  563. },
  564. /** 计算节点宽度 */
  565. getNodeWidth(node) {
  566. const minWidth = 80
  567. const padding = 50 // 图标 + 左右padding
  568. const charWidth = 14 // 每个字符大约宽度
  569. const textWidth = (node.nodeName || '').length * charWidth
  570. return Math.max(minWidth, textWidth + padding)
  571. },
  572. /** 计算输入框宽度 */
  573. getInputWidth(node) {
  574. const minWidth = 40
  575. const charWidth = 14
  576. const textWidth = (node.nodeName || '').length * charWidth
  577. return Math.max(minWidth, textWidth + 10)
  578. },
  579. /** 节点名称输入事件 */
  580. onNodeNameInput(node) {
  581. // 触发视图更新
  582. this.$forceUpdate()
  583. },
  584. /** 获取节点类型名称 */
  585. getNodeTypeName(typeCode) {
  586. const t = this.nodeTypes.find(n => n.typeCode === typeCode)
  587. return t ? t.typeName : typeCode
  588. },
  589. /** 删除节点 */
  590. deleteNode() {
  591. if (!this.selectedNode) return
  592. this.$confirm('是否确认删除该节点?', '警告', {
  593. confirmButtonText: '确定',
  594. cancelButtonText: '取消',
  595. type: 'warning'
  596. }).then(() => {
  597. const key = this.selectedNode.nodeKey
  598. this.nodes = this.nodes.filter(n => n.nodeKey !== key)
  599. this.edges = this.edges.filter(e =>
  600. e.sourceNodeKey !== key && e.targetNodeKey !== key)
  601. this.selectedNode = null
  602. }).catch(() => {})
  603. },
  604. /** 删除连线 */
  605. deleteEdge() {
  606. if (!this.selectedEdge) return
  607. this.$confirm('是否确认删除该连线?', '警告', {
  608. confirmButtonText: '确定',
  609. cancelButtonText: '取消',
  610. type: 'warning'
  611. }).then(() => {
  612. this.edges = this.edges.filter(e => e.edgeKey !== this.selectedEdge.edgeKey)
  613. this.selectedEdge = null
  614. }).catch(() => {})
  615. },
  616. /** 保存工作流 */
  617. handleSave() {
  618. if (!this.form.workflowName) {
  619. this.msgWarning('请输入工作流名称')
  620. return
  621. }
  622. const data = {
  623. workflowId: this.workflowId,
  624. workflowName: this.form.workflowName,
  625. workflowDesc: this.form.workflowDesc,
  626. workflowType: this.form.workflowType,
  627. canvasData: JSON.stringify({ scale: this.scale, offset: this.canvasOffset }),
  628. nodes: this.nodes,
  629. edges: this.edges
  630. }
  631. if (this.workflowId) {
  632. updateWorkflow(data).then(() => {
  633. this.msgSuccess('保存成功')
  634. })
  635. } else {
  636. addWorkflow(data).then(res => {
  637. this.msgSuccess('保存成功')
  638. this.workflowId = res.data
  639. this.$router.replace('/his/aiWorkflow/design/' + this.workflowId)
  640. })
  641. }
  642. }
  643. }
  644. }
  645. </script>
  646. <style lang="scss" scoped>
  647. @import './design.scss';
  648. </style>