feat: [bpm][antd] todo 修改, 一些优化

This commit is contained in:
jason
2025-12-01 15:53:57 +08:00
parent 0731999e7d
commit 29e79448e4
10 changed files with 136 additions and 181 deletions

View File

@@ -7,13 +7,29 @@ import { requestClient } from '#/api/request';
export namespace BpmTaskApi { export namespace BpmTaskApi {
/** 流程任务 */ /** 流程任务 */
export interface Task { export interface Task {
id: number; // 编号 id: string; // 编号
name: string; // 监听器名字 name: string; // 任务名字
type: string; // 监听器类型 status: number; // 任务状态
status: number; // 监听器状态 createTime: number; // 创建时间
event: string; // 监听事件 endTime: number; // 结束时间
valueType: string; // 监听器值类型 durationInMillis: number; // 持续时间
processInstance?: BpmProcessInstanceApi.ProcessInstance; // 流程实例 reason: string; // 审批理由
ownerUser: any; // 负责人
assigneeUser: any; // 处理人
taskDefinitionKey: string; // 任务定义的标识
processInstanceId: string; // 流程实例id
processInstance: BpmProcessInstanceApi.ProcessInstance; // 流程实例
parentTaskId: any; // 父任务id
children: any; // 子任务
formId: any; // 表单id
formName: any; // 表单名称
formConf: any; // 表单配置
formFields: any; // 表单字段
formVariables: any; // 表单变量
buttonsSetting: any; // 按钮设置
signEnable: any; // 签名设置
reasonRequire: any; // 原因设置
nodeType: any; // 节点类型
} }
} }

View File

