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 { 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';
@ -41,24 +41,6 @@ const emit = defineEmits<{
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[] };
//
const deptTree = ref<DataNode[]>([]);
@ -67,25 +49,56 @@ const selectedDeptIds = ref<checkedKeys>([]);
//
const deptData = ref<SystemDeptApi.Dept[]>([]);
/** 打开对话框 */
const open = async (selectedList?: SystemDeptApi.Dept[]) => {
modalApi.open();
// //
if (selectedList?.length) {
const selectedIds = selectedList
.map((dept) => dept.id)
.filter((id): id is number => id !== undefined);
selectedDeptIds.value = props.checkStrictly
? {
checked: selectedIds,
halfChecked: [],
}
: selectedIds;
}
};
//
const [Modal, modalApi] = useVbenModal({
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 (data.selectedList?.length) {
const selectedIds = data.selectedList
.map((dept: SystemDeptApi.Dept) => dept.id)
.filter((id: number) => id !== undefined);
selectedDeptIds.value = props.checkStrictly
? {
checked: selectedIds,
halfChecked: [],
}
: selectedIds;
}
} finally {
modalApi.unlock();
}
},
destroyOnClose: true,
});
/** 处理选中状态变化 */
const handleCheck = () => {
function handleCheck() {
if (!props.multiple) {
//
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>
<template>
<Modal>
<Modal :title="title" key="dept-select-modal" class="w-[40%]">
<Row class="h-full">
<Col :span="24">
<Card class="h-full">
@ -153,9 +139,5 @@ defineExpose({ open });
</Card>
</Col>
</Row>
<template #footer>
<Button @click="handleCancel">{{ cancelText }}</Button>
<Button type="primary" @click="handleConfirm">{{ confirmText }}</Button>
</template>
</Modal>
</template>

View File

@ -17,7 +17,6 @@ import {
message,
Pagination,
Row,
Spin,
Transfer,
Tree,
} from 'ant-design-vue';
@ -66,16 +65,66 @@ const expandedKeys = ref<Key[]>([]);
const selectedDeptId = ref<number>();
const deptSearchKeys = ref('');
//
const loading = ref(false);
//
const userList = ref<SystemUserApi.User[]>([]); //
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({
loading: false,
searchValue: '',
dataSource: [] as SystemUserApi.User[],
pagination: {
@ -145,8 +194,7 @@ const filteredDeptTree = computed(() => {
});
//
const loadUserData = async (pageNo: number, pageSize: number) => {
leftListState.value.loading = true;
async function loadUserData(pageNo: number, pageSize: number) {
try {
const { list, total } = await getUserPage({
pageNo,
@ -167,13 +215,11 @@ const loadUserData = async (pageNo: number, pageSize: number) => {
if (newUsers.length > 0) {
userList.value.push(...newUsers);
}
} finally {
leftListState.value.loading = false;
}
};
} finally {}
}
//
const updateRightListData = () => {
function updateRightListData() {
// 使 Set ID
const uniqueSelectedIds = new Set(selectedUserIds.value);
@ -202,22 +248,22 @@ const updateRightListData = () => {
const endIndex = startIndex + pageSize;
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);
};
}
//
const handleRightPaginationChange = (page: number, pageSize: number) => {
function handleRightPaginationChange(page: number, pageSize: number) {
rightListState.value.pagination.current = page;
rightListState.value.pagination.pageSize = pageSize;
updateRightListData();
};
}
//
const handleUserSearch = async (direction: string, value: string) => {
async function handleUserSearch(direction: string, value: string) {
if (direction === 'left') {
leftListState.value.searchValue = value;
leftListState.value.pagination.current = 1;
@ -227,18 +273,18 @@ const handleUserSearch = async (direction: string, value: string) => {
rightListState.value.pagination.current = 1;
updateRightListData();
}
};
}
//
const handleUserChange = (targetKeys: string[]) => {
function handleUserChange(targetKeys: string[]) {
// 使 Set ID
selectedUserIds.value = [...new Set(targetKeys)];
emit('update:value', selectedUserIds.value.map(Number));
updateRightListData();
};
}
//
const resetData = () => {
function resetData() {
userList.value = [];
selectedUserIds.value = [];
@ -249,7 +295,6 @@ const resetData = () => {
selectedUserIds.value = [];
leftListState.value = {
loading: false,
searchValue: '',
dataSource: [],
pagination: {
@ -268,61 +313,20 @@ const resetData = () => {
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
const filterOption = (inputValue: string, option: any) => {
function filterOption(inputValue: string, option: any) {
return option.username.toLowerCase().includes(inputValue.toLowerCase());
};
}
// /
const handleExpand = (keys: Key[]) => {
function handleExpand(keys: Key[]) {
expandedKeys.value = keys;
};
}
//
const handleDeptSearch = (value: string) => {
function handleDeptSearch(value: string) {
deptSearchKeys.value = value;
//
@ -342,10 +346,10 @@ const handleDeptSearch = (value: string) => {
//
expandedKeys.value = deptTree.value.map((node) => node.key);
}
};
}
//
const handleDeptSelect = async (selectedKeys: Key[], _info: any) => {
async function handleDeptSelect(selectedKeys: Key[], _info: any) {
// ID
const newDeptId =
selectedKeys.length > 0 ? Number(selectedKeys[0]) : undefined;
@ -356,10 +360,10 @@ const handleDeptSelect = async (selectedKeys: Key[], _info: any) => {
const { pageSize } = leftListState.value.pagination;
leftListState.value.pagination.current = 1;
await loadUserData(1, pageSize);
};
}
//
const handleConfirm = () => {
function handleConfirm() {
if (selectedUserIds.value.length === 0) {
message.warning('请选择用户');
return;
@ -371,115 +375,101 @@ const handleConfirm = () => {
),
);
modalApi.close();
};
}
//
const handleCancel = () => {
function handleCancel() {
emit('cancel');
modalApi.close();
//
setTimeout(() => {
resetData();
}, 300);
};
}
//
const handleClosed = () => {
function handleClosed() {
emit('closed');
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 {
key: String(node.id),
title: `${node.name} (${node.id})`,
name: node.name,
children: node.children?.map((child: any) => processDeptNode(child)),
};
};
defineExpose({
open,
});
}
</script>
<template>
<ModalComponent class="w-[1000px]" key="user-select-modal">
<Spin :spinning="loading">
<Row :gutter="[16, 16]">
<Col :span="6">
<div class="h-[500px] overflow-auto rounded border">
<div class="border-b p-2">
<Input
v-model:value="deptSearchKeys"
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"
<Modal class="w-[40%]" key="user-select-modal" :title="title">
<Row :gutter="[16, 16]">
<Col :span="6">
<div class="h-[500px] overflow-auto rounded border">
<div class="border-b p-2">
<Input
v-model:value="deptSearchKeys"
placeholder="搜索部门"
allow-clear
@input="(e) => handleDeptSearch(e.target?.value ?? '')"
/>
</div>
</Col>
<Col :span="18">
<Transfer
:row-key="(record) => String(record.id)"
:data-source="transferDataSource"
v-model:target-keys="selectedUserIds"
:titles="['未选', '已选']"
:show-search="true"
:show-select-all="true"
:filter-option="filterOption"
@change="handleUserChange"
@search="handleUserSearch"
>
<template #render="item">
<span>{{ item?.nickname }} ({{ item?.username }})</span>
</template>
<Tree
:tree-data="filteredDeptTree"
:expanded-keys="expandedKeys"
:selected-keys="selectedDeptId ? [String(selectedDeptId)] : []"
@select="handleDeptSelect"
@expand="handleExpand"
/>
</div>
</Col>
<Col :span="18">
<Transfer
:row-key="(record) => String(record.id)"
:data-source="transferDataSource"
v-model:target-keys="selectedUserIds"
:titles="['未选', '已选']"
:show-search="true"
:show-select-all="true"
:filter-option="filterOption"
@change="handleUserChange"
@search="handleUserSearch"
>
<template #render="item">
<span>{{ item?.nickname }} ({{ item?.username }})</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>
<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>
</Transfer>
</Col>
</Row>
</Spin>
<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>
</Transfer>
</Col>
</Row>
<template #footer>
<Button
type="primary"
@ -490,7 +480,7 @@ defineExpose({
</Button>
<Button @click="handleCancel">{{ cancelText }}</Button>
</template>
</ModalComponent>
</Modal>
</template>
<style lang="scss" scoped>