!100 feat: 完善审批中心、发起流程、查看流程、工作流整体进度 40%
Merge pull request !100 from 子夜/feature/bpm-process-instancepull/105/head
commit
a7dcebc82a
|
@ -0,0 +1,49 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/** 流程定义 */
|
||||
export namespace BpmProcessDefinitionApi {
|
||||
export interface ProcessDefinitionVO {
|
||||
id: string;
|
||||
version: number;
|
||||
deploymentTime: number;
|
||||
suspensionState: number;
|
||||
formType?: number;
|
||||
bpmnXml?: string;
|
||||
simpleModel?: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询流程定义 */
|
||||
export async function getProcessDefinition(id?: string, key?: string) {
|
||||
return requestClient.get<BpmProcessDefinitionApi.ProcessDefinitionVO>(
|
||||
'/bpm/process-definition/get',
|
||||
{
|
||||
params: { id, key },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 分页查询流程定义 */
|
||||
export async function getProcessDefinitionPage(params: PageParam) {
|
||||
return requestClient.get<
|
||||
PageResult<BpmProcessDefinitionApi.ProcessDefinitionVO>
|
||||
>('/bpm/process-definition/page', { params });
|
||||
}
|
||||
|
||||
/** 查询流程定义列表 */
|
||||
export async function getProcessDefinitionList(params: any) {
|
||||
return requestClient.get<
|
||||
PageResult<BpmProcessDefinitionApi.ProcessDefinitionVO>
|
||||
>('/bpm/process-definition/list', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 查询流程定义列表(简单列表) */
|
||||
export async function getSimpleProcessDefinitionList() {
|
||||
return requestClient.get<
|
||||
PageResult<BpmProcessDefinitionApi.ProcessDefinitionVO>
|
||||
>('/bpm/process-definition/simple-list');
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace BpmOALeaveApi {
|
||||
export interface LeaveVO {
|
||||
id: number;
|
||||
status: number;
|
||||
type: number;
|
||||
reason: string;
|
||||
processInstanceId: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
createTime: Date;
|
||||
startUserSelectAssignees?: Record<string, string[]>;
|
||||
}
|
||||
}
|
||||
|
||||
/** 创建请假申请 */
|
||||
export async function createLeave(data: BpmOALeaveApi.LeaveVO) {
|
||||
return requestClient.post('/bpm/oa/leave/create', data);
|
||||
}
|
||||
|
||||
/** 更新请假申请 */
|
||||
export async function updateLeave(data: BpmOALeaveApi.LeaveVO) {
|
||||
return requestClient.post('/bpm/oa/leave/update', data);
|
||||
}
|
||||
|
||||
/** 获得请假申请 */
|
||||
export async function getLeave(id: number) {
|
||||
return requestClient.get<BpmOALeaveApi.LeaveVO>(`/bpm/oa/leave/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 获得请假申请分页 */
|
||||
export async function getLeavePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<BpmOALeaveApi.LeaveVO>>(
|
||||
'/bpm/oa/leave/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import type { BpmModelApi } from '#/api/bpm/model';
|
||||
import type { CandidateStrategyEnum, NodeTypeEnum } from '#/utils';
|
||||
import type { BpmCandidateStrategyEnum, BpmNodeTypeEnum } from '#/utils';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
|
@ -29,12 +29,12 @@ export namespace BpmProcessInstanceApi {
|
|||
|
||||
// 审批节点信息
|
||||
export type ApprovalNodeInfo = {
|
||||
candidateStrategy?: CandidateStrategyEnum;
|
||||
candidateStrategy?: BpmCandidateStrategyEnum;
|
||||
candidateUsers?: User[];
|
||||
endTime?: Date;
|
||||
id: number;
|
||||
name: string;
|
||||
nodeType: NodeTypeEnum;
|
||||
nodeType: BpmNodeTypeEnum;
|
||||
startTime?: Date;
|
||||
status: number;
|
||||
tasks: ApprovalTaskInfo[];
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace BpmTaskApi {
|
||||
/** BPM 流程监听器 VO */
|
||||
export interface TaskVO {
|
||||
id: number; // 编号
|
||||
name: string; // 监听器名字
|
||||
type: string; // 监听器类型
|
||||
status: number; // 监听器状态
|
||||
event: string; // 监听事件
|
||||
valueType: string; // 监听器值类型
|
||||
value: string; // 监听器值
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询待办任务分页 */
|
||||
export async function getTaskTodoPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<BpmTaskApi.TaskVO>>(
|
||||
'/bpm/task/todo-page',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询已办任务分页 */
|
||||
export async function getTaskDonePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<BpmTaskApi.TaskVO>>(
|
||||
'/bpm/task/done-page',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询任务管理分页 */
|
||||
export async function getTaskManagerPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<BpmTaskApi.TaskVO>>(
|
||||
'/bpm/task/manager-page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 审批任务 */
|
||||
export const approveTask = async (data: any) => {
|
||||
return await requestClient.put('/bpm/task/approve', data);
|
||||
};
|
||||
|
||||
/** 驳回任务 */
|
||||
export const rejectTask = async (data: any) => {
|
||||
return await requestClient.put('/bpm/task/reject', data);
|
||||
};
|
||||
|
||||
/** 根据流程实例 ID 查询任务列表 */
|
||||
export const getTaskListByProcessInstanceId = async (data: any) => {
|
||||
return await requestClient.get('/bpm/task/list-by-process-instance-id', data);
|
||||
};
|
||||
|
||||
/** 获取所有可退回的节点 */
|
||||
export const getTaskListByReturn = async (data: any) => {
|
||||
return await requestClient.get('/bpm/task/list-by-return', data);
|
||||
};
|
||||
|
||||
/** 退回 */
|
||||
export const returnTask = async (data: any) => {
|
||||
return await requestClient.put('/bpm/task/return', data);
|
||||
};
|
||||
|
||||
// 委派
|
||||
export const delegateTask = async (data: any) => {
|
||||
return await requestClient.put('/bpm/task/delegate', data);
|
||||
};
|
||||
|
||||
// 转派
|
||||
export const transferTask = async (data: any) => {
|
||||
return await requestClient.put('/bpm/task/transfer', data);
|
||||
};
|
||||
|
||||
// 加签
|
||||
export const signCreateTask = async (data: any) => {
|
||||
return await requestClient.put('/bpm/task/create-sign', data);
|
||||
};
|
||||
|
||||
// 减签
|
||||
export const signDeleteTask = async (data: any) => {
|
||||
return await requestClient.delete('/bpm/task/delete-sign', data);
|
||||
};
|
||||
|
||||
// 抄送
|
||||
export const copyTask = async (data: any) => {
|
||||
return await requestClient.put('/bpm/task/copy', data);
|
||||
};
|
||||
|
||||
// 获取我的待办任务
|
||||
export const myTodoTask = async (processInstanceId: string) => {
|
||||
return await requestClient.get(
|
||||
`/bpm/task/my-todo?processInstanceId=${processInstanceId}`,
|
||||
);
|
||||
};
|
||||
|
||||
// 获取加签任务列表
|
||||
export const getChildrenTaskList = async (id: string) => {
|
||||
return await requestClient.get(
|
||||
`/bpm/task/list-by-parent-task-id?parentTaskId=${id}`,
|
||||
);
|
||||
};
|
|
@ -52,7 +52,8 @@ const props = withDefaults(
|
|||
|
||||
const emit = defineEmits<{
|
||||
cancel: [];
|
||||
confirm: [value: number[]];
|
||||
closed: [];
|
||||
confirm: [value: SystemUserApi.User[]];
|
||||
'update:value': [value: number[]];
|
||||
}>();
|
||||
|
||||
|
@ -167,9 +168,12 @@ const loadUserData = async (pageNo: number, pageSize: number) => {
|
|||
|
||||
// 更新右侧列表数据
|
||||
const updateRightListData = () => {
|
||||
// 获取选中的用户
|
||||
// 使用 Set 来去重选中的用户ID
|
||||
const uniqueSelectedIds = new Set(selectedUserIds.value);
|
||||
|
||||
// 获取选中的用户,确保不重复
|
||||
const selectedUsers = userList.value.filter((user) =>
|
||||
selectedUserIds.value.includes(String(user.id)),
|
||||
uniqueSelectedIds.has(String(user.id)),
|
||||
);
|
||||
|
||||
// 应用搜索过滤
|
||||
|
@ -181,8 +185,10 @@ const updateRightListData = () => {
|
|||
)
|
||||
: selectedUsers;
|
||||
|
||||
// 更新总数
|
||||
rightListState.value.pagination.total = filteredUsers.length;
|
||||
// 更新总数(使用 Set 确保唯一性)
|
||||
rightListState.value.pagination.total = new Set(
|
||||
filteredUsers.map((user) => user.id),
|
||||
).size;
|
||||
|
||||
// 应用分页
|
||||
const { current, pageSize } = rightListState.value.pagination;
|
||||
|
@ -219,8 +225,9 @@ const handleUserSearch = async (direction: string, value: string) => {
|
|||
|
||||
// 处理用户选择变化
|
||||
const handleUserChange = (targetKeys: string[]) => {
|
||||
selectedUserIds.value = targetKeys;
|
||||
emit('update:value', targetKeys.map(Number));
|
||||
// 使用 Set 来去重选中的用户ID
|
||||
selectedUserIds.value = [...new Set(targetKeys)];
|
||||
emit('update:value', selectedUserIds.value.map(Number));
|
||||
updateRightListData();
|
||||
};
|
||||
|
||||
|
@ -258,7 +265,7 @@ const resetData = () => {
|
|||
};
|
||||
|
||||
// 打开弹窗
|
||||
const open = async () => {
|
||||
const open = async (userIds: string[]) => {
|
||||
resetData();
|
||||
loading.value = true;
|
||||
try {
|
||||
|
@ -273,15 +280,22 @@ const open = async () => {
|
|||
await loadUserData(1, leftListState.value.pagination.pageSize);
|
||||
|
||||
// 设置已选用户
|
||||
if (props.value?.length) {
|
||||
selectedUserIds.value = props.value.map(String);
|
||||
// 加载已选用户的完整信息
|
||||
if (userIds?.length) {
|
||||
selectedUserIds.value = userIds.map(String);
|
||||
// 加载已选用户的完整信息 TODO 目前接口暂不支持 多个用户ID 查询, 需要后端支持
|
||||
const { list } = await getUserPage({
|
||||
pageNo: 1,
|
||||
pageSize: props.value.length,
|
||||
userIds: props.value,
|
||||
pageSize: 100, // 临时使用固定值确保能加载所有已选用户
|
||||
userIds,
|
||||
});
|
||||
userList.value.push(...list);
|
||||
// 使用 Map 来去重,以用户 ID 为 key
|
||||
const userMap = new Map(userList.value.map((user) => [user.id, user]));
|
||||
list.forEach((user) => {
|
||||
if (!userMap.has(user.id)) {
|
||||
userMap.set(user.id, user);
|
||||
}
|
||||
});
|
||||
userList.value = [...userMap.values()];
|
||||
updateRightListData();
|
||||
}
|
||||
|
||||
|
@ -344,7 +358,12 @@ const handleConfirm = () => {
|
|||
message.warning('请选择用户');
|
||||
return;
|
||||
}
|
||||
emit('confirm', selectedUserIds.value.map(Number));
|
||||
emit(
|
||||
'confirm',
|
||||
userList.value.filter((user) =>
|
||||
selectedUserIds.value.includes(String(user.id)),
|
||||
),
|
||||
);
|
||||
modalApi.close();
|
||||
};
|
||||
|
||||
|
@ -360,6 +379,7 @@ const handleCancel = () => {
|
|||
|
||||
// 关闭弹窗
|
||||
const handleClosed = () => {
|
||||
emit('closed');
|
||||
resetData();
|
||||
};
|
||||
|
||||
|
@ -421,7 +441,7 @@ defineExpose({
|
|||
@search="handleUserSearch"
|
||||
>
|
||||
<template #render="item">
|
||||
{{ item.nickname }} ({{ item.id }})
|
||||
<span>{{ item?.nickname }} ({{ item?.username }})</span>
|
||||
</template>
|
||||
|
||||
<template #footer="{ direction }">
|
||||
|
|
|
@ -9,6 +9,25 @@ const routes: RouteRecordRaw[] = [
|
|||
hideInMenu: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'task',
|
||||
name: 'BpmTask',
|
||||
meta: {
|
||||
title: '审批中心',
|
||||
icon: 'ant-design:history-outlined',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'my',
|
||||
name: 'BpmTaskMy',
|
||||
component: () => import('#/views/bpm/processInstance/index.vue'),
|
||||
meta: {
|
||||
title: '我的流程',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: 'process-instance/detail',
|
||||
component: () => import('#/views/bpm/processInstance/detail/index.vue'),
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
// OA请假相关路由配置
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/bpm/oa',
|
||||
name: 'OALeave',
|
||||
meta: {
|
||||
title: 'OA请假',
|
||||
hideInMenu: true,
|
||||
redirect: '/bpm/oa/leave/index',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'leave',
|
||||
name: 'OALeaveIndex',
|
||||
component: () => import('#/views/bpm/oa/leave/index.vue'),
|
||||
meta: {
|
||||
title: '请假列表',
|
||||
activePath: '/bpm/oa/leave',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'leave/create',
|
||||
name: 'OALeaveCreate',
|
||||
component: () => import('#/views/bpm/oa/leave/create.vue'),
|
||||
meta: {
|
||||
title: '创建请假',
|
||||
activePath: '/bpm/oa/leave',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'leave/detail',
|
||||
name: 'OALeaveDetail',
|
||||
component: () => import('#/views/bpm/oa/leave/detail.vue'),
|
||||
meta: {
|
||||
title: '请假详情',
|
||||
activePath: '/bpm/oa/leave',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
|
@ -466,7 +466,7 @@ export const BpmAutoApproveType = {
|
|||
};
|
||||
|
||||
// 候选人策略枚举 ( 用于审批节点。抄送节点 )
|
||||
export enum CandidateStrategyEnum {
|
||||
export enum BpmCandidateStrategyEnum {
|
||||
/**
|
||||
* 审批人自选
|
||||
*/
|
||||
|
@ -532,7 +532,7 @@ export enum CandidateStrategyEnum {
|
|||
/**
|
||||
* 节点类型
|
||||
*/
|
||||
export enum NodeTypeEnum {
|
||||
export enum BpmNodeTypeEnum {
|
||||
/**
|
||||
* 子流程节点
|
||||
*/
|
||||
|
@ -597,7 +597,7 @@ export enum NodeTypeEnum {
|
|||
/**
|
||||
* 任务状态枚举
|
||||
*/
|
||||
export enum TaskStatusEnum {
|
||||
export enum BpmTaskStatusEnum {
|
||||
/**
|
||||
* 审批通过
|
||||
*/
|
||||
|
@ -634,3 +634,36 @@ export enum TaskStatusEnum {
|
|||
*/
|
||||
WAIT = 0,
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点 Id 枚举
|
||||
*/
|
||||
export enum BpmNodeIdEnum {
|
||||
/**
|
||||
* 发起人节点 Id
|
||||
*/
|
||||
END_EVENT_NODE_ID = 'EndEvent',
|
||||
|
||||
/**
|
||||
* 发起人节点 Id
|
||||
*/
|
||||
START_USER_NODE_ID = 'StartUserNode',
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单权限的枚举
|
||||
*/
|
||||
export enum BpmFieldPermissionType {
|
||||
/**
|
||||
* 隐藏
|
||||
*/
|
||||
NONE = '3',
|
||||
/**
|
||||
* 只读
|
||||
*/
|
||||
READ = '1',
|
||||
/**
|
||||
* 编辑
|
||||
*/
|
||||
WRITE = '2',
|
||||
}
|
||||
|
|
|
@ -0,0 +1,269 @@
|
|||
<script lang="ts" setup>
|
||||
import type { BpmOALeaveApi } from '#/api/bpm/oa/leave';
|
||||
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { confirm, Page, useVbenForm } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Button, Card, message, Space } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { getProcessDefinition } from '#/api/bpm/definition';
|
||||
import { createLeave, updateLeave } from '#/api/bpm/oa/leave';
|
||||
import { getApprovalDetail as getApprovalDetailApi } from '#/api/bpm/processInstance';
|
||||
import { $t } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
import { BpmCandidateStrategyEnum, BpmNodeIdEnum } from '#/utils';
|
||||
import ProcessInstanceTimeline from '#/views/bpm/processInstance/detail/modules/time-line.vue';
|
||||
|
||||
import { useFormSchema } from './data';
|
||||
|
||||
const formLoading = ref(false); // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
|
||||
// 审批相关:变量
|
||||
const processDefineKey = 'oa_leave'; // 流程定义 Key
|
||||
const startUserSelectTasks = ref<any>([]); // 发起人需要选择审批人的用户任务列表
|
||||
const startUserSelectAssignees = ref({}); // 发起人选择审批人的数据
|
||||
const tempStartUserSelectAssignees = ref({}); // 历史发起人选择审批人的数据,用于每次表单变更时,临时保存
|
||||
const activityNodes = ref<BpmProcessInstanceApi.ApprovalNodeInfo[]>([]); // 审批节点信息
|
||||
const processDefinitionId = ref('');
|
||||
|
||||
const formData = ref<BpmOALeaveApi.LeaveVO>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['请假'])
|
||||
: $t('ui.actionTitle.create', ['请假']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 100,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
/** 提交申请 */
|
||||
async function onSubmit() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1.2 审批相关:校验指定审批人
|
||||
if (startUserSelectTasks.value?.length > 0) {
|
||||
for (const userTask of startUserSelectTasks.value) {
|
||||
if (
|
||||
Array.isArray(startUserSelectAssignees.value[userTask.id]) &&
|
||||
startUserSelectAssignees.value[userTask.id].length === 0
|
||||
) {
|
||||
return message.warning(`请选择${userTask.name}的审批人`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as BpmOALeaveApi.LeaveVO;
|
||||
|
||||
// 审批相关:设置指定审批人
|
||||
if (startUserSelectTasks.value?.length > 0) {
|
||||
data.startUserSelectAssignees = startUserSelectAssignees.value;
|
||||
}
|
||||
|
||||
// 格式化开始时间和结束时间的值
|
||||
const submitData: BpmOALeaveApi.LeaveVO = {
|
||||
...data,
|
||||
startTime: Number(data.startTime),
|
||||
endTime: Number(data.endTime),
|
||||
};
|
||||
|
||||
try {
|
||||
formLoading.value = true;
|
||||
await (formData.value?.id
|
||||
? updateLeave(submitData)
|
||||
: createLeave(submitData));
|
||||
// 关闭并提示
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.operationSuccess'),
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
|
||||
router.push({
|
||||
name: 'BpmOALeaveList',
|
||||
});
|
||||
} catch (error: any) {
|
||||
message.error(error.message);
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存草稿 */
|
||||
async function onDraft() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = (await formApi.getValues()) as BpmOALeaveApi.LeaveVO;
|
||||
|
||||
// 格式化开始时间和结束时间的值
|
||||
const submitData: BpmOALeaveApi.LeaveVO = {
|
||||
...data,
|
||||
startTime: Number(data.startTime),
|
||||
endTime: Number(data.endTime),
|
||||
};
|
||||
|
||||
try {
|
||||
formLoading.value = true;
|
||||
await (formData.value?.id
|
||||
? updateLeave(submitData)
|
||||
: createLeave(submitData));
|
||||
// 关闭并提示
|
||||
message.success({
|
||||
content: '保存草稿成功',
|
||||
});
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function onBack() {
|
||||
confirm({
|
||||
content: '确定要返回上一页吗?请先保存您填写的信息!',
|
||||
icon: 'warning',
|
||||
beforeClose({ isConfirm }) {
|
||||
if (isConfirm) {
|
||||
router.back();
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ============================== 审核流程相关 ==============================
|
||||
|
||||
/** 审批相关:获取审批详情 */
|
||||
const getApprovalDetail = async () => {
|
||||
try {
|
||||
const data = await getApprovalDetailApi({
|
||||
processDefinitionId: processDefinitionId.value,
|
||||
// TODO 小北:可以支持 processDefinitionKey 查询
|
||||
activityId: BpmNodeIdEnum.START_USER_NODE_ID,
|
||||
processVariablesStr: JSON.stringify({
|
||||
day: dayjs(formData.value?.startTime).diff(
|
||||
dayjs(formData.value?.endTime),
|
||||
'day',
|
||||
),
|
||||
}), // 解决 GET 无法传递对象的问题,后端 String 再转 JSON
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
message.error('查询不到审批详情信息!');
|
||||
return;
|
||||
}
|
||||
// 获取审批节点,显示 Timeline 的数据
|
||||
activityNodes.value = data.activityNodes;
|
||||
|
||||
// 获取发起人自选的任务
|
||||
startUserSelectTasks.value = data.activityNodes?.filter(
|
||||
(node: BpmProcessInstanceApi.ApprovalNodeInfo) =>
|
||||
BpmCandidateStrategyEnum.START_USER_SELECT === node.candidateStrategy,
|
||||
);
|
||||
// 恢复之前的选择审批人
|
||||
if (startUserSelectTasks.value?.length > 0) {
|
||||
for (const node of startUserSelectTasks.value) {
|
||||
startUserSelectAssignees.value[node.id] =
|
||||
tempStartUserSelectAssignees.value[node.id] &&
|
||||
tempStartUserSelectAssignees.value[node.id].length > 0
|
||||
? tempStartUserSelectAssignees.value[node.id]
|
||||
: [];
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
}
|
||||
};
|
||||
/** 审批相关:选择发起人 */
|
||||
const selectUserConfirm = (id: string, userList: any[]) => {
|
||||
startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id);
|
||||
};
|
||||
|
||||
/** 审批相关:预测流程节点会因为输入的参数值而产生新的预测结果值,所以需重新预测一次, formData.value可改成实际业务中的特定字段 */
|
||||
watch(
|
||||
formData.value,
|
||||
(newValue, oldValue) => {
|
||||
if (!oldValue) {
|
||||
return;
|
||||
}
|
||||
if (newValue && Object.keys(newValue).length > 0) {
|
||||
// 记录之前的节点审批人
|
||||
tempStartUserSelectAssignees.value = startUserSelectAssignees.value;
|
||||
startUserSelectAssignees.value = {};
|
||||
// 加载最新的审批详情,主要用于节点预测
|
||||
getApprovalDetail();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
// ============================== 生命周期 ==============================
|
||||
onMounted(async () => {
|
||||
const processDefinitionDetail = await getProcessDefinition(
|
||||
undefined,
|
||||
processDefineKey,
|
||||
);
|
||||
|
||||
if (!processDefinitionDetail) {
|
||||
message.error('OA 请假的流程模型未配置,请检查!');
|
||||
return;
|
||||
}
|
||||
|
||||
processDefinitionId.value = processDefinitionDetail.id;
|
||||
startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks;
|
||||
|
||||
getApprovalDetail();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<div class="w-80vw mx-auto max-w-[920px]">
|
||||
<Card :title="getTitle" class="w-full">
|
||||
<template #extra>
|
||||
<Button type="default" @click="onBack">
|
||||
<IconifyIcon icon="mdi:arrow-left" />
|
||||
返回
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<Form />
|
||||
</Card>
|
||||
|
||||
<Card title="流程" class="mt-2 w-full">
|
||||
<ProcessInstanceTimeline
|
||||
:activity-nodes="activityNodes"
|
||||
:show-status-icon="false"
|
||||
@select-user-confirm="selectUserConfirm"
|
||||
/>
|
||||
|
||||
<template #actions>
|
||||
<Space warp :size="12" class="w-full px-6">
|
||||
<Button type="primary" @click="onSubmit"> 提交 </Button>
|
||||
<!-- TODO 后端接口暂不支持保存草稿 (即仅保存数据,不触发流程)-->
|
||||
<!-- <Button type="default" @click="onDraft"> 保存草稿 </Button> -->
|
||||
</Space>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
|
@ -0,0 +1,200 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmCategoryApi } from '#/api/bpm/category';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '请假类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择请假类型',
|
||||
options: getDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE, 'number'),
|
||||
allowClear: true,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
|
||||
{
|
||||
fieldName: 'startTime',
|
||||
label: '开始时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
placeholder: '请选择开始时间',
|
||||
showTime: true,
|
||||
valueFormat: 'x',
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'endTime',
|
||||
label: '结束时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
placeholder: '请选择结束时间',
|
||||
showTime: true,
|
||||
valueFormat: 'x',
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'reason',
|
||||
label: '原因',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入原因',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function GridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '请假类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择请假类型',
|
||||
options: getDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE, 'number'),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '审批结果',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择审批结果',
|
||||
options: getDictOptions(
|
||||
DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
|
||||
'number',
|
||||
),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'reason',
|
||||
label: '原因',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入原因',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = BpmCategoryApi.CategoryVO>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '申请编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'startTime',
|
||||
title: '开始时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'endTime',
|
||||
title: '结束时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '请假类型',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.BPM_OA_LEAVE_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'reason',
|
||||
title: '原因',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '申请时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '请假',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'detail',
|
||||
text: '详情',
|
||||
show: hasAccessByCodes(['bpm:oa-leave:query']),
|
||||
},
|
||||
{
|
||||
code: 'progress',
|
||||
text: '进度',
|
||||
show: hasAccessByCodes(['bpm:oa-leave:query']),
|
||||
},
|
||||
{
|
||||
code: 'cancel',
|
||||
text: '取消',
|
||||
show: (row: any) =>
|
||||
row.status === 1 && hasAccessByCodes(['bpm:oa-leave:query']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<div>
|
||||
<h1>请假详情</h1>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
|
@ -1,34 +1,171 @@
|
|||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
import type { PageParam } from '@vben/request';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { BpmOALeaveApi } from '#/api/bpm/oa/leave';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { Page, prompt } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { Button, message, Textarea } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getLeavePage } from '#/api/bpm/oa/leave';
|
||||
import { cancelProcessInstanceByStartUser } from '#/api/bpm/processInstance';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { router } from '#/router';
|
||||
|
||||
import { GridFormSchema, useGridColumns } from './data';
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: GridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }: PageParam, formValues: any) => {
|
||||
return await getLeavePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<BpmOALeaveApi.LeaveVO>,
|
||||
});
|
||||
|
||||
/** 创建请假 */
|
||||
function onCreate() {
|
||||
router.push({
|
||||
name: 'OALeaveCreate',
|
||||
query: {
|
||||
formType: 'create',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 查看请假详情 */
|
||||
const onDetail = (row: BpmOALeaveApi.LeaveVO) => {
|
||||
router.push({
|
||||
name: 'OALeaveDetail',
|
||||
query: { id: row.id },
|
||||
});
|
||||
};
|
||||
|
||||
/** 取消请假 */
|
||||
const onCancel = (row: BpmOALeaveApi.LeaveVO) => {
|
||||
prompt({
|
||||
async beforeClose(scope) {
|
||||
if (scope.isConfirm) {
|
||||
if (scope.value) {
|
||||
try {
|
||||
await cancelProcessInstanceByStartUser(row.id, scope.value);
|
||||
message.success('取消成功');
|
||||
onRefresh();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
message.error('请输入取消原因');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
component: () => {
|
||||
return h(Textarea, {
|
||||
placeholder: '请输入取消原因',
|
||||
allowClear: true,
|
||||
rows: 2,
|
||||
rules: [{ required: true, message: '请输入取消原因' }],
|
||||
});
|
||||
},
|
||||
content: '请输入取消原因',
|
||||
title: '取消流程',
|
||||
modelPropName: 'value',
|
||||
});
|
||||
};
|
||||
|
||||
/** 审批进度 */
|
||||
const onProgress = (row: BpmOALeaveApi.LeaveVO) => {
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: { id: row.processInstanceId },
|
||||
});
|
||||
};
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<BpmOALeaveApi.LeaveVO>) {
|
||||
switch (code) {
|
||||
case 'cancel': {
|
||||
onCancel(row);
|
||||
break;
|
||||
}
|
||||
case 'detail': {
|
||||
onDetail(row);
|
||||
break;
|
||||
}
|
||||
case 'progress': {
|
||||
onProgress(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<Page auto-content-height>
|
||||
<DocAlert
|
||||
title="审批接入(业务表单)"
|
||||
url="https://doc.iocoder.cn/bpm/use-business-form/"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/oa/leave/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/oa/leave/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
|
||||
<Grid table-title="请假列表">
|
||||
<template #toolbar-tools>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['bpm:category:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
发起请假
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<template #userIds-cell="{ row }">
|
||||
<span
|
||||
v-for="(userId, index) in row.userIds"
|
||||
:key="userId"
|
||||
class="pr-5px"
|
||||
>
|
||||
{{ dataList.find((user) => user.id === userId)?.nickname }}
|
||||
<span v-if="index < row.userIds.length - 1">、</span>
|
||||
</span>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
@ -1,28 +1,355 @@
|
|||
<script lang="ts" setup>
|
||||
import type { BpmProcessDefinitionApi } from '#/api/bpm/definition';
|
||||
|
||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import {
|
||||
Card,
|
||||
Col,
|
||||
InputSearch,
|
||||
message,
|
||||
Row,
|
||||
Space,
|
||||
Tabs,
|
||||
Tooltip,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { getCategorySimpleList } from '#/api/bpm/category';
|
||||
import { getProcessDefinitionList } from '#/api/bpm/definition';
|
||||
import { getProcessInstance } from '#/api/bpm/processInstance';
|
||||
|
||||
import ProcessDefinitionDetail from './modules/form.vue';
|
||||
|
||||
defineOptions({ name: 'BpmProcessInstanceCreate' });
|
||||
|
||||
const route = useRoute(); // 路由
|
||||
|
||||
const searchName = ref(''); // 当前搜索关键字
|
||||
const isSearching = ref(false); // 是否处于搜索状态
|
||||
const processInstanceId: any = route.query.processInstanceId; // 流程实例编号。场景:重新发起时
|
||||
const loading = ref(true); // 加载中
|
||||
const categoryList: any = ref([]); // 分类的列表
|
||||
const activeCategory = ref(''); // 当前选中的分类
|
||||
const processDefinitionList = ref([]); // 流程定义的列表
|
||||
|
||||
// 实现 groupBy 功能
|
||||
const groupBy = (array: any[], key: string) => {
|
||||
const result: Record<string, any[]> = {};
|
||||
for (const item of array) {
|
||||
const groupKey = item[key];
|
||||
if (!result[groupKey]) {
|
||||
result[groupKey] = [];
|
||||
}
|
||||
result[groupKey].push(item);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 所有流程分类数据
|
||||
await getCategoryList();
|
||||
// 所有流程定义数据
|
||||
await handleGetProcessDefinitionList();
|
||||
|
||||
// 如果 processInstanceId 非空,说明是重新发起
|
||||
if (processInstanceId?.length > 0) {
|
||||
const processInstance = await getProcessInstance(processInstanceId);
|
||||
if (!processInstance) {
|
||||
message.error('重新发起流程失败,原因:流程实例不存在');
|
||||
return;
|
||||
}
|
||||
const processDefinition = processDefinitionList.value.find(
|
||||
(item: any) => item.key === processInstance.processDefinition?.key,
|
||||
);
|
||||
if (!processDefinition) {
|
||||
message.error('重新发起流程失败,原因:流程定义不存在');
|
||||
return;
|
||||
}
|
||||
await handleSelect(processDefinition, processInstance.formVariables);
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取所有流程分类数据 */
|
||||
const getCategoryList = async () => {
|
||||
try {
|
||||
// 流程分类
|
||||
categoryList.value = await getCategorySimpleList();
|
||||
} catch {
|
||||
// 错误处理
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取所有流程定义数据 */
|
||||
const handleGetProcessDefinitionList = async () => {
|
||||
try {
|
||||
// 流程定义
|
||||
processDefinitionList.value = await getProcessDefinitionList({
|
||||
suspensionState: 1,
|
||||
});
|
||||
// 初始化过滤列表为全部流程定义
|
||||
filteredProcessDefinitionList.value = processDefinitionList.value;
|
||||
|
||||
// 在获取完所有数据后,设置第一个有效分类为激活状态
|
||||
if (availableCategories.value.length > 0 && !activeCategory.value) {
|
||||
activeCategory.value = availableCategories.value[0].code;
|
||||
}
|
||||
} catch {
|
||||
// 错误处理
|
||||
}
|
||||
};
|
||||
|
||||
/** 搜索流程 */
|
||||
const filteredProcessDefinitionList = ref([]); // 用于存储搜索过滤后的流程定义
|
||||
const handleQuery = () => {
|
||||
if (searchName.value.trim()) {
|
||||
// 如果有搜索关键字,进行过滤
|
||||
isSearching.value = true;
|
||||
filteredProcessDefinitionList.value = processDefinitionList.value.filter(
|
||||
(definition: any) =>
|
||||
definition.name.toLowerCase().includes(searchName.value.toLowerCase()),
|
||||
);
|
||||
|
||||
// 获取搜索结果中的分类
|
||||
const searchResultGroups = groupBy(
|
||||
filteredProcessDefinitionList.value,
|
||||
'category',
|
||||
);
|
||||
const availableCategoryCodes = Object.keys(searchResultGroups);
|
||||
|
||||
// 如果有匹配的分类,切换到第一个包含匹配结果的分类
|
||||
if (availableCategoryCodes.length > 0 && availableCategoryCodes[0]) {
|
||||
activeCategory.value = availableCategoryCodes[0];
|
||||
}
|
||||
} else {
|
||||
// 如果没有搜索关键字,恢复所有数据
|
||||
isSearching.value = false;
|
||||
filteredProcessDefinitionList.value = processDefinitionList.value;
|
||||
}
|
||||
};
|
||||
|
||||
/** 判断流程定义是否匹配搜索 */
|
||||
const isDefinitionMatchSearch = (definition: any) => {
|
||||
if (!isSearching.value) return false;
|
||||
return definition.name.toLowerCase().includes(searchName.value.toLowerCase());
|
||||
};
|
||||
|
||||
/** 流程定义的分组 */
|
||||
const processDefinitionGroup: any = computed(() => {
|
||||
if (!processDefinitionList.value?.length) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const grouped = groupBy(filteredProcessDefinitionList.value, 'category');
|
||||
// 按照 categoryList 的顺序重新组织数据
|
||||
const orderedGroup = {};
|
||||
categoryList.value.forEach((category: any) => {
|
||||
if (grouped[category.code]) {
|
||||
orderedGroup[category.code] = grouped[category.code];
|
||||
}
|
||||
});
|
||||
return orderedGroup;
|
||||
});
|
||||
|
||||
/** 通过分类 code 获取对应的名称 */
|
||||
const getCategoryName = (categoryCode: string) => {
|
||||
return categoryList.value?.find((ctg: any) => ctg.code === categoryCode)
|
||||
?.name;
|
||||
};
|
||||
|
||||
// ========== 表单相关 ==========
|
||||
const selectProcessDefinition = ref(); // 选择的流程定义
|
||||
const processDefinitionDetailRef = ref();
|
||||
|
||||
/** 处理选择流程的按钮操作 */
|
||||
const handleSelect = async (
|
||||
row: BpmProcessDefinitionApi.ProcessDefinitionVO,
|
||||
formVariables?: any,
|
||||
) => {
|
||||
// 设置选择的流程
|
||||
selectProcessDefinition.value = row;
|
||||
// 初始化流程定义详情
|
||||
await nextTick();
|
||||
processDefinitionDetailRef.value?.initProcessInfo(row, formVariables);
|
||||
};
|
||||
|
||||
/** 过滤出有流程的分类列表。目的:只展示有流程的分类 */
|
||||
const availableCategories = computed(() => {
|
||||
if (!categoryList.value?.length || !processDefinitionGroup.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 获取所有有流程的分类代码
|
||||
const availableCategoryCodes = Object.keys(processDefinitionGroup.value);
|
||||
|
||||
// 过滤出有流程的分类
|
||||
return categoryList.value.filter((category: CategoryVO) =>
|
||||
availableCategoryCodes.includes(category.code),
|
||||
);
|
||||
});
|
||||
|
||||
/** 获取 tab 的位置 */
|
||||
|
||||
const tabPosition = computed(() => {
|
||||
return window.innerWidth < 768 ? 'top' : 'left';
|
||||
});
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/create/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/create/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<!-- 第一步,通过流程定义的列表,选择对应的流程 -->
|
||||
<template v-if="!selectProcessDefinition">
|
||||
<Card
|
||||
class="h-full"
|
||||
title="全部流程"
|
||||
:class="{
|
||||
'process-definition-container': filteredProcessDefinitionList?.length,
|
||||
}"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #extra>
|
||||
<div class="flex items-end">
|
||||
<InputSearch
|
||||
v-model:value="searchName"
|
||||
class="!w-50% mb-15px"
|
||||
placeholder="请输入流程名称检索"
|
||||
allow-clear
|
||||
@input="handleQuery"
|
||||
@clear="handleQuery"
|
||||
>
|
||||
<template #prefix>
|
||||
<IconifyIcon icon="mdi:search-web" />
|
||||
</template>
|
||||
</InputSearch>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="filteredProcessDefinitionList?.length">
|
||||
<Tabs v-model:active-key="activeCategory" :tab-position="tabPosition">
|
||||
<Tabs.TabPane
|
||||
v-for="category in availableCategories"
|
||||
:key="category.code"
|
||||
:tab="category.name"
|
||||
>
|
||||
<Row :gutter="[16, 16]">
|
||||
<Col
|
||||
v-for="definition in processDefinitionGroup[category.code]"
|
||||
:key="definition.id"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:md="8"
|
||||
:lg="6"
|
||||
:xl="4"
|
||||
@click="handleSelect(definition)"
|
||||
>
|
||||
<Card
|
||||
hoverable
|
||||
class="definition-item-card w-full cursor-pointer"
|
||||
:class="{
|
||||
'search-match': isDefinitionMatchSearch(definition),
|
||||
}"
|
||||
:body-style="{
|
||||
width: '100%',
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<img
|
||||
v-if="definition.icon"
|
||||
:src="definition.icon"
|
||||
class="h-12 w-12 object-contain"
|
||||
alt="流程图标"
|
||||
/>
|
||||
<div v-else class="flow-icon flex-shrink-0">
|
||||
<Tooltip :title="definition.name">
|
||||
<span class="text-xs text-white">
|
||||
{{ definition.name }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<span class="ml-3 flex-1 truncate text-base">
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
:title="`${definition.name}`"
|
||||
>
|
||||
{{ definition.name }}
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- TODO: 发起流程按钮 -->
|
||||
<!-- <template #actions>
|
||||
<div class="flex justify-end px-4">
|
||||
<Button type="link" @click="handleSelect(definition)">
|
||||
发起流程
|
||||
</Button>
|
||||
</div>
|
||||
</template> -->
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div v-else class="!py-200px text-center">
|
||||
<Space direction="vertical" size="large">
|
||||
<span class="text-gray-500">没有找到搜索结果</span>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<!-- 第二步,填写表单,进行流程的提交 -->
|
||||
<ProcessDefinitionDetail
|
||||
v-else
|
||||
ref="processDefinitionDetailRef"
|
||||
:select-process-definition="selectProcessDefinition"
|
||||
@cancel="selectProcessDefinition = undefined"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.process-definition-container {
|
||||
.definition-item-card {
|
||||
.flow-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: #3f73f7;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&.search-match {
|
||||
background-color: rgb(63 115 247 / 10%);
|
||||
border: 1px solid #3f73f7;
|
||||
animation: bounce 0.5s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,354 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ApiAttrs } from '@form-create/ant-design-vue/types/config';
|
||||
|
||||
import type { BpmProcessDefinitionApi } from '#/api/bpm/definition';
|
||||
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
|
||||
import { useTabs } from '@vben/hooks';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Button, Card, Col, message, Row, Space, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { getProcessDefinition } from '#/api/bpm/definition';
|
||||
import {
|
||||
createProcessInstance,
|
||||
getApprovalDetail as getApprovalDetailApi,
|
||||
} from '#/api/bpm/processInstance';
|
||||
import { router } from '#/router';
|
||||
import {
|
||||
BpmCandidateStrategyEnum,
|
||||
BpmFieldPermissionType,
|
||||
BpmModelFormType,
|
||||
BpmNodeIdEnum,
|
||||
BpmNodeTypeEnum,
|
||||
decodeFields,
|
||||
setConfAndFields2,
|
||||
} from '#/utils';
|
||||
import ProcessInstanceTimeline from '#/views/bpm/processInstance/detail/modules/time-line.vue';
|
||||
|
||||
// 类型定义
|
||||
interface ProcessFormData {
|
||||
rule: any[];
|
||||
option: Record<string, any>;
|
||||
value: Record<string, any>;
|
||||
}
|
||||
|
||||
interface UserTask {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ApprovalNodeInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
candidateStrategy: BpmCandidateStrategyEnum;
|
||||
candidateUsers?: Array<{
|
||||
avatar: string;
|
||||
id: number;
|
||||
nickname: string;
|
||||
}>;
|
||||
endTime?: Date;
|
||||
nodeType: BpmNodeTypeEnum;
|
||||
startTime?: Date;
|
||||
status: number;
|
||||
tasks: any[];
|
||||
}
|
||||
|
||||
defineOptions({ name: 'BpmProcessInstanceCreateForm' });
|
||||
|
||||
const props = defineProps({
|
||||
selectProcessDefinition: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['cancel']);
|
||||
|
||||
const { closeCurrentTab } = useTabs();
|
||||
|
||||
const getTitle = computed(() => {
|
||||
return `流程表单 - ${props.selectProcessDefinition.name}`;
|
||||
});
|
||||
|
||||
const detailForm = ref<ProcessFormData>({
|
||||
rule: [],
|
||||
option: {},
|
||||
value: {},
|
||||
});
|
||||
|
||||
const fApi = ref<ApiAttrs>();
|
||||
const startUserSelectTasks = ref<UserTask[]>([]);
|
||||
const startUserSelectAssignees = ref<Record<number, string[]>>({});
|
||||
const tempStartUserSelectAssignees = ref<Record<number, string[]>>({});
|
||||
const bpmnXML = ref<string | undefined>(undefined);
|
||||
const simpleJson = ref<string | undefined>(undefined);
|
||||
const timelineRef = ref<any>();
|
||||
const activeTab = ref('form');
|
||||
const activityNodes = ref<ApprovalNodeInfo[]>([]);
|
||||
const processInstanceStartLoading = ref(false);
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = async () => {
|
||||
if (!fApi.value || !props.selectProcessDefinition) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 流程表单校验
|
||||
await fApi.value.validate();
|
||||
|
||||
// 校验指定审批人
|
||||
if (startUserSelectTasks.value?.length > 0) {
|
||||
for (const userTask of startUserSelectTasks.value) {
|
||||
const assignees = startUserSelectAssignees.value[userTask.id];
|
||||
if (Array.isArray(assignees) && assignees.length === 0) {
|
||||
message.warning(`请选择${userTask.name}的候选人`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交请求
|
||||
processInstanceStartLoading.value = true;
|
||||
await createProcessInstance({
|
||||
processDefinitionId: props.selectProcessDefinition.id,
|
||||
variables: detailForm.value.value,
|
||||
startUserSelectAssignees: startUserSelectAssignees.value,
|
||||
});
|
||||
|
||||
message.success('发起流程成功');
|
||||
|
||||
closeCurrentTab();
|
||||
|
||||
await router.push({ path: '/bpm/task/my' });
|
||||
} catch (error) {
|
||||
message.error('发起流程失败');
|
||||
console.error('发起流程失败:', error);
|
||||
} finally {
|
||||
processInstanceStartLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 设置表单信息、获取流程图数据 */
|
||||
const initProcessInfo = async (row: any, formVariables?: any) => {
|
||||
// 重置指定审批人
|
||||
startUserSelectTasks.value = [];
|
||||
startUserSelectAssignees.value = {};
|
||||
|
||||
// 情况一:流程表单
|
||||
if (row.formType === BpmModelFormType.NORMAL) {
|
||||
// 设置表单
|
||||
// 注意:需要从 formVariables 中,移除不在 row.formFields 的值。
|
||||
// 原因是:后端返回的 formVariables 里面,会有一些非表单的信息。例如说,某个流程节点的审批人。
|
||||
// 这样,就可能导致一个流程被审批不通过后,重新发起时,会直接后端报错!!!
|
||||
const allowedFields = new Set(
|
||||
decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field),
|
||||
);
|
||||
for (const key in formVariables) {
|
||||
if (!allowedFields.has(key)) {
|
||||
delete formVariables[key];
|
||||
}
|
||||
}
|
||||
setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables);
|
||||
|
||||
await nextTick();
|
||||
fApi.value?.btn.show(false); // 隐藏提交按钮
|
||||
|
||||
// 获取流程审批信息,当再次发起时,流程审批节点要根据原始表单参数预测出来
|
||||
await getApprovalDetail({
|
||||
id: row.id,
|
||||
processVariablesStr: JSON.stringify(formVariables),
|
||||
});
|
||||
|
||||
// 加载流程图
|
||||
const processDefinitionDetail: BpmProcessDefinitionApi.ProcessDefinitionVO =
|
||||
await getProcessDefinition(row.id);
|
||||
if (processDefinitionDetail) {
|
||||
bpmnXML.value = processDefinitionDetail.bpmnXml;
|
||||
simpleJson.value = processDefinitionDetail.simpleModel;
|
||||
}
|
||||
// 情况二:业务表单
|
||||
} else if (row.formCustomCreatePath) {
|
||||
await router.push({
|
||||
path: row.formCustomCreatePath,
|
||||
});
|
||||
// 这里暂时无需加载流程图,因为跳出到另外个 Tab;
|
||||
}
|
||||
};
|
||||
|
||||
/** 预测流程节点会因为输入的参数值而产生新的预测结果值,所以需重新预测一次 */
|
||||
watch(
|
||||
detailForm.value,
|
||||
(newValue) => {
|
||||
if (newValue && Object.keys(newValue.value).length > 0) {
|
||||
// 记录之前的节点审批人
|
||||
tempStartUserSelectAssignees.value = startUserSelectAssignees.value;
|
||||
startUserSelectAssignees.value = {};
|
||||
// 加载最新的审批详情
|
||||
getApprovalDetail({
|
||||
id: props.selectProcessDefinition.id,
|
||||
processVariablesStr: JSON.stringify(newValue.value), // 解决 GET 无法传递对象的问题,后端 String 再转 JSON
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
/** 获取审批详情 */
|
||||
const getApprovalDetail = async (row: {
|
||||
id: string;
|
||||
processVariablesStr: string;
|
||||
}) => {
|
||||
try {
|
||||
const data = await getApprovalDetailApi({
|
||||
processDefinitionId: row.id,
|
||||
activityId: BpmNodeIdEnum.START_USER_NODE_ID,
|
||||
processVariablesStr: row.processVariablesStr,
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
message.error('查询不到审批详情信息!');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取审批节点
|
||||
activityNodes.value = data.activityNodes as unknown as ApprovalNodeInfo[];
|
||||
|
||||
// 获取发起人自选的任务
|
||||
startUserSelectTasks.value = (data.activityNodes?.filter(
|
||||
(node) =>
|
||||
BpmCandidateStrategyEnum.START_USER_SELECT === node.candidateStrategy,
|
||||
) || []) as unknown as UserTask[];
|
||||
|
||||
// 恢复之前的选择审批人
|
||||
if (startUserSelectTasks.value.length > 0) {
|
||||
for (const node of startUserSelectTasks.value) {
|
||||
const tempAssignees = tempStartUserSelectAssignees.value[node.id];
|
||||
startUserSelectAssignees.value[node.id] = tempAssignees?.length
|
||||
? tempAssignees
|
||||
: [];
|
||||
}
|
||||
}
|
||||
|
||||
// 设置表单字段权限
|
||||
const formFieldsPermission = data.formFieldsPermission;
|
||||
if (formFieldsPermission) {
|
||||
Object.entries(formFieldsPermission).forEach(([field, permission]) => {
|
||||
setFieldPermission(field, permission as string);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取审批详情失败');
|
||||
console.error('获取审批详情失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置表单权限
|
||||
*/
|
||||
const setFieldPermission = (field: string, permission: string) => {
|
||||
if (permission === BpmFieldPermissionType.READ) {
|
||||
// @ts-ignore
|
||||
fApi.value?.disabled(true, field);
|
||||
}
|
||||
if (permission === BpmFieldPermissionType.WRITE) {
|
||||
// @ts-ignore
|
||||
fApi.value?.disabled(false, field);
|
||||
}
|
||||
if (permission === BpmFieldPermissionType.NONE) {
|
||||
// @ts-ignore
|
||||
fApi.value?.hidden(true, field);
|
||||
}
|
||||
};
|
||||
|
||||
/** 取消发起审批 */
|
||||
const handleCancel = () => {
|
||||
emit('cancel');
|
||||
};
|
||||
|
||||
/** 选择发起人 */
|
||||
const selectUserConfirm = (activityId: string, userList: any[]) => {
|
||||
if (!activityId || !Array.isArray(userList)) return;
|
||||
startUserSelectAssignees.value[Number(activityId)] = userList.map(
|
||||
(item) => item.id,
|
||||
);
|
||||
};
|
||||
|
||||
defineExpose({ initProcessInfo });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card
|
||||
:title="getTitle"
|
||||
:body-style="{
|
||||
padding: '12px',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
paddingBottom: '62px', // 预留 actions 区域高度
|
||||
}"
|
||||
>
|
||||
<template #extra>
|
||||
<Space wrap>
|
||||
<Button plain type="default" @click="handleCancel">
|
||||
<IconifyIcon icon="mdi:arrow-left" /> 返回
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
|
||||
<Tabs
|
||||
v-model:active-key="activeTab"
|
||||
class="flex flex-1 flex-col overflow-hidden"
|
||||
>
|
||||
<Tabs.TabPane tab="表单填写" key="form">
|
||||
<Row :gutter="[48, 16]" class="pt-4">
|
||||
<Col
|
||||
:xs="24"
|
||||
:sm="24"
|
||||
:md="18"
|
||||
:lg="18"
|
||||
:xl="18"
|
||||
class="flex-1 overflow-auto"
|
||||
>
|
||||
<form-create
|
||||
:rule="detailForm.rule"
|
||||
v-model:api="fApi"
|
||||
v-model="detailForm.value"
|
||||
:option="detailForm.option"
|
||||
@submit="submitForm"
|
||||
/>
|
||||
</Col>
|
||||
<Col :xs="24" :sm="24" :md="6" :lg="6" :xl="6">
|
||||
<ProcessInstanceTimeline
|
||||
ref="timelineRef"
|
||||
:activity-nodes="activityNodes"
|
||||
:show-status-icon="false"
|
||||
@select-user-confirm="selectUserConfirm"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane tab="流程图" key="flow" class="flex flex-1 overflow-hidden">
|
||||
<div>待开发</div>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
|
||||
<template #actions>
|
||||
<template v-if="activeTab === 'form'">
|
||||
<Space wrap class="flex h-[50px] w-full justify-center">
|
||||
<Button plain type="primary" @click="submitForm">
|
||||
<IconifyIcon icon="mdi:check" /> 发起
|
||||
</Button>
|
||||
<Button plain type="default" @click="handleCancel">
|
||||
<IconifyIcon icon="mdi:close" /> 取消
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
|
@ -0,0 +1,173 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { getCategorySimpleList } from '#/api/bpm/category';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
BpmProcessInstanceStatus,
|
||||
DICT_TYPE,
|
||||
getDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
} from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
// {
|
||||
// fieldName: 'startUserId',
|
||||
// label: '发起人',
|
||||
// component: 'ApiSelect',
|
||||
// componentProps: {
|
||||
// placeholder: '请选择发起人',
|
||||
// allowClear: true,
|
||||
// api: getSimpleUserList,
|
||||
// labelField: 'nickname',
|
||||
// valueField: 'id',
|
||||
// },
|
||||
// },
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '流程名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入流程名称',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'processDefinitionId',
|
||||
label: '所属流程',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入流程定义的编号',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
// 流程分类
|
||||
{
|
||||
fieldName: 'category',
|
||||
label: '流程分类',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
placeholder: '请输入流程分类',
|
||||
allowClear: true,
|
||||
api: getCategorySimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'code',
|
||||
},
|
||||
},
|
||||
// 流程状态
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '流程状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(
|
||||
DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
|
||||
'number',
|
||||
),
|
||||
placeholder: '请选择流程状态',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
// 发起时间
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '发起时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = BpmProcessInstanceApi.ProcessInstanceVO>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '流程名称',
|
||||
minWidth: 200,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'summary',
|
||||
title: '摘要',
|
||||
minWidth: 200,
|
||||
slots: {
|
||||
default: 'slot-summary',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
field: 'categoryName',
|
||||
title: '流程分类',
|
||||
minWidth: 120,
|
||||
fixed: 'left',
|
||||
},
|
||||
|
||||
// 流程状态
|
||||
{
|
||||
field: 'status',
|
||||
title: '流程状态',
|
||||
minWidth: 250,
|
||||
slots: {
|
||||
default: 'slot-status',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
field: 'startTime',
|
||||
title: '发起时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'endTime',
|
||||
title: '结束时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '流程名称',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'detail',
|
||||
text: $t('ui.actionTitle.detail'),
|
||||
show: hasAccessByCodes(['bpm:process-instance:query']),
|
||||
},
|
||||
{
|
||||
code: 'cancel',
|
||||
text: $t('ui.actionTitle.cancel'),
|
||||
show: (row: BpmProcessInstanceApi.ProcessInstanceVO) => {
|
||||
return (
|
||||
row.status === BpmProcessInstanceStatus.RUNNING &&
|
||||
hasAccessByCodes(['bpm:process-instance:cancel'])
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
|
@ -33,13 +33,13 @@ import DictTag from '#/components/dict-tag/dict-tag.vue';
|
|||
import {
|
||||
BpmModelFormType,
|
||||
BpmModelType,
|
||||
BpmTaskStatusEnum,
|
||||
DICT_TYPE,
|
||||
registerComponent,
|
||||
setConfAndFields2,
|
||||
TaskStatusEnum,
|
||||
} from '#/utils';
|
||||
|
||||
import TimeLine from './modules/time-line.vue';
|
||||
import ProcessInstanceTimeline from './modules/time-line.vue';
|
||||
|
||||
defineOptions({ name: 'BpmProcessInstanceDetail' });
|
||||
|
||||
|
@ -79,13 +79,13 @@ const auditIconsMap: {
|
|||
| typeof SvgBpmRejectIcon
|
||||
| typeof SvgBpmRunningIcon;
|
||||
} = {
|
||||
[TaskStatusEnum.RUNNING]: SvgBpmRunningIcon,
|
||||
[TaskStatusEnum.APPROVE]: SvgBpmApproveIcon,
|
||||
[TaskStatusEnum.REJECT]: SvgBpmRejectIcon,
|
||||
[TaskStatusEnum.CANCEL]: SvgBpmCancelIcon,
|
||||
[TaskStatusEnum.APPROVING]: SvgBpmApproveIcon,
|
||||
[TaskStatusEnum.RETURN]: SvgBpmRejectIcon,
|
||||
[TaskStatusEnum.WAIT]: SvgBpmRunningIcon,
|
||||
[BpmTaskStatusEnum.RUNNING]: SvgBpmRunningIcon,
|
||||
[BpmTaskStatusEnum.APPROVE]: SvgBpmApproveIcon,
|
||||
[BpmTaskStatusEnum.REJECT]: SvgBpmRejectIcon,
|
||||
[BpmTaskStatusEnum.CANCEL]: SvgBpmCancelIcon,
|
||||
[BpmTaskStatusEnum.APPROVING]: SvgBpmApproveIcon,
|
||||
[BpmTaskStatusEnum.RETURN]: SvgBpmRejectIcon,
|
||||
[BpmTaskStatusEnum.WAIT]: SvgBpmRunningIcon,
|
||||
};
|
||||
|
||||
// ========== 申请信息 ==========
|
||||
|
@ -321,23 +321,23 @@ onMounted(async () => {
|
|||
</Col>
|
||||
<Col :xs="24" :sm="24" :md="6" :lg="6" :xl="8">
|
||||
<div class="mt-2">
|
||||
<TimeLine :activity-nodes="activityNodes" />
|
||||
<ProcessInstanceTimeline :activity-nodes="activityNodes" />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</TabPane>
|
||||
|
||||
<TabPane tab="流程图" key="diagram">
|
||||
<div>流程图</div>
|
||||
<div>待开发</div>
|
||||
</TabPane>
|
||||
|
||||
<TabPane tab="流转记录" key="record">
|
||||
<div>流转记录</div>
|
||||
<div>待开发</div>
|
||||
</TabPane>
|
||||
|
||||
<!-- TODO 待开发 -->
|
||||
<TabPane tab="流转评论" key="comment" v-if="false">
|
||||
<div>流转评论</div>
|
||||
<div>待开发</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<script lang="ts" setup>
|
||||
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||
|
||||
import { h, ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
@ -11,7 +11,11 @@ import { formatDateTime, isEmpty } from '@vben/utils';
|
|||
import { Avatar, Button, Image, Tooltip } from 'ant-design-vue';
|
||||
|
||||
import { UserSelectModal } from '#/components/user-select-modal';
|
||||
import { CandidateStrategyEnum, NodeTypeEnum, TaskStatusEnum } from '#/utils';
|
||||
import {
|
||||
BpmCandidateStrategyEnum,
|
||||
BpmNodeTypeEnum,
|
||||
BpmTaskStatusEnum,
|
||||
} from '#/utils';
|
||||
|
||||
defineOptions({ name: 'BpmProcessInstanceTimeline' });
|
||||
|
||||
|
@ -26,7 +30,7 @@ withDefaults(
|
|||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
selectUserConfirm: [id: string, userList: any[]];
|
||||
selectUserConfirm: [activityId: string, userList: any[]];
|
||||
}>();
|
||||
|
||||
const { push } = useRouter(); // 路由
|
||||
|
@ -59,42 +63,42 @@ const statusIconMap: Record<
|
|||
// 节点类型图标映射
|
||||
const nodeTypeSvgMap = {
|
||||
// 结束节点
|
||||
[NodeTypeEnum.END_EVENT_NODE]: {
|
||||
[BpmNodeTypeEnum.END_EVENT_NODE]: {
|
||||
color: '#909398',
|
||||
icon: 'mdi:power',
|
||||
},
|
||||
// 开始节点
|
||||
[NodeTypeEnum.START_USER_NODE]: {
|
||||
[BpmNodeTypeEnum.START_USER_NODE]: {
|
||||
color: '#909398',
|
||||
icon: 'mdi:account-outline',
|
||||
},
|
||||
// 用户任务节点
|
||||
[NodeTypeEnum.USER_TASK_NODE]: {
|
||||
[BpmNodeTypeEnum.USER_TASK_NODE]: {
|
||||
color: '#ff943e',
|
||||
icon: 'tdesign:seal',
|
||||
},
|
||||
// 事务节点
|
||||
[NodeTypeEnum.TRANSACTOR_NODE]: {
|
||||
[BpmNodeTypeEnum.TRANSACTOR_NODE]: {
|
||||
color: '#ff943e',
|
||||
icon: 'mdi:file-edit-outline',
|
||||
},
|
||||
// 复制任务节点
|
||||
[NodeTypeEnum.COPY_TASK_NODE]: {
|
||||
[BpmNodeTypeEnum.COPY_TASK_NODE]: {
|
||||
color: '#3296fb',
|
||||
icon: 'mdi:content-copy',
|
||||
},
|
||||
// 条件分支节点
|
||||
[NodeTypeEnum.CONDITION_NODE]: {
|
||||
[BpmNodeTypeEnum.CONDITION_NODE]: {
|
||||
color: '#14bb83',
|
||||
icon: 'carbon:flow',
|
||||
},
|
||||
// 并行分支节点
|
||||
[NodeTypeEnum.PARALLEL_BRANCH_NODE]: {
|
||||
[BpmNodeTypeEnum.PARALLEL_BRANCH_NODE]: {
|
||||
color: '#14bb83',
|
||||
icon: 'si:flow-parallel-line',
|
||||
},
|
||||
// 子流程节点
|
||||
[NodeTypeEnum.CHILD_PROCESS_NODE]: {
|
||||
[BpmNodeTypeEnum.CHILD_PROCESS_NODE]: {
|
||||
color: '#14bb83',
|
||||
icon: 'icon-park-outline:tree-diagram',
|
||||
},
|
||||
|
@ -104,22 +108,22 @@ const nodeTypeSvgMap = {
|
|||
const onlyStatusIconShow = [-1, 0, 1];
|
||||
|
||||
// 获取审批节点类型图标
|
||||
const getApprovalNodeTypeIcon = (nodeType: NodeTypeEnum) => {
|
||||
const getApprovalNodeTypeIcon = (nodeType: BpmNodeTypeEnum) => {
|
||||
return nodeTypeSvgMap[nodeType]?.icon;
|
||||
};
|
||||
|
||||
// 获取审批节点图标
|
||||
const getApprovalNodeIcon = (taskStatus: number, nodeType: NodeTypeEnum) => {
|
||||
if (taskStatus === TaskStatusEnum.NOT_START) {
|
||||
const getApprovalNodeIcon = (taskStatus: number, nodeType: BpmNodeTypeEnum) => {
|
||||
if (taskStatus === BpmTaskStatusEnum.NOT_START) {
|
||||
return statusIconMap[taskStatus]?.icon || 'mdi:clock-outline';
|
||||
}
|
||||
|
||||
if (
|
||||
nodeType === NodeTypeEnum.START_USER_NODE ||
|
||||
nodeType === NodeTypeEnum.USER_TASK_NODE ||
|
||||
nodeType === NodeTypeEnum.TRANSACTOR_NODE ||
|
||||
nodeType === NodeTypeEnum.CHILD_PROCESS_NODE ||
|
||||
nodeType === NodeTypeEnum.END_EVENT_NODE
|
||||
nodeType === BpmNodeTypeEnum.START_USER_NODE ||
|
||||
nodeType === BpmNodeTypeEnum.USER_TASK_NODE ||
|
||||
nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE ||
|
||||
nodeType === BpmNodeTypeEnum.CHILD_PROCESS_NODE ||
|
||||
nodeType === BpmNodeTypeEnum.END_EVENT_NODE
|
||||
) {
|
||||
return statusIconMap[taskStatus]?.icon || 'mdi:clock-outline';
|
||||
}
|
||||
|
@ -133,7 +137,7 @@ const getApprovalNodeColor = (taskStatus: number) => {
|
|||
|
||||
// 获取审批节点时间
|
||||
const getApprovalNodeTime = (node: BpmProcessInstanceApi.ApprovalNodeInfo) => {
|
||||
if (node.nodeType === NodeTypeEnum.START_USER_NODE && node.startTime) {
|
||||
if (node.nodeType === BpmNodeTypeEnum.START_USER_NODE && node.startTime) {
|
||||
return formatDateTime(node.startTime);
|
||||
}
|
||||
if (node.endTime) {
|
||||
|
@ -147,19 +151,23 @@ const getApprovalNodeTime = (node: BpmProcessInstanceApi.ApprovalNodeInfo) => {
|
|||
|
||||
// 选择自定义审批人
|
||||
const userSelectFormRef = ref();
|
||||
const selectedActivityNodeId = ref<string>();
|
||||
const customApproveUsers = ref<Record<string, any[]>>({}); // key:activityId,value:用户列表
|
||||
|
||||
// 打开选择用户弹窗
|
||||
const handleSelectUser = (activityId: string, selectedList: any[]) => {
|
||||
console.log(userSelectFormRef.value);
|
||||
userSelectFormRef.value.open(activityId, selectedList);
|
||||
selectedActivityNodeId.value = activityId;
|
||||
userSelectFormRef.value.open(
|
||||
selectedList?.length ? selectedList.map((item) => item.id) : [],
|
||||
);
|
||||
};
|
||||
|
||||
// 选择用户完成
|
||||
const selectedUsers = ref<number[]>([]);
|
||||
const handleUserSelectConfirm = (activityId: string, userList: any[]) => {
|
||||
customApproveUsers.value[activityId] = userList || [];
|
||||
emit('selectUserConfirm', activityId, userList);
|
||||
const handleUserSelectConfirm = (userList: any[]) => {
|
||||
customApproveUsers.value[selectedActivityNodeId.value] = userList || [];
|
||||
|
||||
emit('selectUserConfirm', selectedActivityNodeId.value, userList);
|
||||
};
|
||||
|
||||
/** 跳转子流程 */
|
||||
|
@ -172,31 +180,6 @@ const handleChildProcess = (activity: any) => {
|
|||
});
|
||||
};
|
||||
|
||||
// 渲染用户头像和昵称
|
||||
const renderUserAvatar = (user: any) => {
|
||||
if (!user) return null;
|
||||
|
||||
return h('div', {}, [
|
||||
user.avatar
|
||||
? h(Avatar, {
|
||||
class: '!m-[5px]',
|
||||
size: 28,
|
||||
src: user.avatar,
|
||||
})
|
||||
: h(
|
||||
Avatar,
|
||||
{
|
||||
class: '!m-[5px]',
|
||||
size: 28,
|
||||
},
|
||||
{
|
||||
default: () => user.nickname?.slice(0, 1),
|
||||
},
|
||||
),
|
||||
h('span', { class: 'text-[13px]' }, user.nickname),
|
||||
]);
|
||||
};
|
||||
|
||||
// 判断是否需要显示自定义选择审批人
|
||||
const shouldShowCustomUserSelect = (
|
||||
activity: BpmProcessInstanceApi.ApprovalNodeInfo,
|
||||
|
@ -204,248 +187,279 @@ const shouldShowCustomUserSelect = (
|
|||
return (
|
||||
isEmpty(activity.tasks) &&
|
||||
isEmpty(activity.candidateUsers) &&
|
||||
(CandidateStrategyEnum.START_USER_SELECT === activity.candidateStrategy ||
|
||||
CandidateStrategyEnum.APPROVE_USER_SELECT === activity.candidateStrategy)
|
||||
(BpmCandidateStrategyEnum.START_USER_SELECT ===
|
||||
activity.candidateStrategy ||
|
||||
BpmCandidateStrategyEnum.APPROVE_USER_SELECT ===
|
||||
activity.candidateStrategy)
|
||||
);
|
||||
};
|
||||
|
||||
// 判断是否需要显示审批意见
|
||||
const shouldShowApprovalReason = (task: any, nodeType: NodeTypeEnum) => {
|
||||
const shouldShowApprovalReason = (task: any, nodeType: BpmNodeTypeEnum) => {
|
||||
return (
|
||||
task.reason &&
|
||||
[NodeTypeEnum.END_EVENT_NODE, NodeTypeEnum.USER_TASK_NODE].includes(
|
||||
[BpmNodeTypeEnum.END_EVENT_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
|
||||
nodeType,
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// 用户选择弹窗关闭
|
||||
const handleUserSelectClosed = () => {
|
||||
selectedUsers.value = [];
|
||||
};
|
||||
|
||||
// 用户选择弹窗取消
|
||||
const handleUserSelectCancel = () => {
|
||||
selectedUsers.value = [];
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-timeline class="pt-20px">
|
||||
<!-- 遍历每个审批节点 -->
|
||||
<a-timeline-item
|
||||
v-for="(activity, index) in activityNodes"
|
||||
:key="index"
|
||||
:color="getApprovalNodeColor(activity.status)"
|
||||
>
|
||||
<template #dot>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="position-absolute left--10px top--6px flex h-[32px] w-[32px] items-center justify-center rounded-full border border-solid border-[#dedede] bg-[#3f73f7] p-[6px]"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="getApprovalNodeTypeIcon(activity.nodeType)"
|
||||
class="size-[24px] text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showStatusIcon"
|
||||
class="absolute right-[-10px] top-[18px] flex size-[20px] items-center rounded-full border-[2px] border-solid border-white p-[2px]"
|
||||
:style="{ backgroundColor: getApprovalNodeColor(activity.status) }"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="getApprovalNodeIcon(activity.status, activity.nodeType)"
|
||||
class="text-white"
|
||||
:class="[statusIconMap[activity.status]?.animation]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="ml-2 flex flex-col items-start gap-2"
|
||||
:id="`activity-task-${activity.id}-${index}`"
|
||||
<div>
|
||||
<a-timeline class="pt-20px">
|
||||
<!-- 遍历每个审批节点 -->
|
||||
<a-timeline-item
|
||||
v-for="(activity, index) in activityNodes"
|
||||
:key="index"
|
||||
:color="getApprovalNodeColor(activity.status)"
|
||||
>
|
||||
<!-- 第一行:节点名称、时间 -->
|
||||
<div class="flex w-full">
|
||||
<div class="font-bold">{{ activity.name }}</div>
|
||||
<!-- 信息:时间 -->
|
||||
<div
|
||||
v-if="activity.status !== TaskStatusEnum.NOT_START"
|
||||
class="ml-auto mt-1 text-[13px] text-[#a5a5a5]"
|
||||
>
|
||||
{{ getApprovalNodeTime(activity) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 子流程节点 -->
|
||||
<div v-if="activity.nodeType === NodeTypeEnum.CHILD_PROCESS_NODE">
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
size="small"
|
||||
@click="handleChildProcess(activity)"
|
||||
>
|
||||
查看子流程
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 需要自定义选择审批人 -->
|
||||
<div
|
||||
v-if="true || shouldShowCustomUserSelect(activity)"
|
||||
class="flex flex-wrap items-center gap-2"
|
||||
>
|
||||
<Tooltip title="添加用户" placement="left">
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
ghost
|
||||
@click="
|
||||
handleSelectUser(activity.id, customApproveUsers[activity.id])
|
||||
"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon
|
||||
icon="mdi:account-plus-outline"
|
||||
class="size-[18px]"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<div
|
||||
v-for="(user, userIndex) in customApproveUsers[activity.id]"
|
||||
:key="userIndex"
|
||||
class="relative flex h-[36px] items-center gap-2 rounded-3xl bg-gray-100 pr-[8px] dark:bg-gray-600"
|
||||
>
|
||||
{{ renderUserAvatar(user) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="mt-1 flex flex-wrap items-center gap-2">
|
||||
<!-- 情况一:遍历每个审批节点下的【进行中】task 任务 -->
|
||||
<div
|
||||
v-for="(task, idx) in activity.tasks"
|
||||
:key="idx"
|
||||
class="flex flex-col gap-2 pr-[8px]"
|
||||
>
|
||||
<template #dot>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="relative flex flex-wrap gap-2"
|
||||
v-if="task.assigneeUser || task.ownerUser"
|
||||
>
|
||||
<!-- 信息:头像昵称 -->
|
||||
<div
|
||||
class="h-35px relative flex items-center rounded-3xl bg-gray-100 pr-[8px] dark:bg-gray-600"
|
||||
>
|
||||
<template
|
||||
v-if="
|
||||
task.assigneeUser?.avatar || task.assigneeUser?.nickname
|
||||
"
|
||||
>
|
||||
<Avatar
|
||||
class="!m-[5px]"
|
||||
:size="28"
|
||||
v-if="task.assigneeUser?.avatar"
|
||||
:src="task.assigneeUser?.avatar"
|
||||
/>
|
||||
<Avatar class="!m-[5px]" :size="28" v-else>
|
||||
{{ task.assigneeUser?.nickname.substring(0, 1) }}
|
||||
</Avatar>
|
||||
{{ task.assigneeUser?.nickname }}
|
||||
</template>
|
||||
<template
|
||||
v-else-if="task.ownerUser?.avatar || task.ownerUser?.nickname"
|
||||
>
|
||||
<Avatar
|
||||
class="!m-[5px]"
|
||||
:size="28"
|
||||
v-if="task.ownerUser?.avatar"
|
||||
:src="task.ownerUser?.avatar"
|
||||
/>
|
||||
<Avatar class="!m-[5px]" :size="28" v-else>
|
||||
{{ task.ownerUser?.nickname.substring(0, 1) }}
|
||||
</Avatar>
|
||||
{{ task.ownerUser?.nickname }}
|
||||
</template>
|
||||
|
||||
<!-- 信息:任务状态图标 -->
|
||||
<div
|
||||
v-if="
|
||||
showStatusIcon && onlyStatusIconShow.includes(task.status)
|
||||
"
|
||||
class="absolute left-[24px] top-[20px] flex items-center rounded-full border-2 border-solid border-white p-[2px]"
|
||||
:style="{
|
||||
backgroundColor: statusIconMap[task.status]?.color,
|
||||
}"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="
|
||||
statusIconMap[task.status]?.icon || 'mdi:clock-outline'
|
||||
"
|
||||
class="size-[10px] text-white"
|
||||
:class="[statusIconMap[task.status]?.animation]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审批意见和签名 -->
|
||||
<teleport defer :to="`#activity-task-${activity.id}-${index}`">
|
||||
<div
|
||||
v-if="shouldShowApprovalReason(task, activity.nodeType)"
|
||||
class="mt-1 w-full rounded-md bg-[#f8f8fa] p-2 text-[13px] text-[#a5a5a5]"
|
||||
>
|
||||
审批意见:{{ task.reason }}
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
task.signPicUrl &&
|
||||
activity.nodeType === NodeTypeEnum.USER_TASK_NODE
|
||||
"
|
||||
class="mt-1 w-full rounded-md bg-[#f8f8fa] p-2 text-[13px] text-[#a5a5a5]"
|
||||
>
|
||||
签名:
|
||||
<Image
|
||||
class="ml-[5px] h-[40px] w-[90px]"
|
||||
:src="task.signPicUrl"
|
||||
:preview="{ src: task.signPicUrl }"
|
||||
/>
|
||||
</div>
|
||||
</teleport>
|
||||
</div>
|
||||
|
||||
<!-- 情况二:遍历每个审批节点下的【候选的】task 任务 -->
|
||||
<div
|
||||
v-for="(user, userIndex) in activity.candidateUsers"
|
||||
:key="userIndex"
|
||||
class="relative flex h-[35px] items-center rounded-3xl bg-gray-100 pr-[8px] dark:bg-gray-600"
|
||||
>
|
||||
<Avatar
|
||||
class="!m-[5px]"
|
||||
:size="28"
|
||||
v-if="user.avatar"
|
||||
:src="user.avatar"
|
||||
/>
|
||||
<Avatar class="!m-[5px]" :size="28" v-else>
|
||||
{{ user.nickname.substring(0, 1) }}
|
||||
</Avatar>
|
||||
<span class="text-[13px]">
|
||||
{{ user.nickname }}
|
||||
</span>
|
||||
|
||||
<!-- 候选任务状态图标 -->
|
||||
<div
|
||||
v-if="showStatusIcon"
|
||||
class="absolute left-[24px] top-[20px] flex items-center rounded-full border-2 border-solid border-white p-[1px]"
|
||||
:style="{ backgroundColor: statusIconMap['-1']?.color }"
|
||||
class="position-absolute left--10px top--6px flex h-[32px] w-[32px] items-center justify-center rounded-full border border-solid border-[#dedede] bg-[#3f73f7] p-[6px]"
|
||||
>
|
||||
<IconifyIcon
|
||||
class="text-[11px] text-white"
|
||||
:icon="statusIconMap['-1']?.icon || 'mdi:clock-outline'"
|
||||
:icon="getApprovalNodeTypeIcon(activity.nodeType)"
|
||||
class="size-[24px] text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showStatusIcon"
|
||||
class="absolute right-[-10px] top-[18px] flex size-[20px] items-center rounded-full border-[2px] border-solid border-white p-[2px]"
|
||||
:style="{
|
||||
backgroundColor: getApprovalNodeColor(activity.status),
|
||||
}"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="getApprovalNodeIcon(activity.status, activity.nodeType)"
|
||||
class="text-white"
|
||||
:class="[statusIconMap[activity.status]?.animation]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
</template>
|
||||
|
||||
<!-- 用户选择弹窗 -->
|
||||
<UserSelectModal
|
||||
ref="userSelectFormRef"
|
||||
v-model:value="selectedUsers"
|
||||
:multiple="true"
|
||||
title="选择用户"
|
||||
@confirm="handleUserSelectConfirm"
|
||||
/>
|
||||
<div
|
||||
class="ml-2 flex flex-col items-start gap-2"
|
||||
:id="`activity-task-${activity.id}-${index}`"
|
||||
>
|
||||
<!-- 第一行:节点名称、时间 -->
|
||||
<div class="flex w-full">
|
||||
<div class="font-bold">{{ activity.name }}</div>
|
||||
<!-- 信息:时间 -->
|
||||
<div
|
||||
v-if="activity.status !== BpmTaskStatusEnum.NOT_START"
|
||||
class="ml-auto mt-1 text-[13px] text-[#a5a5a5]"
|
||||
>
|
||||
{{ getApprovalNodeTime(activity) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 子流程节点 -->
|
||||
<div v-if="activity.nodeType === BpmNodeTypeEnum.CHILD_PROCESS_NODE">
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
size="small"
|
||||
@click="handleChildProcess(activity)"
|
||||
>
|
||||
查看子流程
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 需要自定义选择审批人 -->
|
||||
<div
|
||||
v-if="shouldShowCustomUserSelect(activity)"
|
||||
class="flex flex-wrap items-center gap-2"
|
||||
>
|
||||
<Tooltip title="添加用户" placement="left">
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
ghost
|
||||
@click="
|
||||
handleSelectUser(activity.id, customApproveUsers[activity.id])
|
||||
"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon
|
||||
icon="mdi:account-plus-outline"
|
||||
class="size-[18px]"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<div
|
||||
v-for="(user, userIndex) in customApproveUsers[activity.id]"
|
||||
:key="user.id || userIndex"
|
||||
class="relative flex h-[36px] items-center gap-2 rounded-3xl bg-gray-100 pr-[8px] dark:bg-gray-600"
|
||||
>
|
||||
<Avatar
|
||||
class="!m-[5px]"
|
||||
:size="28"
|
||||
v-if="user.avatar"
|
||||
:src="user.avatar"
|
||||
/>
|
||||
|
||||
<Avatar class="!m-[5px]" :size="28" v-else>
|
||||
<span>{{ user.nickname.substring(0, 1) }}</span>
|
||||
</Avatar>
|
||||
<span class="text-[13px]">{{ user.nickname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="mt-1 flex flex-wrap items-center gap-2">
|
||||
<!-- 情况一:遍历每个审批节点下的【进行中】task 任务 -->
|
||||
<div
|
||||
v-for="(task, idx) in activity.tasks"
|
||||
:key="idx"
|
||||
class="flex flex-col gap-2 pr-[8px]"
|
||||
>
|
||||
<div
|
||||
class="relative flex flex-wrap gap-2"
|
||||
v-if="task.assigneeUser || task.ownerUser"
|
||||
>
|
||||
<!-- 信息:头像昵称 -->
|
||||
<div
|
||||
class="h-35px relative flex items-center rounded-3xl bg-gray-100 pr-[8px] dark:bg-gray-600"
|
||||
>
|
||||
<template
|
||||
v-if="
|
||||
task.assigneeUser?.avatar || task.assigneeUser?.nickname
|
||||
"
|
||||
>
|
||||
<Avatar
|
||||
class="!m-[5px]"
|
||||
:size="28"
|
||||
v-if="task.assigneeUser?.avatar"
|
||||
:src="task.assigneeUser?.avatar"
|
||||
/>
|
||||
<Avatar class="!m-[5px]" :size="28" v-else>
|
||||
{{ task.assigneeUser?.nickname.substring(0, 1) }}
|
||||
</Avatar>
|
||||
{{ task.assigneeUser?.nickname }}
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
task.ownerUser?.avatar || task.ownerUser?.nickname
|
||||
"
|
||||
>
|
||||
<Avatar
|
||||
class="!m-[5px]"
|
||||
:size="28"
|
||||
v-if="task.ownerUser?.avatar"
|
||||
:src="task.ownerUser?.avatar"
|
||||
/>
|
||||
<Avatar class="!m-[5px]" :size="28" v-else>
|
||||
{{ task.ownerUser?.nickname.substring(0, 1) }}
|
||||
</Avatar>
|
||||
{{ task.ownerUser?.nickname }}
|
||||
</template>
|
||||
|
||||
<!-- 信息:任务状态图标 -->
|
||||
<div
|
||||
v-if="
|
||||
showStatusIcon && onlyStatusIconShow.includes(task.status)
|
||||
"
|
||||
class="absolute left-[24px] top-[20px] flex items-center rounded-full border-2 border-solid border-white p-[2px]"
|
||||
:style="{
|
||||
backgroundColor: statusIconMap[task.status]?.color,
|
||||
}"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="
|
||||
statusIconMap[task.status]?.icon || 'mdi:clock-outline'
|
||||
"
|
||||
class="size-[10px] text-white"
|
||||
:class="[statusIconMap[task.status]?.animation]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审批意见和签名 -->
|
||||
<teleport defer :to="`#activity-task-${activity.id}-${index}`">
|
||||
<div
|
||||
v-if="shouldShowApprovalReason(task, activity.nodeType)"
|
||||
class="mt-1 w-full rounded-md bg-[#f8f8fa] p-2 text-[13px] text-[#a5a5a5]"
|
||||
>
|
||||
审批意见:{{ task.reason }}
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
task.signPicUrl &&
|
||||
activity.nodeType === BpmNodeTypeEnum.USER_TASK_NODE
|
||||
"
|
||||
class="mt-1 w-full rounded-md bg-[#f8f8fa] p-2 text-[13px] text-[#a5a5a5]"
|
||||
>
|
||||
签名:
|
||||
<Image
|
||||
class="ml-[5px] h-[40px] w-[90px]"
|
||||
:src="task.signPicUrl"
|
||||
:preview="{ src: task.signPicUrl }"
|
||||
/>
|
||||
</div>
|
||||
</teleport>
|
||||
</div>
|
||||
|
||||
<!-- 情况二:遍历每个审批节点下的【候选的】task 任务 -->
|
||||
<div
|
||||
v-for="(user, userIndex) in activity.candidateUsers"
|
||||
:key="userIndex"
|
||||
class="relative flex h-[35px] items-center rounded-3xl bg-gray-100 pr-[8px] dark:bg-gray-600"
|
||||
>
|
||||
<Avatar
|
||||
class="!m-[5px]"
|
||||
:size="28"
|
||||
v-if="user.avatar"
|
||||
:src="user.avatar"
|
||||
/>
|
||||
<Avatar class="!m-[5px]" :size="28" v-else>
|
||||
{{ user.nickname.substring(0, 1) }}
|
||||
</Avatar>
|
||||
<span class="text-[13px]">
|
||||
{{ user.nickname }}
|
||||
</span>
|
||||
|
||||
<!-- 候选任务状态图标 -->
|
||||
<div
|
||||
v-if="showStatusIcon"
|
||||
class="absolute left-[24px] top-[20px] flex items-center rounded-full border-2 border-solid border-white p-[1px]"
|
||||
:style="{ backgroundColor: statusIconMap['-1']?.color }"
|
||||
>
|
||||
<IconifyIcon
|
||||
class="text-[11px] text-white"
|
||||
:icon="statusIconMap['-1']?.icon || 'mdi:clock-outline'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
|
||||
<!-- 用户选择弹窗 -->
|
||||
<UserSelectModal
|
||||
ref="userSelectFormRef"
|
||||
v-model:value="selectedUsers"
|
||||
:multiple="true"
|
||||
title="选择用户"
|
||||
@confirm="handleUserSelectConfirm"
|
||||
@closed="handleUserSelectClosed"
|
||||
@cancel="handleUserSelectCancel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,34 +1,183 @@
|
|||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { BpmTaskApi } from '#/api/bpm/task';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { Page, prompt } from '@vben/common-ui';
|
||||
|
||||
import { Button, message, Textarea } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
cancelProcessInstanceByStartUser,
|
||||
getProcessInstanceMyPage,
|
||||
} from '#/api/bpm/processInstance';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { router } from '#/router';
|
||||
import { BpmProcessInstanceStatus, DICT_TYPE } from '#/utils';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
defineOptions({ name: 'BpmProcessInstanceManager' });
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getProcessInstanceMyPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
cellConfig: {
|
||||
height: 64,
|
||||
},
|
||||
} as VxeTableGridOptions<BpmTaskApi.TaskVO>,
|
||||
});
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({ code, row }: OnActionClickParams<BpmTaskApi.TaskVO>) {
|
||||
switch (code) {
|
||||
case 'cancel': {
|
||||
onCancel(row);
|
||||
break;
|
||||
}
|
||||
case 'detail': {
|
||||
onDetail(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 取消流程实例 */
|
||||
function onCancel(row: BpmTaskApi.TaskVO) {
|
||||
prompt({
|
||||
async beforeClose(scope) {
|
||||
if (scope.isConfirm) {
|
||||
if (scope.value) {
|
||||
try {
|
||||
await cancelProcessInstanceByStartUser(row.id, scope.value);
|
||||
message.success('取消成功');
|
||||
onRefresh();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
message.error('请输入取消原因');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
component: () => {
|
||||
return h(Textarea, {
|
||||
placeholder: '请输入取消原因',
|
||||
allowClear: true,
|
||||
rows: 2,
|
||||
rules: [{ required: true, message: '请输入取消原因' }],
|
||||
});
|
||||
},
|
||||
content: '请输入取消原因',
|
||||
title: '取消流程',
|
||||
modelPropName: 'value',
|
||||
});
|
||||
}
|
||||
|
||||
/** 查看流程实例 */
|
||||
function onDetail(row: BpmTaskApi.TaskVO) {
|
||||
console.warn(row);
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: { id: row.id },
|
||||
});
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<Page auto-content-height>
|
||||
<DocAlert
|
||||
title="流程发起、取消、重新发起"
|
||||
url="https://doc.iocoder.cn/bpm/process-instance/"
|
||||
url="https://doc.iocoder.cn/bpm/process-instance"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="流程状态">
|
||||
<!-- 摘要 -->
|
||||
<template #slot-summary="{ row }">
|
||||
<div
|
||||
class="flex flex-col py-2"
|
||||
v-if="row.summary && row.summary.length > 0"
|
||||
>
|
||||
<div v-for="(item, index) in row.summary" :key="index">
|
||||
<span class="text-gray-500">
|
||||
{{ item.key }} : {{ item.value }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>-</div>
|
||||
</template>
|
||||
|
||||
<template #slot-status="{ row }">
|
||||
<!-- 审批中状态 -->
|
||||
<template
|
||||
v-if="
|
||||
row.status === BpmProcessInstanceStatus.RUNNING &&
|
||||
row.tasks?.length > 0
|
||||
"
|
||||
>
|
||||
<!-- 单人审批 -->
|
||||
<template v-if="row.tasks.length === 1">
|
||||
<span>
|
||||
<Button type="link" @click="onDetail(row)">
|
||||
{{ row.tasks[0].assigneeUser?.nickname }}
|
||||
</Button>
|
||||
({{ row.tasks[0].name }}) 审批中
|
||||
</span>
|
||||
</template>
|
||||
<!-- 多人审批 -->
|
||||
<template v-else>
|
||||
<span>
|
||||
<Button type="link" @click="onDetail(row)">
|
||||
{{ row.tasks[0].assigneeUser?.nickname }}
|
||||
</Button>
|
||||
等 {{ row.tasks.length }} 人 ({{ row.tasks[0].name }})审批中
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 非审批中状态 -->
|
||||
<template v-else>
|
||||
<DictTag
|
||||
:type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS"
|
||||
:value="row.status"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
Loading…
Reference in New Issue