!100 feat: 完善审批中心、发起流程、查看流程、工作流整体进度 40%

Merge pull request !100 from 子夜/feature/bpm-process-instance
pull/105/head
xingyu 2025-05-12 02:16:06 +00:00 committed by Gitee
commit a7dcebc82a
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
18 changed files with 2319 additions and 371 deletions

View File

@ -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');
}

View File

@ -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 },
);
}

View File

@ -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[];

View File

@ -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}`,
);
};

View File

@ -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 }">

View File

@ -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'),

View File

@ -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;

View File

@ -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',
}

View File

@ -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); // 12
//
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>

View File

@ -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']),
},
],
},
},
];
}

View File

@ -0,0 +1,11 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
</script>
<template>
<Page>
<div>
<h1>请假详情</h1>
</div>
</Page>
</template>

View File

@ -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>

View File

@ -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>

View File

@ -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" />&nbsp; 返回
</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" />&nbsp; 发起
</Button>
<Button plain type="default" @click="handleCancel">
<IconifyIcon icon="mdi:close" />&nbsp; 取消
</Button>
</Space>
</template>
</template>
</Card>
</template>

View File

@ -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'])
);
},
},
],
},
},
];
}

View File

@ -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>

View File

@ -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[]>>({}); // keyactivityIdvalue
//
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>

View File

@ -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>