Jelajahi Sumber

【功能新增】 仿钉钉流程模型增加浏览模式

jason 9 bulan lalu
induk
melakukan
137b33e7cf

+ 3 - 0
src/components/SimpleProcessDesignerV2/src/NodeHandler.vue

@@ -6,6 +6,7 @@
         v-model:visible="popoverShow"
         placement="right-start"
         width="auto"
+        v-if="!readonly"
       >
         <div class="handler-item-wrapper">
           <div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)">
@@ -78,6 +79,8 @@ const props = defineProps({
 
 const emits = defineEmits(['update:childNode'])
 
+const readonly = inject<Boolean>('readonly') // 是否只读
+
 const addNode = (type: number) => {
   popoverShow.value = false
   if (type === NodeType.USER_TASK_NODE) {

+ 1 - 1
src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue

@@ -47,7 +47,7 @@
   />
 
   <!-- 结束节点 -->
-  <EndEventNode v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE" />
+  <EndEventNode v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE" :flow-node="currentNode" />
 </template>
 <script setup lang="ts">
 import StartUserNode from './nodes/StartUserNode.vue'

+ 2 - 1
src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue

@@ -57,7 +57,7 @@ const props = defineProps({
     required: true
   }
 })
-const loading = ref(true)
+const loading = ref(false)
 const formFields = ref<string[]>([])
 const formType = ref(20)
 const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
@@ -66,6 +66,7 @@ const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
 const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
 const deptTreeOptions = ref()
 const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
+provide('readonly', false)
 provide('formFields', formFields)
 provide('formType', formType)
 provide('roleList', roleOptions)

+ 63 - 0
src/components/SimpleProcessDesignerV2/src/SimpleProcessViewer.vue

@@ -0,0 +1,63 @@
+<template>
+  <div class="simple-flow-canvas" v-loading="loading">
+    <div  class="simple-flow-container">
+      <div class="scale-container" :style="`transform: scale(${scaleValue / 100});`">
+        <ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree"/>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import ProcessNodeTree from './ProcessNodeTree.vue'
+import { SimpleFlowNode } from './consts'
+
+defineOptions({
+  name: 'SimpleProcessRender'
+})
+
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+const loading = ref(false)
+
+watch(
+  () => props.flowNode,
+  (newValue) => {
+    processNodeTree.value = newValue
+  }
+)
+const processNodeTree = ref<SimpleFlowNode | undefined>(props.flowNode)
+provide('readonly', true)
+let scaleValue = ref(100)
+const MAX_SCALE_VALUE = 200
+const MIN_SCALE_VALUE = 50
+// 放大
+const zoomOut = () => {
+  if (scaleValue.value == MAX_SCALE_VALUE) {
+    return
+  }
+  scaleValue.value += 10
+}
+// 缩小
+const zoomIn = () => {
+  if (scaleValue.value == MIN_SCALE_VALUE) {
+    return
+  }
+  scaleValue.value -= 10
+}
+
+// onMounted(async () => {
+//   try {
+//     loading.value = true
+//     if (props.view) {
+//       processNodeTree.value = props.view.simpleModel
+//     }
+//   } finally {
+//     loading.value = false
+//   }
+// })
+</script>

+ 3 - 1
src/components/SimpleProcessDesignerV2/src/consts.ts

@@ -1,6 +1,6 @@
 // @ts-ignore
 import { DictDataVO } from '@/api/system/dict/types'
-
+import { TaskStatusEnum } from '@/api/bpm/task'
 /**
  * 节点类型
  */
@@ -96,6 +96,8 @@ export interface SimpleFlowNode {
   conditionGroups?: ConditionGroup
   // 是否默认的条件
   defaultFlow?: boolean
+  // 活动的状态,用于前端节点状态展示
+  activityStatus? : TaskStatusEnum
 }
 // 候选人策略枚举 ( 用于审批节点。抄送节点 )
 export enum CandidateStrategy {

+ 2 - 1
src/components/SimpleProcessDesignerV2/src/index.ts

@@ -1,4 +1,5 @@
 import SimpleProcessDesigner from './SimpleProcessDesigner.vue'
+import SimpleProcessViewer from './SimpleProcessViewer.vue'
 import '../theme/simple-process-designer.scss'
 
-export { SimpleProcessDesigner }
+export { SimpleProcessDesigner, SimpleProcessViewer }

+ 24 - 0
src/components/SimpleProcessDesignerV2/src/node.ts

@@ -1,4 +1,5 @@
 import { cloneDeep } from 'lodash-es'
+import { TaskStatusEnum } from '@/api/bpm/task'
 import * as RoleApi from '@/api/system/role'
 import * as DeptApi from '@/api/system/dept'
 import * as PostApi from '@/api/system/post'
@@ -476,3 +477,26 @@ export function useNodeName2(node: Ref<SimpleFlowNode>, nodeType: NodeType) {
     blurEvent
   }
 }
+
+/**
+ * @description 根据节点任务状态,获取节点任务状态样式
+ */
+export function  useTaskStatusClass(taskStatus: TaskStatusEnum | undefined) : string {
+  if (!taskStatus) {
+    return ''
+  }
+  if (taskStatus === TaskStatusEnum.APPROVE ) {
+    return 'status-pass'
+  }
+  if (taskStatus === TaskStatusEnum.RUNNING ) {
+    return 'status-running'
+  }
+  if (taskStatus === TaskStatusEnum.REJECT ) {
+    return 'status-reject'
+  }
+  if (taskStatus === TaskStatusEnum.CANCEL ) {
+    return 'status-cancel'
+  }
+   
+  return '';
+}

+ 9 - 5
src/components/SimpleProcessDesignerV2/src/nodes/CopyTaskNode.vue

@@ -5,7 +5,7 @@
         <div class="node-title-container">
           <div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div>
           <input
-            v-if="showInput"
+            v-if="!readonly && showInput"
             type="text"
             class="editable-title-input"
             @blur="blurEvent()"
@@ -24,9 +24,9 @@
           <div class="node-text" v-else>
             {{ NODE_DEFAULT_TEXT.get(NodeType.COPY_TASK_NODE) }}
           </div>
-          <Icon icon="ep:arrow-right-bold" />
+          <Icon v-if="!readonly" icon="ep:arrow-right-bold" />
         </div>
-        <div class="node-toolbar">
+        <div v-if="!readonly" class="node-toolbar">
           <div class="toolbar-icon"
             ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
           /></div>
@@ -36,7 +36,7 @@
       <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
       <NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
     </div>
-    <CopyTaskNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" />
+    <CopyTaskNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" />
   </div>
 </template>
 <script setup lang="ts">
@@ -57,7 +57,8 @@ const props = defineProps({
 const emits = defineEmits<{
   'update:flowNode': [node: SimpleFlowNode | undefined]
 }>()
-
+// 是否只读
+const readonly = inject<Boolean>('readonly') 
 // 监控节点的变化
 const currentNode = useWatchNode(props)
 // 节点名称编辑
@@ -66,6 +67,9 @@ const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.
 const nodeSetting = ref()
 // 打开节点配置
 const openNodeConfig = () => {
+  if (readonly) {
+    return
+  }
   nodeSetting.value.showCopyTaskNodeConfig(currentNode.value)
   nodeSetting.value.openDrawer()
 }

+ 15 - 1
src/components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue

@@ -1,13 +1,27 @@
 <template>
   <div class="end-node-wrapper">
-    <div class="end-node-box">
+    <div class="end-node-box" :class="taskStatusClass">
       <span class="node-fixed-name" title="结束">结束</span>
     </div>
   </div>
 </template>
 <script setup lang="ts">
+import { SimpleFlowNode } from '../consts'
+import { useWatchNode, useTaskStatusClass } from '../node'
+
 defineOptions({
   name: 'EndEventNode'
 })
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    default: () => null
+  }
+})
+// 监控节点变化
+const currentNode = useWatchNode(props)
+// 节点任务状态样式
+const taskStatusClass = useTaskStatusClass(currentNode.value?.activityStatus)
+
 </script>
 <style lang="scss" scoped></style>

+ 21 - 6
src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue

@@ -1,7 +1,13 @@
 <template>
   <div class="branch-node-wrapper">
     <div class="branch-node-container">
-      <el-button class="branch-node-add" color="#67c23a" @click="addCondition"  plain>添加条件</el-button>
+      <div v-if="readonly" class="branch-node-readonly" :class="taskStatusClass">
+        <span class="iconfont icon-exclusive icon-size"></span>
+      </div>
+      <el-button v-else class="branch-node-add" color="#67c23a" @click="addCondition" plain
+        >添加条件</el-button
+      >
+
       <div
         class="branch-node-item"
         v-for="(item, index) in currentNode.conditionNodes"
@@ -17,9 +23,9 @@
         </template>
         <div class="node-wrapper">
           <div class="node-container">
-            <div class="node-box" :class="{ 'node-config-error': !item.showText }">
+            <div class="node-box" :class="[{ 'node-config-error': !item.showText }, `${useTaskStatusClass(item.activityStatus)}`]">
               <div class="branch-node-title-container">
-                <div v-if="showInputs[index]">
+                <div v-if="!readonly && showInputs[index]">
                   <input
                     type="text"
                     class="input-max-width editable-title-input"
@@ -39,7 +45,10 @@
                   {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
                 </div>
               </div>
-              <div class="node-toolbar" v-if="index + 1 !== currentNode.conditionNodes?.length">
+              <div
+                class="node-toolbar"
+                v-if="!readonly && index + 1 !== currentNode.conditionNodes?.length"
+              >
                 <div class="toolbar-icon">
                   <Icon
                     color="#0089ff"
@@ -87,6 +96,7 @@ import NodeHandler from '../NodeHandler.vue'
 import ProcessNodeTree from '../ProcessNodeTree.vue'
 import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
 import { getDefaultConditionNodeName } from '../utils'
+import { useTaskStatusClass } from '../node'
 import { generateUUID } from '@/utils'
 import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
 const { proxy } = getCurrentInstance() as any
@@ -109,9 +119,11 @@ const emits = defineEmits<{
     nodeType: number
   ]
 }>()
-
+// 是否只读
+const readonly = inject<Boolean>('readonly') 
 const currentNode = ref<SimpleFlowNode>(props.flowNode)
-// const conditionNodes = computed(() => currentNode.value.conditionNodes);
+// 节点状态样式
+const taskStatusClass = useTaskStatusClass(currentNode.value?.activityStatus)
 
 watch(
   () => props.flowNode,
@@ -135,6 +147,9 @@ const clickEvent = (index: number) => {
 }
 
 const conditionNodeConfig = (nodeId: string) => {
+  if (readonly) {
+    return
+  }
   const conditionNode = proxy.$refs[nodeId][0]
   conditionNode.open()
 }

+ 12 - 4
src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue

@@ -1,7 +1,10 @@
 <template>
   <div class="branch-node-wrapper">
     <div class="branch-node-container">
-      <el-button class="branch-node-add" color="#345da2" @click="addCondition"  plain>添加条件</el-button>
+      <div v-if="readonly" class="branch-node-readonly">
+        <span class="iconfont icon-inclusive icon-size"></span>
+      </div>
+      <el-button v-else class="branch-node-add" color="#345da2" @click="addCondition"  plain>添加条件</el-button>
       <div
         class="branch-node-item"
         v-for="(item, index) in currentNode.conditionNodes"
@@ -38,7 +41,7 @@
                   {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
                 </div>
               </div>
-              <div class="node-toolbar" v-if="index + 1 !== currentNode.conditionNodes?.length">
+              <div class="node-toolbar" v-if="!readonly && index + 1 !== currentNode.conditionNodes?.length">
                 <div class="toolbar-icon">
                   <Icon
                     color="#0089ff"
@@ -50,7 +53,7 @@
               </div>
               <div
                 class="branch-node-move move-node-left"
-                v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length"
+                v-if="!readonly && index != 0 && index + 1 !== currentNode.conditionNodes?.length"
                 @click="moveNode(index, -1)"
               >
                 <Icon icon="ep:arrow-left" />
@@ -58,7 +61,7 @@
 
               <div
                 class="branch-node-move move-node-right"
-                v-if="currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2"
+                v-if="!readonly && currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2"
                 @click="moveNode(index, 1)"
               >
                 <Icon icon="ep:arrow-right" />
@@ -108,6 +111,8 @@ const emits = defineEmits<{
     nodeType: number
   ]
 }>()
+// 是否只读
+const readonly = inject<Boolean>('readonly')
 
 const currentNode = ref<SimpleFlowNode>(props.flowNode)
 
@@ -133,6 +138,9 @@ const clickEvent = (index: number) => {
 }
 
 const conditionNodeConfig = (nodeId: string) => {
+  if (readonly) {
+    return
+  }
   const conditionNode = proxy.$refs[nodeId][0]
   conditionNode.open()
 }

+ 7 - 14
src/components/SimpleProcessDesignerV2/src/nodes/ParallelNode.vue

@@ -1,7 +1,10 @@
 <template>
   <div class="branch-node-wrapper">
     <div class="branch-node-container">
-      <el-button class="branch-node-add" color="#626aef" @click="addCondition"  plain>添加分支</el-button>
+      <div v-if="readonly" class="branch-node-readonly">
+        <span class="iconfont icon-parallel icon-size"></span>
+      </div>
+      <el-button v-else class="branch-node-add" color="#626aef" @click="addCondition"  plain>添加分支</el-button>
       <div
         class="branch-node-item"
         v-for="(item, index) in currentNode.conditionNodes"
@@ -39,7 +42,7 @@
                   {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
                 </div>
               </div>
-              <div class="node-toolbar">
+              <div  v-if="!readonly" class="node-toolbar">
                 <div class="toolbar-icon">
                   <Icon
                     color="#0089ff"
@@ -49,18 +52,6 @@
                   />
                 </div>
               </div>
-              <!-- <div 
-                class="branch-node-move move-node-left"
-                v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length" @click="moveNode(index, -1)">
-                <Icon icon="ep:arrow-left" />
-              </div> -->
-
-              <!-- <div 
-                class="branch-node-move move-node-right"
-                v-if="currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2"
-                @click="moveNode(index, 1)">
-                <Icon icon="ep:arrow-right" />
-              </div> -->
             </div>
             <NodeHandler v-model:child-node="item.childNode" />
           </div>
@@ -106,6 +97,8 @@ const emits = defineEmits<{
 }>()
 
 const currentNode = ref<SimpleFlowNode>(props.flowNode)
+// 是否只读
+const readonly = inject<Boolean>('readonly') 
 
 watch(
   () => props.flowNode,

+ 14 - 4
src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue

@@ -1,7 +1,10 @@
 <template>
   <div class="node-wrapper">
     <div class="node-container">
-      <div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
+      <div
+        class="node-box"
+        :class="[{ 'node-config-error': !currentNode.showText }, `${taskStatusClass}`]"
+      >
         <div class="node-title-container">
           <div class="node-title-icon start-user"
             ><span class="iconfont icon-start-user"></span
@@ -26,18 +29,18 @@
           <div class="node-text" v-else>
             {{ NODE_DEFAULT_TEXT.get(NodeType.START_USER_NODE) }}
           </div>
-          <Icon icon="ep:arrow-right-bold" />
+          <Icon icon="ep:arrow-right-bold" v-if="!readonly" />
         </div>
       </div>
       <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
       <NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
     </div>
   </div>
-  <StartUserNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" />
+  <StartUserNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" />
 </template>
 <script setup lang="ts">
 import NodeHandler from '../NodeHandler.vue'
-import { useWatchNode, useNodeName2 } from '../node'
+import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node'
 import { SimpleFlowNode, NODE_DEFAULT_TEXT, NodeType } from '../consts'
 import StartUserNodeConfig from '../nodes-config/StartUserNodeConfig.vue'
 defineOptions({
@@ -49,21 +52,28 @@ const props = defineProps({
     default: () => null
   }
 })
+const readonly = inject<Boolean>('readonly') // 是否只读
 // 定义事件,更新父组件。
 const emits = defineEmits<{
   'update:modelValue': [node: SimpleFlowNode | undefined]
 }>()
 // 监控节点变化
 const currentNode = useWatchNode(props)
+// 节点任务状态样式
+const taskStatusClass = useTaskStatusClass(currentNode.value?.activityStatus)
 // 节点名称编辑
 const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
 
 const nodeSetting = ref()
 // 打开节点配置
 const openNodeConfig = () => {
+  if (readonly) {
+    return
+  }
   // 把当前节点传递给配置组件
   nodeSetting.value.showStartUserNodeConfig(currentNode.value)
   nodeSetting.value.openDrawer()
 }
+
 </script>
 <style lang="scss" scoped></style>

+ 16 - 5
src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue

@@ -1,11 +1,14 @@
 <template>
   <div class="node-wrapper">
     <div class="node-container">
-      <div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
+      <div
+        class="node-box"
+        :class="[{ 'node-config-error': !currentNode.showText }, `${taskStatusClass}`]"
+      >
         <div class="node-title-container">
           <div class="node-title-icon user-task"><span class="iconfont icon-approve"></span></div>
           <input
-            v-if="showInput"
+            v-if="!readonly && showInput"
             type="text"
             class="editable-title-input"
             @blur="blurEvent()"
@@ -24,9 +27,9 @@
           <div class="node-text" v-else>
             {{ NODE_DEFAULT_TEXT.get(NodeType.USER_TASK_NODE) }}
           </div>
-          <Icon icon="ep:arrow-right-bold" />
+          <Icon icon="ep:arrow-right-bold" v-if="!readonly" />
         </div>
-        <div class="node-toolbar">
+        <div v-if="!readonly" class="node-toolbar">
           <div class="toolbar-icon"
             ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
           /></div>
@@ -45,7 +48,7 @@
 </template>
 <script setup lang="ts">
 import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
-import { useWatchNode, useNodeName2 } from '../node'
+import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node'
 import NodeHandler from '../NodeHandler.vue'
 import UserTaskNodeConfig from '../nodes-config/UserTaskNodeConfig.vue'
 defineOptions({
@@ -61,13 +64,21 @@ const emits = defineEmits<{
   'update:flowNode': [node: SimpleFlowNode | undefined]
   'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: NodeType]
 }>()
+
+// 是否只读
+const readonly = inject<Boolean>('readonly')
 // 监控节点变化
 const currentNode = useWatchNode(props)
+// 节点状态样式
+const taskStatusClass = useTaskStatusClass(currentNode.value?.activityStatus)
 // 节点名称编辑
 const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
 const nodeSetting = ref()
 // 打开节点配置
 const openNodeConfig = () => {
+  if (readonly) {
+    return
+  }
   // 把当前节点传递给配置组件
   nodeSetting.value.showUserTaskNodeConfig(currentNode.value)
   nodeSetting.value.openDrawer()

+ 80 - 14
src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss

@@ -1,10 +1,8 @@
 .simple-flow-canvas {
-  position: absolute;
-  inset: 0;
   z-index: 1;
   overflow: auto;
   background-color: #fafafa;
-  user-select: none;
+  // user-select: none;
 
   .simple-flow-container {
     position: relative;
@@ -84,12 +82,32 @@
         background-color: #fff;
         flex-direction: column;
         border: 2px solid transparent;
-        // border-color: #0089ff;
         border-radius: 8px;
-        // border-color: #0089ff;
         box-shadow: 0 1px 4px 0 rgba(10, 30, 65, 0.16);
         transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
 
+        &.status-pass {
+          border-color: #67c23a;
+          background-color: #a9da90;
+        }
+        &.status-pass:hover {
+          border-color: #67c23a;
+        }
+        &.status-running {
+          border-color: #5a9cf8;
+          background-color: #e7f0fe;
+        }
+        &.status-running:hover {
+          border-color: #5a9cf8;
+        }
+        &.status-reject {
+          border-color: #e47470;
+          background-color: #f6e5e5;
+        }
+        &.status-reject:hover {
+          border-color: #e47470;
+        }
+
         &:hover {
           border-color: #0089ff;
           .node-toolbar {
@@ -280,15 +298,10 @@
 
         &::before {
           position: absolute;
-          top: 0;
-          right: 0;
-          left: 0;
-          // bottom: 5px;
-          bottom: 0px;
+          top:0;
           z-index: 0;
           width: 2px;
           height: 100%;
-          // height: calc(100% - 5px);
           margin: auto;
           background-color: #dedede;
           content: '';
@@ -361,6 +374,36 @@
             transform-origin: center center;
           }
 
+          .branch-node-readonly {
+            position: absolute;
+            top: -18px;
+            left: 50%;
+            z-index: 1;
+            width: 36px;
+            height: 36px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            border: 2px solid #dedede;
+            background-color: #fff;
+            border-radius: 50%;
+            transform: translateX(-50%);
+            transform-origin: center center;
+            
+            &.status-pass {
+              border-color: #6bb63c;
+              background-color: #e9f4e2;
+            }
+            &.status-pass:hover {
+              border-color: #6bb63c;
+            }
+
+            .icon-size {
+              font-size: 22px;
+              color: #67c23a;
+            }
+          }
+
           .branch-node-item {
             position: relative;
             display: flex;
@@ -454,7 +497,6 @@
             padding: 3px 4px;
             color: #212121;
             cursor: pointer;
-            // background: #2c2c2c;
             background: #fafafa;
             border-radius: 30px;
             box-shadow: 0 1px 5px 0 rgba(10, 30, 65, 0.08);
@@ -473,12 +515,36 @@
           align-items: center;
           width: 80px;
           height: 36px;
+          border: 2px solid #fafafa;
           color: #212121;
-          // background: #6e6e6e;
-          background: #fafafa;
           border-radius: 30px;
           box-shadow: 0 1px 5px 0 rgba(10, 30, 65, 0.08);
           box-sizing: border-box;
+
+          &.status-pass {
+            border-color: #6bb63c;
+            background-color: #a9da90;
+          }
+
+          &.status-pass:hover {
+            border-color: #6bb63c;
+          }
+
+          &.status-reject {
+            border-color: #e47470;
+            background-color: #f6e5e5;
+          }
+          &.status-reject:hover {
+            border-color: #e47470;
+          }
+
+          &.status-cancel {
+            border-color: #919398;
+            background-color: #eaeaeb
+          }
+          &.status-cancel:hover {
+            border-color: #919398;
+          }
         }
       }
 

+ 160 - 0
src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue

@@ -0,0 +1,160 @@
+<template>
+  <el-card v-loading="loading" class="box-card">
+    <SimpleProcessViewer :flow-node="simpleModel" />
+  </el-card>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { TaskStatusEnum } from '@/api/bpm/task'
+import { BpmProcessInstanceStatus } from '@/utils/constants'
+import { SimpleFlowNode, NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { SimpleProcessViewer } from '@/components/SimpleProcessDesignerV2/src/'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+
+defineOptions({ name: 'BpmProcessInstanceSimpleViewer' })
+
+const props = defineProps({
+  loading: propTypes.bool.def(false), // 是否加载中
+  id: propTypes.string // 流程实例的编号
+})
+
+// const view = ref({
+//   simpleModel: undefined
+// }) // simple 模型数据
+
+const simpleModel = ref()
+
+/** 只有 loading 完成时,才去加载流程列表 */
+watch(
+  () => props.loading,
+  async (value) => {
+    if (value && props.id) {
+      const modelView = await ProcessInstanceApi.getProcessInstanceBpmnModelView(props.id)
+      if (modelView) {
+        // 已经拒绝的活动节点编号集合,只包括 UserTask
+        const rejectedTaskActivityIds: string[] = modelView.rejectedTaskActivityIds
+        // 进行中的活动节点编号集合, 只包括 UserTask
+        const unfinishedTaskActivityIds: string[] = modelView.unfinishedTaskActivityIds
+        // 已经完成的活动节点编号集合, 包括 UserTask、Gateway 等
+        const finishedActivityIds: string[] = modelView.finishedTaskActivityIds
+        // 已经完成的连线节点编号集合,只包括 SequenceFlow
+        const finishedSequenceFlowActivityIds: string[] = modelView.finishedSequenceFlowActivityIds
+        setSimpleModelNodeTaskStatus(
+          modelView.simpleModel,
+          modelView.processInstance.status,
+          rejectedTaskActivityIds,
+          unfinishedTaskActivityIds,
+          finishedActivityIds,
+          finishedSequenceFlowActivityIds
+        )
+        console.log("modelView.simpleModel==>", modelView.simpleModel)
+        simpleModel.value = modelView.simpleModel
+      }
+    }
+  }
+)
+
+const setSimpleModelNodeTaskStatus = (
+  simpleModel: SimpleFlowNode | undefined,
+  processStatus: number,
+  rejectedTaskActivityIds: string[],
+  unfinishedTaskActivityIds: string[],
+  finishedActivityIds: string[],
+  finishedSequenceFlowActivityIds: string[],
+  
+) => {
+  if (!simpleModel) {
+    return
+  }
+  // 结束节点
+  if (simpleModel.type === NodeType.END_EVENT_NODE) {
+    if (finishedActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = processStatus
+    } else {
+      simpleModel.activityStatus = TaskStatusEnum.NOT_START
+    }
+    return
+  }
+  
+  // 审批节点
+  if (
+    simpleModel.type === NodeType.START_USER_NODE ||
+    simpleModel.type === NodeType.USER_TASK_NODE
+  ) {
+    simpleModel.activityStatus = TaskStatusEnum.NOT_START
+    if (rejectedTaskActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.REJECT
+    } else if(unfinishedTaskActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.RUNNING
+    } else if (finishedActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.APPROVE
+    }
+    // TODO 是不是还缺一个 cancel 的状态
+  }
+
+  // 抄送节点
+  if (simpleModel.type === NodeType.COPY_TASK_NODE) {
+    // 抄送节点 只有通过和未执行状态
+    if (finishedActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.APPROVE
+    } else {
+      simpleModel.activityStatus = TaskStatusEnum.NOT_START
+    }
+  }
+  // 条件节点 对应 SequenceFlow
+  if (simpleModel.type === NodeType.CONDITION_NODE) {
+    // 条件节点。只有通过和未执行状态
+    if (finishedSequenceFlowActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.APPROVE
+    } else {
+      simpleModel.activityStatus = TaskStatusEnum.NOT_START
+    }
+  }
+
+  // 网关节点
+  if (
+    simpleModel.type === NodeType.CONDITION_BRANCH_NODE ||
+    simpleModel.type === NodeType.PARALLEL_BRANCH_NODE ||
+    simpleModel.type === NodeType.INCLUSIVE_BRANCH_NODE
+  ) {
+    // 网关节点。只有通过和未执行状态
+    if (finishedActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.APPROVE
+    } else {
+      simpleModel.activityStatus = TaskStatusEnum.NOT_START
+    }
+    simpleModel.conditionNodes?.forEach((node) => {
+      setSimpleModelNodeTaskStatus(
+        node,
+        processStatus,
+        rejectedTaskActivityIds,
+        unfinishedTaskActivityIds,
+        finishedActivityIds,
+        finishedSequenceFlowActivityIds
+      )
+    })
+  }
+
+  setSimpleModelNodeTaskStatus(
+    simpleModel.childNode,
+    processStatus,
+    rejectedTaskActivityIds,
+    unfinishedTaskActivityIds,
+    finishedActivityIds,
+    finishedSequenceFlowActivityIds
+  )
+}
+/** 监听 bpmnXml */
+// watch(
+//   () => props.bpmnXml,
+//   (value) => {
+//     view.value.bpmnXml = value
+//   }
+// )
+</script>
+<style>
+.box-card {
+  width: 100%;
+  margin-bottom: 20px;
+}
+</style>

+ 4 - 1
src/views/bpm/simpleWorkflow/index.vue

@@ -1,5 +1,8 @@
 <template>
-  <SimpleProcessDesigner :model-id="modelId" />
+   <ContentWrap :bodyStyle="{ padding: '0px 0px' }" class="position-relative">
+      <SimpleProcessDesigner :model-id="modelId" />
+   </ContentWrap>
+ 
 </template>
 <script setup lang="ts">
 import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/'