feat: 重构待办任务页面为表格展示并优化交互逻辑

This commit is contained in:
dap 2025-10-20 13:40:33 +08:00
parent fd6eabc4d8
commit 69f5fd8a4f

View File

@ -1,360 +1,235 @@
<!-- eslint-disable no-use-before-define --> <script setup lang="tsx">
<script setup lang="ts"> import type { RadioChangeEvent } from 'ant-design-vue';
import type { User } from '#/api/system/user/model';
import type { TaskInfo } from '#/api/workflow/task/model';
import { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue'; import type { VbenFormProps } from '@vben/common-ui';
import { Page } from '@vben/common-ui'; import type { ComponentType } from '#/adapter/component';
import { useTabs } from '@vben/hooks'; import type { VxeGridProps } from '#/adapter/vxe-table';
import { addFullName, getPopupContainer } from '@vben/utils';
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue'; import { markRaw, onMounted, ref } from 'vue';
import {
Empty,
Form,
FormItem,
Input,
InputSearch,
Popover,
Segmented,
Spin,
Tooltip,
TreeSelect,
} from 'ant-design-vue';
import { cloneDeep, debounce, uniqueId } from 'lodash-es';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { DictEnum } from '@vben/constants';
import { RadioGroup } from 'ant-design-vue';
import { isArray } from 'lodash-es';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { categoryTree } from '#/api/workflow/category'; import { categoryTree } from '#/api/workflow/category';
import { pageByAllTaskFinish, pageByAllTaskWait } from '#/api/workflow/task'; import { pageByAllTaskFinish, pageByAllTaskWait } from '#/api/workflow/task';
import { renderDict } from '#/utils/render';
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components'; import { ApprovalPanelDrawerComp, CopyComponent } from '../components';
import { bottomOffset } from './constant'; import { DefaultSlot } from '../components/helper';
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE; const formOptions: VbenFormProps<ComponentType> = {
commonConfig: {
labelWidth: 80,
componentProps: {
allowClear: true,
},
},
schema: [
{
fieldName: 'category',
component: 'TreeSelect',
label: '流程分类',
},
{
fieldName: 'flowCode',
component: 'Input',
label: '流程编码',
},
{
fieldName: 'createByIds',
defaultValue: [],
component: markRaw(DefaultSlot),
label: '申请人',
renderComponentContent: (model) => ({
default: () => (
<CopyComponent avatarSize={30} v-model:userList={model.createByIds} />
),
}),
},
],
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
//
fieldMappingTime: [
[
'createTime',
['params[beginTime]', 'params[endTime]'],
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
],
],
};
/** onMounted(async () => {
* 流程监控 - 待办任务页面的id不唯一 改为前端处理 const tree = await categoryTree();
*/ tableApi.formApi.updateSchema([
interface TaskItem extends TaskInfo { {
active: boolean; fieldName: 'category',
randomId: string; componentProps: {
} fieldNames: { label: 'label', value: 'id' },
showSearch: true,
const taskList = ref<TaskItem[]>([]); treeData: tree,
const taskTotal = ref(0); treeDefaultExpandAll: true,
const page = ref(1); treeLine: { showLeafIcon: false },
const loading = ref(false); //
treeNodeFilterProp: 'label',
//
treeNodeLabelProp: 'label',
},
},
]);
});
const typeOptions = [ const typeOptions = [
{ label: '待办任务', value: 'todo' }, { label: '运行中', value: 'running' },
{ label: '已办任务', value: 'done' }, { label: '已完成', value: 'finish' },
]; ] as const;
const currentType = ref('todo'); type OptionValue = (typeof typeOptions)[number]['value'];
const currentApi = computed(() => { let currentTypeApi = pageByAllTaskWait;
if (currentType.value === 'todo') { const currentType = ref<OptionValue>('running');
return pageByAllTaskWait; async function handleTypeChange(e: RadioChangeEvent) {
const { value } = e.target;
switch (value) {
case 'finish': {
currentTypeApi = pageByAllTaskFinish;
break;
}
case 'running': {
currentTypeApi = pageByAllTaskWait;
break;
} }
return pageByAllTaskFinish;
});
const approvalType = computed(() => {
if (currentType.value === 'done') {
return 'readonly';
} }
return 'admin';
});
async function handleTypeChange() {
//
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
page.value = 1;
taskList.value = []; await tableApi.reload();
await nextTick();
await reload(true);
} }
const defaultFormData = { const gridOptions: VxeGridProps = {
flowName: '', // checkboxConfig: {
nodeName: '', // //
flowCode: '', // highlight: true,
createByIds: [] as string[], // //
category: null as null | number, // reserve: true,
}; },
const formData = ref(cloneDeep(defaultFormData)); columns: [
{
/** field: 'id',
* 是否已经加载全部数据 taskList.length === taskTotal title: 'ID',
*/ },
const isLoadComplete = computed( {
() => taskList.value.length === taskTotal.value, field: 'businessTitle',
); title: '业务标题',
formatter: ({ cellValue }) => cellValue ?? '-',
// ref },
const cardContainerRef = useTemplateRef('cardContainerRef'); {
field: 'flowName',
/** title: '流程名称',
* @param resetFields 是否清空查询参数 },
*/ {
async function reload(resetFields: boolean = false) { field: 'categoryName',
// title: '流程分类',
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' }); },
{
page.value = 1; field: 'flowCode',
currentTask.value = undefined; title: '流程编码',
taskTotal.value = 0; },
lastSelectId.value = ''; {
field: 'nodeName',
if (resetFields) { title: '当前任务',
formData.value = cloneDeep(defaultFormData); },
selectedUserList.value = []; {
} field: 'processedByName',
title: '办理人',
loading.value = true; formatter: ({ cellValue }) => cellValue?.split?.(',') ?? cellValue,
const resp = await currentApi.value({ },
pageSize: 10, {
pageNum: page.value, field: 'flowStatus',
...formData.value, title: '流程状态',
}); slots: {
taskList.value = resp.rows.map((item) => ({ default: ({ row }) => {
...item, return renderDict(row.flowStatus, DictEnum.WF_BUSINESS_STATUS);
active: false, },
randomId: uniqueId(), },
})); },
taskTotal.value = resp.total; {
field: 'createTime',
loading.value = false; title: '创建时间',
// },
if (taskList.value.length > 0) { ],
const firstTask = taskList.value[0]!; height: 'auto',
currentTask.value = firstTask; keepSource: true,
handleCardClick(firstTask); pagerConfig: {},
} proxyConfig: {
} ajax: {
query: async ({ page }, formValues = {}) => {
onMounted(reload); //
if (isArray(formValues.createByIds)) {
const handleScroll = debounce(async (e: Event) => { formValues.createByIds = (formValues.createByIds as Array<any>).map(
if (!e.target) { (item) => item.userId,
return;
}
// e.target.scrollTop
// e.target.clientHeight
// e.target.scrollHeight
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
//
const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;
console.log('scrollTop + clientHeight', scrollTop + clientHeight);
console.log('scrollHeight', scrollHeight);
//
if (isBottom && !isLoadComplete.value) {
loading.value = true;
page.value += 1;
const resp = await currentApi.value({
pageSize: 10,
pageNum: page.value,
...formData.value,
});
taskList.value.push(
...resp.rows.map((item) => ({
...item,
active: false,
randomId: uniqueId(),
})),
); );
loading.value = false;
} }
}, 200);
const lastSelectId = ref(''); return await currentTypeApi({
const currentTask = ref<TaskInfo>(); pageNum: page.currentPage,
async function handleCardClick(item: TaskItem) { pageSize: page.pageSize,
const { randomId } = item; ...formValues,
//
if (lastSelectId.value === randomId) {
return;
}
currentTask.value = item;
// & &
taskList.value.forEach((item) => {
item.active = item.randomId === randomId;
}); });
lastSelectId.value = randomId; },
} },
},
rowConfig: {
keyField: 'id',
isCurrent: true,
},
id: 'workflow-task-myself',
rowClassName: 'cursor-pointer',
};
const { refreshTab } = useTabs(); const [BasicTable, tableApi] = useVbenVxeGrid({
formOptions,
// 使v-model gridOptions,
const popoverOpen = ref(false); gridEvents: {
const selectedUserList = ref<User[]>([]); cellClick: ({ row }) => {
function handleFinish(userList: User[]) { handleOpen(row);
popoverOpen.value = true; },
selectedUserList.value = userList; },
formData.value.createByIds = userList.map((item) => item.userId);
}
const treeData = ref<any[]>([]);
onMounted(async () => {
// menu
const tree = await categoryTree();
addFullName(tree, 'label', ' / ');
treeData.value = tree;
}); });
const [ApprovalPanelDrawer, drawerApi] = useVbenDrawer({
connectedComponent: ApprovalPanelDrawerComp,
});
function handleOpen(row: any) {
let type = '';
switch (currentType.value) {
case 'finish': {
type = 'readonly';
break;
}
case 'running': {
type = 'admin';
break;
}
}
drawerApi.setData({ task: row, type }).open();
}
</script> </script>
<template> <template>
<Page :auto-content-height="true"> <Page :auto-content-height="true">
<div class="flex h-full gap-2"> <BasicTable>
<div <template #toolbar-actions>
class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg" <RadioGroup
>
<!-- 搜索条件 -->
<div
class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
>
<Segmented
v-model:value="currentType" v-model:value="currentType"
:options="typeOptions" :options="typeOptions"
block button-style="solid"
class="mb-2" option-type="button"
@change="handleTypeChange" @change="handleTypeChange"
/> />
<div class="flex items-center gap-1">
<InputSearch
v-model:value="formData.flowName"
placeholder="流程名称搜索"
@search="reload(false)"
/>
<Tooltip placement="top" title="重置">
<a-button @click="reload(true)">
<RedoOutlined />
</a-button>
</Tooltip>
<Popover
v-model:open="popoverOpen"
:get-popup-container="getPopupContainer"
placement="rightTop"
trigger="click"
>
<template #title>
<div class="w-full border-b pb-[12px] text-[16px]">搜索</div>
</template> </template>
<template #content> </BasicTable>
<Form <ApprovalPanelDrawer @reload="() => tableApi.query()" />
:colon="false"
:label-col="{ span: 6 }"
:model="formData"
autocomplete="off"
class="w-[300px]"
@finish="() => reload(false)"
>
<FormItem label="申请人">
<!-- 弹窗关闭后仍然显示表单浮层 -->
<CopyComponent
v-model:user-list="selectedUserList"
@cancel="() => (popoverOpen = true)"
@finish="handleFinish"
/>
</FormItem>
<FormItem label="流程分类">
<TreeSelect
v-model:value="formData.category"
:allow-clear="true"
:field-names="{ label: 'label', value: 'id' }"
:get-popup-container="getPopupContainer"
:tree-data="treeData"
:tree-default-expand-all="true"
:tree-line="{ showLeafIcon: false }"
placeholder="请选择"
tree-node-filter-prop="label"
tree-node-label-prop="fullName"
/>
</FormItem>
<FormItem label="任务名称">
<Input
v-model:value="formData.nodeName"
placeholder="请输入"
/>
</FormItem>
<FormItem label="流程编码">
<Input
v-model:value="formData.flowCode"
placeholder="请输入"
/>
</FormItem>
<FormItem>
<div class="flex">
<a-button block html-type="submit" type="primary">
搜索
</a-button>
<a-button block class="ml-2" @click="reload(true)">
重置
</a-button>
</div>
</FormItem>
</Form>
</template>
<a-button>
<FilterOutlined />
</a-button>
</Popover>
</div>
</div>
<div
ref="cardContainerRef"
class="thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3"
@scroll="handleScroll"
>
<template v-if="taskList.length > 0">
<ApprovalCard
v-for="item in taskList"
:key="item.randomId"
:info="item"
class="mx-2"
row-key="randomId"
@click="handleCardClick(item)"
/>
</template>
<Empty v-else :image="emptyImage" />
<div
v-if="isLoadComplete && taskList.length > 0"
class="flex items-center justify-center text-[14px] opacity-50"
>
没有更多数据了
</div>
<!-- 遮罩loading层 -->
<div
v-if="loading"
class="absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]"
>
<Spin tip="加载中..." />
</div>
</div>
<!-- total显示 -->
<div
class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2"
>
<div class="flex items-center justify-center">
{{ taskTotal }} 条记录
</div>
</div>
</div>
<ApprovalPanel
:task="currentTask"
:type="approvalType"
@reload="refreshTab"
/>
</div>
</Page> </Page>
</template> </template>
<style lang="scss" scoped>
.thin-scrollbar {
&::-webkit-scrollbar {
width: 5px;
}
}
:deep(.ant-card-body) {
@apply thin-scrollbar;
}
</style>