From bc50357548af5489930e5e8e4397a5a29d98edb4 Mon Sep 17 00:00:00 2001 From: ziye <278898052@qq.com> Date: Fri, 9 May 2025 01:26:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=BC=B9=E7=AA=97=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=88=86=E9=A1=B5=E5=92=8C=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96=E9=83=A8=E9=97=A8?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E9=80=BB=E8=BE=91=20=20=E8=BF=9B=E5=BA=A6=20?= =?UTF-8?q?30%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user-select-modal/user-select-modal.vue | 400 ++++++++++++------ .../bpm/processInstance/detail/index.vue | 7 +- 2 files changed, 281 insertions(+), 126 deletions(-) diff --git a/apps/web-antd/src/components/user-select-modal/user-select-modal.vue b/apps/web-antd/src/components/user-select-modal/user-select-modal.vue index 4c8db339..7437a8b2 100644 --- a/apps/web-antd/src/components/user-select-modal/user-select-modal.vue +++ b/apps/web-antd/src/components/user-select-modal/user-select-modal.vue @@ -14,6 +14,7 @@ import { Col, Input, message, + Pagination, Row, Spin, Transfer, @@ -21,7 +22,7 @@ import { } from 'ant-design-vue'; import { getSimpleDeptList } from '#/api/system/dept'; -import { getSimpleUserList } from '#/api/system/user'; +import { getUserPage } from '#/api/system/user'; // 部门树节点接口 interface DeptTreeNode { @@ -60,50 +61,53 @@ const deptTree = ref([]); const deptList = ref([]); const expandedKeys = ref([]); const selectedDeptId = ref(); -const searchValue = ref(''); - -// 用户数据 -const userList = ref([]); -const filteredUserList = ref([]); -const selectedUserIds = ref([]); - -// 分页数据 -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 deptSearchKeys = ref(''); // 加载状态 const loading = ref(false); -// 计算属性:合并已选择的用户和当前部门过滤后的用户 -const transferUserList = computed(() => { - // 1. 获取所有已选择的用户 - const selectedUsers = userList.value.filter((user) => - selectedUserIds.value.includes(String(user.id)), - ); +// 用户数据管理 +const userList = ref([]); // 存储所有已知用户 +const selectedUserIds = ref([]); - // 2. 获取当前部门过滤后的未选择用户 - const filteredUnselectedUsers = filteredUserList.value.filter( - (user) => !selectedUserIds.value.includes(String(user.id)), - ); +// 左侧列表状态 +const leftListState = ref({ + 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(() => { - if (!searchValue.value) return deptTree.value; + if (!deptSearchKeys.value) return deptTree.value; const filterNode = (node: any): any => { const title = node?.title?.toLowerCase(); - const search = searchValue.value.toLowerCase(); + const search = deptSearchKeys.value.toLowerCase(); // 如果当前节点匹配 if (title.includes(search)) { @@ -133,87 +137,123 @@ const filteredDeptTree = computed(() => { 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; +// 加载用户数据 +const loadUserData = async (pageNo: number, pageSize: number) => { + leftListState.value.loading = true; try { - // 获取部门及其子部门的所有用户 - const deptIds = getChildDeptIds(deptId, deptList.value); - filteredUserList.value = userList.value.filter((user) => - deptIds.includes(user.deptId), + const { list, total } = await getUserPage({ + pageNo, + pageSize, + 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 { - loading.value = false; + leftListState.value.loading = false; } }; -// 处理部门选择 -const handleDeptSelect = (selectedKeys: Key[], _info: any) => { - if (selectedKeys.length === 0) return; - const deptId = Number(selectedKeys[0]); - selectedDeptId.value = deptId; - filterUserList(deptId); +// 更新右侧列表数据 +const updateRightListData = () => { + // 获取选中的用户 + const selectedUsers = userList.value.filter((user) => + selectedUserIds.value.includes(String(user.id)), + ); + + // 应用搜索过滤 + 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[]) => { 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(); + updateRightListData(); }; // 重置数据 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)), + // 取消部门选中 + selectedDeptId.value = undefined; + + // 取消选中的用户 + selectedUserIds.value = []; + + leftListState.value = { + loading: false, + searchValue: '', + dataSource: [], + pagination: { + current: 1, + pageSize: 10, + total: 0, + }, + }; + + rightListState.value = { + searchValue: '', + dataSource: [], + pagination: { + current: 1, + pageSize: 10, + total: 0, + }, }; }; @@ -229,13 +269,20 @@ const open = async () => { deptTree.value = treeData.map((node) => processDeptNode(node)); expandedKeys.value = deptTree.value.map((node) => node.key); - // 加载用户数据 - userList.value = await getSimpleUserList(); - filteredUserList.value = [...userList.value]; + // 加载初始用户数据 + await loadUserData(1, leftListState.value.pagination.pageSize); // 设置已选用户 if (props.value?.length) { 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(); @@ -244,11 +291,10 @@ const open = async () => { } }; -// 弹窗配置 -const [ModalComponent, modalApi] = useVbenModal({ - title: props.title, - onCancel: handleCancel, -}); +// TODO 后端接口目前仅支持 username 检索, 筛选条件需要跟后端请求参数保持一致。 +const filterOption = (inputValue: string, option: any) => { + return option.username.toLowerCase().includes(inputValue.toLowerCase()); +}; // 处理部门树展开/折叠 const handleExpand = (keys: Key[]) => { @@ -257,7 +303,7 @@ const handleExpand = (keys: Key[]) => { // 处理部门搜索 const handleDeptSearch = (value: string) => { - searchValue.value = value; + deptSearchKeys.value = 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({ open, });