refactor: modal select

pull/133/MERGE
xingyu4j 2025-06-06 20:45:27 +08:00
parent 5e77558efd
commit 7e8f2a1328
2 changed files with 198 additions and 226 deletions

View File

@ -9,7 +9,7 @@ import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { handleTree } from '@vben/utils'; import { handleTree } from '@vben/utils';
import { Button, Card, Col, Row, Tree } from 'ant-design-vue'; import { Card, Col, Row, Tree } from 'ant-design-vue';
import { getSimpleDeptList } from '#/api/system/dept'; import { getSimpleDeptList } from '#/api/system/dept';
@ -41,24 +41,6 @@ const emit = defineEmits<{
confirm: [deptList: SystemDeptApi.Dept[]]; confirm: [deptList: SystemDeptApi.Dept[]];
}>(); }>();
//
const [Modal, modalApi] = useVbenModal({
title: props.title,
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
resetData();
return;
}
modalApi.setState({ loading: true });
try {
deptData.value = await getSimpleDeptList();
deptTree.value = handleTree(deptData.value) as DataNode[];
} finally {
modalApi.setState({ loading: false });
}
},
destroyOnClose: true,
});
type checkedKeys = number[] | { checked: number[]; halfChecked: number[] }; type checkedKeys = number[] | { checked: number[]; halfChecked: number[] };
// //
const deptTree = ref<DataNode[]>([]); const deptTree = ref<DataNode[]>([]);
@ -67,14 +49,40 @@ const selectedDeptIds = ref<checkedKeys>([]);
// //
const deptData = ref<SystemDeptApi.Dept[]>([]); const deptData = ref<SystemDeptApi.Dept[]>([]);
/** 打开对话框 */ //
const open = async (selectedList?: SystemDeptApi.Dept[]) => { const [Modal, modalApi] = useVbenModal({
modalApi.open(); async onConfirm() {
// ID
const selectedIds: number[] = Array.isArray(selectedDeptIds.value)
? selectedDeptIds.value
: selectedDeptIds.value.checked || [];
const deptArray = deptData.value.filter((dept) =>
selectedIds.includes(dept.id!),
);
emit('confirm', deptArray);
//
await modalApi.close();
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
deptTree.value = [];
selectedDeptIds.value = [];
return;
}
//
const data = modalApi.getData();
if (!data) {
return;
}
modalApi.lock();
try {
deptData.value = await getSimpleDeptList();
deptTree.value = handleTree(deptData.value) as DataNode[];
// // // //
if (selectedList?.length) { if (data.selectedList?.length) {
const selectedIds = selectedList const selectedIds = data.selectedList
.map((dept) => dept.id) .map((dept: SystemDeptApi.Dept) => dept.id)
.filter((id): id is number => id !== undefined); .filter((id: number) => id !== undefined);
selectedDeptIds.value = props.checkStrictly selectedDeptIds.value = props.checkStrictly
? { ? {
checked: selectedIds, checked: selectedIds,
@ -82,10 +90,15 @@ const open = async (selectedList?: SystemDeptApi.Dept[]) => {
} }
: selectedIds; : selectedIds;
} }
}; } finally {
modalApi.unlock();
}
},
destroyOnClose: true,
});
/** 处理选中状态变化 */ /** 处理选中状态变化 */
const handleCheck = () => { function handleCheck() {
if (!props.multiple) { if (!props.multiple) {
// //
if (Array.isArray(selectedDeptIds.value)) { if (Array.isArray(selectedDeptIds.value)) {
@ -106,37 +119,10 @@ const handleCheck = () => {
} }
} }
} }
}; }
/** 提交选择 */
const handleConfirm = async () => {
// ID
const selectedIds: number[] = Array.isArray(selectedDeptIds.value)
? selectedDeptIds.value
: selectedDeptIds.value.checked || [];
const deptArray = deptData.value.filter((dept) =>
selectedIds.includes(dept.id!),
);
//
await modalApi.close();
emit('confirm', deptArray);
};
const handleCancel = () => {
modalApi.close();
};
/** 重置数据 */
const resetData = () => {
deptTree.value = [];
selectedDeptIds.value = [];
};
/** 提供 open 方法,用于打开对话框 */
defineExpose({ open });
</script> </script>
<template> <template>
<Modal> <Modal :title="title" key="dept-select-modal" class="w-[40%]">
<Row class="h-full"> <Row class="h-full">
<Col :span="24"> <Col :span="24">
<Card class="h-full"> <Card class="h-full">
@ -153,9 +139,5 @@ defineExpose({ open });
</Card> </Card>
</Col> </Col>
</Row> </Row>
<template #footer>
<Button @click="handleCancel">{{ cancelText }}</Button>
<Button type="primary" @click="handleConfirm">{{ confirmText }}</Button>
</template>
</Modal> </Modal>
</template> </template>