@@ -9,24 +9,6 @@ const routes: RouteRecordRaw[] = [
hideInMenu: true, hideInMenu: true,
}, },
children: [ 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', path: 'process-instance/detail',
component: () => import('#/views/bpm/processInstance/detail/index.vue'), component: () => import('#/views/bpm/processInstance/detail/index.vue'),

View File

@@ -259,9 +259,11 @@ async function validateAllSteps() {
return true; return true;
} }
const saveLoading = ref<boolean>(false);
/** 保存操作 */ /** 保存操作 */
async function handleSave() { async function handleSave() {
try { try {
saveLoading.value = true;
// 保存前校验所有步骤的数据 // 保存前校验所有步骤的数据
const result = await validateAllSteps(); const result = await validateAllSteps();
if (!result) { if (!result) {
@@ -309,9 +311,12 @@ async function handleSave() {
} }
} catch (error: any) { } catch (error: any) {
console.error('保存失败:', error); console.error('保存失败:', error);
} finally {
saveLoading.value = false;
} }
} }
// 发布加载中状态
const deployLoading = ref<boolean>(false);
/** 发布操作 */ /** 发布操作 */
async function handleDeploy() { async function handleDeploy() {
try { try {
@@ -319,6 +324,7 @@ async function handleDeploy() {
if (!formData.value.id) { if (!formData.value.id) {
await confirm('是否确认发布该流程?'); await confirm('是否确认发布该流程?');
} }
deployLoading.value = true;
// 1.2 校验所有步骤 // 1.2 校验所有步骤
await validateAllSteps(); await validateAllSteps();
@@ -342,6 +348,8 @@ async function handleDeploy() {
} catch (error: any) { } catch (error: any) {
console.error('发布失败:', error); console.error('发布失败:', error);
message.warning(error.message || '发布失败'); message.warning(error.message || '发布失败');
} finally {
deployLoading.value = false;
} }
} }
@@ -448,11 +456,12 @@ onBeforeUnmount(() => {
<Button <Button
v-if="actionType === 'update'" v-if="actionType === 'update'"
type="primary" type="primary"
:loading="deployLoading"
@click="handleDeploy" @click="handleDeploy"
> >
</Button> </Button>
<Button type="primary" @click="handleSave"> <Button type="primary" @click="handleSave" :loading="saveLoading">
<span v-if="actionType === 'definition'"> </span> <span v-if="actionType === 'definition'"> </span>
<span v-else> </span> <span v-else> </span>
</Button> </Button>

View File

@@ -228,9 +228,10 @@ onMounted(() => {
> >
<Card <Card
hoverable hoverable
class="definition-item-card w-full cursor-pointer" class="w-full cursor-pointer"
:class="{ :class="{
'search-match': searchName.trim().length > 0, 'animate-bounce-once !bg-[rgb(63_115_247_/_10%)]':
searchName.trim().length > 0,
}" }"
:body-style="{ :body-style="{
width: '100%', width: '100%',
@@ -241,10 +242,13 @@ onMounted(() => {
<img <img
v-if="definition.icon" v-if="definition.icon"
:src="definition.icon" :src="definition.icon"
class="flow-icon-img object-contain" class="size-12 rounded object-contain"
alt="流程图标" alt="流程图标"
/> />
<div v-else class="flow-icon flex-shrink-0"> <div
v-else
class="flex size-12 flex-shrink-0 items-center justify-center rounded bg-primary"
>
<span class="text-xs text-white"> <span class="text-xs text-white">
{{ definition.name?.slice(0, 2) }} {{ definition.name?.slice(0, 2) }}
</span> </span>
@@ -283,7 +287,6 @@ onMounted(() => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
// @jason看看能不能通过 tailwindcss 简化下
@keyframes bounce { @keyframes bounce {
0%, 0%,
50% { 50% {
@@ -295,30 +298,7 @@ onMounted(() => {
} }
} }
.process-definition-container { .animate-bounce-once {
.definition-item-card { animation: bounce 0.5s ease;
.flow-icon-img {
width: 48px;
height: 48px;
border-radius: 0.25rem;
}
.flow-icon {
@apply bg-primary;
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: 0.25rem;
}
&.search-match {
background-color: rgb(63 115 247 / 10%);
border: 1px solid var(--primary);
animation: bounce 0.5s ease;
}
}
} }
</style> </style>

View File

@@ -104,7 +104,7 @@ async function submitForm() {
// 关闭并提示 // 关闭并提示
message.success('发起流程成功'); message.success('发起流程成功');
await closeCurrentTab(); await closeCurrentTab();
await router.push({ name: 'BpmTaskMy' }); await router.push({ name: 'BpmProcessInstanceMy' });
} finally { } finally {
processInstanceStartLoading.value = false; processInstanceStartLoading.value = false;
} }

View File

@@ -212,20 +212,27 @@ watch(
} }
}, },
); );
const loading = ref(false);
/** 初始化 */ /** 初始化 */
onMounted(async () => { onMounted(async () => {
await getDetail(); try {
// 获得用户列表 loading.value = true;
userOptions.value = await getSimpleUserList(); await getDetail();
// 获得用户列表
userOptions.value = await getSimpleUserList();
} finally {
loading.value = false;
}
}); });
</script> </script>
<template> <template>
<Page auto-content-height> <Page auto-content-height v-loading="loading">
<Card <Card
class="flex h-full flex-col"
:body-style="{ :body-style="{
overflowY: 'auto', flex: 1,
overflowY: 'hidden',
paddingTop: '12px', paddingTop: '12px',
}" }"
> >
@@ -286,24 +293,16 @@ onMounted(async () => {
</div> </div>
<!-- 流程操作 --> <!-- 流程操作 -->
<div class="process-tabs-container flex flex-1 flex-col"> <div class="flex h-full flex-1 flex-col">
<Tabs v-model:active-key="activeTab" class="mt-0 h-full"> <Tabs v-model:active-key="activeTab">
<TabPane tab="审批详情" key="form" class="tab-pane-content"> <TabPane tab="审批详情" key="form" class="pb-20 pr-3">
<Row :gutter="[48, 24]" class="h-full"> <Row :gutter="[48, 24]">
<Col <Col :xs="24" :sm="24" :md="18" :lg="18" :xl="16">
:xs="24"
:sm="24"
:md="18"
:lg="18"
:xl="16"
class="h-full"
>
<!-- 流程表单 --> <!-- 流程表单 -->
<div <div
v-if=" v-if="
processDefinition?.formType === BpmModelFormType.NORMAL processDefinition?.formType === BpmModelFormType.NORMAL
" "
class="h-full"
> >
<form-create <form-create
v-model="detailForm.value" v-model="detailForm.value"
@@ -316,13 +315,12 @@ onMounted(async () => {
v-else-if=" v-else-if="
processDefinition?.formType === BpmModelFormType.CUSTOM processDefinition?.formType === BpmModelFormType.CUSTOM
" "
class="h-full"
> >
<BusinessFormComponent :id="processInstance?.businessKey" /> <BusinessFormComponent :id="processInstance?.businessKey" />
</div> </div>
</Col> </Col>
<Col :xs="24" :sm="24" :md="6" :lg="6" :xl="8" class="h-full"> <Col :xs="24" :sm="24" :md="6" :lg="6" :xl="8">
<div class="mt-4 h-full"> <div class="mt-4">
<ProcessInstanceTimeline :activity-nodes="activityNodes" /> <ProcessInstanceTimeline :activity-nodes="activityNodes" />
</div> </div>
</Col> </Col>
@@ -331,44 +329,35 @@ onMounted(async () => {
<TabPane <TabPane
tab="流程图" tab="流程图"
key="diagram" key="diagram"
class="tab-pane-content" class="pb-20 pr-3"
:force-render="true" :force-render="true"
> >
<div class="h-full"> <ProcessInstanceSimpleViewer
<ProcessInstanceSimpleViewer v-show="
v-show=" processDefinition.modelType &&
processDefinition.modelType && processDefinition.modelType === BpmModelType.SIMPLE
processDefinition.modelType === BpmModelType.SIMPLE "
" :loading="processInstanceLoading"
:loading="processInstanceLoading" :model-view="processModelView"
:model-view="processModelView" />
/> <ProcessInstanceBpmnViewer
<ProcessInstanceBpmnViewer v-show="
v-show=" processDefinition.modelType &&
processDefinition.modelType && processDefinition.modelType === BpmModelType.BPMN
processDefinition.modelType === BpmModelType.BPMN "
" :loading="processInstanceLoading"
:loading="processInstanceLoading" :model-view="processModelView"
:model-view="processModelView" />
/>
</div>
</TabPane> </TabPane>
<TabPane tab="流转记录" key="record" class="tab-pane-content"> <TabPane tab="流转记录" key="record" class="pb-20 pr-3">
<div class="h-full"> <BpmProcessInstanceTaskList
<BpmProcessInstanceTaskList ref="taskListRef"
ref="taskListRef" :loading="processInstanceLoading"
:loading="processInstanceLoading" :id="id"
:id="id" />
/>
</div>
</TabPane> </TabPane>
<!-- TODO 待开发 --> <!-- TODO 待开发 -->
<TabPane <TabPane tab="流转评论" key="comment" v-if="false" class="pr-3">
tab="流转评论"
key="comment"
v-if="false"
class="tab-pane-content"
>
<div class="h-full">待开发</div> <div class="h-full">待开发</div>
</TabPane> </TabPane>
</Tabs> </Tabs>
@@ -396,35 +385,18 @@ onMounted(async () => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
// @jason看看能不能通过 tailwindcss 简化下
.ant-tabs-content {
height: 100%;
}
.process-tabs-container {
display: flex;
flex-direction: column;
height: 100%;
}
:deep(.ant-tabs) { :deep(.ant-tabs) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
}
:deep(.ant-tabs-content) { .ant-tabs-content {
flex: 1; height: 100%;
overflow-y: auto; }
} }
:deep(.ant-tabs-tabpane) { :deep(.ant-tabs-tabpane) {
height: 100%; height: 100%;
} overflow-y: auto;
.tab-pane-content {
height: calc(100vh - 420px);
padding-right: 12px;
overflow: hidden auto;
} }
</style> </style>

View File

@@ -5,7 +5,7 @@ import { useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { base64ToFile } from '@vben/utils'; import { base64ToFile } from '@vben/utils';
import { Button, Space, Tooltip } from 'ant-design-vue'; import { Button, Tooltip } from 'ant-design-vue';
import Vue3Signature from 'vue3-signature'; import Vue3Signature from 'vue3-signature';
import { uploadFile } from '#/api/infra/file'; import { uploadFile } from '#/api/infra/file';
@@ -36,30 +36,29 @@ const [Modal, modalApi] = useVbenModal({
<template> <template>
<Modal title="流程签名" class="w-3/5"> <Modal title="流程签名" class="w-3/5">
<div class="mb-2 flex justify-end"> <div class="flex h-[50vh] flex-col">
<Space> <div class="mb-2 flex justify-end gap-2">
<Tooltip title="撤销上一步操作"> <Tooltip title="撤销上一步操作">
<Button @click="signature?.undo()"> <Button @click="signature?.undo()" size="small">
<template #icon> <template #icon>
<IconifyIcon icon="lucide:undo" class="mb-1 size-4" /> <IconifyIcon icon="lucide:undo" class="mb-1 size-3" />
</template> </template>
撤销 撤销
</Button> </Button>
</Tooltip> </Tooltip>
<Tooltip title="清空画布"> <Tooltip title="清空画布">
<Button @click="signature?.clear()"> <Button @click="signature?.clear()" size="small">
<template #icon> <template #icon>
<IconifyIcon icon="lucide:trash" class="mb-1 size-4" /> <IconifyIcon icon="lucide:trash" class="mb-1 size-3" />
</template> </template>
<span>清除</span> <span>清除</span>
</Button> </Button>
</Tooltip> </Tooltip>
</Space> </div>
<Vue3Signature
class="h-full flex-1 border border-solid border-gray-300"
ref="signature"
/>
</div> </div>
<Vue3Signature
class="mx-auto !h-80 border border-solid border-gray-300"
ref="signature"
/>
</Modal> </Modal>
</template> </template>

View File

@@ -44,7 +44,7 @@ function useGridColumns(): VxeTableGridOptions['columns'] {
field: 'approver', field: 'approver',
title: '审批人', title: '审批人',
slots: { slots: {
default: ({ row }: { row: BpmTaskApi.TaskManager }) => { default: ({ row }: { row: BpmTaskApi.Task }) => {
return row.assigneeUser?.nickname || row.ownerUser?.nickname; return row.assigneeUser?.nickname || row.ownerUser?.nickname;
}, },
}, },
@@ -106,7 +106,7 @@ function handleRefresh() {
} }
/** 显示表单详情 */ /** 显示表单详情 */
async function handleShowFormDetail(row: BpmTaskApi.TaskManager) { async function handleShowFormDetail(row: BpmTaskApi.Task) {
// 设置表单配置和表单字段 // 设置表单配置和表单字段
taskForm.value = { taskForm.value = {
rule: [], rule: [],
@@ -141,7 +141,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
keepSource: true, keepSource: true,
showFooter: true, showFooter: true,
border: true, border: true,
height: 'auto',
proxyConfig: { proxyConfig: {
ajax: { ajax: {
query: async () => { query: async () => {
@@ -159,7 +158,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
toolbarConfig: { toolbarConfig: {
enabled: false, enabled: false,
}, },
} as VxeTableGridOptions<BpmTaskApi.TaskManager>, } as VxeTableGridOptions<BpmTaskApi.Task>,
}); });
defineExpose({ defineExpose({
@@ -168,7 +167,7 @@ defineExpose({
</script> </script>
<template> <template>
<div class="flex h-full flex-col"> <div>
<Grid> <Grid>
<template #slot-reason="{ row }"> <template #slot-reason="{ row }">
<div class="flex flex-wrap items-center justify-center"> <div class="flex flex-wrap items-center justify-center">
@@ -188,13 +187,13 @@ defineExpose({
</div> </div>
</template> </template>
</Grid> </Grid>
<Modal class="w-[800px]">
<form-create
ref="formRef"
v-model="taskForm.value"
:option="taskForm.option"
:rule="taskForm.rule"
/>
</Modal>
</div> </div>
<Modal class="w-3/5">
<form-create
ref="formRef"
v-model="taskForm.value"
:option="taskForm.option"
:rule="taskForm.rule"
/>
</Modal>
</template> </template>

View File

@@ -15,7 +15,7 @@ import { useGridColumns, useGridFormSchema } from './data';
defineOptions({ name: 'BpmDoneTask' }); defineOptions({ name: 'BpmDoneTask' });
/** 查看历史 */ /** 查看历史 */
function handleHistory(row: BpmTaskApi.TaskManager) { function handleHistory(row: BpmTaskApi.Task) {
router.push({ router.push({
name: 'BpmProcessInstanceDetail', name: 'BpmProcessInstanceDetail',
query: { query: {
@@ -26,7 +26,7 @@ function handleHistory(row: BpmTaskApi.TaskManager) {
} }
/** 撤回任务 */ /** 撤回任务 */
async function handleWithdraw(row: BpmTaskApi.TaskManager) { async function handleWithdraw(row: BpmTaskApi.Task) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: '正在撤回中...', content: '正在撤回中...',
duration: 0, duration: 0,
@@ -67,7 +67,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true, refresh: true,
search: true, search: true,
}, },
} as VxeTableGridOptions<BpmTaskApi.TaskManager>, } as VxeTableGridOptions<BpmTaskApi.Task>,
}); });
</script> </script>

View File

@@ -344,24 +344,22 @@ onMounted(async () => {
</ElRow> </ElRow>
</ElTabPane> </ElTabPane>
<ElTabPane label="流程图" name="diagram" class="pb-20 pr-3"> <ElTabPane label="流程图" name="diagram" class="pb-20 pr-3">
<div> <ProcessInstanceSimpleViewer
<ProcessInstanceSimpleViewer v-show="
v-show=" processDefinition.modelType &&
processDefinition.modelType && processDefinition.modelType === BpmModelType.SIMPLE
processDefinition.modelType === BpmModelType.SIMPLE "
" :loading="processInstanceLoading"
:loading="processInstanceLoading" :model-view="processModelView"
:model-view="processModelView" />
/> <ProcessInstanceBpmnViewer
<ProcessInstanceBpmnViewer v-show="
v-show=" processDefinition.modelType &&
processDefinition.modelType && processDefinition.modelType === BpmModelType.BPMN
processDefinition.modelType === BpmModelType.BPMN "
" :loading="processInstanceLoading"
:loading="processInstanceLoading" :model-view="processModelView"
:model-view="processModelView" />
/>
</div>
</ElTabPane> </ElTabPane>
<ElTabPane label="流转记录" name="record" class="pb-20 pr-3"> <ElTabPane label="流转记录" name="record" class="pb-20 pr-3">
<BpmProcessInstanceTaskList <BpmProcessInstanceTaskList