feat: 完善流程详情模块,进度 28%,调整样式并添加用户选择弹窗

pull/96/head
子夜 2025-05-08 18:29:28 +08:00
parent e40a29ccb4
commit c201766bdb
4 changed files with 387 additions and 3 deletions

View File

@ -0,0 +1 @@
export { default as UserSelectModal } from './user-select-modal.vue';

View File

@ -0,0 +1,370 @@
<script lang="ts" setup>
import type { Key } from 'ant-design-vue/es/table/interface';
import type { SystemDeptApi } from '#/api/system/dept';
import type { SystemUserApi } from '#/api/system/user';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { handleTree } from '@vben/utils';
import {
Button,
Col,
Input,
message,
Row,
Spin,
Transfer,
Tree,
} from 'ant-design-vue';
import { getSimpleDeptList } from '#/api/system/dept';
import { getSimpleUserList } from '#/api/system/user';
//
interface DeptTreeNode {
key: string;
title: string;
children?: DeptTreeNode[];
}
defineOptions({ name: 'UserSelectModal' });
const props = withDefaults(
defineProps<{
cancelText?: string;
confirmText?: string;
multiple?: boolean;
title?: string;
value?: number[];
}>(),
{
title: '选择用户',
multiple: true,
value: () => [],
confirmText: '确定',
cancelText: '取消',
},
);
const emit = defineEmits<{
cancel: [];
confirm: [value: number[]];
'update:value': [value: number[]];
}>();
//
const deptTree = ref<any[]>([]);
const deptList = ref<SystemDeptApi.Dept[]>([]);
const expandedKeys = ref<Key[]>([]);
const selectedDeptId = ref<number>();
const searchValue = ref('');
//
const userList = ref<SystemUserApi.User[]>([]);
const filteredUserList = ref<SystemUserApi.User[]>([]);
const selectedUserIds = ref<string[]>([]);
//
const pagination = ref({
pageSize: 10,
simple: true,
showSizeChanger: true,
onChange: (page: number, pageSize: number) => {
console.log('🚀 ~ pagination ~ page:', page);
console.log('🚀 ~ pagination ~ pageSize:', pageSize);
},
});
//
const loading = ref(false);
//
const transferUserList = computed(() => {
// 1.
const selectedUsers = userList.value.filter((user) =>
selectedUserIds.value.includes(String(user.id)),
);
// 2.
const filteredUnselectedUsers = filteredUserList.value.filter(
(user) => !selectedUserIds.value.includes(String(user.id)),
);
// 3.
return [...selectedUsers, ...filteredUnselectedUsers];
});
//
const filteredDeptTree = computed(() => {
if (!searchValue.value) return deptTree.value;
const filterNode = (node: any): any => {
const title = node?.title?.toLowerCase();
const search = searchValue.value.toLowerCase();
//
if (title.includes(search)) {
return {
...node,
children: node.children?.map((child: any) => filterNode(child)),
};
}
//
if (node.children) {
const filteredChildren = node.children
.map((child: any) => filterNode(child))
.filter(Boolean);
if (filteredChildren.length > 0) {
return {
...node,
children: filteredChildren,
};
}
}
return null;
};
return deptTree.value.map((node: any) => filterNode(node)).filter(Boolean);
});
// ID
const getChildDeptIds = (
deptId: number | undefined,
deptList: SystemDeptApi.Dept[],
): number[] => {
if (!deptId) return [];
const ids = [deptId];
const children = deptList.filter((dept) => dept.parentId === deptId);
children.forEach((child) => {
ids.push(...getChildDeptIds(child.id, deptList));
});
return ids;
};
//
const filterUserList = async (deptId: number) => {
loading.value = true;
try {
//
const deptIds = getChildDeptIds(deptId, deptList.value);
filteredUserList.value = userList.value.filter((user) =>
deptIds.includes(user.deptId),
);
} finally {
loading.value = false;
}
};
//
const handleDeptSelect = (selectedKeys: Key[], _info: any) => {
if (selectedKeys.length === 0) return;
const deptId = Number(selectedKeys[0]);
selectedDeptId.value = deptId;
filterUserList(deptId);
};
//
const handleUserChange = (targetKeys: string[]) => {
selectedUserIds.value = targetKeys;
emit('update:value', targetKeys.map(Number));
};
//
const handleConfirm = () => {
if (selectedUserIds.value.length === 0) {
message.warning('请选择用户');
return;
}
emit('confirm', selectedUserIds.value.map(Number));
modalApi.close();
};
//
const handleCancel = () => {
emit('cancel');
modalApi.close();
};
//
const resetData = () => {
deptTree.value = [];
deptList.value = [];
userList.value = [];
filteredUserList.value = [];
selectedUserIds.value = [];
selectedDeptId.value = undefined;
expandedKeys.value = [];
searchValue.value = '';
pagination.value = {
pageSize: 10,
simple: false,
showSizeChanger: true,
};
};
//
const processDeptNode = (node: any): DeptTreeNode => {
return {
key: String(node.id),
title: `${node.name} (${node.id})`,
children: node.children?.map((child: any) => processDeptNode(child)),
};
};
//
const open = async () => {
resetData();
loading.value = true;
try {
//
const deptData = await getSimpleDeptList();
deptList.value = deptData;
const treeData = handleTree(deptData);
deptTree.value = treeData.map((node) => processDeptNode(node));
expandedKeys.value = deptTree.value.map((node) => node.key);
//
userList.value = await getSimpleUserList();
filteredUserList.value = [...userList.value];
//
if (props.value?.length) {
selectedUserIds.value = props.value.map(String);
}
modalApi.open();
} finally {
loading.value = false;
}
};
//
const [ModalComponent, modalApi] = useVbenModal({
title: props.title,
onCancel: handleCancel,
});
// /
const handleExpand = (keys: Key[]) => {
expandedKeys.value = keys;
};
//
const handleDeptSearch = (value: string) => {
searchValue.value = value;
//
if (value) {
const getAllKeys = (nodes: any[]): string[] => {
const keys: string[] = [];
for (const node of nodes) {
keys.push(node.key);
if (node.children) {
keys.push(...getAllKeys(node.children));
}
}
return keys;
};
expandedKeys.value = getAllKeys(deptTree.value);
} else {
//
expandedKeys.value = deptTree.value.map((node) => node.key);
}
};
defineExpose({
open,
});
</script>
<template>
<ModalComponent class="w-[900px]">
<Spin :spinning="loading">
<Row :gutter="[16, 16]">
<Col :span="6">
<div class="h-[500px] overflow-auto rounded border border-gray-200">
<div class="border-b border-gray-200 p-2">
<Input
v-model:value="searchValue"
placeholder="搜索部门"
allow-clear
@input="(e) => handleDeptSearch(e.target?.value ?? '')"
/>
</div>
<Tree
:tree-data="filteredDeptTree"
:expanded-keys="expandedKeys"
:selected-keys="selectedDeptId ? [String(selectedDeptId)] : []"
@select="handleDeptSelect"
@expand="handleExpand"
/>
</div>
</Col>
<Col :span="17">
<Transfer
:row-key="(record) => String(record.id)"
v-model:target-keys="selectedUserIds"
:data-source="transferUserList"
:titles="['未选', '已选']"
:show-search="true"
:filter-option="
(inputValue, item) =>
item.nickname.toLowerCase().includes(inputValue.toLowerCase())
"
:pagination="pagination"
@change="handleUserChange"
>
<template #render="item">
<span class="custom-item"
>{{ item.nickname }} ({{ item.id }})</span
>
</template>
</Transfer>
</Col>
</Row>
</Spin>
<template #footer>
<Button
type="primary"
:disabled="selectedUserIds.length === 0"
@click="handleConfirm"
>
{{ confirmText }}
</Button>
<Button @click="handleCancel">{{ cancelText }}</Button>
</template>
</ModalComponent>
</template>
<style lang="scss" scoped>
:deep(.ant-transfer) {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
}
:deep(.ant-transfer-list) {
flex: 1;
height: 100%;
}
:deep(.ant-transfer-list-search) {
padding: 8px;
}
:deep(.ant-transfer-list-body) {
height: calc(100% - 40px);
}
:deep(.ant-transfer-list-pagination) {
margin: 8px 0;
text-align: right;
}
</style>

