【工作流】- 流程实例新界面修改

This commit is contained in:
jason 2024-10-01 23:35:11 +08:00
parent 4680a7dcfd
commit 27d24cd02d
7 changed files with 501 additions and 155 deletions

View File

@ -1,6 +1,6 @@
import request from '@/config/axios'
import { ProcessDefinitionVO } from '@/api/bpm/model'
import { NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
export type Task = {
id: string
name: string
@ -22,6 +22,35 @@ export type ProcessInstanceVO = {
processDefinition?: ProcessDefinitionVO
}
// 用户信息
export type User = {
id: number,
nickname: string,
avatar: string
}
// 审批任务信息
export type ApprovalTaskInfo = {
id: number,
ownerUser: User,
assigneeUser: User,
status: number,
reason: string
}
// 审批节点信息
export type ApprovalNodeInfo = {
id : number
name: string
nodeType: NodeType
status: number
startTime?: Date
endTime?: Date
candidateUserList?: User[]
tasks: ApprovalTaskInfo[]
}
export const getProcessInstanceMyPage = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/my-page', params })
}
@ -57,3 +86,8 @@ export const getProcessInstance = async (id: string) => {
export const getProcessInstanceCopyPage = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/copy/page', params })
}
export const getApprovalDetail = async (processInstanceId?:string, processDefinitionId?:string) => {
const param = processInstanceId ? '?processInstanceId='+ processInstanceId : '?processDefinitionId='+ processDefinitionId
return await request.get({ url: 'bpm/process-instance/get-approval-detail'+ param })
}

View File

@ -1,5 +1,51 @@
import request from '@/config/axios'
/**
*
*/
export enum TaskStatusEnum {
/**
*
*/
NOT_START = -1,
/**
*
*/
WAIT = 0,
/**
*
*/
RUNNING = 1,
/**
*
*/
APPROVE = 2,
/**
*
*/
REJECT = 3,
/**
*
*/
CANCEL = 4,
/**
* 退
*/
RETURN = 5,
/**
*
*/
DELEGATE = 6,
/**
*
*/
APPROVING = 7,
}
export type TaskVO = {
id: number
}

View File

@ -1,6 +1,6 @@
<template>
<div class="simple-flow-canvas">
<div class="simple-flow-container">
<div class="simple-flow-canvas" v-loading="loading">
<div class="simple-flow-container" >
<div class="top-area-container">
<div class="top-actions">
<div class="canvas-control">
@ -15,7 +15,10 @@
</div>
</div>
<div class="scale-container" :style="`transform: scale(${scaleValue / 100});`">
<ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" />
<ProcessNodeTree
v-if="processNodeTree"
v-model:flow-node="processNodeTree"
/>
</div>
</div>
<Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false">
@ -46,6 +49,7 @@ import * as DeptApi from '@/api/system/dept'
import * as PostApi from '@/api/system/post'
import * as UserApi from '@/api/system/user'
import * as UserGroupApi from '@/api/bpm/userGroup'
import { fa } from 'element-plus/es/locale'
defineOptions({
name: 'SimpleProcessDesigner'
})
@ -56,7 +60,7 @@ const props = defineProps({
required: true
}
})
const loading = ref(true)
const formFields = ref<string[]>([])
const formType = ref(20)
const roleOptions = ref<RoleApi.RoleVO[]>([]) //
@ -163,44 +167,49 @@ const zoomIn = () => {
}
onMounted(async () => {
//
const bpmnModel = await getModel(props.modelId)
if (bpmnModel) {
formType.value = bpmnModel.formType
if (formType.value === 10) {
const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
formFields.value = bpmnForm?.fields
}
}
//
roleOptions.value = await RoleApi.getSimpleRoleList()
//
postOptions.value = await PostApi.getSimplePostList()
//
userOptions.value = await UserApi.getSimpleUserList()
//
deptOptions.value = await DeptApi.getSimpleDeptList()
deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id')
//
userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
// SIMPLE
const result = await getBpmSimpleModel(props.modelId)
if (result) {
processNodeTree.value = result
} else {
//
processNodeTree.value = {
name: '发起人',
type: NodeType.START_USER_NODE,
id: NodeId.START_USER_NODE_ID,
childNode: {
id: NodeId.END_EVENT_NODE_ID,
name: '结束',
type: NodeType.END_EVENT_NODE
try {
loading.value = true
//
const bpmnModel = await getModel(props.modelId)
if (bpmnModel) {
formType.value = bpmnModel.formType
if (formType.value === 10) {
const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
formFields.value = bpmnForm?.fields
}
}
//
roleOptions.value = await RoleApi.getSimpleRoleList()
//
postOptions.value = await PostApi.getSimplePostList()
//
userOptions.value = await UserApi.getSimpleUserList()
//
deptOptions.value = await DeptApi.getSimpleDeptList()
deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id')
//
userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
// SIMPLE
const result = await getBpmSimpleModel(props.modelId)
if (result) {
processNodeTree.value = result
} else {
//
processNodeTree.value = {
name: '发起人',
type: NodeType.START_USER_NODE,
id: NodeId.START_USER_NODE_ID,
childNode: {
id: NodeId.END_EVENT_NODE_ID,
name: '结束',
type: NodeType.END_EVENT_NODE
}
}
}
} finally {
loading.value = false
}
})
</script>

View File

@ -292,7 +292,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
},
{
path: 'process-instance/detail',
component: () => import('@/views/bpm/processInstance/detail/index.vue'),
component: () => import('@/views/bpm/processInstance/detail/index_new.vue'),
name: 'BpmProcessInstanceDetail',
meta: {
noCache: true,

View File

@ -1,6 +1,6 @@
<template>
<div
class="h-50px position-fixed bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
>
<el-popover :visible="passVisible" placement="top-end" :width="500" trigger="click">
<template #reference>
@ -120,6 +120,7 @@
<div @click="handleSign"> <Icon :size="14" icon="ep:plus" />&nbsp;加签 </div>
<div @click="handleBack"> <Icon :size="14" icon="fa:mail-reply" />&nbsp;退回 </div>
</div>
<!-- </div> -->
<!-- 弹窗转派审批人 -->
<TaskTransferForm ref="taskTransferFormRef" @success="getDetail" />
<!-- 弹窗回退节点 -->
@ -299,10 +300,11 @@ defineExpose({ loadRunningTask })
.btn-container {
> div {
display: flex;
margin: 0 15px;
cursor: pointer;
display: flex;
align-items: center;
&:hover {
color: #6db5ff;
}

View File

@ -1,9 +1,94 @@
<template>
<el-timeline class="pt-20px">
<el-timeline-item v-for="(activity, index) in mockData" :key="index" size="large">
<el-timeline-item
v-for="(activity, index) in approveNodes"
:key="index"
size="large"
:icon="getApprovalNodeIcon(activity.status, activity.nodeType)"
:color="getApprovalNodeColor(activity.status)"
>
<div class="flex flex-col items-start">
<div class="font-bold"> {{ activity.name }}</div>
<div class="color-#a1a6ae text-12px mb-10px"> {{ activity.assigneeUser.nickname }}</div>
<div class="flex items-center mt-1">
<div v-for="(task, idx) in activity.tasks" :key="idx" class="flex items-center">
<div class="flex items-center flex-col pr-2">
<div class="position-relative" v-if="task.assigneeUser || task.ownerUser">
<el-avatar
:size="36"
v-if="task.assigneeUser && task.assigneeUser.avatar"
:src="task.assigneeUser.avatar"
/>
<el-avatar v-else-if="task.assigneeUser && task.assigneeUser.nickname">
{{ task.assigneeUser.nickname.substring(0, 1) }}</el-avatar
>
<el-avatar
v-else-if="task.ownerUser && task.ownerUser.avatar"
:src="task.ownerUser.avatar"
/>
<el-avatar v-else-if="task.ownerUser && task.ownerUser.nickname">
{{ task.ownerUser.nickname.substring(0, 1) }}</el-avatar
>
<div
class="position-absolute top-26px left-26px bg-#fff rounded-full flex items-center p-2px"
>
<Icon
:size="12"
:icon="statusIconMap2[task.status]?.icon"
:color="statusIconMap2[task.status]?.color"
/>
</div>
</div>
<div class="flex flex-col mt-1">
<div
v-if="task.assigneeUser && task.assigneeUser.nickname"
class="text-10px text-align-center"
>{{ task.assigneeUser.nickname }}</div
>
<div
v-else-if="task.ownerUser && task.ownerUser.nickname"
class="text-10px text-align-center"
>
{{ task.ownerUser.nickname }}</div
>
<!-- <div v-if="task.reason" :title="task.reason" class="text-13px text-truncate w-150px mt-1"> 审批意见: {{ task.reason }}</div> -->
</div>
</div>
</div>
<div
v-for="(user, idx1) in activity.candidateUserList"
:key="idx1"
class="flex items-center"
>
<div class="flex items-center flex-col pr-2">
<div class="position-relative">
<el-avatar :size="36" v-if="user.avatar" :src="user.avatar" />
<el-avatar v-else-if="user.nickname && user.nickname">
{{ user.nickname.substring(0, 1) }}</el-avatar
>
<div
class="position-absolute top-26px left-26px bg-#fff rounded-full flex items-center p-2px"
>
<Icon
:size="12"
:icon="statusIconMap2['-1']?.icon"
:color="statusIconMap2['-1']?.color"
/>
</div>
</div>
<div class="flex flex-col mt-1">
<div v-if="user.nickname" class="text-10px text-align-center">{{
user.nickname
}}</div>
<!-- <div v-if="task.reason" :title="task.reason" class="text-13px text-truncate w-150px mt-1"> 审批意见: {{ task.reason }}</div> -->
</div>
</div>
</div>
</div>
<div v-if="activity.status !== TaskStatusEnum.NOT_START" class="text-#a5a5a5 text-13px mt-1">
{{ getApprovalNodeTime(activity) }}
</div>
<!-- <div class="color-#a1a6ae text-12px mb-10px"> {{ activity.assigneeUser.nickname }}</div>
<div v-if="activity.opinion" class="text-#a5a5a5 text-12px w-100%">
<div class="mb-5px">审批意见</div>
<div
@ -14,50 +99,113 @@
</div>
<div v-if="activity.createTime" class="text-#a5a5a5 text-13px">
{{ formatDate(activity.createTime) }}
</div>
</div> -->
</div>
<!-- 该节点用户的头像 -->
<template #dot>
<div class="w-35px h-35px position-relative">
<img
src="@/assets/imgs/avatar.jpg"
class="rounded-full w-full h-full position-absolute bottom-6px right-12px"
alt=""
/>
<div
class="position-absolute top-16px left-8px bg-#fff rounded-full flex items-center content-center p-2px"
>
<Icon
:size="12"
:icon="optIconMap[activity.status]?.icon"
:color="optIconMap[activity.status]?.color"
/>
</div>
</div>
</template>
</el-timeline-item>
</el-timeline>
</template>
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import { propTypes } from '@/utils/propTypes'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { TaskStatusEnum } from '@/api/bpm/task'
import { NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
import { Check, Close, Loading, Clock, Minus, Delete } from '@element-plus/icons-vue'
defineOptions({ name: 'BpmProcessInstanceTimeline' })
defineProps({
tasks: propTypes.array //
const props = defineProps({
//
processInstanceId: {
type: String,
required: false,
default: ''
},
//
processDefinitionId: {
type: String,
required: false,
default: ''
}
})
const optIconMap = {
//
const approveNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([])
const statusIconMap2 = {
//
'-1': { color: '#e5e7ec', icon: 'ep-clock' },
//
'0': { color: '#e5e7ec', icon: 'ep:loading' },
//
'1': {
color: '#00b32a',
icon: 'fa-solid:clock'
},
'1': { color: '#448ef7', icon: 'ep:loading'},
//
'2': { color: '#00b32a', icon: 'fa-solid:check-circle' },
'2': { color: '#00b32a', icon: 'ep:circle-check-filled' },
//
'3': { color: '#f46b6c', icon: 'fa-solid:times-circle' }
'3': { color: '#f46b6c', icon: 'fa-solid:times-circle' },
//
'4': { color: '#cccccc', icon: 'ep:delete-filled' },
// 退
'5': { color: '#f46b6c', icon: 'ep:remove-filled' },
//
'6': { color: '#448ef7', icon: 'ep:loading' },
//
'7': { color: '#00b32a', icon: 'ep:circle-check-filled' },
}
const statusIconMap = {
//
'-1': { color: '#e5e7ec', icon: Clock },
'0': { color: '#e5e7ec', icon: Clock },
//
'1': { color: '#448ef7', icon: Loading },
//
'2': { color: '#00b32a', icon: Check },
//
'3': { color: '#f46b6c', icon: Close },
//
'4': { color: '#cccccc', icon: Delete },
// 退
'5' : { color: '#f46b6c', icon: Minus },
//
'6': { color: '#448ef7', icon: Loading },
//
'7': { color: '#00b32a', icon: Check },
}
/** 获得审批详情 */
const getApprovalDetail = async () => {
const data = await ProcessInstanceApi.getApprovalDetail(
props.processInstanceId,
props.processDefinitionId
)
console.log('approveNodes is []', data)
approveNodes.value = data.approveNodes
}
const getApprovalNodeIcon = (taskStatus: number , nodeType: NodeType) => {
if(taskStatus == TaskStatusEnum.NOT_START) {
return statusIconMap[taskStatus]?.icon
}
if (nodeType === NodeType.START_USER_NODE || nodeType === NodeType.USER_TASK_NODE) {
return statusIconMap[taskStatus]?.icon
}
}
const getApprovalNodeColor = (taskStatus: number) => {
return statusIconMap[taskStatus]?.color
}
const getApprovalNodeTime = (node: ProcessInstanceApi.ApprovalNodeInfo) => {
if(node.endTime) {
return `结束时间:${formatDate(node.endTime)}`
}
if(node.startTime) {
return `创建时间:${formatDate(node.startTime)}`
}
}
const mockData: any = [
@ -125,6 +273,70 @@ const mockData: any = [
formFields: null,
formVariables: null
},
{
id: 'fe1190ee-68c3-11ef-9c7d-00a6181404fd',
name: '财务总监审核',
createTime: 1725237646192,
endTime: null,
durationInMillis: null,
status: 3,
reason: null,
ownerUser: null,
assigneeUser: {
id: 104,
nickname: '财务总监',
deptId: 107,
deptName: '运维部门'
},
taskDefinitionKey: 'task-01',
processInstanceId: 'fe0c60c6-68c3-11ef-9c7d-00a6181404fd',
processInstance: {
id: 'fe0c60c6-68c3-11ef-9c7d-00a6181404fd',
name: 'oa_leave',
createTime: null,
processDefinitionId: 'oa_leave:1:6e5ac269-5f87-11ef-bdb6-00a6181404fd',
startUser: null
},
parentTaskId: null,
children: null,
formId: null,
formName: null,
formConf: null,
formFields: null,
formVariables: null
},
{
id: 'fe1190ee-68c3-11ef-9c7d-00a6181404fd',
name: '领导审批',
createTime: 1725237646192,
endTime: null,
durationInMillis: null,
status: 2,
reason: null,
ownerUser: null,
assigneeUser: {
id: 104,
nickname: '领导',
deptId: 107,
deptName: '运维部门'
},
taskDefinitionKey: 'task-01',
processInstanceId: 'fe0c60c6-68c3-11ef-9c7d-00a6181404fd',
processInstance: {
id: 'fe0c60c6-68c3-11ef-9c7d-00a6181404fd',
name: 'oa_leave',
createTime: null,
processDefinitionId: 'oa_leave:1:6e5ac269-5f87-11ef-bdb6-00a6181404fd',
startUser: null
},
parentTaskId: null,
children: null,
formId: null,
formName: null,
formConf: null,
formFields: null,
formVariables: null
},
{
id: 'fe1190ee-68c3-11ef-9c7d-00a6181404fd',
name: '财务总监审核',
@ -158,4 +370,8 @@ const mockData: any = [
formVariables: null
}
]
onMounted(async () => {
getApprovalDetail()
})
</script>

View File

@ -1,88 +1,104 @@
<template>
<ContentWrap :bodyStyle="{ padding: '10px 20px' }" class="position-relative">
<img
class="position-absolute right-20px"
width="150"
:src="auditIcons[processInstance.status]"
alt=""
/>
<div class="text-#878c93">编号{{ id }}</div>
<el-divider class="!my-8px" />
<div class="flex items-center gap-5 mb-10px">
<div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="processInstance.status" />
</div>
<div class="processInstance-wrap-main">
<el-scrollbar>
<img
class="position-absolute right-20px"
width="150"
:src="auditIcons[processInstance.status]"
alt=""
/>
<div class="text-#878c93 h-15px">编号{{ id }}</div>
<el-divider class="!my-8px" />
<div class="flex items-center gap-5 mb-10px h-40px">
<div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="processInstance.status" />
</div>
<div class="flex items-center gap-5 mb-10px text-13px">
<div class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600">
<img class="rounded-full h-28px" src="@/assets/imgs/avatar.jpg" alt="" />
{{ processInstance?.startUser?.nickname }}
</div>
<div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div>
</div>
<div class="flex items-center gap-5 mb-10px text-13px h-35px">
<div
class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600"
>
<img class="rounded-full h-28px" src="@/assets/imgs/avatar.jpg" alt="" />
{{ processInstance?.startUser?.nickname }}
</div>
<div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div>
</div>
<el-tabs>
<!-- 表单信息 -->
<el-tab-pane label="表单信息">
<el-row :gutter="10">
<el-col :span="18" class="!flex !flex-col formCol">
<!-- 表单信息 -->
<div v-loading="processInstanceLoading" class="form-box flex flex-col mb-30px flex-1">
<!-- 情况一流程表单 -->
<el-col
v-if="processInstance?.processDefinition?.formType === 10"
:offset="6"
:span="16"
>
<form-create
v-model="detailForm.value"
v-model:api="fApi"
:option="detailForm.option"
:rule="detailForm.rule"
/>
</el-col>
<!-- 情况二业务表单 -->
<div v-if="processInstance?.processDefinition?.formType === 20">
<BusinessFormComponent :id="processInstance.businessKey" />
</div>
<el-tabs v-model="activeTab">
<!-- 表单信息 -->
<el-tab-pane label="表单信息" name="form">
<div class="form-scoll-area">
<el-scrollbar>
<el-row :gutter="10">
<el-col :span="18" class="!flex !flex-col formCol">
<!-- 表单信息 -->
<div
v-loading="processInstanceLoading"
class="form-box flex flex-col mb-30px flex-1"
>
<!-- 情况一流程表单 -->
<el-col
v-if="processInstance?.processDefinition?.formType === 10"
:offset="6"
:span="16"
>
<form-create
v-model="detailForm.value"
v-model:api="fApi"
:option="detailForm.option"
:rule="detailForm.rule"
/>
</el-col>
<!-- 情况二业务表单 -->
<div v-if="processInstance?.processDefinition?.formType === 20">
<BusinessFormComponent :id="processInstance.businessKey" />
</div>
</div>
</el-col>
<el-col :span="6">
<!-- 审批记录时间线 -->
<ProcessInstanceTimeline :process-instance-id="id" />
</el-col>
</el-row>
</el-scrollbar>
</div>
</el-tab-pane>
<!-- 操作栏按钮 -->
<ProcessInstanceOperationButton
ref="operationButtonRef"
:processInstance="processInstance"
:userOptions="userOptions"
@success="getDetail"
<!-- 流程图 -->
<el-tab-pane label="流程图" name="diagram">
<ProcessInstanceBpmnViewer
:id="`${id}`"
:bpmn-xml="bpmnXml"
:loading="processInstanceLoading"
:process-instance="processInstance"
:tasks="tasks"
/>
</el-col>
<el-col :span="6">
<!-- 审批记录时间线 -->
<ProcessInstanceTimeline :process-instance="processInstance" :tasks="tasks" />
</el-col>
</el-row>
</el-tab-pane>
<!-- 流程图 -->
<el-tab-pane label="流程图">
<ProcessInstanceBpmnViewer
:id="`${id}`"
:bpmn-xml="bpmnXml"
:loading="processInstanceLoading"
:process-instance="processInstance"
:tasks="tasks"
/>
</el-tab-pane>
<!-- 流转记录 -->
<el-tab-pane label="流转记录">
<ProcessInstanceTaskList
:loading="tasksLoad"
:process-instance="processInstance"
:tasks="tasks"
@refresh="getTaskList"
/>
</el-tab-pane>
<!-- 流转评论 -->
<el-tab-pane label="流转评论"> 流转评论 </el-tab-pane>
</el-tabs>
</el-tab-pane>
<!-- 流转记录 -->
<el-tab-pane label="流转记录" name="record">
<ProcessInstanceTaskList
:loading="tasksLoad"
:process-instance="processInstance"
:tasks="tasks"
@refresh="getTaskList"
/>
</el-tab-pane>
<!-- 流转评论 -->
<el-tab-pane label="流转评论" name="comment"> 流转评论 </el-tab-pane>
</el-tabs>
<div class=" b-t-solid border-t-1px border-[var(--el-border-color)]" v-if="activeTab === 'form'">
<!-- 操作栏按钮 -->
<ProcessInstanceOperationButton
ref="operationButtonRef"
:processInstance="processInstance"
:userOptions="userOptions"
@success="getDetail"
/>
</div>
</el-scrollbar>
</div>
</ContentWrap>
</template>
<script lang="ts" setup>
@ -209,6 +225,9 @@ const getTaskList = async () => {
}
}
/** 当前的Tab */
const activeTab = ref('form')
/** 初始化 */
const userOptions = ref<UserApi.UserVO[]>([]) //
onMounted(async () => {
@ -219,9 +238,29 @@ onMounted(async () => {
</script>
<style lang="scss" scoped>
$wrap-padding-height: 30px ;
$wrap-margin-height: 15px;
$button-height: 51px;
$process-header-height: 194px;
.processInstance-wrap-main {
height: calc(100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px);
max-height: calc(100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px);
overflow: auto;
.form-scoll-area {
height: calc(100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px - $process-header-height - 40px);
max-height: calc(100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px - $process-header-height - 40px);
overflow: auto;
}
}
.form-box {
:deep(.el-card) {
border: none;
}
}
</style>