feat: [BPM 工作流] 流程报表

pull/142/head
jason 2025-06-15 10:56:53 +08:00
parent 1dd0588a39
commit e2a449351e
5 changed files with 384 additions and 9 deletions

View File

@ -15,6 +15,7 @@ export namespace BpmProcessDefinitionApi {
formType?: number; formType?: number;
bpmnXml?: string; bpmnXml?: string;
simpleModel?: string; simpleModel?: string;
formFields?: string[];
} }
} }

View File

@ -98,6 +98,18 @@ const routes: RouteRecordRaw[] = [
keepAlive: true, keepAlive: true,
}, },
}, },
{
path: 'process-instance/report',
component: () => import('#/views/bpm/processInstance/report/index.vue'),
name: 'BpmProcessInstanceReport',
meta: {
title: '数据报表',
activeMenu: '/bpm/manager/model',
icon: 'carbon:data-2',
hideInMenu: true,
keepAlive: true,
},
},
], ],
}, },
]; ];

View File

@ -284,7 +284,7 @@ function handleModelCommand(command: string, row: any) {
break; break;
} }
case 'handleReport': { case 'handleReport': {
console.warn('报表待实现', row); handleReport(row);
break; break;
} }
default: { default: {
@ -360,6 +360,17 @@ function handleDefinitionList(row: any) {
}); });
} }
/** 跳转到流程报表页面 */
function handleReport(row: any) {
router.push({
name: 'BpmProcessInstanceReport',
query: {
processDefinitionId: row.processDefinition.id,
processDefinitionKey: row.key,
},
});
}
/** 更新 modelList 模型列表 */ /** 更新 modelList 模型列表 */
const updateModelList = useDebounceFn(() => { const updateModelList = useDebounceFn(() => {
const newModelList = props.categoryInfo.modelList; const newModelList = props.categoryInfo.modelList;
@ -567,7 +578,6 @@ const handleRenameSuccess = () => {
> >
{{ row.formName }} {{ row.formName }}
</Button> </Button>
<!-- TODO BpmModelFormType.CUSTOM -->
<Button <Button
v-else-if="row.formType === BpmModelFormType.CUSTOM" v-else-if="row.formType === BpmModelFormType.CUSTOM"
type="link" type="link"
@ -624,13 +634,12 @@ const handleRenameSuccess = () => {
<Menu.Item key="handleCopy"> 复制 </Menu.Item> <Menu.Item key="handleCopy"> 复制 </Menu.Item>
<Menu.Item key="handleDefinitionList"> 历史 </Menu.Item> <Menu.Item key="handleDefinitionList"> 历史 </Menu.Item>
<!-- TODO 待实现报表 <Menu.Item
<Menu.Item key="handleReport"
key="handleReport" :disabled="!isManagerUser(row)"
:disabled="!isManagerUser(record)" >
> 报表
报表 </Menu.Item>
</Menu.Item> -->
<Menu.Item <Menu.Item
key="handleChangeState" key="handleChangeState"
v-if="row.processDefinition" v-if="row.processDefinition"

View File

@ -0,0 +1,153 @@
import type { VbenFormSchema } from '#/adapter/form';
import type {
VxeGridPropTypes,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
/** 搜索的表单 */
export function useGridFormSchema(
userList: any[] = [],
formFields: any[] = [],
): VbenFormSchema[] {
// 基础搜索字段
const baseFormSchema = [
{
fieldName: 'startUserId',
label: '发起人',
component: 'Select',
componentProps: {
placeholder: '请选择发起人',
allowClear: true,
options: userList.map((user) => ({
label: user.nickname,
value: user.id,
})),
},
},
{
fieldName: 'name',
label: '流程名称',
component: 'Input',
componentProps: {
placeholder: '请输入流程名称',
allowClear: true,
},
},
{
fieldName: 'status',
label: '流程状态',
component: 'Select',
componentProps: {
placeholder: '请选择流程状态',
allowClear: true,
options: getDictOptions(
DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
'number',
),
},
},
{
fieldName: 'createTime',
label: '发起时间',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
placeholder: ['开始日期', '结束日期'],
allowClear: true,
},
},
{
fieldName: 'endTime',
label: '结束时间',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
placeholder: ['开始日期', '结束日期'],
allowClear: true,
},
},
];
// 动态表单字段 暂时只支持 input 和 textarea, TODO 其他类型的支持
const dynamicFormSchema = formFields
.filter((item) => item.type === 'input' || item.type === 'textarea')
// 根据类型选择合适的表单组件
.map((item) => {
return {
fieldName: `formFieldsParams.${item.field}`,
label: item.title,
component: 'Input',
componentProps: {
placeholder: `请输入${item.title}`,
allowClear: true,
},
};
});
return [...baseFormSchema, ...dynamicFormSchema];
}
/** 列表的字段 */
export function useGridColumns(
formFields: any[] = [],
): VxeTableGridOptions<BpmProcessInstanceApi.ProcessInstanceVO>['columns'] {
const baseColumns: VxeGridPropTypes.Columns<BpmProcessInstanceApi.ProcessInstanceVO> =
[
{
field: 'name',
title: '流程名称',
minWidth: 250,
fixed: 'left',
},
{
field: 'startUser.nickname',
title: '流程发起人',
minWidth: 200,
},
{
field: 'status',
title: '流程状态',
minWidth: 120,
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',
},
];
// 添加动态表单字段列暂时全部以字符串TODO 展示优化, 按 type 展示控制
const formFieldColumns = (formFields || []).map((item) => ({
field: `formVariables.${item.field}`,
title: item.title,
minWidth: 120,
formatter: ({ row }: any) => {
return row.formVariables?.[item.field] ?? '';
},
}));
return [
...baseColumns,
...formFieldColumns,
{
title: '操作',
width: 180,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@ -0,0 +1,200 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
import { h, nextTick, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { confirm, Page } from '@vben/common-ui';
import { Input, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProcessDefinition } from '#/api/bpm/definition';
import {
cancelProcessInstanceByAdmin,
getProcessInstanceManagerPage,
} from '#/api/bpm/processInstance';
import { getSimpleUserList } from '#/api/system/user';
import { parseFormFields } from '#/components/simple-process-design';
import { useGridColumns, useGridFormSchema } from './data';
defineOptions({ name: 'BpmProcessInstanceReport' });
const router = useRouter(); //
const { query } = useRoute();
const processDefinitionId = query.processDefinitionId as string;
const formFields = ref<any[]>([]);
const userList = ref<any[]>([]); //
const gridReady = ref(false); //
const cancelReason = ref(''); //
//
let Grid: any = null;
let gridApi: any = null;
/** 获取流程定义 */
const getProcessDefinitionData = async () => {
try {
const processDefinition = await getProcessDefinition(processDefinitionId);
if (processDefinition && processDefinition.formFields) {
formFields.value = parseFormCreateFields(processDefinition.formFields);
}
} catch (error) {
console.error('获取流程定义失败', error);
}
};
/** 解析表单字段 */
const parseFormCreateFields = (formFields?: string[]) => {
const result: Array<Record<string, any>> = [];
if (formFields) {
formFields.forEach((fieldStr: string) => {
try {
parseFormFields(JSON.parse(fieldStr), result);
} catch (error) {
console.error('解析表单字段失败', error);
}
});
}
return result;
};
/** 刷新表格 */
function onRefresh() {
if (gridApi) {
gridApi.query();
}
}
/** 查看详情 */
const handleDetail = (row: BpmProcessInstanceApi.ProcessInstanceVO) => {
router.push({
name: 'BpmProcessInstanceDetail',
query: {
id: row.id,
},
});
};
/** 取消按钮操作 */
const handleCancel = async (row: BpmProcessInstanceApi.ProcessInstanceVO) => {
cancelReason.value = ''; //
confirm({
title: '取消流程',
content: h('div', [
h('p', '请输入取消原因:'),
h(Input, {
value: cancelReason.value,
'onUpdate:value': (val: string) => {
cancelReason.value = val;
},
placeholder: '请输入取消原因',
}),
]),
beforeClose: async ({ isConfirm }) => {
if (!isConfirm) return;
if (!cancelReason.value.trim()) {
message.warning('请输入取消原因');
return false;
}
await cancelProcessInstanceByAdmin(row.id, cancelReason.value);
return true;
},
}).then(() => {
message.success('取消成功');
onRefresh();
});
};
/** 创建表格 */
const createGrid = () => {
const [GridCompnent, api] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(userList.value, formFields.value),
},
gridOptions: {
columns: useGridColumns(formFields.value),
height: 'auto',
keepSource: true,
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
// formFieldsParams
const { formFieldsParams = {}, ...restValues } = formValues || {};
const params = {
pageNo: page.currentPage,
pageSize: page.pageSize,
...restValues,
processDefinitionKey: query.processDefinitionKey,
formFieldsParams: JSON.stringify(formFieldsParams),
};
return await getProcessInstanceManagerPage(params);
},
},
},
} as VxeTableGridOptions,
});
Grid = GridCompnent;
gridApi = api;
gridReady.value = true;
};
/** 初始化 */
onMounted(async () => {
//
userList.value = await getSimpleUserList();
//
await getProcessDefinitionData();
//
createGrid();
// DOM
await nextTick();
//
gridApi.query();
});
</script>
<template>
<Page auto-content-height>
<!-- 动态渲染表格 -->
<component :is="Grid" v-if="gridReady" table-title="流程实例列表">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '详情',
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['bpm:process-instance:query'],
onClick: handleDetail.bind(null, row),
},
{
label: '取消',
type: 'link',
icon: ACTION_ICON.DELETE,
auth: ['bpm:process-instance:cancel'],
ifShow: row.status === 1,
onClick: handleCancel.bind(null, row),
},
]"
/>
</template>
</component>
</Page>
</template>