Compare commits
4 Commits
main
...
flow-refac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69f5fd8a4f | ||
|
|
fd6eabc4d8 | ||
|
|
7d48dba86a | ||
|
|
c37a0fefa1 |
@ -1,5 +1,5 @@
|
||||
import type { TaskInfo } from '../task/model';
|
||||
import type { FlowInfoResponse } from './model';
|
||||
import type { FlowInfoResponse, FlowInstanceVariableResp } from './model';
|
||||
|
||||
import type { ID, IDS, PageQuery, PageResult } from '#/api/common';
|
||||
|
||||
@ -104,8 +104,8 @@ export function flowInfo(businessId: string) {
|
||||
* @returns Map<string,any>
|
||||
*/
|
||||
export function instanceVariable(instanceId: string) {
|
||||
return requestClient.get<Record<string, any>>(
|
||||
`/workflow/instance/variable/${instanceId}`,
|
||||
return requestClient.get<FlowInstanceVariableResp>(
|
||||
`/workflow/instance/instanceVariable/${instanceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -118,3 +118,22 @@ export function workflowInstanceInvalid(data: {
|
||||
}) {
|
||||
return requestClient.postWithMsg<void>('/workflow/instance/invalid', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改流程参数
|
||||
* @param data 参数
|
||||
* @param data.instanceId 实例ID
|
||||
* @param data.key 参数key
|
||||
* @param data.value 值
|
||||
* @returns void
|
||||
*/
|
||||
export function updateFlowVariable(data: {
|
||||
instanceId: string;
|
||||
key: string;
|
||||
value: any;
|
||||
}) {
|
||||
return requestClient.putWithMsg<void>(
|
||||
'/workflow/instance/updateVariable',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
export {};
|
||||
|
||||
export interface Flow {
|
||||
id: string;
|
||||
createTime: string;
|
||||
@ -39,3 +41,14 @@ export interface FlowInfoResponse {
|
||||
instanceId: string;
|
||||
list: Flow[];
|
||||
}
|
||||
|
||||
export interface FlowInstanceVariableResp {
|
||||
/**
|
||||
* json字符串 流程变量
|
||||
*/
|
||||
variable: string;
|
||||
variableList: {
|
||||
key: string;
|
||||
value: any;
|
||||
}[];
|
||||
}
|
||||
|
||||
@ -0,0 +1,423 @@
|
||||
<script setup lang="ts">
|
||||
import type { ApprovalType } from '../type';
|
||||
|
||||
import type { User } from '#/api/core/user';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, h } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { cn, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
CheckOutlined,
|
||||
EditOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
MenuOutlined,
|
||||
RollbackOutlined,
|
||||
UsergroupAddOutlined,
|
||||
UsergroupDeleteOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { Dropdown, Menu, MenuItem, Modal, Space } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
cancelProcessApply,
|
||||
deleteByInstanceIds,
|
||||
} from '#/api/workflow/instance';
|
||||
import {
|
||||
taskOperation,
|
||||
terminationTask,
|
||||
updateAssignee,
|
||||
} from '#/api/workflow/task';
|
||||
|
||||
import { approvalModal, approvalRejectionModal, flowInterfereModal } from '..';
|
||||
import { approveWithReasonModal } from '../helper';
|
||||
import userSelectModal from '../user-select-modal.vue';
|
||||
|
||||
interface Props {
|
||||
task?: TaskInfo;
|
||||
type: ApprovalType;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
reload: [];
|
||||
}>();
|
||||
|
||||
/**
|
||||
* 按钮权限
|
||||
*/
|
||||
const buttonPermissions = computed(() => {
|
||||
const record: Record<string, boolean> = {};
|
||||
if (!props.task) {
|
||||
return record;
|
||||
}
|
||||
props.task.buttonList.forEach((item) => {
|
||||
record[item.code] = item.show;
|
||||
});
|
||||
return record;
|
||||
});
|
||||
|
||||
// 是否显示 `其他` 按钮
|
||||
const showButtonOther = computed(() => {
|
||||
const moreCollections = new Set(['addSign', 'subSign', 'transfer', 'trust']);
|
||||
return Object.keys(buttonPermissions.value).some(
|
||||
(key) => moreCollections.has(key) && buttonPermissions.value[key],
|
||||
);
|
||||
});
|
||||
|
||||
// 进行中 可以撤销
|
||||
const revocable = computed(() => props.task?.flowStatus === 'waiting');
|
||||
async function handleCancel() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要撤销该申请吗?',
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await cancelProcessApply({
|
||||
businessId: props.task!.businessId,
|
||||
message: '申请人撤销流程!',
|
||||
});
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可编辑/删除
|
||||
*/
|
||||
const editableAndRemoveable = computed(() => {
|
||||
if (!props.task) {
|
||||
return false;
|
||||
}
|
||||
return ['back', 'cancel', 'draft'].includes(props.task.flowStatus);
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
function handleEdit() {
|
||||
const path = props.task?.formPath;
|
||||
if (path) {
|
||||
router.push({ path, query: { id: props.task!.businessId } });
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemove() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定删除该申请吗?',
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await deleteByInstanceIds([props.task!.id]);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批驳回
|
||||
*/
|
||||
const [RejectionModal, rejectionModalApi] = useVbenModal({
|
||||
connectedComponent: approvalRejectionModal,
|
||||
});
|
||||
function handleRejection() {
|
||||
rejectionModalApi.setData({
|
||||
taskId: props.task?.id,
|
||||
definitionId: props.task?.definitionId,
|
||||
nodeCode: props.task?.nodeCode,
|
||||
});
|
||||
rejectionModalApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批终止
|
||||
*/
|
||||
function handleTermination() {
|
||||
approveWithReasonModal({
|
||||
title: '审批终止',
|
||||
description: '确定终止当前审批流程吗?',
|
||||
onOk: async (reason) => {
|
||||
await terminationTask({ taskId: props.task!.id, comment: reason });
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批通过
|
||||
*/
|
||||
const [ApprovalModal, approvalModalApi] = useVbenModal({
|
||||
connectedComponent: approvalModal,
|
||||
});
|
||||
function handleApproval() {
|
||||
// 是否具有抄送权限
|
||||
const copyPermission = buttonPermissions.value?.copy ?? false;
|
||||
// 是否具有选人权限
|
||||
const assignPermission = buttonPermissions.value?.pop ?? false;
|
||||
approvalModalApi.setData({
|
||||
taskId: props.task?.id,
|
||||
copyPermission,
|
||||
assignPermission,
|
||||
});
|
||||
approvalModalApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 委托
|
||||
*/
|
||||
const [DelegationModal, delegationModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleDelegation(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
approveWithReasonModal({
|
||||
title: '委托',
|
||||
description: `确定委托给[${current?.nickName}]吗?`,
|
||||
onOk: async (reason) => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userId: current!.userId, message: reason },
|
||||
'delegateTask',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 转办
|
||||
*/
|
||||
const [TransferModal, transferModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleTransfer(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
approveWithReasonModal({
|
||||
title: '转办',
|
||||
description: `确定转办给[${current?.nickName}]吗?`,
|
||||
onOk: async (reason) => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userId: current!.userId, message: reason },
|
||||
'transferTask',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [AddSignatureModal, addSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleAddSignature(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认加签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation({ taskId: props.task!.id, userIds }, 'addSignature');
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [ReductionSignatureModal, reductionSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleReductionSignature(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认减签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userIds },
|
||||
'reductionSignature',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 流程干预
|
||||
const [FlowInterfereModal, flowInterfereModalApi] = useVbenModal({
|
||||
connectedComponent: flowInterfereModal,
|
||||
});
|
||||
function handleFlowInterfere() {
|
||||
flowInterfereModalApi.setData({ taskId: props.task?.id });
|
||||
flowInterfereModalApi.open();
|
||||
}
|
||||
|
||||
// 修改办理人
|
||||
const [UpdateAssigneeModal, updateAssigneeModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleUpdateAssignee(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
if (!current) return;
|
||||
Modal.confirm({
|
||||
title: '修改办理人',
|
||||
content: `确定修改办理人为${current?.nickName}吗?`,
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await updateAssignee([props.task!.id], current.userId);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否显示 加签/减签操作
|
||||
*/
|
||||
const showMultiActions = computed(() => {
|
||||
if (!props.task) {
|
||||
return false;
|
||||
}
|
||||
if (Number(props.task.nodeRatio) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'absolute bottom-0 left-0',
|
||||
'border-t-solid border-t-[1px]',
|
||||
'bg-background w-full p-3',
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="flex justify-end">
|
||||
<Space v-if="type === 'myself'">
|
||||
<a-button
|
||||
v-if="revocable"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(RollbackOutlined)"
|
||||
@click="handleCancel"
|
||||
>
|
||||
撤销申请
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
v-if="editableAndRemoveable"
|
||||
:icon="h(EditOutlined)"
|
||||
@click="handleEdit"
|
||||
>
|
||||
重新编辑
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="editableAndRemoveable"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(EditOutlined)"
|
||||
@click="handleRemove"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</Space>
|
||||
<Space v-if="type === 'approve'">
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
:icon="h(CheckOutlined)"
|
||||
@click="handleApproval"
|
||||
>
|
||||
通过
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="buttonPermissions?.termination"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(ExclamationCircleOutlined)"
|
||||
@click="handleTermination"
|
||||
>
|
||||
终止
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="buttonPermissions?.back"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(ArrowLeftOutlined)"
|
||||
@click="handleRejection"
|
||||
>
|
||||
驳回
|
||||
</a-button>
|
||||
<Dropdown
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<template #overlay>
|
||||
<Menu>
|
||||
<MenuItem
|
||||
v-if="buttonPermissions?.trust"
|
||||
key="1"
|
||||
@click="() => delegationModalApi.open()"
|
||||
>
|
||||
<UserOutlined class="mr-2" />委托
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="buttonPermissions?.transfer"
|
||||
key="2"
|
||||
@click="() => transferModalApi.open()"
|
||||
>
|
||||
<RollbackOutlined class="mr-2" /> 转办
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="showMultiActions && buttonPermissions?.addSign"
|
||||
key="3"
|
||||
@click="() => addSignatureModalApi.open()"
|
||||
>
|
||||
<UsergroupAddOutlined class="mr-2" /> 加签
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="showMultiActions && buttonPermissions?.subSign"
|
||||
key="4"
|
||||
@click="() => reductionSignatureModalApi.open()"
|
||||
>
|
||||
<UsergroupDeleteOutlined class="mr-2" /> 减签
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</template>
|
||||
<a-button v-if="showButtonOther" :icon="h(MenuOutlined)">
|
||||
其他
|
||||
</a-button>
|
||||
</Dropdown>
|
||||
<ApprovalModal @complete="$emit('reload')" />
|
||||
<RejectionModal @complete="$emit('reload')" />
|
||||
<DelegationModal mode="single" @finish="handleDelegation" />
|
||||
<TransferModal mode="single" @finish="handleTransfer" />
|
||||
<AddSignatureModal mode="multiple" @finish="handleAddSignature" />
|
||||
<ReductionSignatureModal
|
||||
mode="multiple"
|
||||
@finish="handleReductionSignature"
|
||||
/>
|
||||
</Space>
|
||||
<Space v-if="type === 'admin'">
|
||||
<a-button @click="handleFlowInterfere"> 流程干预 </a-button>
|
||||
<a-button @click="() => updateAssigneeModalApi.open()">
|
||||
修改办理人
|
||||
</a-button>
|
||||
<FlowInterfereModal @complete="$emit('reload')" />
|
||||
<UpdateAssigneeModal mode="single" @finish="handleUpdateAssignee" />
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1 @@
|
||||
export { default as FlowActions } from './flow-actions.vue';
|
||||
@ -1,6 +1,6 @@
|
||||
<!--
|
||||
审批详情
|
||||
动态渲染要显示的内容 需要再flowDescripionsMap先定义好组件
|
||||
动态渲染要显示的内容 需要在flowDescripionsMap先定义好组件
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import type { FlowComponentsMapMapKey } from '../register';
|
||||
@ -20,8 +20,6 @@ defineOptions({
|
||||
|
||||
defineProps<{
|
||||
currentFlowInfo: FlowInfoResponse;
|
||||
iframeHeight: number;
|
||||
iframeLoaded: boolean;
|
||||
task: TaskInfo;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@ -0,0 +1,169 @@
|
||||
<script setup lang="ts">
|
||||
import type { ApprovalType } from './type';
|
||||
|
||||
import type { FlowInfoResponse } from '#/api/workflow/instance/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer, VbenAvatar } from '@vben/common-ui';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
import { cloneDeep, cn } from '@vben/utils';
|
||||
|
||||
import { Divider, Skeleton, TabPane, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { flowInfo } from '#/api/workflow/instance';
|
||||
import { getTaskByTaskId } from '#/api/workflow/task';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
import { FlowActions } from './actions';
|
||||
import ApprovalDetails from './approval-details.vue';
|
||||
import FlowPreview from './flow-preview.vue';
|
||||
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
interface DrawerProps {
|
||||
task: TaskInfo;
|
||||
type: ApprovalType;
|
||||
}
|
||||
|
||||
const currentTask = ref<null | TaskInfo>(null);
|
||||
const currentFlowInfo = ref<FlowInfoResponse | null>(null);
|
||||
const currentType = ref<DrawerProps['type'] | null>(null);
|
||||
|
||||
const [BasicDrawer, drawerApi] = useVbenDrawer({
|
||||
title: '流程',
|
||||
onOpenChange: async (isOpen) => {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
const { task, type } = drawerApi.getData() as DrawerProps;
|
||||
const { businessId, id: taskId } = task;
|
||||
|
||||
const flowResp = await flowInfo(businessId);
|
||||
currentFlowInfo.value = flowResp;
|
||||
|
||||
/**
|
||||
* 审批需要查询按钮权限 通过drawer传递的task是空的
|
||||
* TODO: promise.all
|
||||
*/
|
||||
if (type === 'approve') {
|
||||
const taskResp = await getTaskByTaskId(taskId);
|
||||
const cloneTask = cloneDeep(task);
|
||||
// 赋值 按钮权限
|
||||
cloneTask.buttonList = taskResp.buttonList;
|
||||
currentTask.value = cloneTask;
|
||||
} else {
|
||||
// default逻辑
|
||||
currentTask.value = task;
|
||||
}
|
||||
// 最后赋值type
|
||||
currentType.value = type;
|
||||
// // 设置是否显示footer
|
||||
// drawerApi.setState({
|
||||
// footer: type !== 'readonly',
|
||||
// });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
onClosed() {
|
||||
currentTask.value = null;
|
||||
currentType.value = null;
|
||||
currentFlowInfo.value = null;
|
||||
},
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
/**
|
||||
* 底部按钮操作后
|
||||
*/
|
||||
async function handleAfterAction() {
|
||||
emit('reload');
|
||||
drawerApi.close();
|
||||
}
|
||||
|
||||
const showFooter = computed(() => {
|
||||
return ![null, 'readonly'].includes(currentType.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicDrawer class="w-[900px]" content-class="flex w-full">
|
||||
<div :class="cn('flex-1 text-[#323639E0]')">
|
||||
<Skeleton active :loading="loading">
|
||||
<div v-if="currentTask" class="flex flex-col gap-5 p-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-2xl font-bold">
|
||||
{{ currentTask.businessTitle ?? currentTask.flowName }}
|
||||
</div>
|
||||
<div>
|
||||
<component
|
||||
:is="
|
||||
renderDict(
|
||||
currentTask.flowStatus,
|
||||
DictEnum.WF_BUSINESS_STATUS,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<VbenAvatar
|
||||
:alt="currentTask.createByName ?? ''"
|
||||
class="bg-primary size-[28px] rounded-full text-white"
|
||||
src=""
|
||||
/>
|
||||
<span>{{ currentTask.createByName }}</span>
|
||||
|
||||
<div class="flex items-center opacity-50">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="icon-[bxs--category-alt] size-[16px]"></span>
|
||||
流程分类: {{ currentTask.categoryName }}
|
||||
</div>
|
||||
|
||||
<Divider type="vertical" />
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="icon-[mdi--clock-outline] size-[16px]"></span>
|
||||
提交时间: {{ currentTask.createTime }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs v-if="currentFlowInfo" class="flex-1">
|
||||
<TabPane key="1" tab="审批详情">
|
||||
<ApprovalDetails
|
||||
:current-flow-info="currentFlowInfo"
|
||||
:task="currentTask"
|
||||
/>
|
||||
</TabPane>
|
||||
|
||||
<TabPane key="2" tab="审批流程图">
|
||||
<FlowPreview :instance-id="currentFlowInfo.instanceId" />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Skeleton>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<!-- TODO: 暂时只能这样处理 footer常驻但不显示内容 这个插槽有点迷 -->
|
||||
<FlowActions
|
||||
v-if="showFooter && currentTask && currentType"
|
||||
:task="currentTask"
|
||||
:type="currentType"
|
||||
@reload="handleAfterAction"
|
||||
/>
|
||||
<div v-else></div>
|
||||
</template>
|
||||
</BasicDrawer>
|
||||
</template>
|
||||
@ -1,6 +1,6 @@
|
||||
<!--抄送组件-->
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import type { CSSProperties, PropType } from 'vue';
|
||||
|
||||
import type { User } from '#/api/system/user/model';
|
||||
|
||||
@ -18,7 +18,11 @@ defineOptions({
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{ allowUserIds?: string; ellipseNumber?: number }>(),
|
||||
defineProps<{
|
||||
allowUserIds?: string;
|
||||
avatarSize?: number;
|
||||
ellipseNumber?: number;
|
||||
}>(),
|
||||
{
|
||||
/**
|
||||
* 最大显示的头像数量 超过显示为省略号头像
|
||||
@ -28,6 +32,10 @@ const props = withDefaults(
|
||||
* 允许选择允许选择的人员ID 会当做参数拼接在uselist接口
|
||||
*/
|
||||
allowUserIds: '',
|
||||
/**
|
||||
* 头像大小
|
||||
*/
|
||||
avatarSize: 36,
|
||||
},
|
||||
);
|
||||
|
||||
@ -57,6 +65,14 @@ function handleFinish(userList: User[]) {
|
||||
const displayedList = computed(() => {
|
||||
return userListModel.value.slice(0, props.ellipseNumber);
|
||||
});
|
||||
|
||||
const avatarStyle = computed<CSSProperties>(() => {
|
||||
const { avatarSize } = props;
|
||||
return {
|
||||
width: `${avatarSize}px`,
|
||||
height: `${avatarSize}px`,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -71,8 +87,9 @@ const displayedList = computed(() => {
|
||||
<div>
|
||||
<VbenAvatar
|
||||
:alt="user?.nickName ?? ''"
|
||||
class="bg-primary size-[36px] cursor-pointer rounded-full border text-white"
|
||||
class="bg-primary cursor-pointer rounded-full border text-white"
|
||||
src=""
|
||||
:size="avatarSize"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
@ -82,13 +99,14 @@ const displayedList = computed(() => {
|
||||
>
|
||||
<Avatar
|
||||
v-if="userListModel.length > ellipseNumber"
|
||||
class="flex size-[36px] cursor-pointer items-center justify-center rounded-full border bg-[gray] text-white"
|
||||
class="flex cursor-pointer items-center justify-center rounded-full border bg-[gray] text-white"
|
||||
:style="avatarStyle"
|
||||
>
|
||||
+{{ userListModel.length - props.ellipseNumber }}
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
</AvatarGroup>
|
||||
<a-button size="small" @click="handleOpen">选择人员</a-button>
|
||||
<a-button class="ml-1" size="small" @click="handleOpen">选择人员</a-button>
|
||||
<UserSelectModal
|
||||
:allow-user-ids="allowUserIds"
|
||||
@cancel="$emit('cancel')"
|
||||
|
||||
@ -28,5 +28,5 @@ const { iframeRef } = useWarmflowIframe();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<iframe ref="iframeRef" :src="url" class="h-[500px] w-full border"></iframe>
|
||||
<iframe ref="iframeRef" :src="url" class="h-[600px] w-full border"></iframe>
|
||||
</template>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable vue/one-component-per-file */
|
||||
import { defineComponent, h, ref } from 'vue';
|
||||
|
||||
import { Modal } from 'ant-design-vue';
|
||||
@ -58,3 +59,13 @@ export function getDiffTimeString(dateTime: string) {
|
||||
const diffText = dayjs.duration(diffSeconds, 'seconds').humanize();
|
||||
return diffText;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用默认插槽来定义schema
|
||||
*/
|
||||
export const DefaultSlot = defineComponent({
|
||||
name: 'DefaultSlot',
|
||||
render() {
|
||||
return <div>{this.$slots.default?.()}</div>;
|
||||
},
|
||||
});
|
||||
|
||||
@ -4,21 +4,23 @@ export { default as ApprovalCard } from './approval-card.vue';
|
||||
* 审批同意
|
||||
*/
|
||||
export { default as approvalModal } from './approval-modal.vue';
|
||||
export { default as ApprovalPanelDrawerComp } from './approval-panel-drawer.vue';
|
||||
export { default as ApprovalPanel } from './approval-panel.vue';
|
||||
/**
|
||||
* 审批驳回
|
||||
*/
|
||||
export { default as approvalRejectionModal } from './approval-rejection-modal.vue';
|
||||
export { default as ApprovalTimeline } from './approval-timeline.vue';
|
||||
|
||||
/**
|
||||
* 选择抄送人
|
||||
*/
|
||||
export { default as CopyComponent } from './copy-component.vue';
|
||||
|
||||
/**
|
||||
* 详情信息 modal
|
||||
*/
|
||||
export { default as flowInfoModal } from './flow-info-modal.vue';
|
||||
|
||||
/**
|
||||
* 流程干预 modal
|
||||
*/
|
||||
|
||||
85
apps/web-antd/src/views/workflow/components/task/data.tsx
Normal file
85
apps/web-antd/src/views/workflow/components/task/data.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { DictEnum } from '@vben/constants';
|
||||
import { getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { getDictOptions } from '#/utils/dict';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'configName',
|
||||
label: '参数名称',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'configKey',
|
||||
label: '参数键名',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
getPopupContainer,
|
||||
options: getDictOptions(DictEnum.SYS_YES_NO),
|
||||
},
|
||||
fieldName: 'configType',
|
||||
label: '系统内置',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
},
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID',
|
||||
},
|
||||
{
|
||||
field: 'businessTitle',
|
||||
title: '业务标题',
|
||||
formatter: ({ cellValue }) => cellValue ?? '-',
|
||||
},
|
||||
{
|
||||
field: 'flowName',
|
||||
title: '流程名称',
|
||||
},
|
||||
{
|
||||
field: 'flowCode',
|
||||
title: '流程编码',
|
||||
},
|
||||
{
|
||||
field: 'nodeName',
|
||||
title: '当前任务',
|
||||
},
|
||||
{
|
||||
field: 'processedByName',
|
||||
title: '办理人',
|
||||
formatter: ({ cellValue }) => cellValue?.split?.(',') ?? cellValue,
|
||||
},
|
||||
{
|
||||
field: 'flowStatus',
|
||||
title: '流程状态',
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return renderDict(row.flowStatus, DictEnum.WF_BUSINESS_STATUS);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
},
|
||||
// {
|
||||
// field: 'action',
|
||||
// fixed: 'right',
|
||||
// slots: { default: 'action' },
|
||||
// title: '操作',
|
||||
// resizable: false,
|
||||
// width: 'auto',
|
||||
// },
|
||||
];
|
||||
@ -0,0 +1 @@
|
||||
export { default as TaskPage } from './task.vue';
|
||||
92
apps/web-antd/src/views/workflow/components/task/task.vue
Normal file
92
apps/web-antd/src/views/workflow/components/task/task.vue
Normal file
@ -0,0 +1,92 @@
|
||||
<script setup lang="ts">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import type { TaskPageProps } from './types';
|
||||
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
import { ApprovalPanelDrawerComp } from '..';
|
||||
import { columns, querySchema } from './data';
|
||||
|
||||
const props = defineProps<TaskPageProps>();
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
schema: querySchema(),
|
||||
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'],
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
},
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
return await props.listApi({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
/**
|
||||
* TODO: id
|
||||
*/
|
||||
id: 'workflow-task',
|
||||
rowClassName: 'cursor-pointer',
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents: {
|
||||
cellClick: ({ row }) => {
|
||||
handleOpen(row);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [ApprovalPanelDrawer, drawerApi] = useVbenDrawer({
|
||||
connectedComponent: ApprovalPanelDrawerComp,
|
||||
});
|
||||
|
||||
function handleOpen(row: any) {
|
||||
drawerApi.setData({ task: row, type: props.type }).open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<BasicTable :table-title="tableTitle" />
|
||||
<ApprovalPanelDrawer @reload="() => tableApi.query()" />
|
||||
</Page>
|
||||
</template>
|
||||
18
apps/web-antd/src/views/workflow/components/task/types.d.ts
vendored
Normal file
18
apps/web-antd/src/views/workflow/components/task/types.d.ts
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
import type { ApprovalType } from '../type';
|
||||
|
||||
import type { PageResult } from '#/api/common';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
export {};
|
||||
|
||||
export interface TaskPageProps {
|
||||
/**
|
||||
* 表格显示的标题
|
||||
*/
|
||||
tableTitle?: string;
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
type: ApprovalType;
|
||||
listApi: (params?: any) => Promise<PageResult<TaskInfo>>;
|
||||
}
|
||||
9
apps/web-antd/src/views/workflow/components/type.d.ts
vendored
Normal file
9
apps/web-antd/src/views/workflow/components/type.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
export {};
|
||||
|
||||
/**
|
||||
* myself 我发起的
|
||||
* readonly 只读 只用于查看
|
||||
* approve 审批
|
||||
* admin 流程监控 - 待办任务使用
|
||||
*/
|
||||
export type ApprovalType = 'admin' | 'approve' | 'myself' | 'readonly';
|
||||
@ -1,18 +1,27 @@
|
||||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { markRaw } from 'vue';
|
||||
|
||||
import { DictEnum } from '@vben/constants';
|
||||
|
||||
import { OptionsTag } from '#/components/table';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
import { CopyComponent } from '../components';
|
||||
import { DefaultSlot } from '../components/helper';
|
||||
import { activityStatusOptions } from '../processDefinition/constant';
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
/**
|
||||
* https://github.com/facebook/react/issues/6284
|
||||
* https://github.com/ant-design/ant-design/issues/2950#issuecomment-245852795
|
||||
* nodeName算一种`关键字` 会导致报错
|
||||
*/
|
||||
{
|
||||
component: 'Input',
|
||||
label: '任务名称',
|
||||
fieldName: 'nodeName',
|
||||
fieldName: '_nodeName',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
@ -24,6 +33,17 @@ export const querySchema: FormSchemaGetter = () => [
|
||||
label: '流程编码',
|
||||
fieldName: 'flowCode',
|
||||
},
|
||||
{
|
||||
fieldName: 'createByIds',
|
||||
defaultValue: [],
|
||||
component: markRaw(DefaultSlot),
|
||||
label: '申请人',
|
||||
renderComponentContent: (model) => ({
|
||||
default: () => (
|
||||
<CopyComponent avatarSize={30} v-model:userList={model.createByIds} />
|
||||
),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
|
||||
@ -13,6 +13,7 @@ import { $t } from '@vben/locales';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Popconfirm, RadioGroup, Space } from 'ant-design-vue';
|
||||
import { isArray } from 'lodash-es';
|
||||
|
||||
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
||||
import {
|
||||
@ -95,6 +96,18 @@ const gridOptions: VxeGridProps = {
|
||||
Reflect.deleteProperty(formValues, 'category');
|
||||
}
|
||||
|
||||
// 转换数据
|
||||
if (isArray(formValues.createByIds)) {
|
||||
formValues.createByIds = (formValues.createByIds as Array<any>).map(
|
||||
(item) => item.userId,
|
||||
);
|
||||
}
|
||||
// 使用nodeName会导致选人组件hover报错
|
||||
if (formValues._nodeName) {
|
||||
formValues.nodeName = formValues._nodeName;
|
||||
Reflect.deleteProperty(formValues, '_nodeName');
|
||||
}
|
||||
|
||||
return await currentTypeApi({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
@ -150,7 +163,9 @@ const [InstanceVariableModal, instanceVariableModalApi] = useVbenModal({
|
||||
connectedComponent: instanceVariableModal,
|
||||
});
|
||||
function handleVariable(row: Recordable<any>) {
|
||||
instanceVariableModalApi.setData({ record: row.variable });
|
||||
instanceVariableModalApi.setData({
|
||||
instanceId: row.id,
|
||||
});
|
||||
instanceVariableModalApi.open();
|
||||
}
|
||||
const [FlowInfoModal, flowInfoModalApi] = useVbenModal({
|
||||
|
||||
@ -1,28 +1,178 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="tsx">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { JsonPreview, useVbenModal } from '@vben/common-ui';
|
||||
import { cn, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { instanceVariable, updateFlowVariable } from '#/api/workflow/instance';
|
||||
|
||||
interface ModalData {
|
||||
/**
|
||||
* 变量 json字符串
|
||||
*/
|
||||
record: string;
|
||||
instanceId: string;
|
||||
}
|
||||
|
||||
const data = ref({});
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '流程变量',
|
||||
fullscreenButton: false,
|
||||
footer: false,
|
||||
onOpenChange: (visible) => {
|
||||
onOpenChange: async (visible) => {
|
||||
if (!visible) {
|
||||
data.value = {};
|
||||
return null;
|
||||
}
|
||||
const recordString = modalApi.getData().record;
|
||||
data.value = JSON.parse(recordString);
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
await loadData();
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
const fieldTypeColors = {
|
||||
string: 'cyan',
|
||||
number: 'blue',
|
||||
boolean: 'orange',
|
||||
object: 'purple',
|
||||
};
|
||||
function getFieldTypeColor(fieldType: string) {
|
||||
return (
|
||||
fieldTypeColors[fieldType as keyof typeof fieldTypeColors] ?? 'default'
|
||||
);
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
const { instanceId } = modalApi.getData() as ModalData;
|
||||
const resp = await instanceVariable(instanceId);
|
||||
const jsonObj = JSON.parse(resp.variable);
|
||||
data.value = jsonObj;
|
||||
|
||||
// 表单
|
||||
const objEntry = Object.entries(jsonObj);
|
||||
|
||||
interface OptionsType {
|
||||
label: string;
|
||||
value: string;
|
||||
fieldType: string;
|
||||
}
|
||||
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'key',
|
||||
componentProps: {
|
||||
options: objEntry.map(
|
||||
([key, value]) =>
|
||||
({
|
||||
label: key,
|
||||
value: key,
|
||||
fieldType: typeof value,
|
||||
}) as OptionsType,
|
||||
),
|
||||
},
|
||||
renderComponentContent: () => ({
|
||||
option: (option: OptionsType) => (
|
||||
<div>
|
||||
{option.label}
|
||||
<Tag class="ml-1" color={getFieldTypeColor(option.fieldType)}>
|
||||
{option.fieldType}
|
||||
</Tag>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
allowClear: true,
|
||||
},
|
||||
labelWidth: 80,
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'key',
|
||||
component: 'Select',
|
||||
label: '变量名称',
|
||||
rules: 'selectRequired',
|
||||
componentProps: {
|
||||
getPopupContainer,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'value',
|
||||
component: 'Input',
|
||||
label: '变量值',
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
],
|
||||
resetButtonOptions: {
|
||||
show: false,
|
||||
},
|
||||
submitButtonOptions: {
|
||||
content: '修改',
|
||||
},
|
||||
handleSubmit: async (values) => {
|
||||
console.log(values);
|
||||
Modal.confirm({
|
||||
title: '修改流程变量',
|
||||
content: '确认修改流程变量吗?',
|
||||
centered: true,
|
||||
okButtonProps: {
|
||||
danger: true,
|
||||
},
|
||||
onOk: async () => {
|
||||
await handleSubmit(values);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
async function handleSubmit(values: any) {
|
||||
try {
|
||||
modalApi.lock(true);
|
||||
|
||||
const { instanceId } = modalApi.getData() as ModalData;
|
||||
|
||||
// 修改
|
||||
const requestData = {
|
||||
instanceId,
|
||||
key: values.key,
|
||||
value: values.value,
|
||||
};
|
||||
await updateFlowVariable(requestData);
|
||||
await formApi.resetForm();
|
||||
|
||||
// 查询修改后的
|
||||
const resp = await instanceVariable(instanceId);
|
||||
const jsonObj = JSON.parse(resp.variable);
|
||||
data.value = jsonObj;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.lock(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<div class="min-h-[400px] overflow-y-auto">
|
||||
<div
|
||||
:class="cn('min-h-[400px] overflow-y-auto border', 'rounded-[4px] p-2')"
|
||||
>
|
||||
<JsonPreview :data="data" />
|
||||
</div>
|
||||
<div class="mt-2 text-sm font-medium text-red-500">
|
||||
由于限制 只能变更字段为string类型
|
||||
</div>
|
||||
<Form class="mt-2" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
@ -1,360 +1,235 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/system/user/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
<script setup lang="tsx">
|
||||
import type { RadioChangeEvent } from 'ant-design-vue';
|
||||
|
||||
import { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue';
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
import { addFullName, getPopupContainer } from '@vben/utils';
|
||||
import type { ComponentType } from '#/adapter/component';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Empty,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputSearch,
|
||||
Popover,
|
||||
Segmented,
|
||||
Spin,
|
||||
Tooltip,
|
||||
TreeSelect,
|
||||
} from 'ant-design-vue';
|
||||
import { cloneDeep, debounce, uniqueId } from 'lodash-es';
|
||||
import { markRaw, onMounted, ref } from 'vue';
|
||||
|
||||
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 { pageByAllTaskFinish, pageByAllTaskWait } from '#/api/workflow/task';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
|
||||
import { bottomOffset } from './constant';
|
||||
import { ApprovalPanelDrawerComp, CopyComponent } from '../components';
|
||||
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'],
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* 流程监控 - 待办任务页面的id不唯一 改为前端处理
|
||||
*/
|
||||
interface TaskItem extends TaskInfo {
|
||||
active: boolean;
|
||||
randomId: string;
|
||||
}
|
||||
|
||||
const taskList = ref<TaskItem[]>([]);
|
||||
const taskTotal = ref(0);
|
||||
const page = ref(1);
|
||||
const loading = ref(false);
|
||||
onMounted(async () => {
|
||||
const tree = await categoryTree();
|
||||
tableApi.formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'category',
|
||||
componentProps: {
|
||||
fieldNames: { label: 'label', value: 'id' },
|
||||
showSearch: true,
|
||||
treeData: tree,
|
||||
treeDefaultExpandAll: true,
|
||||
treeLine: { showLeafIcon: false },
|
||||
// 筛选的字段
|
||||
treeNodeFilterProp: 'label',
|
||||
// 选中后显示在输入框的值
|
||||
treeNodeLabelProp: 'label',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '待办任务', value: 'todo' },
|
||||
{ label: '已办任务', value: 'done' },
|
||||
];
|
||||
const currentType = ref('todo');
|
||||
const currentApi = computed(() => {
|
||||
if (currentType.value === 'todo') {
|
||||
return pageByAllTaskWait;
|
||||
{ label: '运行中', value: 'running' },
|
||||
{ label: '已完成', value: 'finish' },
|
||||
] as const;
|
||||
type OptionValue = (typeof typeOptions)[number]['value'];
|
||||
let currentTypeApi = pageByAllTaskWait;
|
||||
const currentType = ref<OptionValue>('running');
|
||||
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 nextTick();
|
||||
await reload(true);
|
||||
await tableApi.reload();
|
||||
}
|
||||
|
||||
const defaultFormData = {
|
||||
flowName: '', // 流程定义名称
|
||||
nodeName: '', // 任务名称
|
||||
flowCode: '', // 流程定义编码
|
||||
createByIds: [] as string[], // 创建人
|
||||
category: null as null | number, // 流程分类
|
||||
};
|
||||
const formData = ref(cloneDeep(defaultFormData));
|
||||
|
||||
/**
|
||||
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||
*/
|
||||
const isLoadComplete = computed(
|
||||
() => taskList.value.length === taskTotal.value,
|
||||
);
|
||||
|
||||
// 卡片父容器的ref
|
||||
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||
|
||||
/**
|
||||
* @param resetFields 是否清空查询参数
|
||||
*/
|
||||
async function reload(resetFields: boolean = false) {
|
||||
// 需要先滚动到顶部
|
||||
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||
|
||||
page.value = 1;
|
||||
currentTask.value = undefined;
|
||||
taskTotal.value = 0;
|
||||
lastSelectId.value = '';
|
||||
|
||||
if (resetFields) {
|
||||
formData.value = cloneDeep(defaultFormData);
|
||||
selectedUserList.value = [];
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const resp = await currentApi.value({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value = resp.rows.map((item) => ({
|
||||
...item,
|
||||
active: false,
|
||||
randomId: uniqueId(),
|
||||
}));
|
||||
taskTotal.value = resp.total;
|
||||
|
||||
loading.value = false;
|
||||
// 默认选中第一个
|
||||
if (taskList.value.length > 0) {
|
||||
const firstTask = taskList.value[0]!;
|
||||
currentTask.value = firstTask;
|
||||
handleCardClick(firstTask);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
|
||||
const handleScroll = debounce(async (e: Event) => {
|
||||
if (!e.target) {
|
||||
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(),
|
||||
})),
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID',
|
||||
},
|
||||
{
|
||||
field: 'businessTitle',
|
||||
title: '业务标题',
|
||||
formatter: ({ cellValue }) => cellValue ?? '-',
|
||||
},
|
||||
{
|
||||
field: 'flowName',
|
||||
title: '流程名称',
|
||||
},
|
||||
{
|
||||
field: 'categoryName',
|
||||
title: '流程分类',
|
||||
},
|
||||
{
|
||||
field: 'flowCode',
|
||||
title: '流程编码',
|
||||
},
|
||||
{
|
||||
field: 'nodeName',
|
||||
title: '当前任务',
|
||||
},
|
||||
{
|
||||
field: 'processedByName',
|
||||
title: '办理人',
|
||||
formatter: ({ cellValue }) => cellValue?.split?.(',') ?? cellValue,
|
||||
},
|
||||
{
|
||||
field: 'flowStatus',
|
||||
title: '流程状态',
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return renderDict(row.flowStatus, DictEnum.WF_BUSINESS_STATUS);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 转换数据
|
||||
if (isArray(formValues.createByIds)) {
|
||||
formValues.createByIds = (formValues.createByIds as Array<any>).map(
|
||||
(item) => item.userId,
|
||||
);
|
||||
loading.value = false;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const lastSelectId = ref('');
|
||||
const currentTask = ref<TaskInfo>();
|
||||
async function handleCardClick(item: TaskItem) {
|
||||
const { randomId } = item;
|
||||
// 点击的是同一个
|
||||
if (lastSelectId.value === randomId) {
|
||||
return;
|
||||
}
|
||||
currentTask.value = item;
|
||||
// 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
|
||||
taskList.value.forEach((item) => {
|
||||
item.active = item.randomId === randomId;
|
||||
return await currentTypeApi({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
lastSelectId.value = randomId;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isCurrent: true,
|
||||
},
|
||||
id: 'workflow-task-myself',
|
||||
rowClassName: 'cursor-pointer',
|
||||
};
|
||||
|
||||
const { refreshTab } = useTabs();
|
||||
|
||||
// 由于失去焦点浮层会消失 使用v-model选择人员完毕后强制显示
|
||||
const popoverOpen = ref(false);
|
||||
const selectedUserList = ref<User[]>([]);
|
||||
function handleFinish(userList: User[]) {
|
||||
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 [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents: {
|
||||
cellClick: ({ row }) => {
|
||||
handleOpen(row);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-2">
|
||||
<div
|
||||
class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
|
||||
>
|
||||
<!-- 搜索条件 -->
|
||||
<div
|
||||
class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
|
||||
>
|
||||
<Segmented
|
||||
<BasicTable>
|
||||
<template #toolbar-actions>
|
||||
<RadioGroup
|
||||
v-model:value="currentType"
|
||||
:options="typeOptions"
|
||||
block
|
||||
class="mb-2"
|
||||
button-style="solid"
|
||||
option-type="button"
|
||||
@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 #content>
|
||||
<Form
|
||||
: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>
|
||||
</BasicTable>
|
||||
<ApprovalPanelDrawer @reload="() => tableApi.query()" />
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.thin-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
@apply thin-scrollbar;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,257 +1,162 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
import type { ComponentType } from '#/adapter/component';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
import { getPopupContainer } from '@vben/utils';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Empty,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputSearch,
|
||||
Popover,
|
||||
Spin,
|
||||
Tooltip,
|
||||
} from 'ant-design-vue';
|
||||
import { cloneDeep, debounce } from 'lodash-es';
|
||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { categoryTree } from '#/api/workflow/category';
|
||||
import { pageByCurrent } from '#/api/workflow/instance';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
import { ApprovalCard, ApprovalPanel } from '../components';
|
||||
import { bottomOffset } from './constant';
|
||||
import { ApprovalPanelDrawerComp } from '../components';
|
||||
|
||||
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
|
||||
const taskList = ref<(TaskInfo & { active: boolean })[]>([]);
|
||||
const taskTotal = ref(0);
|
||||
const page = ref(1);
|
||||
const loading = ref(false);
|
||||
|
||||
const defaultFormData = {
|
||||
flowName: '', // 流程定义名称
|
||||
nodeName: '', // 任务名称
|
||||
flowCode: '', // 流程定义编码
|
||||
category: null as null | number, // 流程分类
|
||||
const formOptions: VbenFormProps<ComponentType> = {
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'category',
|
||||
component: 'TreeSelect',
|
||||
label: '流程分类',
|
||||
},
|
||||
{
|
||||
fieldName: 'flowCode',
|
||||
component: 'Input',
|
||||
label: '流程编码',
|
||||
},
|
||||
],
|
||||
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'],
|
||||
],
|
||||
],
|
||||
};
|
||||
const formData = ref(cloneDeep(defaultFormData));
|
||||
|
||||
/**
|
||||
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||
*/
|
||||
const isLoadComplete = computed(
|
||||
() => taskList.value.length === taskTotal.value,
|
||||
);
|
||||
onMounted(async () => {
|
||||
const tree = await categoryTree();
|
||||
tableApi.formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'category',
|
||||
componentProps: {
|
||||
fieldNames: { label: 'label', value: 'id' },
|
||||
showSearch: true,
|
||||
treeData: tree,
|
||||
treeDefaultExpandAll: true,
|
||||
treeLine: { showLeafIcon: false },
|
||||
// 筛选的字段
|
||||
treeNodeFilterProp: 'label',
|
||||
// 选中后显示在输入框的值
|
||||
treeNodeLabelProp: 'label',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
// 卡片父容器的ref
|
||||
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||
|
||||
/**
|
||||
* @param resetFields 是否清空查询参数
|
||||
*/
|
||||
async function reload(resetFields: boolean = false) {
|
||||
// 需要先滚动到顶部
|
||||
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||
|
||||
page.value = 1;
|
||||
currentTask.value = undefined;
|
||||
taskTotal.value = 0;
|
||||
lastSelectId.value = '';
|
||||
|
||||
if (resetFields) {
|
||||
formData.value = cloneDeep(defaultFormData);
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const resp = await pageByCurrent({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID',
|
||||
},
|
||||
// {
|
||||
// field: 'businessTitle',
|
||||
// title: '业务标题',
|
||||
// formatter: ({ cellValue }) => cellValue ?? '-',
|
||||
// },
|
||||
{
|
||||
field: 'flowName',
|
||||
title: '流程名称',
|
||||
},
|
||||
{
|
||||
field: 'categoryName',
|
||||
title: '流程分类',
|
||||
},
|
||||
{
|
||||
field: 'flowCode',
|
||||
title: '流程编码',
|
||||
},
|
||||
{
|
||||
field: 'nodeName',
|
||||
title: '当前任务',
|
||||
},
|
||||
{
|
||||
field: 'flowStatus',
|
||||
title: '流程状态',
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return renderDict(row.flowStatus, DictEnum.WF_BUSINESS_STATUS);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
return await pageByCurrent({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
taskList.value = resp.rows.map((item) => ({ ...item, active: false }));
|
||||
taskTotal.value = resp.total;
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isCurrent: true,
|
||||
},
|
||||
id: 'workflow-task-myself',
|
||||
rowClassName: 'cursor-pointer',
|
||||
};
|
||||
|
||||
loading.value = false;
|
||||
// 默认选中第一个
|
||||
if (taskList.value.length > 0) {
|
||||
const firstTask = taskList.value[0]!;
|
||||
currentTask.value = firstTask;
|
||||
handleCardClick(firstTask);
|
||||
}
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents: {
|
||||
cellClick: ({ row }) => {
|
||||
handleOpen(row);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [ApprovalPanelDrawer, drawerApi] = useVbenDrawer({
|
||||
connectedComponent: ApprovalPanelDrawerComp,
|
||||
});
|
||||
|
||||
function handleOpen(row: any) {
|
||||
drawerApi.setData({ task: row, type: 'myself' }).open();
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
|
||||
const handleScroll = debounce(async (e: Event) => {
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
// e.target.scrollTop 是元素顶部到当前可视区域顶部的距离,即已滚动的高度。
|
||||
// e.target.clientHeight 是元素的可视高度。
|
||||
// e.target.scrollHeight 是元素的总高度。
|
||||
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
|
||||
// 判断是否滚动到底部
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;
|
||||
|
||||
// 滚动到底部且没有加载完成
|
||||
if (isBottom && !isLoadComplete.value) {
|
||||
loading.value = true;
|
||||
page.value += 1;
|
||||
const resp = await pageByCurrent({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value.push(
|
||||
...resp.rows.map((item) => ({ ...item, active: false })),
|
||||
);
|
||||
loading.value = false;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const lastSelectId = ref('');
|
||||
const currentTask = ref<TaskInfo>();
|
||||
async function handleCardClick(item: TaskInfo) {
|
||||
const { id } = item;
|
||||
// 点击的是同一个
|
||||
if (lastSelectId.value === id) {
|
||||
return;
|
||||
}
|
||||
currentTask.value = item;
|
||||
// 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
|
||||
taskList.value.forEach((item) => {
|
||||
item.active = item.id === id;
|
||||
});
|
||||
lastSelectId.value = id;
|
||||
}
|
||||
|
||||
const { refreshTab } = useTabs();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-2">
|
||||
<div
|
||||
class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
|
||||
>
|
||||
<!-- 搜索条件 -->
|
||||
<div
|
||||
class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
|
||||
>
|
||||
<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
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="rightTop"
|
||||
trigger="click"
|
||||
>
|
||||
<template #title>
|
||||
<div class="w-full border-b pb-[12px] text-[16px]">搜索</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<Form
|
||||
:colon="false"
|
||||
:label-col="{ span: 6 }"
|
||||
:model="formData"
|
||||
autocomplete="off"
|
||||
class="w-[300px]"
|
||||
@finish="() => reload(false)"
|
||||
>
|
||||
<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.id"
|
||||
:info="item"
|
||||
class="mx-2"
|
||||
@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="myself" @reload="refreshTab" />
|
||||
</div>
|
||||
<BasicTable table-title="我发起的" />
|
||||
<ApprovalPanelDrawer @reload="() => tableApi.query()" />
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.thin-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
@apply thin-scrollbar;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,299 +1,183 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/system/user/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
<script setup lang="tsx">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
import type { ComponentType } from '#/adapter/component';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { addFullName, getPopupContainer } from '@vben/utils';
|
||||
import { markRaw, onMounted } from 'vue';
|
||||
|
||||
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Empty,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputSearch,
|
||||
Popover,
|
||||
Spin,
|
||||
Tooltip,
|
||||
TreeSelect,
|
||||
} from 'ant-design-vue';
|
||||
import { cloneDeep, debounce } from 'lodash-es';
|
||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
|
||||
import { isArray } from 'lodash-es';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { categoryTree } from '#/api/workflow/category';
|
||||
import { pageByTaskCopy } from '#/api/workflow/task';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
|
||||
import { bottomOffset } from './constant';
|
||||
import { ApprovalPanelDrawerComp, CopyComponent } from '../components';
|
||||
import { DefaultSlot } from '../components/helper';
|
||||
|
||||
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
|
||||
const taskList = ref<(TaskInfo & { active: boolean })[]>([]);
|
||||
const taskTotal = ref(0);
|
||||
const page = ref(1);
|
||||
const loading = ref(false);
|
||||
|
||||
const defaultFormData = {
|
||||
flowName: '', // 流程定义名称
|
||||
nodeName: '', // 任务名称
|
||||
flowCode: '', // 流程定义编码
|
||||
createByIds: [] as string[], // 创建人
|
||||
category: null as null | number, // 流程分类
|
||||
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'],
|
||||
],
|
||||
],
|
||||
};
|
||||
const formData = ref(cloneDeep(defaultFormData));
|
||||
|
||||
/**
|
||||
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||
*/
|
||||
const isLoadComplete = computed(
|
||||
() => taskList.value.length === taskTotal.value,
|
||||
);
|
||||
|
||||
// 卡片父容器的ref
|
||||
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||
|
||||
/**
|
||||
* @param resetFields 是否清空查询参数
|
||||
*/
|
||||
async function reload(resetFields: boolean = false) {
|
||||
// 需要先滚动到顶部
|
||||
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||
|
||||
page.value = 1;
|
||||
currentTask.value = undefined;
|
||||
taskTotal.value = 0;
|
||||
lastSelectId.value = '';
|
||||
|
||||
if (resetFields) {
|
||||
formData.value = cloneDeep(defaultFormData);
|
||||
selectedUserList.value = [];
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const resp = await pageByTaskCopy({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value = resp.rows.map((item) => ({ ...item, active: false }));
|
||||
taskTotal.value = resp.total;
|
||||
|
||||
loading.value = false;
|
||||
// 默认选中第一个
|
||||
if (taskList.value.length > 0) {
|
||||
const firstTask = taskList.value[0]!;
|
||||
currentTask.value = firstTask;
|
||||
handleCardClick(firstTask);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
|
||||
const handleScroll = debounce(async (e: Event) => {
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
// e.target.scrollTop 是元素顶部到当前可视区域顶部的距离,即已滚动的高度。
|
||||
// e.target.clientHeight 是元素的可视高度。
|
||||
// e.target.scrollHeight 是元素的总高度。
|
||||
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
|
||||
// 判断是否滚动到底部
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;
|
||||
|
||||
// 滚动到底部且没有加载完成
|
||||
if (isBottom && !isLoadComplete.value) {
|
||||
loading.value = true;
|
||||
page.value += 1;
|
||||
const resp = await pageByTaskCopy({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value.push(
|
||||
...resp.rows.map((item) => ({ ...item, active: false })),
|
||||
);
|
||||
loading.value = false;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const lastSelectId = ref('');
|
||||
const currentTask = ref<TaskInfo>();
|
||||
async function handleCardClick(item: TaskInfo) {
|
||||
const { id } = item;
|
||||
// 点击的是同一个
|
||||
if (lastSelectId.value === id) {
|
||||
return;
|
||||
}
|
||||
currentTask.value = item;
|
||||
// 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
|
||||
taskList.value.forEach((item) => {
|
||||
item.active = item.id === id;
|
||||
});
|
||||
lastSelectId.value = id;
|
||||
}
|
||||
|
||||
// 由于失去焦点浮层会消失 使用v-model选择人员完毕后强制显示
|
||||
const popoverOpen = ref(false);
|
||||
const selectedUserList = ref<User[]>([]);
|
||||
function handleFinish(userList: User[]) {
|
||||
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;
|
||||
tableApi.formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'category',
|
||||
componentProps: {
|
||||
fieldNames: { label: 'label', value: 'id' },
|
||||
showSearch: true,
|
||||
treeData: tree,
|
||||
treeDefaultExpandAll: true,
|
||||
treeLine: { showLeafIcon: false },
|
||||
// 筛选的字段
|
||||
treeNodeFilterProp: 'label',
|
||||
// 选中后显示在输入框的值
|
||||
treeNodeLabelProp: 'label',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID',
|
||||
},
|
||||
// {
|
||||
// field: 'businessTitle',
|
||||
// title: '业务标题',
|
||||
// formatter: ({ cellValue }) => cellValue ?? '-',
|
||||
// },
|
||||
{
|
||||
field: 'flowName',
|
||||
title: '流程名称',
|
||||
},
|
||||
{
|
||||
field: 'categoryName',
|
||||
title: '流程分类',
|
||||
},
|
||||
{
|
||||
field: 'flowCode',
|
||||
title: '流程编码',
|
||||
},
|
||||
{
|
||||
field: 'nodeName',
|
||||
title: '当前任务',
|
||||
},
|
||||
{
|
||||
field: 'flowStatus',
|
||||
title: '流程状态',
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return renderDict(row.flowStatus, DictEnum.WF_BUSINESS_STATUS);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 转换数据
|
||||
if (isArray(formValues.createByIds)) {
|
||||
formValues.createByIds = (formValues.createByIds as Array<any>).map(
|
||||
(item) => item.userId,
|
||||
);
|
||||
}
|
||||
|
||||
return await pageByTaskCopy({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isCurrent: true,
|
||||
},
|
||||
id: 'workflow-task-finish',
|
||||
rowClassName: 'cursor-pointer',
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents: {
|
||||
cellClick: ({ row }) => {
|
||||
handleOpen(row);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [ApprovalPanelDrawer, drawerApi] = useVbenDrawer({
|
||||
connectedComponent: ApprovalPanelDrawerComp,
|
||||
});
|
||||
|
||||
function handleOpen(row: any) {
|
||||
drawerApi.setData({ task: row, type: 'readonly' }).open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-2">
|
||||
<div
|
||||
class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
|
||||
>
|
||||
<!-- 搜索条件 -->
|
||||
<div
|
||||
class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
|
||||
>
|
||||
<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 #content>
|
||||
<Form
|
||||
: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.id"
|
||||
:info="item"
|
||||
class="mx-2"
|
||||
@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="readonly" />
|
||||
</div>
|
||||
<BasicTable table-title="我的抄送" />
|
||||
<ApprovalPanelDrawer @reload="() => tableApi.query()" />
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.thin-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
@apply thin-scrollbar;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,299 +1,183 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/system/user/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
<script setup lang="tsx">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
import type { ComponentType } from '#/adapter/component';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { addFullName, getPopupContainer } from '@vben/utils';
|
||||
import { markRaw, onMounted } from 'vue';
|
||||
|
||||
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Empty,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputSearch,
|
||||
Popover,
|
||||
Spin,
|
||||
Tooltip,
|
||||
TreeSelect,
|
||||
} from 'ant-design-vue';
|
||||
import { cloneDeep, debounce } from 'lodash-es';
|
||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
|
||||
import { isArray } from 'lodash-es';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { categoryTree } from '#/api/workflow/category';
|
||||
import { pageByTaskFinish } from '#/api/workflow/task';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
|
||||
import { bottomOffset } from './constant';
|
||||
import { ApprovalPanelDrawerComp, CopyComponent } from '../components';
|
||||
import { DefaultSlot } from '../components/helper';
|
||||
|
||||
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
|
||||
const taskList = ref<(TaskInfo & { active: boolean })[]>([]);
|
||||
const taskTotal = ref(0);
|
||||
const page = ref(1);
|
||||
const loading = ref(false);
|
||||
|
||||
const defaultFormData = {
|
||||
flowName: '', // 流程定义名称
|
||||
nodeName: '', // 任务名称
|
||||
flowCode: '', // 流程定义编码
|
||||
createByIds: [] as string[], // 创建人
|
||||
category: null as null | number, // 流程分类
|
||||
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'],
|
||||
],
|
||||
],
|
||||
};
|
||||
const formData = ref(cloneDeep(defaultFormData));
|
||||
|
||||
/**
|
||||
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||
*/
|
||||
const isLoadComplete = computed(
|
||||
() => taskList.value.length === taskTotal.value,
|
||||
);
|
||||
|
||||
// 卡片父容器的ref
|
||||
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||
|
||||
/**
|
||||
* @param resetFields 是否清空查询参数
|
||||
*/
|
||||
async function reload(resetFields: boolean = false) {
|
||||
// 需要先滚动到顶部
|
||||
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||
|
||||
page.value = 1;
|
||||
currentTask.value = undefined;
|
||||
taskTotal.value = 0;
|
||||
lastSelectId.value = '';
|
||||
|
||||
if (resetFields) {
|
||||
formData.value = cloneDeep(defaultFormData);
|
||||
selectedUserList.value = [];
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const resp = await pageByTaskFinish({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value = resp.rows.map((item) => ({ ...item, active: false }));
|
||||
taskTotal.value = resp.total;
|
||||
|
||||
loading.value = false;
|
||||
// 默认选中第一个
|
||||
if (taskList.value.length > 0) {
|
||||
const firstTask = taskList.value[0]!;
|
||||
currentTask.value = firstTask;
|
||||
handleCardClick(firstTask);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
|
||||
const handleScroll = debounce(async (e: Event) => {
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
// e.target.scrollTop 是元素顶部到当前可视区域顶部的距离,即已滚动的高度。
|
||||
// e.target.clientHeight 是元素的可视高度。
|
||||
// e.target.scrollHeight 是元素的总高度。
|
||||
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
|
||||
// 判断是否滚动到底部
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;
|
||||
|
||||
// 滚动到底部且没有加载完成
|
||||
if (isBottom && !isLoadComplete.value) {
|
||||
loading.value = true;
|
||||
page.value += 1;
|
||||
const resp = await pageByTaskFinish({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value.push(
|
||||
...resp.rows.map((item) => ({ ...item, active: false })),
|
||||
);
|
||||
loading.value = false;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const lastSelectId = ref('');
|
||||
const currentTask = ref<TaskInfo>();
|
||||
async function handleCardClick(item: TaskInfo) {
|
||||
const { id } = item;
|
||||
// 点击的是同一个
|
||||
if (lastSelectId.value === id) {
|
||||
return;
|
||||
}
|
||||
currentTask.value = item;
|
||||
// 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
|
||||
taskList.value.forEach((item) => {
|
||||
item.active = item.id === id;
|
||||
});
|
||||
lastSelectId.value = id;
|
||||
}
|
||||
|
||||
// 由于失去焦点浮层会消失 使用v-model选择人员完毕后强制显示
|
||||
const popoverOpen = ref(false);
|
||||
const selectedUserList = ref<User[]>([]);
|
||||
function handleFinish(userList: User[]) {
|
||||
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;
|
||||
tableApi.formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'category',
|
||||
componentProps: {
|
||||
fieldNames: { label: 'label', value: 'id' },
|
||||
showSearch: true,
|
||||
treeData: tree,
|
||||
treeDefaultExpandAll: true,
|
||||
treeLine: { showLeafIcon: false },
|
||||
// 筛选的字段
|
||||
treeNodeFilterProp: 'label',
|
||||
// 选中后显示在输入框的值
|
||||
treeNodeLabelProp: 'label',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID',
|
||||
},
|
||||
// {
|
||||
// field: 'businessTitle',
|
||||
// title: '业务标题',
|
||||
// formatter: ({ cellValue }) => cellValue ?? '-',
|
||||
// },
|
||||
{
|
||||
field: 'flowName',
|
||||
title: '流程名称',
|
||||
},
|
||||
{
|
||||
field: 'categoryName',
|
||||
title: '流程分类',
|
||||
},
|
||||
{
|
||||
field: 'flowCode',
|
||||
title: '流程编码',
|
||||
},
|
||||
{
|
||||
field: 'nodeName',
|
||||
title: '当前任务',
|
||||
},
|
||||
{
|
||||
field: 'flowStatus',
|
||||
title: '流程状态',
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return renderDict(row.flowStatus, DictEnum.WF_BUSINESS_STATUS);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 转换数据
|
||||
if (isArray(formValues.createByIds)) {
|
||||
formValues.createByIds = (formValues.createByIds as Array<any>).map(
|
||||
(item) => item.userId,
|
||||
);
|
||||
}
|
||||
|
||||
return await pageByTaskFinish({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isCurrent: true,
|
||||
},
|
||||
id: 'workflow-task-finish',
|
||||
rowClassName: 'cursor-pointer',
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents: {
|
||||
cellClick: ({ row }) => {
|
||||
handleOpen(row);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [ApprovalPanelDrawer, drawerApi] = useVbenDrawer({
|
||||
connectedComponent: ApprovalPanelDrawerComp,
|
||||
});
|
||||
|
||||
function handleOpen(row: any) {
|
||||
drawerApi.setData({ task: row, type: 'readonly' }).open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-2">
|
||||
<div
|
||||
class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
|
||||
>
|
||||
<!-- 搜索条件 -->
|
||||
<div
|
||||
class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
|
||||
>
|
||||
<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 #content>
|
||||
<Form
|
||||
: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.id"
|
||||
:info="item"
|
||||
class="mx-2"
|
||||
@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="readonly" />
|
||||
</div>
|
||||
<BasicTable table-title="已办理任务" />
|
||||
<ApprovalPanelDrawer @reload="() => tableApi.query()" />
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.thin-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
@apply thin-scrollbar;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,302 +1,188 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/system/user/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
<script setup lang="tsx">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
import type { ComponentType } from '#/adapter/component';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
import { addFullName, getPopupContainer } from '@vben/utils';
|
||||
import { markRaw, onMounted } from 'vue';
|
||||
|
||||
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Empty,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputSearch,
|
||||
Popover,
|
||||
Spin,
|
||||
Tooltip,
|
||||
TreeSelect,
|
||||
} from 'ant-design-vue';
|
||||
import { cloneDeep, debounce } from 'lodash-es';
|
||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
|
||||
import { isArray } from 'lodash-es';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { categoryTree } from '#/api/workflow/category';
|
||||
import { pageByTaskWait } from '#/api/workflow/task';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
|
||||
import { bottomOffset } from './constant';
|
||||
import { ApprovalPanelDrawerComp, CopyComponent } from '../components';
|
||||
import { DefaultSlot } from '../components/helper';
|
||||
|
||||
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
|
||||
const taskList = ref<(TaskInfo & { active: boolean })[]>([]);
|
||||
const taskTotal = ref(0);
|
||||
const page = ref(1);
|
||||
const loading = ref(false);
|
||||
|
||||
const defaultFormData = {
|
||||
flowName: '', // 流程定义名称
|
||||
nodeName: '', // 任务名称
|
||||
flowCode: '', // 流程定义编码
|
||||
createByIds: [] as string[], // 创建人
|
||||
category: null as null | number, // 流程分类
|
||||
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'],
|
||||
],
|
||||
],
|
||||
};
|
||||
const formData = ref(cloneDeep(defaultFormData));
|
||||
|
||||
/**
|
||||
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||
*/
|
||||
const isLoadComplete = computed(
|
||||
() => taskList.value.length === taskTotal.value,
|
||||
);
|
||||
|
||||
// 卡片父容器的ref
|
||||
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||
|
||||
/**
|
||||
* @param resetFields 是否清空查询参数
|
||||
*/
|
||||
async function reload(resetFields: boolean = false) {
|
||||
// 需要先滚动到顶部
|
||||
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||
|
||||
page.value = 1;
|
||||
currentTask.value = undefined;
|
||||
taskTotal.value = 0;
|
||||
lastSelectId.value = '';
|
||||
|
||||
if (resetFields) {
|
||||
formData.value = cloneDeep(defaultFormData);
|
||||
selectedUserList.value = [];
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const resp = await pageByTaskWait({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value = resp.rows.map((item) => ({ ...item, active: false }));
|
||||
taskTotal.value = resp.total;
|
||||
|
||||
loading.value = false;
|
||||
// 默认选中第一个
|
||||
if (taskList.value.length > 0) {
|
||||
const firstTask = taskList.value[0]!;
|
||||
currentTask.value = firstTask;
|
||||
handleCardClick(firstTask);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
|
||||
const handleScroll = debounce(async (e: Event) => {
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
// e.target.scrollTop 是元素顶部到当前可视区域顶部的距离,即已滚动的高度。
|
||||
// e.target.clientHeight 是元素的可视高度。
|
||||
// e.target.scrollHeight 是元素的总高度。
|
||||
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
|
||||
// 判断是否滚动到底部
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;
|
||||
|
||||
// 滚动到底部且没有加载完成
|
||||
if (isBottom && !isLoadComplete.value) {
|
||||
loading.value = true;
|
||||
page.value += 1;
|
||||
const resp = await pageByTaskWait({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value.push(
|
||||
...resp.rows.map((item) => ({ ...item, active: false })),
|
||||
);
|
||||
loading.value = false;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const lastSelectId = ref('');
|
||||
const currentTask = ref<TaskInfo>();
|
||||
async function handleCardClick(item: TaskInfo) {
|
||||
const { id } = item;
|
||||
// 点击的是同一个
|
||||
if (lastSelectId.value === id) {
|
||||
return;
|
||||
}
|
||||
currentTask.value = item;
|
||||
// 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
|
||||
taskList.value.forEach((item) => {
|
||||
item.active = item.id === id;
|
||||
});
|
||||
lastSelectId.value = id;
|
||||
}
|
||||
|
||||
const { refreshTab } = useTabs();
|
||||
|
||||
// 由于失去焦点浮层会消失 使用v-model选择人员完毕后强制显示
|
||||
const popoverOpen = ref(false);
|
||||
const selectedUserList = ref<User[]>([]);
|
||||
function handleFinish(userList: User[]) {
|
||||
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;
|
||||
tableApi.formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'category',
|
||||
componentProps: {
|
||||
fieldNames: { label: 'label', value: 'id' },
|
||||
showSearch: true,
|
||||
treeData: tree,
|
||||
treeDefaultExpandAll: true,
|
||||
treeLine: { showLeafIcon: false },
|
||||
// 筛选的字段
|
||||
treeNodeFilterProp: 'label',
|
||||
// 选中后显示在输入框的值
|
||||
treeNodeLabelProp: 'label',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID',
|
||||
},
|
||||
{
|
||||
field: 'businessTitle',
|
||||
title: '业务标题',
|
||||
formatter: ({ cellValue }) => cellValue ?? '-',
|
||||
},
|
||||
{
|
||||
field: 'flowName',
|
||||
title: '流程名称',
|
||||
},
|
||||
{
|
||||
field: 'categoryName',
|
||||
title: '流程分类',
|
||||
},
|
||||
{
|
||||
field: 'flowCode',
|
||||
title: '流程编码',
|
||||
},
|
||||
{
|
||||
field: 'nodeName',
|
||||
title: '当前任务',
|
||||
},
|
||||
{
|
||||
field: 'processedByName',
|
||||
title: '办理人',
|
||||
formatter: ({ cellValue }) => cellValue?.split?.(',') ?? cellValue,
|
||||
},
|
||||
{
|
||||
field: 'flowStatus',
|
||||
title: '流程状态',
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return renderDict(row.flowStatus, DictEnum.WF_BUSINESS_STATUS);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 转换数据
|
||||
if (isArray(formValues.createByIds)) {
|
||||
formValues.createByIds = (formValues.createByIds as Array<any>).map(
|
||||
(item) => item.userId,
|
||||
);
|
||||
}
|
||||
|
||||
return await pageByTaskWait({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isCurrent: true,
|
||||
},
|
||||
id: 'workflow-task-myself',
|
||||
rowClassName: 'cursor-pointer',
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents: {
|
||||
cellClick: ({ row }) => {
|
||||
handleOpen(row);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [ApprovalPanelDrawer, drawerApi] = useVbenDrawer({
|
||||
connectedComponent: ApprovalPanelDrawerComp,
|
||||
});
|
||||
|
||||
function handleOpen(row: any) {
|
||||
drawerApi.setData({ task: row, type: 'approve' }).open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-2">
|
||||
<div
|
||||
class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
|
||||
>
|
||||
<!-- 搜索条件 -->
|
||||
<div
|
||||
class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
|
||||
>
|
||||
<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 #content>
|
||||
<Form
|
||||
: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.id"
|
||||
:info="item"
|
||||
class="mx-2"
|
||||
@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="approve" @reload="refreshTab" />
|
||||
</div>
|
||||
<BasicTable table-title="我的待办" />
|
||||
<ApprovalPanelDrawer @reload="() => tableApi.query()" />
|
||||
</Page>
|
||||
</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