Jelajahi Sumber

流程进度展示调整

ye-zhaojia 2 minggu lalu
induk
melakukan
386e127d2a

+ 196 - 56
src/views/flow/execute/FlowTimeline.vue

@@ -7,34 +7,63 @@
         <div class="endpoint-name">{{ row.nodes?.[0]?.name }}</div>
       </div>
 
-      <!-- 分支区 -->
+      <!-- 分支区(线性:在分支处展示可能的分支路线) -->
       <div v-else-if="row.kind === 'branch'" class="flow-branch">
-        <div class="branch-junction">
-          <span class="junction-icon"><el-icon><Operation /></el-icon></span>
-          <span class="junction-text">{{ row.title || '条件分支' }}</span>
+        <div class="branch-title">
+          <span class="branch-icon"><el-icon><Operation /></el-icon></span>
+          <span>{{ row.title || '条件分支' }}</span>
         </div>
-        <div class="branch-columns">
-          <div v-for="(br, bi) in row.branches" :key="bi" class="branch-column">
-            <div class="branch-head">
-              <span class="branch-name">{{ br.label }}</span>
+        <div class="branch-routes">
+          <div v-for="(br, bi) in row.branches" :key="bi" class="branch-route">
+            <div class="branch-label">
+              <span class="branch-mark">{{ bi === 0 ? '├─' : '└─' }}</span>
+              <span class="branch-name" :class="{ default: br.isDefault }">{{ br.label }}</span>
               <span v-if="br.condition" class="branch-cond">{{ br.condition }}</span>
               <el-tag v-if="br.isDefault" size="small" type="info" effect="plain">默认</el-tag>
             </div>
-            <div v-if="br.nodes.length" class="branch-nodes">
-              <NodeCard
-                v-for="n in br.nodes"
-                :key="n.id"
-                :node="n"
-                :is-expanded="isNodeExpanded(n.id)"
-                @toggle-node="toggleNodeExpand"
-              />
+            <div class="branch-nodes">
+              <template v-if="br.nodes.length">
+                <div
+                  v-for="n in br.nodes"
+                  :key="n.id"
+                  class="branch-chip"
+                  :class="n.status"
+                  @click="toggleBranchDetail(n.id)"
+                >
+                  <span class="chip-dot" :class="n.status" />
+                  <span class="chip-name">{{ n.name }}</span>
+                  <span v-if="n.isMyTurn" class="my-turn-tag">待我处理</span>
+                  <span class="chip-expand">{{ branchDetailId === n.id ? '收起' : '详情' }}</span>
+                </div>
+              </template>
+              <span v-else class="branch-empty">无审批节点</span>
+            </div>
+
+            <!-- 分支节点详情(任务 + 审批记录) -->
+            <div v-if="branchDetailNode" class="branch-detail">
+              <template v-if="branchDetailNode.nodeProgress?.tasks?.length">
+                <div class="detail-title">任务</div>
+                <div v-for="t in branchDetailNode.nodeProgress.tasks" :key="t.id" class="task-item">
+                  <span class="task-assignee">{{ t.assigneeName || '用户' + t.assigneeId }}</span>
+                  <span class="task-status" :class="taskStatusType(t.status)">{{ taskStatusText(t.status) }}</span>
+                  <span v-if="t.comment" class="task-comment">{{ t.comment }}</span>
+                </div>
+              </template>
+              <template v-if="branchDetailNode.records.length">
+                <div class="detail-title">审批记录</div>
+                <div v-for="r in branchDetailNode.records" :key="r.id" class="record-item">
+                  <span class="record-operator">{{ r.operatorName || '用户' + r.operatorId }}</span>
+                  <span class="record-action" :class="recordActionType(r.actionResult)">{{ recordActionText(r.actionResult) }}</span>
+                  <span v-if="r.comment" class="record-comment">{{ r.comment }}</span>
+                  <span v-if="r.createTime" class="record-time">{{ r.createTime }}</span>
+                </div>
+              </template>
             </div>
-            <div v-else class="branch-empty">无审批节点</div>
           </div>
         </div>
       </div>
 
