feat: 完善用户选择弹窗功能,添加分页和搜索功能,优化部门选择逻辑 进度 30%

This commit is contained in:
ziye
2025-05-09 01:26:33 +08:00
parent c201766bdb
commit bc50357548
2 changed files with 281 additions and 126 deletions

View File

@@ -14,6 +14,7 @@ import {
Col, Col,
Input, Input,
message, message,
Pagination,
Row, Row,
Spin, Spin,
Transfer, Transfer,
@@ -21,7 +22,7 @@ import {
} from 'ant-design-vue'; } from 'ant-design-vue';
import { getSimpleDeptList } from '#/api/system/dept'; import { getSimpleDeptList } from '#/api/system/dept';
import { getSimpleUserList } from '#/api/system/user'; import { getUserPage } from '#/api/system/user';
// 部门树节点接口 // 部门树节点接口
interface DeptTreeNode { interface DeptTreeNode {
@@ -60,50 +61,53 @@ const deptTree = ref<any[]>([]);
const deptList = ref<SystemDeptApi.Dept[]>([]); const deptList = ref<SystemDeptApi.Dept[]>([]);
const expandedKeys = ref<Key[]>([]); const expandedKeys = ref<Key[]>([]);
const selectedDeptId = ref<number>(); const selectedDeptId = ref<number>();
const searchValue = ref(''); const deptSearchKeys = 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 loading = ref(false);
// 计算属性:合并已选择的用户和当前部门过滤后的用户 // 用户数据管理
const transferUserList = computed(() => { const userList = ref<SystemUserApi.User[]>([]); // 存储所有已知用户
// 1. 获取所有已选择的用户 const selectedUserIds = ref<string[]>([]);
const selectedUsers = userList.value.filter((user) =>
selectedUserIds.value.includes(String(user.id)),
);
// 2. 获取当前部门过滤后的未选择用户 // 左侧列表状态
const filteredUnselectedUsers = filteredUserList.value.filter( const leftListState = ref({
(user) => !selectedUserIds.value.includes(String(user.id)), loading: false,
); searchValue: '',
dataSource: [] as SystemUserApi.User[],
pagination: {
current: 1,
pageSize: 10,
total: 0,
},
});
// 3. 合并并去重 // 右侧列表状态
return [...selectedUsers, ...filteredUnselectedUsers]; const rightListState = ref({
searchValue: '',
dataSource: [] as SystemUserApi.User[],
pagination: {
current: 1,
pageSize: 10,
total: 0,
},
});
// 计算属性Transfer 数据源
const transferDataSource = computed(() => {
return [
...leftListState.value.dataSource,
...rightListState.value.dataSource,
];
}); });
// 过滤部门树数据 // 过滤部门树数据
const filteredDeptTree = computed(() => { const filteredDeptTree = computed(() => {
if (!searchValue.value) return deptTree.value; if (!deptSearchKeys.value) return deptTree.value;
const filterNode = (node: any): any => { const filterNode = (node: any): any => {
const title = node?.title?.toLowerCase(); const title = node?.title?.toLowerCase();
const search = searchValue.value.toLowerCase(); const search = deptSearchKeys.value.toLowerCase();
// 如果当前节点匹配 // 如果当前节点匹配
if (title.includes(search)) { if (title.includes(search)) {
@@ -133,87 +137,123 @@ const filteredDeptTree = computed(() => {
return deptTree.value.map((node: any) => filterNode(node)).filter(Boolean); return deptTree.value.map((node: any) => filterNode(node)).filter(Boolean);
}); });
// 获取指定部门及其所有子部门的ID列表 // 加载用户数据
const getChildDeptIds = ( const loadUserData = async (pageNo: number, pageSize: number) => {
deptId: number | undefined, leftListState.value.loading = true;
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 { try {
// 获取部门及其子部门的所有用户 const { list, total } = await getUserPage({
const deptIds = getChildDeptIds(deptId, deptList.value); pageNo,
filteredUserList.value = userList.value.filter((user) => pageSize,
deptIds.includes(user.deptId), deptId: selectedDeptId.value,
username: leftListState.value.searchValue || undefined,
});
leftListState.value.dataSource = list;
leftListState.value.pagination.total = total;
leftListState.value.pagination.current = pageNo;
leftListState.value.pagination.pageSize = pageSize;
// 更新用户列表缓存
const newUsers = list.filter(
(user) => !userList.value.some((u) => u.id === user.id),
); );
if (newUsers.length > 0) {
userList.value.push(...newUsers);
}
} finally { } finally {
loading.value = false; leftListState.value.loading = false;
} }
}; };
// 处理部门选择 // 更新右侧列表数据
const handleDeptSelect = (selectedKeys: Key[], _info: any) => { const updateRightListData = () => {
if (selectedKeys.length === 0) return; // 获取选中的用户
const deptId = Number(selectedKeys[0]); const selectedUsers = userList.value.filter((user) =>
selectedDeptId.value = deptId; selectedUserIds.value.includes(String(user.id)),
filterUserList(deptId); );
// 应用搜索过滤
const filteredUsers = rightListState.value.searchValue
? selectedUsers.filter((user) =>
user.nickname
.toLowerCase()
.includes(rightListState.value.searchValue.toLowerCase()),
)
: selectedUsers;
// 更新总数
rightListState.value.pagination.total = filteredUsers.length;
// 应用分页
const { current, pageSize } = rightListState.value.pagination;
const startIndex = (current - 1) * pageSize;
const endIndex = startIndex + pageSize;
rightListState.value.dataSource = filteredUsers.slice(startIndex, endIndex);
}; };
// 处理用户选择 // 处理左侧分页变化
const handleLeftPaginationChange = async (page: number, pageSize: number) => {
await loadUserData(page, pageSize);
};
// 处理右侧分页变化
const handleRightPaginationChange = (page: number, pageSize: number) => {
rightListState.value.pagination.current = page;
rightListState.value.pagination.pageSize = pageSize;
updateRightListData();
};
// 处理用户搜索
const handleUserSearch = async (direction: string, value: string) => {
if (direction === 'left') {
leftListState.value.searchValue = value;
leftListState.value.pagination.current = 1;
await loadUserData(1, leftListState.value.pagination.pageSize);
} else {
rightListState.value.searchValue = value;
rightListState.value.pagination.current = 1;
updateRightListData();
}
};
// 处理用户选择变化
const handleUserChange = (targetKeys: string[]) => { const handleUserChange = (targetKeys: string[]) => {
selectedUserIds.value = targetKeys; selectedUserIds.value = targetKeys;
emit('update:value', targetKeys.map(Number)); emit('update:value', targetKeys.map(Number));
}; updateRightListData();
// 确认选择
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 = () => { const resetData = () => {
deptTree.value = [];
deptList.value = [];
userList.value = []; userList.value = [];
filteredUserList.value = [];
selectedUserIds.value = []; selectedUserIds.value = [];
// 取消部门选中
selectedDeptId.value = undefined; selectedDeptId.value = undefined;
expandedKeys.value = [];
searchValue.value = ''; // 取消选中的用户
pagination.value = { selectedUserIds.value = [];
leftListState.value = {
loading: false,
searchValue: '',
dataSource: [],
pagination: {
current: 1,
pageSize: 10, pageSize: 10,
simple: false, total: 0,
showSizeChanger: true, },
};
}; };
// 递归处理部门树节点 rightListState.value = {
const processDeptNode = (node: any): DeptTreeNode => { searchValue: '',
return { dataSource: [],
key: String(node.id), pagination: {
title: `${node.name} (${node.id})`, current: 1,
children: node.children?.map((child: any) => processDeptNode(child)), pageSize: 10,
total: 0,
},
}; };
}; };
@@ -229,13 +269,20 @@ const open = async () => {
deptTree.value = treeData.map((node) => processDeptNode(node)); deptTree.value = treeData.map((node) => processDeptNode(node));
expandedKeys.value = deptTree.value.map((node) => node.key); expandedKeys.value = deptTree.value.map((node) => node.key);
// 加载用户数据 // 加载初始用户数据
userList.value = await getSimpleUserList(); await loadUserData(1, leftListState.value.pagination.pageSize);
filteredUserList.value = [...userList.value];
// 设置已选用户 // 设置已选用户
if (props.value?.length) { if (props.value?.length) {
selectedUserIds.value = props.value.map(String); selectedUserIds.value = props.value.map(String);
// 加载已选用户的完整信息
const { list } = await getUserPage({
pageNo: 1,
pageSize: props.value.length,
userIds: props.value,
});
userList.value.push(...list);
updateRightListData();
} }
modalApi.open(); modalApi.open();
@@ -244,11 +291,10 @@ const open = async () => {
} }
}; };
// 弹窗配置 // TODO 后端接口目前仅支持 username 检索, 筛选条件需要跟后端请求参数保持一致。
const [ModalComponent, modalApi] = useVbenModal({ const filterOption = (inputValue: string, option: any) => {
title: props.title, return option.username.toLowerCase().includes(inputValue.toLowerCase());
onCancel: handleCancel, };
});
// 处理部门树展开/折叠 // 处理部门树展开/折叠
const handleExpand = (keys: Key[]) => { const handleExpand = (keys: Key[]) => {
@@ -257,7 +303,7 @@ const handleExpand = (keys: Key[]) => {
// 处理部门搜索 // 处理部门搜索
const handleDeptSearch = (value: string) => { const handleDeptSearch = (value: string) => {
searchValue.value = value; deptSearchKeys.value = value;
// 如果有搜索结果,自动展开所有节点 // 如果有搜索结果,自动展开所有节点
if (value) { if (value) {
@@ -278,20 +324,76 @@ const handleDeptSearch = (value: string) => {
} }
}; };
// 处理部门选择
const handleDeptSelect = async (selectedKeys: Key[], _info: any) => {
// 更新选中的部门ID
const newDeptId =
selectedKeys.length > 0 ? Number(selectedKeys[0]) : undefined;
selectedDeptId.value =
newDeptId === selectedDeptId.value ? undefined : newDeptId;
// 重置分页并加载数据
const { pageSize } = leftListState.value.pagination;
leftListState.value.pagination.current = 1;
await loadUserData(1, pageSize);
};
// 确认选择
const handleConfirm = () => {
if (selectedUserIds.value.length === 0) {
message.warning('请选择用户');
return;
}
emit('confirm', selectedUserIds.value.map(Number));
modalApi.close();
};
// 取消选择
const handleCancel = () => {
emit('cancel');
modalApi.close();
// 确保在动画结束后再重置数据
setTimeout(() => {
resetData();
}, 300);
};
// 关闭弹窗
const handleClosed = () => {
resetData();
};
// 弹窗配置
const [ModalComponent, modalApi] = useVbenModal({
title: props.title,
onCancel: handleCancel,
onClosed: handleClosed,
destroyOnClose: true,
});
// 递归处理部门树节点
const processDeptNode = (node: any): DeptTreeNode => {
return {
key: String(node.id),
title: `${node.name} (${node.id})`,
children: node.children?.map((child: any) => processDeptNode(child)),
};
};
defineExpose({ defineExpose({
open, open,
}); });
</script> </script>
<template> <template>
<ModalComponent class="w-[900px]"> <ModalComponent class="w-[1000px]" key="user-select-modal">
<Spin :spinning="loading"> <Spin :spinning="loading">
<Row :gutter="[16, 16]"> <Row :gutter="[16, 16]">
<Col :span="6"> <Col :span="6">
<div class="h-[500px] overflow-auto rounded border border-gray-200"> <div class="h-[500px] overflow-auto rounded border border-gray-200">
<div class="border-b border-gray-200 p-2"> <div class="border-b border-gray-200 p-2">
<Input <Input
v-model:value="searchValue" v-model:value="deptSearchKeys"
placeholder="搜索部门" placeholder="搜索部门"
allow-clear allow-clear
@input="(e) => handleDeptSearch(e.target?.value ?? '')" @input="(e) => handleDeptSearch(e.target?.value ?? '')"
@@ -306,24 +408,46 @@ defineExpose({
/> />
</div> </div>
</Col> </Col>
<Col :span="17"> <Col :span="18">
<Transfer <Transfer
:row-key="(record) => String(record.id)" :row-key="(record) => String(record.id)"
:data-source="transferDataSource"
v-model:target-keys="selectedUserIds" v-model:target-keys="selectedUserIds"
:data-source="transferUserList"
:titles="['未选', '已选']" :titles="['未选', '已选']"
:show-search="true" :show-search="true"
:filter-option=" :show-select-all="true"
(inputValue, item) => :filter-option="filterOption"
item.nickname.toLowerCase().includes(inputValue.toLowerCase())
"
:pagination="pagination"
@change="handleUserChange" @change="handleUserChange"
@search="handleUserSearch"
> >
<template #render="item"> <template #render="item">
<span class="custom-item" {{ item.nickname }} ({{ item.id }})
>{{ item.nickname }} ({{ item.id }})</span </template>
>
<template #footer="{ direction }">
<div v-if="direction === 'left'">
<Pagination
v-model:current="leftListState.pagination.current"
v-model:page-size="leftListState.pagination.pageSize"
:total="leftListState.pagination.total"
:show-size-changer="true"
:show-total="(total) => `${total}`"
size="small"
@change="handleLeftPaginationChange"
/>
</div>
<div v-if="direction === 'right'">
<Pagination
v-model:current="rightListState.pagination.current"
v-model:page-size="rightListState.pagination.pageSize"
:total="rightListState.pagination.total"
:show-size-changer="true"
:show-total="(total) => `${total}`"
size="small"
@change="handleRightPaginationChange"
/>
</div>
</template> </template>
</Transfer> </Transfer>
</Col> </Col>
@@ -345,26 +469,60 @@ defineExpose({
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.ant-transfer) { :deep(.ant-transfer) {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
height: 100%; justify-content: space-between;
height: 500px;
} }
:deep(.ant-transfer-list) { :deep(.ant-transfer-list) {
display: flex;
flex: 1; flex: 1;
flex-direction: column;
width: 300px !important;
height: 100%; height: 100%;
} }
:deep(.ant-transfer-list-header) {
flex-shrink: 0;
}
:deep(.ant-transfer-list-search) { :deep(.ant-transfer-list-search) {
flex-shrink: 0;
padding: 8px; padding: 8px;
} }
:deep(.ant-transfer-list-body) { :deep(.ant-transfer-list-body) {
height: calc(100% - 40px); flex: 1;
overflow: auto;
} }
:deep(.ant-transfer-list-pagination) { :deep(.ant-transfer-list-content) {
margin: 8px 0; height: auto !important;
}
:deep(.ant-transfer-list-content-item) {
padding: 6px 12px;
}
:deep(.ant-transfer-operation) {
padding: 0 8px;
}
:deep(.ant-transfer-list-footer) {
flex-shrink: 0;
}
:deep(.ant-pagination) {
margin: 8px;
font-size: 12px;
text-align: right; text-align: right;
} }
:deep(.ant-pagination-options) {
margin-left: 8px;
}
:deep(.ant-pagination-options-size-changer) {
margin-right: 8px;
}
</style> </style>

View File

@@ -147,9 +147,6 @@ async function getApprovalDetail() {
processDefinition.value.formFields, processDefinition.value.formFields,
processInstance.value.formVariables, processInstance.value.formVariables,
); );
detailForm.value.value.Fx21maervo4ratc = undefined;
detailForm.value.value.F3yvmaervlwuanc = undefined;
} }
nextTick().then(() => { nextTick().then(() => {
fApi.value?.btn.show(false); fApi.value?.btn.show(false);
@@ -171,8 +168,8 @@ async function getApprovalDetail() {
// 获取审批节点,显示 Timeline 的数据 // 获取审批节点,显示 Timeline 的数据
activityNodes.value = data.activityNodes; activityNodes.value = data.activityNodes;
} catch (error) { } catch {
console.error('🚀 ~ getApprovalDetail ~ error:', error); message.error('获取审批详情失败!');
} finally { } finally {
processInstanceLoading.value = false; processInstanceLoading.value = false;
} }