物业代码生成
This commit is contained in:
149
apps/web-antd/src/views/workflow/components/apply-modal.vue
Normal file
149
apps/web-antd/src/views/workflow/components/apply-modal.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<!-- 流程发起(启动)的弹窗 -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CompleteTaskReqData } from '#/api/workflow/task/model';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { completeTask, getTaskByTaskId } from '#/api/workflow/task';
|
||||
|
||||
import { CopyComponent } from '.';
|
||||
|
||||
interface Emits {
|
||||
/**
|
||||
* 完成
|
||||
*/
|
||||
complete: [];
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
interface ModalProps {
|
||||
taskId: string;
|
||||
taskVariables: Record<string, any>;
|
||||
variables?: any; // 这个干啥的
|
||||
}
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '流程发起',
|
||||
fullscreenButton: false,
|
||||
onConfirm: handleSubmit,
|
||||
async onOpenChange(isOpen) {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
const { taskId } = modalApi.getData() as ModalProps;
|
||||
|
||||
// 查询是否有按钮权限
|
||||
const resp = await getTaskByTaskId(taskId);
|
||||
const buttonPermissions: Record<string, boolean> = {};
|
||||
resp.buttonList.forEach((item) => {
|
||||
buttonPermissions[item.code] = item.show;
|
||||
});
|
||||
|
||||
// 是否具有抄送权限
|
||||
const copyPermission = buttonPermissions?.copy ?? false;
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'flowCopyList',
|
||||
dependencies: {
|
||||
if: copyPermission,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 默认占满两列
|
||||
formItemClass: 'col-span-2',
|
||||
// 默认label宽度 px
|
||||
labelWidth: 100,
|
||||
// 通用配置项 会影响到所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'messageType',
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '站内信', value: '1', disabled: true },
|
||||
{ label: '邮件', value: '2' },
|
||||
{ label: '短信', value: '3' },
|
||||
],
|
||||
},
|
||||
label: '通知方式',
|
||||
defaultValue: ['1'],
|
||||
},
|
||||
{
|
||||
fieldName: 'attachment',
|
||||
component: 'FileUpload',
|
||||
componentProps: {
|
||||
maxCount: 10,
|
||||
maxSize: 20,
|
||||
accept: 'png, jpg, jpeg, doc, docx, xlsx, xls, ppt, pdf',
|
||||
},
|
||||
defaultValue: [],
|
||||
label: '附件上传',
|
||||
formItemClass: 'items-start',
|
||||
},
|
||||
{
|
||||
fieldName: 'flowCopyList',
|
||||
component: 'Input',
|
||||
defaultValue: [],
|
||||
label: '抄送人',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
modalApi.modalLoading(true);
|
||||
const { messageType, flowCopyList, attachment } = cloneDeep(
|
||||
await formApi.getValues(),
|
||||
);
|
||||
const { taskId, taskVariables, variables } =
|
||||
modalApi.getData() as ModalProps;
|
||||
// 需要转换数据 抄送人员
|
||||
const flowCCList = (flowCopyList as Array<any>).map((item) => ({
|
||||
userId: item.userId,
|
||||
userName: item.nickName,
|
||||
}));
|
||||
const requestData = {
|
||||
fileId: attachment.join(','),
|
||||
messageType,
|
||||
flowCopyList: flowCCList,
|
||||
taskId,
|
||||
taskVariables,
|
||||
variables,
|
||||
} as CompleteTaskReqData;
|
||||
await completeTask(requestData);
|
||||
modalApi.close();
|
||||
emit('complete');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<BasicForm>
|
||||
<template #flowCopyList="slotProps">
|
||||
<CopyComponent v-model:user-list="slotProps.modelValue" />
|
||||
</template>
|
||||
</BasicForm>
|
||||
</BasicModal>
|
||||
</template>
|
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { VbenAvatar } from '@vben/common-ui';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
|
||||
import { Descriptions, DescriptionsItem, Tooltip } from 'ant-design-vue';
|
||||
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
import { getDiffTimeString } from './helper';
|
||||
|
||||
interface Props extends TaskInfo {
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{ info: Props; rowKey?: string }>(), {
|
||||
rowKey: 'id',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ click: [string] }>();
|
||||
|
||||
/**
|
||||
* TODO: 这里要优化 事件没有用到
|
||||
*/
|
||||
function handleClick() {
|
||||
const idKey = props.rowKey as keyof TaskInfo;
|
||||
emit('click', props.info[idKey]);
|
||||
}
|
||||
|
||||
const diffUpdateTimeString = computed(() => {
|
||||
return getDiffTimeString(props.info.updateTime);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'border-primary': info.active,
|
||||
}"
|
||||
class="cursor-pointer rounded-lg border-[1px] border-solid p-3 transition-shadow duration-300 ease-in-out hover:shadow-lg"
|
||||
@click.stop="handleClick"
|
||||
>
|
||||
<Descriptions :column="1" :title="info.flowName" size="middle">
|
||||
<template #extra>
|
||||
<component
|
||||
:is="renderDict(info.flowStatus, DictEnum.WF_BUSINESS_STATUS)"
|
||||
/>
|
||||
</template>
|
||||
<DescriptionsItem label="当前任务">
|
||||
<div class="font-bold">{{ info.nodeName }}</div>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="提交时间">
|
||||
{{ info.createTime }}
|
||||
</DescriptionsItem>
|
||||
<!-- <DescriptionsItem label="更新时间">
|
||||
{{ info.updateTime }}
|
||||
</DescriptionsItem> -->
|
||||
</Descriptions>
|
||||
<div class="flex w-full items-center justify-between text-[14px]">
|
||||
<div class="flex items-center gap-1 overflow-hidden whitespace-nowrap">
|
||||
<VbenAvatar
|
||||
:alt="info.createByName"
|
||||
class="bg-primary size-[24px] rounded-full text-[10px] text-white"
|
||||
src=""
|
||||
/>
|
||||
<span class="overflow-hidden text-ellipsis opacity-50">
|
||||
{{ info.createByName }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-nowrap opacity-50">
|
||||
<Tooltip placement="top" :title="`更新时间: ${info.updateTime}`">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="icon-[mdi--clock-outline] size-[16px]"></span>
|
||||
<span>{{ diffUpdateTimeString }}前更新</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.ant-descriptions .ant-descriptions-header) {
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
|
||||
:deep(.ant-descriptions-item) {
|
||||
padding-bottom: 8px !important;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,26 @@
|
||||
<!-- 审批终止 Modal弹窗的content属性专用 用于填写审批意见 -->
|
||||
<script setup lang="ts">
|
||||
import { Textarea } from 'ant-design-vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ApprovalContent',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
defineProps<{ description: string; value: string }>();
|
||||
|
||||
defineEmits<{ 'update:value': [string] }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div>{{ description }}</div>
|
||||
<Textarea
|
||||
:allow-clear="true"
|
||||
:auto-size="true"
|
||||
:value="value"
|
||||
placeholder="审批意见(可选)"
|
||||
@change="(e) => $emit('update:value', e.target.value!)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,41 @@
|
||||
<!--
|
||||
审批详情
|
||||
约定${task.formPath}/frame 为内嵌表单 用于展示 需要在本地路由添加
|
||||
apps/web-antd/src/router/routes/workflow-iframe.ts
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FlowInfoResponse } from '#/api/workflow/instance/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { Divider, Skeleton } from 'ant-design-vue';
|
||||
|
||||
import { ApprovalTimeline } from '.';
|
||||
|
||||
defineOptions({
|
||||
name: 'ApprovalDetails',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
defineProps<{
|
||||
currentFlowInfo: FlowInfoResponse;
|
||||
iframeHeight: number;
|
||||
iframeLoaded: boolean;
|
||||
task: TaskInfo;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 约定${task.formPath}/frame 为内嵌表单 用于展示 需要在本地路由添加 -->
|
||||
<iframe
|
||||
v-show="iframeLoaded"
|
||||
:src="`${task.formPath}/iframe?readonly=true&id=${task.businessId}`"
|
||||
:style="{ height: `${iframeHeight}px` }"
|
||||
class="w-full"
|
||||
></iframe>
|
||||
<Skeleton v-show="!iframeLoaded" :paragraph="{ rows: 6 }" active />
|
||||
<Divider />
|
||||
<ApprovalTimeline :list="currentFlowInfo.list" />
|
||||
</div>
|
||||
</template>
|
227
apps/web-antd/src/views/workflow/components/approval-modal.vue
Normal file
227
apps/web-antd/src/views/workflow/components/approval-modal.vue
Normal file
@@ -0,0 +1,227 @@
|
||||
<!-- 审批同意的弹窗 -->
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/system/user/model';
|
||||
import type {
|
||||
CompleteTaskReqData,
|
||||
NextNodeInfo,
|
||||
} from '#/api/workflow/task/model';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import { omit } from 'lodash-es';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { completeTask, getNextNodeList } from '#/api/workflow/task';
|
||||
|
||||
import { CopyComponent } from '.';
|
||||
|
||||
const emit = defineEmits<{ complete: [] }>();
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 默认占满两列
|
||||
formItemClass: 'col-span-2',
|
||||
// 默认label宽度 px
|
||||
labelWidth: 100,
|
||||
// 通用配置项 会影响到所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'taskId',
|
||||
component: 'Input',
|
||||
label: '任务ID',
|
||||
dependencies: {
|
||||
show: false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'messageType',
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '站内信', value: '1', disabled: true },
|
||||
{ label: '邮件', value: '2' },
|
||||
{ label: '短信', value: '3' },
|
||||
],
|
||||
},
|
||||
label: '通知方式',
|
||||
defaultValue: ['1'],
|
||||
},
|
||||
{
|
||||
fieldName: 'attachment',
|
||||
component: 'FileUpload',
|
||||
componentProps: {
|
||||
maxCount: 10,
|
||||
maxSize: 20,
|
||||
accept: 'png, jpg, jpeg, doc, docx, xlsx, xls, ppt, pdf',
|
||||
},
|
||||
defaultValue: [],
|
||||
label: '附件上传',
|
||||
formItemClass: 'items-start',
|
||||
},
|
||||
{
|
||||
fieldName: 'flowCopyList',
|
||||
component: 'Input',
|
||||
defaultValue: [],
|
||||
label: '抄送人',
|
||||
},
|
||||
{
|
||||
fieldName: 'assigneeMap',
|
||||
component: 'Input',
|
||||
label: '下一步审批人',
|
||||
},
|
||||
{
|
||||
fieldName: 'message',
|
||||
component: 'Textarea',
|
||||
label: '审批意见',
|
||||
formItemClass: 'items-start',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
interface ModalProps {
|
||||
taskId: string;
|
||||
// 是否具有抄送权限
|
||||
copyPermission: boolean;
|
||||
// 是有具有选人权限
|
||||
assignPermission: boolean;
|
||||
}
|
||||
|
||||
// 自定义添加选人属性 给组件v-for绑定
|
||||
const nextNodeInfo = ref<(NextNodeInfo & { selectUserList: User[] })[]>([]);
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '审批通过',
|
||||
fullscreenButton: false,
|
||||
class: 'min-h-[365px]',
|
||||
onConfirm: handleSubmit,
|
||||
async onOpenChange(isOpen) {
|
||||
if (!isOpen) {
|
||||
await formApi.resetForm();
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
const { taskId, copyPermission, assignPermission } =
|
||||
modalApi.getData() as ModalProps;
|
||||
// 是否显示抄送选择
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'flowCopyList',
|
||||
dependencies: {
|
||||
if: copyPermission,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'assigneeMap',
|
||||
dependencies: {
|
||||
if: assignPermission,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 获取下一节点名称
|
||||
if (assignPermission) {
|
||||
const resp = await getNextNodeList({ taskId });
|
||||
nextNodeInfo.value = resp.map((item) => ({
|
||||
...item,
|
||||
// 用于给组件绑定
|
||||
selectUserList: [],
|
||||
}));
|
||||
}
|
||||
|
||||
await formApi.setFieldValue('taskId', taskId);
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
modalApi.modalLoading(true);
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
// 需要转换数据 抄送人员
|
||||
const flowCopyList = (data.flowCopyList as Array<any>).map((item) => ({
|
||||
userId: item.userId,
|
||||
userName: item.nickName,
|
||||
}));
|
||||
const requestData = {
|
||||
...omit(data, ['attachment']),
|
||||
fileId: data.attachment.join(','),
|
||||
taskVariables: {},
|
||||
variables: {},
|
||||
flowCopyList,
|
||||
} as CompleteTaskReqData;
|
||||
|
||||
// 选人
|
||||
if (modalApi.getData()?.assignPermission) {
|
||||
// 判断是否选中
|
||||
for (const item of nextNodeInfo.value) {
|
||||
if (item.selectUserList.length === 0) {
|
||||
message.warn(`未选择节点[${item.nodeName}]审批人`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const assigneeMap: { [key: string]: string } = {};
|
||||
nextNodeInfo.value.forEach((item) => {
|
||||
assigneeMap[item.nodeCode] = item.selectUserList
|
||||
.map((u) => u.userId)
|
||||
.join(',');
|
||||
});
|
||||
requestData.assigneeMap = assigneeMap;
|
||||
}
|
||||
|
||||
await completeTask(requestData);
|
||||
modalApi.close();
|
||||
emit('complete');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<BasicForm>
|
||||
<template #flowCopyList="slotProps">
|
||||
<CopyComponent v-model:user-list="slotProps.modelValue" />
|
||||
</template>
|
||||
<template #assigneeMap>
|
||||
<div
|
||||
v-for="item in nextNodeInfo"
|
||||
:key="item.nodeCode"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<template v-if="item.permissionFlag">
|
||||
<span class="opacity-70">{{ item.nodeName }}</span>
|
||||
<CopyComponent
|
||||
:allow-user-ids="item.permissionFlag"
|
||||
v-model:user-list="item.selectUserList"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="text-red-500">没有权限, 请联系管理员</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</BasicForm>
|
||||
</BasicModal>
|
||||
</template>
|
556
apps/web-antd/src/views/workflow/components/approval-panel.vue
Normal file
556
apps/web-antd/src/views/workflow/components/approval-panel.vue
Normal file
@@ -0,0 +1,556 @@
|
||||
<!-- 该文件需要重构 但我没空 -->
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/core/user';
|
||||
import type { FlowInfoResponse } from '#/api/workflow/instance/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Fallback, useVbenModal, VbenAvatar } from '@vben/common-ui';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
import { getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { CopyOutlined } from '@ant-design/icons-vue';
|
||||
import { useClipboard, useEventListener } from '@vueuse/core';
|
||||
import {
|
||||
Card,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Menu,
|
||||
MenuItem,
|
||||
message,
|
||||
Modal,
|
||||
Space,
|
||||
TabPane,
|
||||
Tabs,
|
||||
} from 'ant-design-vue';
|
||||
import { isObject } from 'lodash-es';
|
||||
|
||||
import {
|
||||
cancelProcessApply,
|
||||
deleteByInstanceIds,
|
||||
flowInfo,
|
||||
} from '#/api/workflow/instance';
|
||||
import {
|
||||
getTaskByTaskId,
|
||||
taskOperation,
|
||||
terminationTask,
|
||||
updateAssignee,
|
||||
} from '#/api/workflow/task';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
import { approvalModal, approvalRejectionModal, flowInterfereModal } from '.';
|
||||
import ApprovalDetails from './approval-details.vue';
|
||||
import FlowPreview from './flow-preview.vue';
|
||||
import { approveWithReasonModal } from './helper';
|
||||
import userSelectModal from './user-select-modal.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ApprovalPanel',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
const props = defineProps<{ task?: TaskInfo; type: ApprovalType }>();
|
||||
|
||||
/**
|
||||
* 下面按钮点击后会触发的事件
|
||||
*/
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
const currentTask = ref<TaskInfo>();
|
||||
/**
|
||||
* 是否显示 加签/减签操作
|
||||
*/
|
||||
const showMultiActions = computed(() => {
|
||||
if (!currentTask.value) {
|
||||
return false;
|
||||
}
|
||||
if (Number(currentTask.value.nodeRatio) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
/**
|
||||
* 按钮权限
|
||||
*/
|
||||
const buttonPermissions = computed(() => {
|
||||
const record: Record<string, boolean> = {};
|
||||
if (!currentTask.value) {
|
||||
return record;
|
||||
}
|
||||
currentTask.value.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],
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* myself 我发起的
|
||||
* readonly 只读 只用于查看
|
||||
* approve 审批
|
||||
* admin 流程监控 - 待办任务使用
|
||||
*/
|
||||
type ApprovalType = 'admin' | 'approve' | 'myself' | 'readonly';
|
||||
const showFooter = computed(() => {
|
||||
if (props.type === 'readonly') {
|
||||
return false;
|
||||
}
|
||||
// 我发起的 && [已完成, 已作废] 不显示
|
||||
if (
|
||||
props.type === 'myself' &&
|
||||
['finish', 'invalid'].includes(props.task?.flowStatus ?? '')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const currentFlowInfo = ref<FlowInfoResponse>();
|
||||
/**
|
||||
* card的loading状态
|
||||
*/
|
||||
const loading = ref(false);
|
||||
const iframeLoaded = ref(false);
|
||||
const iframeHeight = ref(300);
|
||||
useEventListener('message', (event) => {
|
||||
const data = event.data as { [key: string]: any; type: string };
|
||||
if (!isObject(data)) return;
|
||||
/**
|
||||
* iframe通信 加载完毕后才显示表单 解决卡顿问题
|
||||
*/
|
||||
if (data.type === 'mounted') {
|
||||
iframeLoaded.value = true;
|
||||
}
|
||||
/**
|
||||
* 高度与表单高度保持一致
|
||||
*/
|
||||
if (data.type === 'height') {
|
||||
const height = data.height;
|
||||
iframeHeight.value = height;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleLoadInfo(task: TaskInfo | undefined) {
|
||||
try {
|
||||
if (!task) return null;
|
||||
loading.value = true;
|
||||
iframeLoaded.value = false;
|
||||
const resp = await flowInfo(task.businessId);
|
||||
currentFlowInfo.value = resp;
|
||||
|
||||
const taskResp = await getTaskByTaskId(props.task!.id);
|
||||
currentTask.value = taskResp;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.task, handleLoadInfo);
|
||||
|
||||
onUnmounted(() => (currentFlowInfo.value = undefined));
|
||||
|
||||
// 进行中 可以撤销
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: 1提取公共函数 2原版是可以填写意见的(message参数)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 委托
|
||||
*/
|
||||
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');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 不加legacy在本地开发没有问题
|
||||
* 打包后在一些设备会无法复制 使用legacy来保证兼容性
|
||||
*/
|
||||
const { copy } = useClipboard({ legacy: true });
|
||||
async function handleCopy(text: string) {
|
||||
await copy(text);
|
||||
message.success('复制成功');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card
|
||||
v-if="task"
|
||||
:body-style="{ overflowY: 'auto', height: '100%' }"
|
||||
:loading="loading"
|
||||
class="thin-scrollbar flex-1 overflow-y-hidden"
|
||||
size="small"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2">
|
||||
<div>编号: {{ task.id }}</div>
|
||||
<CopyOutlined class="cursor-pointer" @click="handleCopy(task.id)" />
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-button size="small" @click="() => handleLoadInfo(task)">
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="icon-[material-symbols--refresh] size-24px"></span>
|
||||
</div>
|
||||
</a-button>
|
||||
</template>
|
||||
<div 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">{{ task.flowName }}</div>
|
||||
<div>
|
||||
<component
|
||||
:is="renderDict(task.flowStatus, DictEnum.WF_BUSINESS_STATUS)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<VbenAvatar
|
||||
:alt="task.createByName"
|
||||
class="bg-primary size-[28px] rounded-full text-white"
|
||||
src=""
|
||||
/>
|
||||
<span>{{ task.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>
|
||||
流程分类: {{ task.categoryName }}
|
||||
</div>
|
||||
<Divider type="vertical" />
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="icon-[mdi--clock-outline] size-[16px]"></span>
|
||||
提交时间: {{ task.createTime }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs v-if="currentFlowInfo" class="flex-1">
|
||||
<TabPane key="1" tab="审批详情">
|
||||
<ApprovalDetails
|
||||
:current-flow-info="currentFlowInfo"
|
||||
:iframe-loaded="iframeLoaded"
|
||||
:iframe-height="iframeHeight"
|
||||
:task="task"
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane key="2" tab="审批流程图">
|
||||
<FlowPreview :instance-id="currentFlowInfo.instanceId" />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
<!-- 固定底部 -->
|
||||
<div class="h-[57px]"></div>
|
||||
<div
|
||||
v-if="showFooter"
|
||||
class="border-t-solid bg-background absolute bottom-0 left-0 w-full border-t-[1px] p-3"
|
||||
>
|
||||
<div class="flex justify-end">
|
||||
<Space v-if="type === 'myself'">
|
||||
<a-button
|
||||
v-if="revocable"
|
||||
danger
|
||||
type="primary"
|
||||
@click="handleCancel"
|
||||
>
|
||||
撤销申请
|
||||
</a-button>
|
||||
<a-button v-if="editableAndRemoveable" @click="handleEdit">
|
||||
重新编辑
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="editableAndRemoveable"
|
||||
danger
|
||||
type="primary"
|
||||
@click="handleRemove"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</Space>
|
||||
<Space v-if="type === 'approve'">
|
||||
<a-button type="primary" @click="handleApproval">通过</a-button>
|
||||
<a-button
|
||||
v-if="buttonPermissions?.termination"
|
||||
danger
|
||||
type="primary"
|
||||
@click="handleTermination"
|
||||
>
|
||||
终止
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="buttonPermissions?.back"
|
||||
danger
|
||||
type="primary"
|
||||
@click="handleRejection"
|
||||
>
|
||||
驳回
|
||||
</a-button>
|
||||
<Dropdown
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<template #overlay>
|
||||
<Menu>
|
||||
<MenuItem
|
||||
v-if="buttonPermissions?.trust"
|
||||
key="1"
|
||||
@click="() => delegationModalApi.open()"
|
||||
>
|
||||
委托
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="buttonPermissions?.transfer"
|
||||
key="2"
|
||||
@click="() => transferModalApi.open()"
|
||||
>
|
||||
转办
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="showMultiActions && buttonPermissions?.addSign"
|
||||
key="3"
|
||||
@click="() => addSignatureModalApi.open()"
|
||||
>
|
||||
加签
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="showMultiActions && buttonPermissions?.subSign"
|
||||
key="4"
|
||||
@click="() => reductionSignatureModalApi.open()"
|
||||
>
|
||||
减签
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</template>
|
||||
<a-button v-if="showButtonOther"> 其他 </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>
|
||||
</Card>
|
||||
<Fallback v-else title="点击左侧选择" />
|
||||
</template>
|
@@ -0,0 +1,146 @@
|
||||
<!-- 审批驳回窗口 -->
|
||||
<script setup lang="ts">
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { cloneDeep, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { backProcess, getBackTaskNode } from '#/api/workflow/task';
|
||||
|
||||
const emit = defineEmits<{ complete: [] }>();
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 默认占满两列
|
||||
formItemClass: 'col-span-2',
|
||||
// 默认label宽度 px
|
||||
labelWidth: 100,
|
||||
// 通用配置项 会影响到所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'taskId',
|
||||
component: 'Input',
|
||||
label: '任务ID',
|
||||
dependencies: {
|
||||
show: false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'messageType',
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '站内信', value: '1', disabled: true },
|
||||
{ label: '邮件', value: '2' },
|
||||
{ label: '短信', value: '3' },
|
||||
],
|
||||
},
|
||||
label: '通知方式',
|
||||
defaultValue: ['1'],
|
||||
},
|
||||
{
|
||||
fieldName: 'nodeCode',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
getPopupContainer,
|
||||
},
|
||||
label: '驳回节点',
|
||||
},
|
||||
{
|
||||
fieldName: 'attachment',
|
||||
component: 'FileUpload',
|
||||
componentProps: {
|
||||
maxCount: 10,
|
||||
maxSize: 20,
|
||||
accept: 'png, jpg, jpeg, doc, docx, xlsx, xls, ppt, pdf',
|
||||
},
|
||||
defaultValue: [],
|
||||
label: '附件上传',
|
||||
formItemClass: 'items-start',
|
||||
},
|
||||
{
|
||||
fieldName: 'message',
|
||||
component: 'Textarea',
|
||||
label: '审批意见',
|
||||
formItemClass: 'items-start',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
interface ModalProps {
|
||||
taskId: string;
|
||||
definitionId: string;
|
||||
nodeCode: string;
|
||||
}
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '审批驳回',
|
||||
fullscreenButton: false,
|
||||
class: 'min-h-[365px]',
|
||||
onConfirm: handleSubmit,
|
||||
async onOpenChange(isOpen) {
|
||||
if (!isOpen) {
|
||||
await formApi.resetForm();
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
const { taskId, definitionId, nodeCode } = modalApi.getData() as ModalProps;
|
||||
await formApi.setFieldValue('taskId', taskId);
|
||||
|
||||
const resp = await getBackTaskNode(definitionId, nodeCode);
|
||||
const options = resp.map((item) => ({
|
||||
label: item.nodeName,
|
||||
value: item.nodeCode,
|
||||
}));
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'nodeCode',
|
||||
componentProps: {
|
||||
options,
|
||||
},
|
||||
},
|
||||
]);
|
||||
// 默认选中第一个节点
|
||||
if (options.length > 0) {
|
||||
formApi.setFieldValue('nodeCode', options[0]?.value);
|
||||
}
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
modalApi.modalLoading(true);
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
// 附件join
|
||||
data.fileId = data.attachment?.join?.(',');
|
||||
// 取消attachment参数的传递
|
||||
data.attachment = undefined;
|
||||
await backProcess(data);
|
||||
modalApi.close();
|
||||
emit('complete');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<BasicForm />
|
||||
</BasicModal>
|
||||
</template>
|
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import type { Flow } from '#/api/workflow/instance/model';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { VbenAvatar } from '@vben/common-ui';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
|
||||
import { TimelineItem } from 'ant-design-vue';
|
||||
|
||||
import { ossInfo } from '#/api/system/oss';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
defineOptions({
|
||||
name: 'ApprovalTimelineItem',
|
||||
});
|
||||
|
||||
const props = defineProps<{ item: Flow }>();
|
||||
|
||||
interface AttachmentInfo {
|
||||
ossId: string;
|
||||
url: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理附件信息
|
||||
*/
|
||||
const attachmentInfo = ref<AttachmentInfo[]>([]);
|
||||
onMounted(async () => {
|
||||
if (!props.item.ext) {
|
||||
return null;
|
||||
}
|
||||
const resp = await ossInfo(props.item.ext.split(','));
|
||||
attachmentInfo.value = resp.map((item) => ({
|
||||
ossId: item.ossId,
|
||||
url: item.url,
|
||||
name: item.originalName,
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TimelineItem>
|
||||
<template #dot>
|
||||
<div class="relative rounded-full border">
|
||||
<VbenAvatar
|
||||
:alt="item.approveName"
|
||||
class="bg-primary size-[36px] rounded-full text-white"
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="ml-2 flex flex-col gap-0.5">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="font-bold">{{ item.nodeName }}</div>
|
||||
<component :is="renderDict(item.flowStatus, DictEnum.WF_TASK_STATUS)" />
|
||||
</div>
|
||||
<div>{{ item.approveName }}</div>
|
||||
<div>{{ item.updateTime }}</div>
|
||||
<div v-if="item.message" class="rounded-lg border p-1">
|
||||
<div class="break-all opacity-70">{{ item.message }}</div>
|
||||
</div>
|
||||
<div v-if="attachmentInfo.length > 0" class="flex flex-wrap gap-2">
|
||||
<!-- 这里下载的文件名不是原始文件名 -->
|
||||
<a
|
||||
v-for="attachment in attachmentInfo"
|
||||
:key="attachment.ossId"
|
||||
:href="attachment.url"
|
||||
class="text-primary"
|
||||
target="_blank"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="icon-[mingcute--attachment-line] size-[18px]"></span>
|
||||
<span>{{ attachment.name }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</TimelineItem>
|
||||
</template>
|
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { Flow } from '#/api/workflow/instance/model';
|
||||
|
||||
import { Timeline } from 'ant-design-vue';
|
||||
|
||||
import ApprovalTimelineItem from './approval-timeline-item.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
list: Flow[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Timeline v-if="props.list.length > 0">
|
||||
<ApprovalTimelineItem
|
||||
v-for="item in props.list"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
/>
|
||||
</Timeline>
|
||||
</template>
|
@@ -0,0 +1,98 @@
|
||||
<!--抄送组件-->
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { User } from '#/api/system/user/model';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useVbenModal, VbenAvatar } from '@vben/common-ui';
|
||||
|
||||
import { Avatar, AvatarGroup, Tooltip } from 'ant-design-vue';
|
||||
|
||||
import { userSelectModal } from '.';
|
||||
|
||||
defineOptions({
|
||||
name: 'CopyComponent',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{ allowUserIds?: string; ellipseNumber?: number }>(),
|
||||
{
|
||||
/**
|
||||
* 最大显示的头像数量 超过显示为省略号头像
|
||||
*/
|
||||
ellipseNumber: 3,
|
||||
/**
|
||||
* 允许选择允许选择的人员ID 会当做参数拼接在uselist接口
|
||||
*/
|
||||
allowUserIds: '',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{ cancel: []; finish: [User[]] }>();
|
||||
|
||||
const [UserSelectModal, modalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
|
||||
const userListModel = defineModel('userList', {
|
||||
type: Array as PropType<User[]>,
|
||||
default: () => [],
|
||||
});
|
||||
|
||||
function handleOpen() {
|
||||
modalApi.setData({ userList: userListModel.value });
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
function handleFinish(userList: User[]) {
|
||||
// 清空 直接赋值[]会丢失响应性
|
||||
userListModel.value.splice(0, userListModel.value.length);
|
||||
userListModel.value.push(...userList);
|
||||
emit('finish', userList);
|
||||
}
|
||||
|
||||
const displayedList = computed(() => {
|
||||
return userListModel.value.slice(0, props.ellipseNumber);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<AvatarGroup v-if="userListModel.length > 0">
|
||||
<Tooltip
|
||||
v-for="user in displayedList"
|
||||
:key="user.userId"
|
||||
:title="user.nickName"
|
||||
placement="top"
|
||||
>
|
||||
<div>
|
||||
<VbenAvatar
|
||||
:alt="user.nickName"
|
||||
class="bg-primary size-[36px] cursor-pointer rounded-full border text-white"
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
:title="`等${userListModel.length - props.ellipseNumber}人`"
|
||||
placement="top"
|
||||
>
|
||||
<Avatar
|
||||
v-if="userListModel.length > ellipseNumber"
|
||||
class="flex size-[36px] cursor-pointer items-center justify-center rounded-full border bg-[gray] text-white"
|
||||
>
|
||||
+{{ userListModel.length - props.ellipseNumber }}
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
</AvatarGroup>
|
||||
<a-button size="small" @click="handleOpen">选择人员</a-button>
|
||||
<UserSelectModal
|
||||
:allow-user-ids="allowUserIds"
|
||||
@cancel="$emit('cancel')"
|
||||
@finish="handleFinish"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { useAppConfig, useTabs } from '@vben/hooks';
|
||||
import { stringify } from '@vben/request';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
|
||||
defineOptions({ name: 'FlowDesigner' });
|
||||
|
||||
const route = useRoute();
|
||||
const definitionId = route.query.definitionId as string;
|
||||
const disabled = route.query.disabled === 'true';
|
||||
|
||||
const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const params = {
|
||||
Authorization: `Bearer ${accessStore.accessToken}`,
|
||||
id: definitionId,
|
||||
clientid: clientId,
|
||||
disabled,
|
||||
};
|
||||
|
||||
/**
|
||||
* iframe设计器的地址
|
||||
*/
|
||||
const url = `${import.meta.env.VITE_GLOB_API_URL}/warm-flow-ui/index.html?${stringify(params)}`;
|
||||
|
||||
const { closeCurrentTab } = useTabs();
|
||||
const router = useRouter();
|
||||
|
||||
function messageHandler(event: MessageEvent) {
|
||||
switch (event.data.method) {
|
||||
case 'close': {
|
||||
// 关闭当前tab
|
||||
closeCurrentTab();
|
||||
// 跳转到流程定义列表
|
||||
router.push('/workflow/processDefinition');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// iframe监听组件内设计器保存事件
|
||||
useEventListener('message', messageHandler);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<iframe :src="url" class="size-full"></iframe>
|
||||
</template>
|
@@ -0,0 +1,38 @@
|
||||
<!-- 弹窗查看流程信息 -->
|
||||
<script setup lang="ts">
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { getTaskByBusinessId } from '#/api/workflow/instance';
|
||||
|
||||
import { ApprovalPanel } from '.';
|
||||
|
||||
interface ModalProps {
|
||||
businessId: string;
|
||||
}
|
||||
|
||||
const taskInfo = ref<TaskInfo>();
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '流程信息',
|
||||
class: 'w-[1000px]',
|
||||
footer: false,
|
||||
onOpenChange: async (isOpen) => {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
const { businessId } = modalApi.getData() as ModalProps;
|
||||
const taskResp = await getTaskByBusinessId(businessId);
|
||||
taskInfo.value = taskResp;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<ApprovalPanel :task="taskInfo" type="readonly" />
|
||||
</BasicModal>
|
||||
</template>
|
@@ -0,0 +1,171 @@
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/system/user/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Descriptions, DescriptionsItem, Modal } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
getTaskByTaskId,
|
||||
taskOperation,
|
||||
terminationTask,
|
||||
} from '#/api/workflow/task';
|
||||
|
||||
import { userSelectModal } from '.';
|
||||
|
||||
const emit = defineEmits<{ complete: [] }>();
|
||||
|
||||
const taskInfo = ref<TaskInfo>();
|
||||
|
||||
/**
|
||||
* 是否显示 加签/减签操作
|
||||
*/
|
||||
const showMultiActions = computed(() => {
|
||||
if (!taskInfo.value) {
|
||||
return false;
|
||||
}
|
||||
if (Number(taskInfo.value.nodeRatio) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '流程干预',
|
||||
class: 'w-[800px]',
|
||||
fullscreenButton: false,
|
||||
async onOpenChange(isOpen) {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
const { taskId } = modalApi.getData() as { taskId: string };
|
||||
taskInfo.value = await getTaskByTaskId(taskId);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 转办
|
||||
*/
|
||||
const [TransferModal, transferModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleTransfer(userList: User[]) {
|
||||
if (userList.length === 0 || !taskInfo.value) return;
|
||||
const current = userList[0];
|
||||
Modal.confirm({
|
||||
title: '转办',
|
||||
content: `确定转办给${current?.nickName}吗?`,
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation(
|
||||
{ taskId: taskInfo.value!.id, userId: current!.userId },
|
||||
'transferTask',
|
||||
);
|
||||
emit('complete');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批终止
|
||||
*/
|
||||
function handleTermination() {
|
||||
if (!taskInfo.value) {
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '审批终止',
|
||||
content: '确定终止当前审批流程吗?',
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await terminationTask({ taskId: taskInfo.value!.id });
|
||||
emit('complete');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [AddSignatureModal, addSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleAddSignature(userList: User[]) {
|
||||
if (userList.length === 0 || !taskInfo.value) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认加签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation(
|
||||
{ taskId: taskInfo.value!.id, userIds },
|
||||
'addSignature',
|
||||
);
|
||||
emit('complete');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [ReductionSignatureModal, reductionSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleReductionSignature(userList: User[]) {
|
||||
if (userList.length === 0 || !taskInfo.value) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认减签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation(
|
||||
{ taskId: taskInfo.value!.id, userIds },
|
||||
'reductionSignature',
|
||||
);
|
||||
emit('complete');
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<Descriptions v-if="taskInfo" :column="2" bordered size="small">
|
||||
<DescriptionsItem label="任务名称">
|
||||
{{ taskInfo.nodeName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="节点编码">
|
||||
{{ taskInfo.nodeCode }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="开始时间">
|
||||
{{ taskInfo.createTime }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="流程实例ID">
|
||||
{{ taskInfo.instanceId }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="版本号">
|
||||
{{ taskInfo.version }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="业务ID">
|
||||
{{ taskInfo.businessId }}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
<TransferModal mode="single" @finish="handleTransfer" />
|
||||
<AddSignatureModal mode="multiple" @finish="handleAddSignature" />
|
||||
<ReductionSignatureModal
|
||||
mode="multiple"
|
||||
@finish="handleReductionSignature"
|
||||
/>
|
||||
<template #footer>
|
||||
<template v-if="showMultiActions">
|
||||
<a-button @click="() => addSignatureModalApi.open()">加签</a-button>
|
||||
<a-button @click="() => reductionSignatureModalApi.open()">
|
||||
减签
|
||||
</a-button>
|
||||
</template>
|
||||
<a-button @click="() => transferModalApi.open()">转办</a-button>
|
||||
<a-button danger type="primary" @click="handleTermination">终止</a-button>
|
||||
</template>
|
||||
</BasicModal>
|
||||
</template>
|
28
apps/web-antd/src/views/workflow/components/flow-preview.vue
Normal file
28
apps/web-antd/src/views/workflow/components/flow-preview.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { stringify } from '@vben/request';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
defineOptions({ name: 'FlowPreview' });
|
||||
|
||||
const props = defineProps<{ instanceId: string }>();
|
||||
|
||||
const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const params = {
|
||||
Authorization: `Bearer ${accessStore.accessToken}`,
|
||||
id: props.instanceId,
|
||||
clientid: clientId,
|
||||
type: 'FlowChart',
|
||||
};
|
||||
|
||||
/**
|
||||
* iframe地址
|
||||
*/
|
||||
const url = `${import.meta.env.VITE_GLOB_API_URL}/warm-flow-ui/index.html?${stringify(params)}`;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<iframe :src="url" class="h-[500px] w-full border"></iframe>
|
||||
</template>
|
60
apps/web-antd/src/views/workflow/components/helper.tsx
Normal file
60
apps/web-antd/src/views/workflow/components/helper.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { defineComponent, h, ref } from 'vue';
|
||||
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
import ApprovalContent from './approval-content.vue';
|
||||
|
||||
export interface ApproveWithReasonModalProps {
|
||||
title: string;
|
||||
description: string;
|
||||
onOk: (reason: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 带审批意见的confirm
|
||||
* @param props props
|
||||
*/
|
||||
export function approveWithReasonModal(props: ApproveWithReasonModalProps) {
|
||||
const { onOk, title, description } = props;
|
||||
const content = ref('');
|
||||
Modal.confirm({
|
||||
title,
|
||||
content: h(
|
||||
defineComponent({
|
||||
setup() {
|
||||
return () =>
|
||||
h(ApprovalContent, {
|
||||
description,
|
||||
value: content.value,
|
||||
'onUpdate:value': (v) => (content.value = v),
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => onOk(content.value),
|
||||
});
|
||||
}
|
||||
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(relativeTime);
|
||||
/**
|
||||
* 计算相差的时间
|
||||
* @param dateTime 时间字符串
|
||||
* @returns 相差的时间
|
||||
*/
|
||||
export function getDiffTimeString(dateTime: string) {
|
||||
// 计算相差秒数
|
||||
const diffSeconds = dayjs().diff(dayjs(dateTime), 'second');
|
||||
/**
|
||||
* 转为时间显示(x月 x天)
|
||||
* https://dayjs.fenxianglu.cn/category/duration.html#%E4%BA%BA%E6%80%A7%E5%8C%96
|
||||
*
|
||||
*/
|
||||
const diffText = dayjs.duration(diffSeconds, 'seconds').humanize();
|
||||
return diffText;
|
||||
}
|
30
apps/web-antd/src/views/workflow/components/index.ts
Normal file
30
apps/web-antd/src/views/workflow/components/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export { default as applyModal } from './apply-modal.vue';
|
||||
export { default as ApprovalCard } from './approval-card.vue';
|
||||
/**
|
||||
* 审批同意
|
||||
*/
|
||||
export { default as approvalModal } from './approval-modal.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
|
||||
*/
|
||||
export { default as flowInterfereModal } from './flow-interfere-modal.vue';
|
||||
|
||||
/**
|
||||
* 选人 支持单选/多选
|
||||
*/
|
||||
export { default as userSelectModal } from './user-select-modal.vue';
|
@@ -0,0 +1,378 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { User } from '#/api';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal, VbenAvatar } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { userList } from '#/api/system/user';
|
||||
import DeptTree from '#/views/system/user/dept-tree.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'UserSelectModal',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{ allowUserIds?: string; mode?: 'multiple' | 'single' }>(),
|
||||
{
|
||||
mode: 'multiple',
|
||||
/**
|
||||
* 允许选择允许选择的人员ID 会当做参数拼接在uselist接口
|
||||
*/
|
||||
allowUserIds: '',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* 取消的事件
|
||||
*/
|
||||
cancel: [];
|
||||
/**
|
||||
* 选择完成的事件
|
||||
*/
|
||||
finish: [User[]];
|
||||
}>();
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '选择人员',
|
||||
class: 'w-[1060px]',
|
||||
fullscreenButton: false,
|
||||
onClosed: () => emit('cancel'),
|
||||
onConfirm: handleSubmit,
|
||||
async onOpened() {
|
||||
const { userList = [] } = modalApi.getData() as { userList: User[] };
|
||||
// 暂时只处理多选 目前并没有单选的情况
|
||||
if (props.mode === 'multiple') {
|
||||
// 左边选中
|
||||
await tableApi.grid.setCheckboxRow(userList, true);
|
||||
// 右边赋值
|
||||
await rightTableApi.grid.loadData(userList);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 左边部门用
|
||||
const selectDeptId = ref<string[]>([]);
|
||||
const formOptions: VbenFormProps = {
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'userName',
|
||||
label: '用户账号',
|
||||
hideLabel: true,
|
||||
componentProps: {
|
||||
placeholder: '请输入账号',
|
||||
},
|
||||
},
|
||||
],
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
wrapperClass: 'grid-cols-2',
|
||||
handleReset: async () => {
|
||||
selectDeptId.value = [];
|
||||
const { formApi, reload } = tableApi;
|
||||
await formApi.resetForm();
|
||||
const formValues = formApi.form.values;
|
||||
formApi.setLatestSubmissionValues(formValues);
|
||||
await reload(formValues);
|
||||
},
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
// 点击行选中
|
||||
trigger: 'row',
|
||||
},
|
||||
radioConfig: {
|
||||
trigger: 'row',
|
||||
strict: true,
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
type: props.mode === 'single' ? 'radio' : 'checkbox',
|
||||
width: 60,
|
||||
resizable: false,
|
||||
},
|
||||
{
|
||||
field: 'userName',
|
||||
title: '用户',
|
||||
headerAlign: 'left',
|
||||
resizable: false,
|
||||
slots: {
|
||||
default: 'user',
|
||||
},
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
layouts: ['PrevPage', 'Number', 'NextPage', 'Sizes', 'Total'],
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 部门树选择处理
|
||||
if (selectDeptId.value.length === 1) {
|
||||
formValues.deptId = selectDeptId.value[0];
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'deptId');
|
||||
}
|
||||
|
||||
// 加载完毕需要设置选中的行
|
||||
if (props.mode === 'multiple') {
|
||||
const records = rightTableApi.grid.getData();
|
||||
await tableApi.grid.setCheckboxRow(records, true);
|
||||
}
|
||||
if (props.mode === 'single') {
|
||||
const records = rightTableApi.grid.getData();
|
||||
if (records.length === 1) {
|
||||
await tableApi.grid.setRadioRow(records[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const params: any = {
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
};
|
||||
// 添加参数
|
||||
if (props.allowUserIds) {
|
||||
params.userIds = props.allowUserIds;
|
||||
}
|
||||
|
||||
return await userList(params);
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'userId',
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
showOverflow: false,
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents: {
|
||||
// 需要控制不同的事件 radio也会触发checkbox事件
|
||||
checkboxChange: checkBoxEvent,
|
||||
checkboxAll: checkBoxEvent,
|
||||
radioChange: radioEvent,
|
||||
},
|
||||
});
|
||||
|
||||
function checkBoxEvent() {
|
||||
if (props.mode !== 'multiple') {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* 给右边表格赋值
|
||||
* records拿到的是当前页的选中数据
|
||||
* reserveRecords拿到的是其他页选中的数据
|
||||
*/
|
||||
const records = tableApi.grid.getCheckboxRecords();
|
||||
const reserveRecords = tableApi.grid.getCheckboxReserveRecords();
|
||||
const realRecords = [...records, ...reserveRecords];
|
||||
rightTableApi.grid.loadData(realRecords);
|
||||
}
|
||||
|
||||
function radioEvent() {
|
||||
if (props.mode !== 'single') {
|
||||
return;
|
||||
}
|
||||
// 给右边表格赋值
|
||||
const records = tableApi.grid.getRadioRecord();
|
||||
rightTableApi.grid.loadData([records]);
|
||||
}
|
||||
|
||||
const rightGridOptions: VxeGridProps = {
|
||||
checkboxConfig: {},
|
||||
columns: [
|
||||
{
|
||||
field: 'nickName',
|
||||
title: '昵称',
|
||||
width: 200,
|
||||
resizable: false,
|
||||
slots: {
|
||||
default: 'user',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
width: 120,
|
||||
slots: { default: 'action' },
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'userId',
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
showOverflow: false,
|
||||
};
|
||||
|
||||
const [RightBasicTable, rightTableApi] = useVbenVxeGrid({
|
||||
gridOptions: rightGridOptions,
|
||||
});
|
||||
|
||||
async function handleRemoveItem(row: any) {
|
||||
if (props.mode === 'multiple') {
|
||||
await tableApi.grid.setCheckboxRow(row, false);
|
||||
}
|
||||
if (props.mode === 'single') {
|
||||
await tableApi.grid.clearRadioRow();
|
||||
}
|
||||
const data = rightTableApi.grid.getData();
|
||||
await rightTableApi.grid.loadData(data.filter((item) => item !== row));
|
||||
// 这个方法有问题
|
||||
// await rightTableApi.grid.remove(row);
|
||||
}
|
||||
|
||||
function handleRemoveAll() {
|
||||
if (props.mode === 'multiple') {
|
||||
tableApi.grid.clearCheckboxRow();
|
||||
tableApi.grid.clearCheckboxReserve();
|
||||
}
|
||||
if (props.mode === 'single') {
|
||||
tableApi.grid.clearRadioRow();
|
||||
}
|
||||
rightTableApi.grid.loadData([]);
|
||||
}
|
||||
|
||||
async function handleDeptQuery() {
|
||||
await tableApi.reload();
|
||||
// 重置后恢复 保存勾选的数据
|
||||
const records = rightTableApi.grid.getData();
|
||||
if (props.mode === 'multiple') {
|
||||
tableApi?.grid.setCheckboxRow(records, true);
|
||||
}
|
||||
if (props.mode === 'single' && records.length === 1) {
|
||||
tableApi.grid.setRadioRow(records[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
const records = rightTableApi.grid.getData();
|
||||
console.log(records);
|
||||
emit('finish', records);
|
||||
modalApi.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<div class="flex min-h-[600px]">
|
||||
<DeptTree
|
||||
v-model:select-dept-id="selectDeptId"
|
||||
:show-search="false"
|
||||
class="w-[230px]"
|
||||
@reload="() => tableApi.reload()"
|
||||
@select="handleDeptQuery"
|
||||
/>
|
||||
<div class="h-[600px] w-[450px]">
|
||||
<BasicTable>
|
||||
<template #user="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<VbenAvatar
|
||||
:alt="row.nickName"
|
||||
:src="row.avatar ?? ''"
|
||||
:class="{ 'bg-primary': !row.avatar }"
|
||||
class="size-[32px] rounded-full text-white"
|
||||
/>
|
||||
<div class="flex flex-col items-baseline text-[12px]">
|
||||
<div>{{ row.nickName }}</div>
|
||||
<div class="opacity-50">
|
||||
{{ row.phonenumber || '暂无手机号' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
<div class="flex h-[600px] flex-col">
|
||||
<div class="flex w-full px-4">
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<div>已选中人员</div>
|
||||
<div>
|
||||
<a-button size="small" @click="handleRemoveAll">
|
||||
清空选中
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RightBasicTable id="user-select-right-table">
|
||||
<template #user="{ row }">
|
||||
<div class="flex items-center gap-2 overflow-hidden">
|
||||
<VbenAvatar
|
||||
:alt="row.nickName"
|
||||
:src="row.avatar ?? ''"
|
||||
:class="{ 'bg-primary': !row.avatar }"
|
||||
class="size-[32px] rounded-full text-white"
|
||||
/>
|
||||
<div class="flex flex-col items-baseline text-[12px]">
|
||||
<div class="overflow-ellipsis whitespace-nowrap">
|
||||
{{ row.nickName }}
|
||||
</div>
|
||||
<div class="opacity-50">
|
||||
{{ row.phonenumber || '暂无手机号' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<a-button size="small" @click="handleRemoveItem(row)">
|
||||
移除
|
||||
</a-button>
|
||||
</template>
|
||||
</RightBasicTable>
|
||||
</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(div.vben-link) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.vxe-body--row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/**
|
||||
默认显示右边的滚动条 防止出现滚动条被挤压
|
||||
*/
|
||||
#user-select-right-table {
|
||||
div.vxe-table--body-wrapper.body--wrapper {
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user