-      <!-- 主流程节点 / 抄送节点 -->
+      <!-- 主流程节点 / 抄送节点(线性主线) -->
       <div v-else class="flow-main-row">
         <NodeCard
           v-for="n in row.nodes"
@@ -51,7 +80,8 @@
 <script setup lang="ts">
 import { computed, ref, watch } from 'vue'
 import { Operation } from '@element-plus/icons-vue'
-import { buildFlowLayout, type FlowRow } from '@/utils/flowLayout'
+import { buildFlowLayout, type FlowRow, type FlowLayoutNode } from '@/utils/flowLayout'
+import { taskStatusText, taskStatusType, recordActionText, recordActionType } from '@/utils/flow'
 import type { ProcessProgress } from '@/types/flow'
 import NodeCard from './NodeCard.vue'
 
@@ -79,13 +109,11 @@ function rowKey(row: FlowRow, idx: number): string {
   return `${idx}-${row.kind}-${row.nodes?.[0]?.id || row.title || ''}`
 }
 
-// 展开/收起
+// 主流程节点展开/收起
 const expandedSet = ref<Set<string>>(new Set())
-
 function isNodeExpanded(id: string): boolean {
   return expandedSet.value.has(id)
 }
-
 function toggleNodeExpand(id: string) {
   const next = new Set(expandedSet.value)
   if (next.has(id)) next.delete(id)
@@ -93,7 +121,25 @@ function toggleNodeExpand(id: string) {
   expandedSet.value = next
 }
 
-// 加载后自动展开"进行中"的节点及子节点,并定位到当前子节点
+// 分支节点详情展开/收起(同一时间仅展开一个)
+const branchDetailId = ref<string | null>(null)
+function toggleBranchDetail(id: string) {
+  branchDetailId.value = branchDetailId.value === id ? null : id
+}
+const branchDetailNode = computed<FlowLayoutNode | null>(() => {
+  const id = branchDetailId.value
+  if (!id) return null
+  for (const row of rows.value) {
+    if (row.kind !== 'branch') continue
+    for (const br of row.branches || []) {
+      const found = br.nodes.find((n) => n.id === id)
+      if (found) return found
+    }
+  }
+  return null
+})
+
+// 数据变化后自动展开"进行中"的节点及子节点
 watch(
   () => props.progress,
   () => {
@@ -173,69 +219,163 @@ watch(
   position: relative;
   margin: 8px 0;
   z-index: 1;
+  background: #fafafa;
+  border: 1px solid #ebeef5;
+  border-left: 4px solid #909399;
+  border-radius: 6px;
+  padding: 8px 12px;
 }
-.branch-junction {
-  display: inline-flex;
+.branch-title {
+  display: flex;
   align-items: center;
   gap: 4px;
-  margin: 6px 0;
-  margin-left: -18px;
-  padding: 2px 10px 2px 6px;
-  background: #f4f4f5;
-  border: 1px solid #e4e7ed;
-  border-radius: 4px;
   font-size: 12px;
+  font-weight: 600;
   color: #909399;
+  margin-bottom: 6px;
 }
-.junction-icon {
+.branch-icon {
   display: inline-flex;
   align-items: center;
 }
-.branch-columns {
-  display: flex;
-  gap: 12px;
-  align-items: stretch;
-  margin-left: 14px;
-  padding: 10px;
-  border-left: 2px solid #dcdfe6;
-  border-radius: 6px;
-  background: #fafafa;
-  overflow-x: auto;
-}
-.branch-column {
-  flex: 1;
-  min-width: 210px;
+.branch-routes {
   display: flex;
   flex-direction: column;
-  gap: 8px;
+  gap: 6px;
+}
+.branch-route {
+  border-left: 2px solid #e4e7ed;
+  padding-left: 8px;
 }
-.branch-head {
+.branch-label {
   display: flex;
   align-items: center;
   gap: 6px;
   flex-wrap: wrap;
-  padding: 4px 6px;
-  border-bottom: 1px dashed #dcdfe6;
+  font-size: 12px;
+}
+.branch-mark {
+  color: #c0c4cc;
+  font-family: monospace;
 }
 .branch-name {
-  font-size: 12px;
   font-weight: 600;
   color: #303133;
 }
 .branch-cond {
-  font-size: 11px;
   color: #909399;
   font-family: monospace;
 }
 .branch-nodes {
   display: flex;
-  flex-direction: column;
-  gap: 8px;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 6px;
+  margin-top: 4px;
+}
+.branch-chip {
+  display: inline-flex;
+  align-items: center;
+  gap: 5px;
+  padding: 2px 8px;
+  border: 1px solid #ebeef5;
+  border-radius: 12px;
+  background: #fff;
+  font-size: 12px;
+  color: #606266;
+  cursor: pointer;
+  user-select: none;
+  transition: all 0.15s;
+}
+.branch-chip:hover {
+  border-color: #c6e2ff;
+  box-shadow: 0 1px 4px rgba(64, 158, 255, 0.15);
+}
+.branch-chip.completed {
+  border-color: #b3e19d;
+  background: #f0f9eb;
+  color: #529b2e;
+}
+.branch-chip.current {
+  border-color: #f0c78a;
+  background: #fdf6ec;
+  color: #b88230;
+}
+.branch-chip.pending {
+  border-color: #d5d5d5;
+  background: #f5f7fa;
+  color: #909399;
+}
+.chip-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  flex-shrink: 0;
+}
+.chip-dot.completed {
+  background: #67c23a;
+}
+.chip-dot.current {
+  background: #e6a23c;
+}
+.chip-dot.pending {
+  background: #c0c4cc;
+}
+.chip-name {
+  font-weight: 500;
+}
+.chip-expand {
+  font-size: 11px;
+  color: #409eff;
+}
+.my-turn-tag {
+  font-size: 11px;
+  color: #fff;
+  background: #f56c6c;
+  border-radius: 3px;
+  padding: 0 5px;
+  line-height: 16px;
 }
 .branch-empty {
+  font-size: 12px;
   color: #c0c4cc;
+}
+.branch-detail {
+  margin-top: 6px;
+  padding: 6px 8px;
+  background: #fff;
+  border: 1px dashed #e4e7ed;
+  border-radius: 4px;
+}
+.detail-title {
+  font-size: 11px;
+  color: #909399;
+  margin-bottom: 2px;
+}
+.task-item,
+.record-item {
+  display: flex;
+  align-items: center;
+  gap: 6px;
   font-size: 12px;
-  text-align: center;
-  padding: 12px 0;
+  color: #606266;
+  padding: 2px 0;
+  flex-wrap: wrap;
+}
+.task-assignee,
+.record-operator {
+  font-weight: 600;
+}
+.task-status,
+.record-action {
+  font-size: 11px;
+}
+.task-comment,
+.record-comment {
+  color: #909399;
+}
+.record-time {
+  color: #c0c4cc;
+  margin-left: auto;
 }
 </style>

+ 6 - 2
src/views/flow/execute/InstanceDetail.vue

@@ -82,10 +82,10 @@
       <el-divider />
 
       <div class="detail-body">
-        <!-- 左侧:流程进度(支持分支可视化) -->
+        <!-- 左侧:流程进度(线性主线 + 分支路线) -->
         <div class="timeline-section">
           <h4>流程进度</h4>
-          <FlowTimeline :progress="progress" />
+          <FlowTimeline :key="timelineVersion" :progress="progress" />
         </div>
 
         <!-- 右侧:审批操作区 -->
@@ -199,6 +199,8 @@ const visible = computed({
 
 const loading = ref(false)
 const progress = ref<ProcessProgress | null>(null)
+// 每次加载进度成功后自增,用于强制重建流程进度组件,保证审批后进度刷新
+const timelineVersion = ref(0)
 
 type ApprovalActionType = 'pass' | 'reject' | 'rollback'
 
@@ -299,6 +301,8 @@ async function loadProgress() {
     // 静默加载进度,无权限时不在详情页弹全局错误提示
     const res = await getProgress(props.instanceId, { silent: true })
     progress.value = res
+    // 强制重建进度组件,确保审批操作后进度立即刷新
+    timelineVersion.value++
   } catch {
     progress.value = null
   } finally {