View File

@ -238,7 +238,11 @@ onMounted(async () => {
<Page auto-content-height>
<Card
class="h-full"
:body-style="{ height: 'calc(100% - 140px)', overflowY: 'auto' }"
:body-style="{
height: 'calc(100% - 140px)',
overflowY: 'auto',
paddingTop: '12px',
}"
>
<template #title>
<span class="text-[#878c93]">编号{{ id || '-' }}</span>

View File

@ -10,6 +10,7 @@ import { formatDateTime, isEmpty } from '@vben/utils';
import { Avatar, Button, Image, Tooltip } from 'ant-design-vue';
import { UserSelectModal } from '#/components/user-select-modal';
import { CandidateStrategyEnum, NodeTypeEnum, TaskStatusEnum } from '#/utils';
defineOptions({ name: 'BpmProcessInstanceTimeline' });
@ -150,10 +151,12 @@ const customApproveUsers = ref<Record<string, any[]>>({}); // keyactivityId
//
const handleSelectUser = (activityId: string, selectedList: any[]) => {
console.log(userSelectFormRef.value);
userSelectFormRef.value.open(activityId, selectedList);
};
//
const selectedUsers = ref<number[]>([]);
const handleUserSelectConfirm = (activityId: string, userList: any[]) => {
customApproveUsers.value[activityId] = userList || [];
emit('selectUserConfirm', activityId, userList);
@ -280,7 +283,7 @@ const shouldShowApprovalReason = (task: any, nodeType: NodeTypeEnum) => {
<!-- 需要自定义选择审批人 -->
<div
v-if="shouldShowCustomUserSelect(activity)"
v-if="true || shouldShowCustomUserSelect(activity)"
class="flex flex-wrap items-center gap-2"
>
<Tooltip title="添加用户" placement="left">
@ -438,5 +441,11 @@ const shouldShowApprovalReason = (task: any, nodeType: NodeTypeEnum) => {
</a-timeline>
<!-- 用户选择弹窗 -->
<UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
<UserSelectModal
ref="userSelectFormRef"
v-model:value="selectedUsers"
:multiple="true"
title="选择用户"
@confirm="handleUserSelectConfirm"
/>
</template>