- src/api/workflow: 工作流模型、实例、任务接口与类型定义 - src/views/workflow: 流程设计、发起、审批中心(待办/已办/我发起)页面与 BPMN 设计器、预览器 - src/views/dynamic-form/components/publish: 发布弹窗拆分为多步(入口配置/方式/结果),原 PublishDialog 大幅精简 - src/api/form: 新增 getWorkflowOptions,表单定义增加 category 区分普通/工作流表单 - src/api/system/role: 新增 getCodeOptions 供流程候选组选择 - src/stores/permission: 路由重载先摘旧路由再注册新路由,避免空窗触发 404 - vite.config: 预构建 bpmn-js,避免设计器首进重优化刷新 - src/views/login、.env.development、LayoutToolbar: 本地开发联调调整(昵称展示、本地接口) - src/api/system/user: 重置密码改用 data 传参 - src/views/codegen: 支持 FormTypeEnum 日期字段范围查询 - package.json: 版本号 4.9.0;删除误存的 dev-output.txt
175 lines
5.8 KiB
Vue
175 lines
5.8 KiB
Vue
<template>
|
||
<el-timeline v-if="stages.length > 0" class="approval-flow">
|
||
<template v-for="(stage, index) in stages" :key="stage.nodeId">
|
||
<!-- 环节的实际办理记录:驳回重办同一节点产生多条,全部按时间展示 -->
|
||
<el-timeline-item
|
||
v-for="record in recordsOf(stage.nodeId)"
|
||
:key="`${stage.nodeId}-${record.startTime ?? index}`"
|
||
:type="record.endTime ? 'success' : 'primary'"
|
||
:hollow="!record.endTime"
|
||
>
|
||
<div class="approval-flow__title">
|
||
<div class="approval-flow__name">
|
||
<span>{{ stage.nodeName }}</span>
|
||
<el-tag v-if="record.assignee" size="small" effect="plain">
|
||
{{ record.assignee }}
|
||
</el-tag>
|
||
</div>
|
||
<el-tag v-if="record.endTime" size="small" type="success" effect="light">已办理</el-tag>
|
||
<el-tag v-else size="small" type="warning" effect="light">进行中</el-tag>
|
||
</div>
|
||
<div class="approval-flow__time">
|
||
<template v-if="record.endTime">办理于 {{ record.endTime }}</template>
|
||
<template v-else>
|
||
{{
|
||
record.assignee ? `等待 ${record.assignee} 办理` : `待 ${describe(stage)} 认领办理`
|
||
}}
|
||
</template>
|
||
</div>
|
||
<div v-if="record.comment" class="approval-flow__comment">{{ record.comment }}</div>
|
||
</el-timeline-item>
|
||
|
||
<!-- 无办理记录:运行中按位置区分"未到"与"未经过"(网关分支跳过),已结束均为未经过;
|
||
两种情况均展示配置的办理人,便于了解该环节本应由谁处理 -->
|
||
<el-timeline-item v-if="!hasRecord(stage.nodeId)" type="info" hollow>
|
||
<div class="approval-flow__title" :class="{ 'is-skipped': !isUpcoming(index) }">
|
||
<span>{{ stage.nodeName }}</span>
|
||
<el-tag size="small" type="info" effect="plain">{{ pendingLabel(index) }}</el-tag>
|
||
</div>
|
||
<div
|
||
v-if="describe(stage)"
|
||
class="approval-flow__time"
|
||
:class="{ 'is-skipped': !isUpcoming(index) }"
|
||
>
|
||
{{ describe(stage) }}
|
||
</div>
|
||
</el-timeline-item>
|
||
</template>
|
||
</el-timeline>
|
||
<el-empty v-else description="暂无审批流程" :image-size="64" />
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import type { ApprovalHistoryItem, InstanceStatus, ProcessStageItem } from "@/api/workflow";
|
||
|
||
defineOptions({
|
||
name: "ApprovalFlowTimeline",
|
||
});
|
||
|
||
/**
|
||
* 审批流程时间线(流程走向 + 审批记录融合)
|
||
*
|
||
* @description 以设计环节为骨架按编排顺序展示,实际办理记录按 activityId 归位到环节:
|
||
* 已办(办理人/意见/时间)、进行中(待认领或等待办理)、未到(运行中位于当前环节之后)、
|
||
* 未经过(网关分支跳过或流程结束仍未到达),驳回重办的多轮记录全部保留
|
||
*/
|
||
const props = defineProps<{
|
||
/** 审批环节(按 BPMN 编排顺序,含分支未走节点) */
|
||
stages: ProcessStageItem[];
|
||
/** 审批记录(按时间正序) */
|
||
history: ApprovalHistoryItem[];
|
||
/** 当前进行中环节下标(运行中有效) */
|
||
active: number;
|
||
/** 实例状态:非运行中视为已结束,无记录环节均为"未经过" */
|
||
status: InstanceStatus;
|
||
}>();
|
||
|
||
/** 节点ID -> 办理记录(驳回重办为多条) */
|
||
const recordMap = computed(() => {
|
||
const map = new Map<string, ApprovalHistoryItem[]>();
|
||
props.history.forEach((item) => {
|
||
if (!item.activityId) return;
|
||
const list = map.get(item.activityId) ?? [];
|
||
list.push(item);
|
||
map.set(item.activityId, list);
|
||
});
|
||
return map;
|
||
});
|
||
|
||
function recordsOf(nodeId: string): ApprovalHistoryItem[] {
|
||
return recordMap.value.get(nodeId) ?? [];
|
||
}
|
||
|
||
function hasRecord(nodeId: string): boolean {
|
||
return recordsOf(nodeId).length > 0;
|
||
}
|
||
|
||
/** 是否"未到":流程运行中且位于当前环节之后 */
|
||
function isUpcoming(index: number): boolean {
|
||
return props.status === "running" && index >= props.active;
|
||
}
|
||
|
||
/** 无记录环节状态标签:未到(后续环节)/ 未经过(分支跳过或已结束) */
|
||
function pendingLabel(index: number): string {
|
||
return isUpcoming(index) ? "未到" : "未经过";
|
||
}
|
||
|
||
/** 环节办理人描述:角色(成员账号);发起人办理环节标注"发起人" */
|
||
function describe(stage: ProcessStageItem): string {
|
||
if (stage.initiator) {
|
||
return "发起人";
|
||
}
|
||
const roles = stage.roleNames.join("、");
|
||
const users = stage.userNames.join("、");
|
||
if (roles && users) {
|
||
return `${roles}(${users})`;
|
||
}
|
||
return roles || users || "未配置办理人";
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.approval-flow {
|
||
padding-left: 4px;
|
||
|
||
/* Element Plus 默认 wrapper 上移 3px(为单行纯文本设计),多行内容下
|
||
标签顶部与节点连线贴边,视觉上边框被裁切;归零后按内容自然排版 */
|
||
:deep(.el-timeline-item__wrapper) {
|
||
top: 0;
|
||
}
|
||
|
||
&__title {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
min-height: 24px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
|
||
/* 未经过环节整体弱化,与已办/进行中形成层级差 */
|
||
&.is-skipped {
|
||
font-weight: 400;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
}
|
||
|
||
&__name {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
min-width: 0;
|
||
}
|
||
|
||
&__time {
|
||
margin-top: 4px;
|
||
font-size: 12px;
|
||
line-height: 20px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
/* 审批意见引用块:左侧色条 + 浅底,与时间/标题形成视觉分层 */
|
||
&__comment {
|
||
padding: 6px 10px;
|
||
margin-top: 6px;
|
||
font-size: 13px;
|
||
line-height: 20px;
|
||
color: var(--el-text-color-regular);
|
||
word-break: break-all;
|
||
background: var(--el-fill-color-light);
|
||
border-left: 3px solid var(--el-color-primary-light-5);
|
||
border-radius: 4px;
|
||
}
|
||
}
|
||
</style>
|