|
@@ -1,88 +1,19 @@
|
|
|
<template>
|
|
<template>
|
|
|
<div class="flow-timeline">
|
|
<div class="flow-timeline">
|
|
|
- <template v-for="(row, idx) in rows" :key="rowKey(row, idx)">
|
|
|
|
|
- <!-- 起点 / 终点 -->
|
|
|
|
|
- <div v-if="row.kind === 'start' || row.kind === 'end'" class="flow-endpoint" :class="row.kind">
|
|
|
|
|
- <div class="endpoint-dot" :class="row.kind" />
|
|
|
|
|
- <div class="endpoint-name">{{ row.nodes?.[0]?.name }}</div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 分支区(线性:在分支处展示可能的分支路线) -->
|
|
|
|
|
- <div v-else-if="row.kind === 'branch'" class="flow-branch">
|
|
|
|
|
- <div class="branch-title">
|
|
|
|
|
- <span class="branch-icon"><el-icon><Operation /></el-icon></span>
|
|
|
|
|
- <span>{{ row.title || '条件分支' }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="branch-routes">
|
|
|
|
|
- <div v-for="(br, bi) in row.branches" :key="bi" class="branch-route" :class="{ chosen: br.isChosen }">
|
|
|
|
|
- <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>
|
|
|
|
|
- <el-tag v-if="br.isChosen" size="small" type="success" effect="light">✓ 已选择</el-tag>
|
|
|
|
|
- </div>
|
|
|
|
|
- <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 && br.nodes.some(n => n.id === branchDetailId)" 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>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 主流程节点 / 抄送节点(线性主线) -->
|
|
|
|
|
- <div v-else class="flow-main-row">
|
|
|
|
|
- <NodeCard
|
|
|
|
|
- v-for="n in row.nodes"
|
|
|
|
|
- :key="n.id"
|
|
|
|
|
- :node="n"
|
|
|
|
|
- :is-expanded="isNodeExpanded(n.id)"
|
|
|
|
|
- @toggle-node="toggleNodeExpand"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- </template>
|
|
|
|
|
|
|
+ <!-- 线性展示:所有审批节点按流程拓扑顺序依次排列(分支节点同样顺序展示) -->
|
|
|
|
|
+ <div v-for="node in linearNodes" :key="node.id" class="flow-main-row">
|
|
|
|
|
+ <NodeCard
|
|
|
|
|
+ :node="node"
|
|
|
|
|
+ :is-expanded="isNodeExpanded(node.id)"
|
|
|
|
|
+ @toggle-node="toggleNodeExpand"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<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 { buildFlowLayout, type FlowRow, type FlowLayoutNode } from '@/utils/flowLayout'
|
|
|
|
|
-import { taskStatusText, taskStatusType, recordActionText, recordActionType } from '@/utils/flow'
|
|
|
|
|
|
|
+import { buildFlowLayout, type FlowLayoutNode } from '@/utils/flowLayout'
|
|
|
import type { ProcessProgress } from '@/types/flow'
|
|
import type { ProcessProgress } from '@/types/flow'
|
|
|
import NodeCard from './NodeCard.vue'
|
|
import NodeCard from './NodeCard.vue'
|
|
|
|
|
|
|
@@ -90,28 +21,36 @@ const props = defineProps<{
|
|
|
progress: ProcessProgress | null
|
|
progress: ProcessProgress | null
|
|
|
}>()
|
|
}>()
|
|
|
|
|
|
|
|
-const rows = computed<FlowRow[]>(() => {
|
|
|
|
|
|
|
+// 用布局算法解析状态,但输出"线性平铺"的节点列表(分支节点按顺序列出,不分组展示)
|
|
|
|
|
+const linearNodes = computed<FlowLayoutNode[]>(() => {
|
|
|
const def = props.progress?.definition
|
|
const def = props.progress?.definition
|
|
|
if (!def?.flowJson) return []
|
|
if (!def?.flowJson) return []
|
|
|
try {
|
|
try {
|
|
|
const model = JSON.parse(def.flowJson)
|
|
const model = JSON.parse(def.flowJson)
|
|
|
- return buildFlowLayout(
|
|
|
|
|
|
|
+ const rows = buildFlowLayout(
|
|
|
model.nodes || [],
|
|
model.nodes || [],
|
|
|
model.edges || [],
|
|
model.edges || [],
|
|
|
props.progress?.nodes || [],
|
|
props.progress?.nodes || [],
|
|
|
props.progress?.records || [],
|
|
props.progress?.records || [],
|
|
|
props.progress?.instance?.currentNode
|
|
props.progress?.instance?.currentNode
|
|
|
)
|
|
)
|
|
|
|
|
+ const nodes: FlowLayoutNode[] = []
|
|
|
|
|
+ for (const row of rows) {
|
|
|
|
|
+ if (row.kind === 'start' || row.kind === 'end') continue // 线性视图不展示起止标记
|
|
|
|
|
+ if (row.kind === 'branch') {
|
|
|
|
|
+ // 分支行:按分支顺序平铺其中的节点
|
|
|
|
|
+ for (const br of row.branches || []) nodes.push(...br.nodes)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ nodes.push(...(row.nodes || []))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return nodes
|
|
|
} catch {
|
|
} catch {
|
|
|
return []
|
|
return []
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-function rowKey(row: FlowRow, idx: number): string {
|
|
|
|
|
- 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)
|
|
@@ -123,24 +62,6 @@ 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,
|
|
@@ -179,221 +100,10 @@ watch(
|
|
|
background: #e4e7ed;
|
|
background: #e4e7ed;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/* 起点/终点 */
|
|
|
|
|
-.flow-endpoint {
|
|
|
|
|
- position: relative;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
- padding: 6px 0;
|
|
|
|
|
- z-index: 1;
|
|
|
|
|
-}
|
|
|
|
|
-.endpoint-dot {
|
|
|
|
|
- width: 16px;
|
|
|
|
|
- height: 16px;
|
|
|
|
|
- border-radius: 50%;
|
|
|
|
|
- border: 3px solid #fff;
|
|
|
|
|
- box-shadow: 0 0 0 2px #e4e7ed;
|
|
|
|
|
- margin-left: -11px;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
-}
|
|
|
|
|
-.endpoint-dot.start {
|
|
|
|
|
- background: #67c23a;
|
|
|
|
|
-}
|
|
|
|
|
-.endpoint-dot.end {
|
|
|
|
|
- background: #f56c6c;
|
|
|
|
|
-}
|
|
|
|
|
-.endpoint-name {
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #606266;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 主流程行 */
|
|
|
|
|
|
|
+/* 线性主流程行 */
|
|
|
.flow-main-row {
|
|
.flow-main-row {
|
|
|
position: relative;
|
|
position: relative;
|
|
|
padding: 4px 0;
|
|
padding: 4px 0;
|
|
|
z-index: 1;
|
|
z-index: 1;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-/* 分支区 */
|
|
|
|
|
-.flow-branch {
|
|
|
|
|
- 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-title {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 4px;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #909399;
|
|
|
|
|
- margin-bottom: 6px;
|
|
|
|
|
-}
|
|
|
|
|
-.branch-icon {
|
|
|
|
|
- display: inline-flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
-}
|
|
|
|
|
-.branch-routes {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- gap: 6px;
|
|
|
|
|
-}
|
|
|
|
|
-.branch-route {
|
|
|
|
|
- border-left: 2px solid #e4e7ed;
|
|
|
|
|
- padding-left: 8px;
|
|
|
|
|
- padding-bottom: 4px;
|
|
|
|
|
- margin-bottom: 2px;
|
|
|
|
|
- border-radius: 0 4px 4px 0;
|
|
|
|
|
-}
|
|
|
|
|
-.branch-route.chosen {
|
|
|
|
|
- border-left-color: #67c23a;
|
|
|
|
|
- background: #f0f9eb;
|
|
|
|
|
- padding: 4px 8px;
|
|
|
|
|
-}
|
|
|
|
|
-.branch-label {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 6px;
|
|
|
|
|
- flex-wrap: wrap;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
-}
|
|
|
|
|
-.branch-mark {
|
|
|
|
|
- color: #c0c4cc;
|
|
|
|
|
- font-family: monospace;
|
|
|
|
|
-}
|
|
|
|
|
-.branch-name {
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #303133;
|
|
|
|
|
-}
|
|
|
|
|
-.branch-cond {
|
|
|
|
|
- color: #909399;
|
|
|
|
|
- font-family: monospace;
|
|
|
|
|
-}
|
|
|
|
|
-.branch-nodes {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- 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;
|
|
|
|
|
-}
|
|
|
|
|
-.branch-chip.passed {
|
|
|
|
|
- border-color: #b3e19d;
|
|
|
|
|
- background: #f0f9eb;
|
|
|
|
|
- color: #529b2e;
|
|
|
|
|
-}
|
|
|
|
|
-.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-dot.passed {
|
|
|
|
|
- background: #67c23a;
|
|
|
|
|
-}
|
|
|
|
|
-.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;
|
|
|
|
|
- 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>
|