ソースを参照

外呼工作流新增版本管理,一键回退旧版本

zyy 2 日 前
コミット
019ececfba

+ 24 - 0
src/api/company/companyWorkflow.js

@@ -115,3 +115,27 @@ export function optionList() {
     method: 'get'
   })
 }
+
+//查看工作流历史版本
+export function getWorkflowVersionList(workflowId) {
+    return request({
+        url: '/company/companyWorkflow/versionList/' + workflowId,
+        method: 'get'
+    })
+}
+
+// 查询版本详情
+export function getWorkflowVersionDetail(versionId) {
+    return request({
+        url: '/company/companyWorkflow/versionDetail/' + versionId,
+        method: 'get'
+    })
+}
+
+// 回退到指定版本
+export function rollbackWorkflowVersion(versionId) {
+    return request({
+        url: '/company/companyWorkflow/versionRollback/' + versionId,
+        method: 'post'
+    })
+}

+ 16 - 0
src/router/index.js

@@ -404,6 +404,22 @@ export const constantRoutes = [
             }
         ]
     },
+    {
+        path: '/companyWorkflow/version-preview/:versionId',
+        component: Layout,
+        hidden: true,
+        children: [
+            {
+                path: '',
+                component: () => import('@/views/company/companyWorkflow/design'),
+                name: 'WorkflowVersionPreview',
+                meta: {
+                    title: '历史版本预览',
+                    activeMenu: '/companyWx/companyWorkflow'
+                }
+            }
+        ]
+    }
 
 ]
 

+ 221 - 110
src/views/company/companyWorkflow/design.vue

@@ -27,13 +27,20 @@
         <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>
+          <el-button
+              v-if="!readonlyMode"
+              type="primary"
+              icon="el-icon-check"
+              size="small"
+              @click="handleSave"
+          >保存</el-button>
+          <el-tag v-if="readonlyMode" type="info">历史版本预览</el-tag>
       </div>
     </div>
 
     <div class="main-content">
       <!-- 左侧节点面板 -->
-      <div class="node-panel">
+      <div class="node-panel" v-if="!readonlyMode">
         <div class="panel-title">节点类型</div>
         <div class="node-category" v-for="category in nodeCategories" :key="category.key">
           <div class="category-title">{{ category.name }}</div>
@@ -242,7 +249,7 @@
       </div>
 
       <!-- 右侧属性面板 -->
-      <div class="property-panel" v-if="selectedNode || selectedEdge">
+      <div class="property-panel" v-if="!readonlyMode && (selectedNode || selectedEdge)">
         <div class="panel-title">
           <span>
             <i :class="selectedNode ? 'el-icon-setting' : 'el-icon-share'"></i>
@@ -637,7 +644,7 @@ import {
   getSmsTempList,
   getCIDGroupList
 } from "@/api/company/companyVoiceRobotic";
-import { getWorkflow, addWorkflow, updateWorkflow, getNodeTypes } from '@/api/company/companyWorkflow'
+import { getWorkflow, addWorkflow, updateWorkflow, getNodeTypes, getWorkflowVersionDetail } from '@/api/company/companyWorkflow'
 import {getDicts} from "@/api/system/dict/data";
 import { getGatewayList, getLlmAccountList, getVoiceCodeList, getBusiGroupList } from '@/api/company/easyCall'
 // import { listAll } from '@/api/company/wxDialog';