View File

@ -17,7 +17,6 @@ import {
message, message,
Pagination, Pagination,
Row, Row,
Spin,
Transfer, Transfer,
Tree, Tree,
} from 'ant-design-vue'; } from 'ant-design-vue';
@ -66,16 +65,66 @@ const expandedKeys = ref<Key[]>([]);
const selectedDeptId = ref<number>(); const selectedDeptId = ref<number>();
const deptSearchKeys = ref(''); const deptSearchKeys = ref('');
//
const loading = ref(false);
// //
const userList = ref<SystemUserApi.User[]>([]); // const userList = ref<SystemUserApi.User[]>([]); //
const selectedUserIds = ref<string[]>([]); const selectedUserIds = ref<string[]>([]);
//
const [Modal, modalApi] = useVbenModal({
onCancel: handleCancel,
onClosed: handleClosed,
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
resetData();
return;
}
//
const data = modalApi.getData();
if (!data) {
return;
}
modalApi.lock();
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);
//
await loadUserData(1, leftListState.value.pagination.pageSize);
//
if (data.userIds?.length) {
selectedUserIds.value = data.userIds.map(String);
// TODO ID
const { list } = await getUserPage({
pageNo: 1,
pageSize: 100, // 使
userIds: data.userIds,
});
// 使 Map ID key
const userMap = new Map(userList.value.map((user) => [user.id, user]));
list.forEach((user) => {
if (!userMap.has(user.id)) {
userMap.set(user.id, user);
}
});
userList.value = [...userMap.values()];
updateRightListData();
}
modalApi.open();
} finally {
modalApi.unlock();
}
},
destroyOnClose: true,
});
// //
const leftListState = ref({ const leftListState = ref({
loading: false,
searchValue: '', searchValue: '',
dataSource: [] as SystemUserApi.User[], dataSource: [] as SystemUserApi.User[],
pagination: { pagination: {
@ -145,8 +194,7 @@ const filteredDeptTree = computed(() => {
}); });
// //
const loadUserData = async (pageNo: number, pageSize: number) => { async function loadUserData(pageNo: number, pageSize: number) {
leftListState.value.loading = true;
try { try {
const { list, total } = await getUserPage({ const { list, total } = await getUserPage({
pageNo, pageNo,
@ -167,13 +215,11 @@ const loadUserData = async (pageNo: number, pageSize: number) => {
if (newUsers.length > 0) { if (newUsers.length > 0) {
userList.value.push(...newUsers); userList.value.push(...newUsers);
} }
} finally { } finally {}
leftListState.value.loading = false;
} }
};
// //
const updateRightListData = () => { function updateRightListData() {
// 使 Set ID // 使 Set ID
const uniqueSelectedIds = new Set(selectedUserIds.value); const uniqueSelectedIds = new Set(selectedUserIds.value);
@ -202,22 +248,22 @@ const updateRightListData = () => {
const endIndex = startIndex + pageSize; const endIndex = startIndex + pageSize;
rightListState.value.dataSource = filteredUsers.slice(startIndex, endIndex); rightListState.value.dataSource = filteredUsers.slice(startIndex, endIndex);
}; }
// //
const handleLeftPaginationChange = async (page: number, pageSize: number) => { async function handleLeftPaginationChange(page: number, pageSize: number) {
await loadUserData(page, pageSize); await loadUserData(page, pageSize);
}; }
// //
const handleRightPaginationChange = (page: number, pageSize: number) => { function handleRightPaginationChange(page: number, pageSize: number) {
rightListState.value.pagination.current = page; rightListState.value.pagination.current = page;
rightListState.value.pagination.pageSize = pageSize; rightListState.value.pagination.pageSize = pageSize;
updateRightListData(); updateRightListData();
}; }
// //
const handleUserSearch = async (direction: string, value: string) => { async function handleUserSearch(direction: string, value: string) {
if (direction === 'left') { if (direction === 'left') {
leftListState.value.searchValue = value; leftListState.value.searchValue = value;
leftListState.value.pagination.current = 1; leftListState.value.pagination.current = 1;
@ -227,18 +273,18 @@ const handleUserSearch = async (direction: string, value: string) => {
rightListState.value.pagination.current = 1; rightListState.value.pagination.current = 1;
updateRightListData(); updateRightListData();
} }
}; }
// //
const handleUserChange = (targetKeys: string[]) => { function handleUserChange(targetKeys: string[]) {
// 使 Set ID // 使 Set ID
selectedUserIds.value = [...new Set(targetKeys)]; selectedUserIds.value = [...new Set(targetKeys)];
emit('update:value', selectedUserIds.value.map(Number)); emit('update:value', selectedUserIds.value.map(Number));
updateRightListData(); updateRightListData();
}; }
// //
const resetData = () => { function resetData() {
userList.value = []; userList.value = [];
selectedUserIds.value = []; selectedUserIds.value = [];
@ -249,7 +295,6 @@ const resetData = () => {
selectedUserIds.value = []; selectedUserIds.value = [];
leftListState.value = { leftListState.value = {
loading: false,
searchValue: '', searchValue: '',
dataSource: [], dataSource: [],
pagination: { pagination: {
@ -268,61 +313,20 @@ const resetData = () => {
total: 0, total: 0,
}, },
}; };
};
//
const open = async (userIds: string[]) => {
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);
//
await loadUserData(1, leftListState.value.pagination.pageSize);
//
if (userIds?.length) {
selectedUserIds.value = userIds.map(String);
// TODO ID
const { list } = await getUserPage({
pageNo: 1,
pageSize: 100, // 使
userIds,
});
// 使 Map ID key
const userMap = new Map(userList.value.map((user) => [user.id, user]));
list.forEach((user) => {
if (!userMap.has(user.id)) {
userMap.set(user.id, user);
} }
});
userList.value = [...userMap.values()];
updateRightListData();
}
modalApi.open();
} finally {
loading.value = false;
}
};
// TODO username // TODO username
const filterOption = (inputValue: string, option: any) => { function filterOption(inputValue: string, option: any) {
return option.username.toLowerCase().includes(inputValue.toLowerCase()); return option.username.toLowerCase().includes(inputValue.toLowerCase());
}; }
// / // /
const handleExpand = (keys: Key[]) => { function handleExpand(keys: Key[]) {
expandedKeys.value = keys; expandedKeys.value = keys;
}; }
// //
const handleDeptSearch = (value: string) => { function handleDeptSearch(value: string) {
deptSearchKeys.value = value; deptSearchKeys.value = value;
// //
@ -342,10 +346,10 @@ const handleDeptSearch = (value: string) => {
// //
expandedKeys.value = deptTree.value.map((node) => node.key); expandedKeys.value = deptTree.value.map((node) => node.key);
} }
}; }
// //
const handleDeptSelect = async (selectedKeys: Key[], _info: any) => { async function handleDeptSelect(selectedKeys: Key[], _info: any) {
// ID // ID
const newDeptId = const newDeptId =
selectedKeys.length > 0 ? Number(selectedKeys[0]) : undefined; selectedKeys.length > 0 ? Number(selectedKeys[0]) : undefined;
@ -356,10 +360,10 @@ const handleDeptSelect = async (selectedKeys: Key[], _info: any) => {
const { pageSize } = leftListState.value.pagination; const { pageSize } = leftListState.value.pagination;
leftListState.value.pagination.current = 1; leftListState.value.pagination.current = 1;
await loadUserData(1, pageSize); await loadUserData(1, pageSize);
}; }
// //
const handleConfirm = () => { function handleConfirm() {
if (selectedUserIds.value.length === 0) { if (selectedUserIds.value.length === 0) {
message.warning('请选择用户'); message.warning('请选择用户');
return; return;
@ -371,50 +375,37 @@ const handleConfirm = () => {
), ),
); );
modalApi.close(); modalApi.close();
}; }
// //
const handleCancel = () => { function handleCancel() {
emit('cancel'); emit('cancel');
modalApi.close(); modalApi.close();
// //
setTimeout(() => { setTimeout(() => {
resetData(); resetData();
}, 300); }, 300);
}; }
// //
const handleClosed = () => { function handleClosed() {
emit('closed'); emit('closed');
resetData(); resetData();
}; }
//
const [ModalComponent, modalApi] = useVbenModal({
title: props.title,
onCancel: handleCancel,
onClosed: handleClosed,
destroyOnClose: true,
});
// //
const processDeptNode = (node: any): DeptTreeNode => { function processDeptNode(node: any): DeptTreeNode {
return { return {
key: String(node.id), key: String(node.id),
title: `${node.name} (${node.id})`, title: `${node.name} (${node.id})`,
name: node.name, name: node.name,
children: node.children?.map((child: any) => processDeptNode(child)), children: node.children?.map((child: any) => processDeptNode(child)),
}; };
}; }
defineExpose({
open,
});
</script> </script>
<template> <template>
<ModalComponent class="w-[1000px]" key="user-select-modal"> <Modal class="w-[40%]" key="user-select-modal" :title="title">
<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"> <div class="h-[500px] overflow-auto rounded border">
@ -479,7 +470,6 @@ defineExpose({
</Transfer> </Transfer>
</Col> </Col>
</Row> </Row>
</Spin>
<template #footer> <template #footer>
<Button <Button
type="primary" type="primary"
@ -490,7 +480,7 @@ defineExpose({
</Button> </Button>
<Button @click="handleCancel">{{ cancelText }}</Button> <Button @click="handleCancel">{{ cancelText }}</Button>
</template> </template>
</ModalComponent> </Modal>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>