feat: 重构待办任务页面为表格展示并优化交互逻辑
This commit is contained in:
parent
fd6eabc4d8
commit
69f5fd8a4f
@ -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>
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user