|
@@ -7,34 +7,63 @@
|
|
|
<div class="endpoint-name">{{ row.nodes?.[0]?.name }}</div>
|
|
<div class="endpoint-name">{{ row.nodes?.[0]?.name }}</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 分支区 -->
|
|
|
|
|
|
|
+ <!-- 分支区(线性:在分支处展示可能的分支路线) -->
|
|
|
<div v-else-if="row.kind === 'branch'" class="flow-branch">
|
|
<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>
|
|
|
- <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>
|
|
<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>
|
|
<el-tag v-if="br.isDefault" size="small" type="info" effect="plain">默认</el-tag>
|
|
|
</div>
|
|
</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>
|
|
|
- <div v-else class="branch-empty">无审批节点</div>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 主流程节点 / 抄送节点 -->
|
|
|
|
|
|
|
+ <!-- 主流程节点 / 抄送节点(线性主线) -->
|
|
|
<div v-else class="flow-main-row">
|
|
<div v-else class="flow-main-row">
|
|
|
<NodeCard
|
|
<NodeCard
|
|
|
v-for="n in row.nodes"
|
|
v-for="n in row.nodes"
|
|
@@ -51,7 +80,8 @@
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
import { computed, ref, watch } from 'vue'
|
|
import { computed, ref, watch } from 'vue'
|
|
|
import { Operation } from '@element-plus/icons-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 type { ProcessProgress } from '@/types/flow'
|
|
|
import NodeCard from './NodeCard.vue'
|
|
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 || ''}`
|
|
return `${idx}-${row.kind}-${row.nodes?.[0]?.id || row.title || ''}`
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 展开/收起
|
|
|
|
|
|
|
+// 主流程节点展开/收起
|
|
|
const expandedSet = ref<Set<string>>(new Set())
|
|
const expandedSet = ref<Set<string>>(new Set())
|
|
|
-
|
|
|
|
|
function isNodeExpanded(id: string): boolean {
|
|
function isNodeExpanded(id: string): boolean {
|
|
|
return expandedSet.value.has(id)
|
|
return expandedSet.value.has(id)
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
function toggleNodeExpand(id: string) {
|
|
function toggleNodeExpand(id: string) {
|
|
|
const next = new Set(expandedSet.value)
|
|
const next = new Set(expandedSet.value)
|
|
|
if (next.has(id)) next.delete(id)
|
|
if (next.has(id)) next.delete(id)
|
|
@@ -93,7 +121,25 @@ function toggleNodeExpand(id: string) {
|
|
|
expandedSet.value = next
|
|
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(
|
|
watch(
|
|
|
() => props.progress,
|
|
() => props.progress,
|
|
|
() => {
|
|
() => {
|
|
@@ -173,69 +219,163 @@ watch(
|
|
|
position: relative;
|
|
position: relative;
|
|
|
margin: 8px 0;
|
|
margin: 8px 0;
|
|
|
z-index: 1;
|
|
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;
|
|
align-items: center;
|
|
|
gap: 4px;
|
|
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-size: 12px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
color: #909399;
|
|
color: #909399;
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
}
|
|
}
|
|
|
-.junction-icon {
|
|
|
|
|
|
|
+.branch-icon {
|
|
|
display: inline-flex;
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
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;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
- gap: 8px;
|
|
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+.branch-route {
|
|
|
|
|
+ border-left: 2px solid #e4e7ed;
|
|
|
|
|
+ padding-left: 8px;
|
|
|
}
|
|
}
|
|
|
-.branch-head {
|
|
|
|
|
|
|
+.branch-label {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
gap: 6px;
|
|
gap: 6px;
|
|
|
flex-wrap: wrap;
|
|
flex-wrap: wrap;
|
|
|
- padding: 4px 6px;
|
|
|
|
|
- border-bottom: 1px dashed #dcdfe6;
|
|
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+.branch-mark {
|
|
|
|
|
+ color: #c0c4cc;
|
|
|
|
|
+ font-family: monospace;
|
|
|
}
|
|
}
|
|
|
.branch-name {
|
|
.branch-name {
|
|
|
- font-size: 12px;
|
|
|
|
|
font-weight: 600;
|
|
font-weight: 600;
|
|
|
color: #303133;
|
|
color: #303133;
|
|
|
}
|
|
}
|
|
|
.branch-cond {
|
|
.branch-cond {
|
|
|
- font-size: 11px;
|
|
|
|
|
color: #909399;
|
|
color: #909399;
|
|
|
font-family: monospace;
|
|
font-family: monospace;
|
|
|
}
|
|
}
|
|
|
.branch-nodes {
|
|
.branch-nodes {
|
|
|
display: flex;
|
|
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 {
|
|
.branch-empty {
|
|
|
|
|
+ font-size: 12px;
|
|
|
color: #c0c4cc;
|
|
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;
|
|
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>
|
|
</style>
|