@@ -657,6 +664,10 @@ export default {
       qwWxAddWayOptions: [],
       // 工作流ID
       workflowId: null,
+      // 历史版本ID
+      versionId: null,
+      // 是否只读预览
+      readonlyMode: false,
       // 表单数据
       form: {
         workflowName: '新建工作流',
@@ -737,8 +748,17 @@ export default {
   },
   created() {
     this.workflowId = this.$route.params.id
+    this.versionId = this.$route.params.versionId
+
+    // 如果有 versionId,说明是历史版本预览模式
+    if (this.versionId) {
+      this.readonlyMode = true
+    }
+
     this.loadNodeTypes()
-    if (this.workflowId) {
+    if (this.versionId) {
+      this.loadVersionWorkflow()
+    } else if (this.workflowId) {
       this.loadWorkflow()
     }
     // listAll().then(e => {
@@ -894,8 +914,9 @@ export default {
 
     // 局部键盘事件
     handleDelete(event) {
-      event.preventDefault() // 阻止默认行为(如浏览器后退)
-      this.deleteSelected()
+          if (this.readonlyMode) return
+          event.preventDefault()
+          this.deleteSelected()
     },
 
     // 全局键盘事件
@@ -1043,9 +1064,86 @@ export default {
         this.edges = data.edges || []
       })
     },
+    /** 加载历史版本详情(只读预览) */
+    loadVersionWorkflow() {
+      getWorkflowVersionDetail(this.versionId).then(res => {
+          const data = res.data || {}
+          const version = data.workflowVersion || {}
+
+          this.form = {
+              workflowName: version.workflowName || '',
+              workflowDesc: version.workflowDesc || '',
+              workflowType: String(version.workflowType || '1'),
+              canvasData: version.canvasData || ''
+          }
+
+          let nodes = data.nodes || []
+          let edges = data.edges || []
+
+          // 处理连线条件
+          if (edges && edges.length > 0) {
+              edges.forEach(edge => {
+                  const con = edge.conditionExpr
+                  if (con == null || con === '') {
+                      edge.conditionExprObj = []
+                  } else {
+                      try {
+                          edge.conditionExprObj = JSON.parse(con)
+                      } catch (e) {
+                          edge.conditionExprObj = []
+                      }
+                  }
+              })
+          }
+
+          // 处理节点配置
+          if (nodes && nodes.length > 0) {
+              nodes.forEach(node => {
+                  if (typeof node.nodeConfig === 'string') {
+                      try {
+                          const parsedConfig = JSON.parse(node.nodeConfig)
+                          node.nodeConfig = Object.assign({}, parsedConfig)
+                      } catch (e) {
+                          node.nodeConfig = {}
+                      }
+                  }
+                  if (!node.nodeConfig || typeof node.nodeConfig !== 'object') {
+                      node.nodeConfig = {}
+                  }
+              })
+          }
+
+          this.nodes = nodes
+          this.edges = edges
+
+          // 恢复画布状态
+          if (this.form.canvasData) {
+              try {
+                  const canvasData = JSON.parse(this.form.canvasData)
+                  this.scale = canvasData.scale || 1
+                  this.canvasOffset = canvasData.offset || { x: 0, y: 0 }
+              } catch (e) {
+                  this.scale = 1
+                  this.canvasOffset = { x: 0, y: 0 }
+              }
+          }
+      })
+    },
     /** 返回列表 */
     goBack() {
-      this.$router.push('/companyWx/companyWorkflow')
+        // 历史版本预览模式下,返回到列表页并自动打开对应工作流的历史版本弹窗
+        if (this.readonlyMode && this.$route.query.workflowId) {
+            this.$router.push({
+                path: '/companyWx/companyWorkflow',
+                query: {
+                    openVersionWorkflowId: this.$route.query.workflowId
+                }
+            })
+            return
+        }
+
+        // 普通编辑模式保持原逻辑
+        this.$router.push('/companyWx/companyWorkflow')
     },
     /** 放大 */
     zoomIn() {
@@ -1067,51 +1165,48 @@ export default {
     },
     /** 复制节点 */
     copyNode(e) {
-      if (!this.selectedNode) {
-        return
-      }
-      e.preventDefault()
-      // 深拷贝选中的节点,包括所有属性
-      this.copiedNode = JSON.parse(JSON.stringify(this.selectedNode))
-      this.$message.success('节点已复制')
+        if (this.readonlyMode) return
+        if (!this.selectedNode) {
+            return
+        }
+        e.preventDefault()
+        this.copiedNode = JSON.parse(JSON.stringify(this.selectedNode))
+        this.$message.success('节点已复制')
     },
     /** 粘贴节点 */
     pasteNode(e) {
-      if (!this.copiedNode) {
-        this.$message.warning('没有可粘贴的节点')
-        return
-      }
-      e.preventDefault()
+        if (this.readonlyMode) return
+        if (!this.copiedNode) {
+            this.$message.warning('没有可粘贴的节点')
+            return
+        }
+        e.preventDefault()
 
-      // 创建新节点,复制所有属性
-      const newNode = {
-        ...this.copiedNode,
-        nodeKey: this.generateKey(),
-        // 在原位置偏移一定距离
-        posX: this.copiedNode.posX + 30,
-        posY: this.copiedNode.posY + 30,
-        // 深拷贝 nodeConfig
-        nodeConfig: this.copiedNode.nodeConfig ? JSON.parse(JSON.stringify(this.copiedNode.nodeConfig)) : {}
-      }
+        const newNode = {
+            ...this.copiedNode,
+            nodeKey: this.generateKey(),
+            posX: this.copiedNode.posX + 30,
+            posY: this.copiedNode.posY + 30,
+            nodeConfig: this.copiedNode.nodeConfig ? JSON.parse(JSON.stringify(this.copiedNode.nodeConfig)) : {}
+        }
 
-      this.nodes.push(newNode)
-      // 选中新节点
-      this.selectedNode = newNode
-      this.selectedEdge = null
+        this.nodes.push(newNode)
+        this.selectedNode = newNode
+        this.selectedEdge = null
 
-      // 检查是否需要扩展画布
-      this.checkAndExpandCanvas(newNode)
+        this.checkAndExpandCanvas(newNode)
 
-      this.$message.success('节点已粘贴')
+        this.$message.success('节点已粘贴')
     },
     /** 显示右键菜单 */
     showContextMenu(e) {
-      // 获取鼠标相对于画布容器的位置
-      const rect = this.$refs.canvasContainer.getBoundingClientRect()
-      this.contextMenu.x = e.clientX - rect.left
-      this.contextMenu.y = e.clientY - rect.top
-      this.contextMenu.node = this.selectedNode
-      this.contextMenu.visible = true
+        if (this.readonlyMode) return
+
+        const rect = this.$refs.canvasContainer.getBoundingClientRect()
+        this.contextMenu.x = e.clientX - rect.left
+        this.contextMenu.y = e.clientY - rect.top
+        this.contextMenu.node = this.selectedNode
+        this.contextMenu.visible = true
     },
     /** 隐藏右键菜单 */
     hideContextMenu() {
@@ -1169,62 +1264,63 @@ export default {
     },
     /** 放置节点 */
     onDrop(e) {
-      const nodeTypeStr = e.dataTransfer.getData('nodeType')
-      if (!nodeTypeStr) return
-      const nodeType = JSON.parse(nodeTypeStr)
-
-      // 检查开始/结束节点是否已存在(限制只能有一个)
-      if (this.isStartNodeType(nodeType.typeCode)) {
-        const existingStart = this.nodes.find(n => this.isStartNodeType(n.nodeType))
-        if (existingStart) {
-          this.$message.warning('已存在开始节点,工作流只能有一个开始节点')
-          return
+        if (this.readonlyMode) return
+
+        const nodeTypeStr = e.dataTransfer.getData('nodeType')
+        if (!nodeTypeStr) return
+        const nodeType = JSON.parse(nodeTypeStr)
+
+        if (this.isStartNodeType(nodeType.typeCode)) {
+            const existingStart = this.nodes.find(n => this.isStartNodeType(n.nodeType))
+            if (existingStart) {
+                this.$message.warning('已存在开始节点,工作流只能有一个开始节点')
+                return
+            }
         }
-      }
-      if (this.isEndNodeType(nodeType.typeCode)) {
-        const existingEnd = this.nodes.find(n => this.isEndNodeType(n.nodeType))
-        if (existingEnd) {
-          this.$message.warning('已存在结束节点,工作流只能有一个结束节点')
-          return
+        if (this.isEndNodeType(nodeType.typeCode)) {
+            const existingEnd = this.nodes.find(n => this.isEndNodeType(n.nodeType))
+            if (existingEnd) {
+                this.$message.warning('已存在结束节点,工作流只能有一个结束节点')
+                return
+            }
         }
-      }
 
-      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: {} // 初始化为空对象
-      }
-      this.nodes.push(newNode)
-      this.selectNode(newNode)
-      this.focusCanvasContainer()
-      // 检测并扩展画布
-      this.checkAndExpandCanvas(newNode)
+        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: {}
+        }
+        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
+        if (this.readonlyMode) return
+
+        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()
         }
-        e.preventDefault()
-      }
     },
 
     /** 清除选中 */
@@ -1234,16 +1330,21 @@ export default {
     },
     /** 节点鼠标按下 */
     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)
+        if (this.readonlyMode) {
+            this.selectNode(node)
+            return
+        }
+
+        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) {
@@ -1320,15 +1421,17 @@ export default {
     },
     /** 开始连线 */
     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)
+        if (this.readonlyMode) return
+
+        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) {
@@ -1508,6 +1611,14 @@ export default {
     },
     /** 保存工作流 */
     handleSave() {
+      if (this.readonlyMode) {
+        return
+      }
+
+      if (!this.form.workflowName) {
+        this.$message.warning('请输入工作流名称')
+        return
+      }
       if (!this.form.workflowName) {
         this.$message.warning('请输入工作流名称')
         return

+ 170 - 8
src/views/company/companyWorkflow/index.vue

@@ -121,6 +121,12 @@
             icon="el-icon-delete"
             @click="handleDelete(scope.row)"
           >删除</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleVersion(scope.row)"
+          >历史版本</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -185,11 +191,72 @@
         <el-button type="primary" :disabled="selectedSalesList.length === 0" @click="confirmBindSales">确 定</el-button>
       </div>
     </el-dialog>
+
+
+    <!-- 历史版本弹窗 -->
+    <el-dialog
+      title="历史版本"
+      :visible.sync="versionOpen"
+      width="60%"
+      custom-class="version-dialog"
+      append-to-body
+    >
+      <el-table
+          v-loading="versionLoading"
+          :data="versionList"
+          border
+          size="small"
+      >
+          <el-table-column label="版本号" align="center" width="120">
+              <template slot-scope="scope">
+                  <el-tag v-if="scope.row.versionNo === currentVersionNo" type="warning">
+                      v{{ scope.row.versionNo }}(当前)
+                  </el-tag>
+                  <el-tag v-else type="success">
+                      v{{ scope.row.versionNo }}
+                  </el-tag>
+              </template>
+          </el-table-column>
+
+          <el-table-column label="工作流名称" align="center" prop="workflowName" min-width="60" />
+
+          <el-table-column label="快照时间" align="center" width="170">
+              <template slot-scope="scope">
+                  <span>{{scope.row.snapshotTime }}</span>
+              </template>
+          </el-table-column>
+
+          <el-table-column label="操作人" align="center" prop="snapshotBy" width="120" />
+
+          <el-table-column label="操作" align="center" width="180">
+              <template slot-scope="scope">
+                  <el-button
+                      type="text"
+                      size="mini"
+                      @click="handleViewVersion(scope.row)"
+                  >查看</el-button>
+                  <el-button
+                      v-if="versionList.length > 1 && scope.row.versionNo !== currentVersionNo"
+                      type="text"
+                      size="mini"
+                      style="color: #f56c6c"
+                      @click="handleRollbackVersion(scope.row)"
+                  >回退</el-button>
+              </template>
+          </el-table-column>
+      </el-table>
+
+      <div slot="footer" class="dialog-footer">
+          <el-button @click="versionOpen = false">关闭</el-button>
+      </div>
+    </el-dialog>
+
   </div>
 </template>
 
 <script>
-import { listWorkflow, delWorkflow, updateWorkflowStatus, copyWorkflow, exportWorkflow, getBindCompanyUserByWorkflowId, listCompanyUser, updateWorkflowBindCompanyUser } from '@/api/company/companyWorkflow'
+import { listWorkflow, delWorkflow, updateWorkflowStatus, copyWorkflow, exportWorkflow, getBindCompanyUserByWorkflowId,
+         listCompanyUser, updateWorkflowBindCompanyUser, getWorkflowVersionList, getWorkflowVersionDetail, rollbackWorkflowVersion} from '@/api/company/companyWorkflow'
 
 export default {
   name: 'CompanyWorkflow',
@@ -243,7 +310,24 @@ export default {
       // 选中的销售列表
       selectedSalesList: [],
       // 销售搜索关键字
-      salesSearchKey: ''
+      salesSearchKey: '',
+      // 历史版本弹窗
+      versionOpen: false,
+      // 版本列表
+      versionList: [],
+      // 加载状态
+      versionLoading: false,
+      // 当前版本详情弹窗
+      versionDetailOpen: false,
+      // 当前版本详情加载
+      versionDetailLoading: false,
+      // 当前版本详情
+      versionDetail: {
+        workflowVersion: {},
+        nodes: [],
+        edges: []
+      },
+      currentVersionNo: null,
     }
   },
   computed: {
@@ -265,15 +349,36 @@ export default {
     //   this.workflowTypeOptions = response.data
     // })
   },
+  watch: {
+    '$route.query.openVersionWorkflowId': {
+        handler(val) {
+            if (val) {
+                this.$nextTick(() => {
+                    this.autoOpenVersionDialog(val)
+                })
+            }
+        },
+        immediate: true
+    }
+  },
   methods: {
     /** 查询工作流列表 */
     getList() {
-      this.loading = true
-      listWorkflow(this.queryParams).then(response => {
-        this.workflowList = response.rows
-        this.total = response.total
-        this.loading = false
-      })
+        this.loading = true
+        listWorkflow(this.queryParams).then(response => {
+            this.workflowList = response.rows
+            this.total = response.total
+            this.loading = false
+
+            const workflowId = this.$route.query.openVersionWorkflowId
+            if (workflowId) {
+                this.$nextTick(() => {
+                    this.autoOpenVersionDialog(workflowId)
+                })
+            }
+        }).catch(() => {
+            this.loading = false
+        })
     },
     /** 搜索按钮操作 */
     handleQuery() {
@@ -436,6 +541,63 @@ export default {
         this.bindSalesOpen = false
         this.getList()
       })
+    },
+    /** 打开历史版本弹窗 */
+    handleVersion(row) {
+      this.currentWorkflow = row
+      this.currentVersionNo = row.version
+      this.versionOpen = true
+      this.versionLoading = true
+      this.versionList = []
+
+      getWorkflowVersionList(row.workflowId).then(res => {
+          this.versionList = res.data || []
+          this.versionLoading = false
+      }).catch(() => {
+          this.versionLoading = false
+      })
+    },
+
+    /** 查看版本详情 */
+    handleViewVersion(row) {
+        this.$router.push({
+            path: '/companyWorkflow/version-preview/' + row.versionId,
+            query: {
+                workflowId: this.currentWorkflow.workflowId
+            }
+        })
+    },
+
+    /** 一键回退 */
+    handleRollbackVersion(row) {
+      this.$confirm('是否确认回退到版本 v' + row.versionNo + ' ?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+      }).then(() => {
+          return rollbackWorkflowVersion(row.versionId)
+      }).then(() => {
+          this.msgSuccess('回退成功')
+          this.versionOpen = false
+          this.versionDetailOpen = false
+          this.getList()
+      }).catch(() => {})
+    },
+    /** 自动打开历史版本弹窗 */
+    autoOpenVersionDialog(workflowId) {
+      const row = this.workflowList.find(item => String(item.workflowId) === String(workflowId))
+      if (!row) {
+          return
+      }
+      this.handleVersion(row)
+
+      // 清掉 query,避免重复触发
+      const query = { ...this.$route.query }
+      delete query.openVersionWorkflowId
+      this.$router.replace({
+          path: this.$route.path,
+          query
+      })
     }
   }
 }