feat: [bpm][ele] 用户选择弹窗,部门选择弹窗组件迁移
This commit is contained in:
@@ -9,6 +9,7 @@ import type { SystemUserApi } from '#/api/system/user';
|
|||||||
|
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
@@ -26,8 +27,9 @@ import {
|
|||||||
ElTooltip,
|
ElTooltip,
|
||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
|
|
||||||
// import { DeptSelectModal, UserSelectModal } from '#/components/select-modal';
|
|
||||||
import { ImageUpload } from '#/components/upload';
|
import { ImageUpload } from '#/components/upload';
|
||||||
|
import { DeptSelectModal } from '#/views/system/dept/components';
|
||||||
|
import { UserSelectModal } from '#/views/system/user/components';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
categoryList: {
|
categoryList: {
|
||||||
@@ -44,15 +46,15 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// const [UserSelectModalComp, userSelectModalApi] = useVbenModal({
|
const [UserSelectModalComp, userSelectModalApi] = useVbenModal({
|
||||||
// connectedComponent: UserSelectModal,
|
connectedComponent: UserSelectModal,
|
||||||
// destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
// });
|
});
|
||||||
|
|
||||||
// const [DeptSelectModalComp, deptSelectModalApi] = useVbenModal({
|
const [DeptSelectModalComp, deptSelectModalApi] = useVbenModal({
|
||||||
// connectedComponent: DeptSelectModal,
|
connectedComponent: DeptSelectModal,
|
||||||
// destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
// });
|
});
|
||||||
|
|
||||||
const formRef = ref(); // 表单引用
|
const formRef = ref(); // 表单引用
|
||||||
const modelData = defineModel<any>(); // 创建本地数据副本
|
const modelData = defineModel<any>(); // 创建本地数据副本
|
||||||
@@ -125,21 +127,22 @@ function openStartUserSelect() {
|
|||||||
selectedUsers.value = selectedStartUsers.value.map(
|
selectedUsers.value = selectedStartUsers.value.map(
|
||||||
(user) => user.id,
|
(user) => user.id,
|
||||||
) as number[];
|
) as number[];
|
||||||
// userSelectModalApi.setData({ userIds: selectedUsers.value }).open();
|
userSelectModalApi.setData({ userIds: selectedUsers.value }).open();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 打开部门选择 */
|
/** 打开部门选择 */
|
||||||
function openStartDeptSelect() {
|
function openStartDeptSelect() {
|
||||||
// deptSelectModalApi.setData({ selectedList: selectedStartDepts.value }).open();
|
deptSelectModalApi.setData({ selectedList: selectedStartDepts.value }).open();
|
||||||
}
|
}
|
||||||
|
|
||||||
// /** 处理部门选择确认 */
|
/** 处理部门选择确认 */
|
||||||
// function handleDeptSelectConfirm(depts: SystemDeptApi.Dept[]) {
|
function handleDeptSelectConfirm(depts: SystemDeptApi.Dept[]) {
|
||||||
// modelData.value = {
|
selectedStartDepts.value = depts;
|
||||||
// ...modelData.value,
|
modelData.value = {
|
||||||
// startDeptIds: depts.map((d) => d.id),
|
...modelData.value,
|
||||||
// };
|
startDeptIds: depts.map((d) => d.id),
|
||||||
// }
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** 打开管理员选择 */
|
/** 打开管理员选择 */
|
||||||
function openManagerUserSelect() {
|
function openManagerUserSelect() {
|
||||||
@@ -147,32 +150,32 @@ function openManagerUserSelect() {
|
|||||||
selectedUsers.value = selectedManagerUsers.value.map(
|
selectedUsers.value = selectedManagerUsers.value.map(
|
||||||
(user) => user.id,
|
(user) => user.id,
|
||||||
) as number[];
|
) as number[];
|
||||||
// userSelectModalApi.setData({ userIds: selectedUsers.value }).open();
|
userSelectModalApi.setData({ userIds: selectedUsers.value }).open();
|
||||||
}
|
}
|
||||||
|
|
||||||
// /** 处理用户选择确认 */
|
/** 处理用户选择确认 */
|
||||||
// function handleUserSelectConfirm(userList: SystemUserApi.User[]) {
|
function handleUserSelectConfirm(userList: SystemUserApi.User[]) {
|
||||||
// modelData.value =
|
modelData.value =
|
||||||
// currentSelectType.value === 'start'
|
currentSelectType.value === 'start'
|
||||||
// ? {
|
? {
|
||||||
// ...modelData.value,
|
...modelData.value,
|
||||||
// startUserIds: userList.map((u) => u.id),
|
startUserIds: userList.map((u) => u.id),
|
||||||
// }
|
}
|
||||||
// : {
|
: {
|
||||||
// ...modelData.value,
|
...modelData.value,
|
||||||
// managerUserIds: userList.map((u) => u.id),
|
managerUserIds: userList.map((u) => u.id),
|
||||||
// };
|
};
|
||||||
// }
|
}
|
||||||
|
|
||||||
// /** 用户选择弹窗关闭 */
|
/** 用户选择弹窗关闭 */
|
||||||
// function handleUserSelectClosed() {
|
function handleUserSelectClosed() {
|
||||||
// selectedUsers.value = [];
|
selectedUsers.value = [];
|
||||||
// }
|
}
|
||||||
|
|
||||||
// /** 用户选择弹窗取消 */
|
/** 用户选择弹窗取消 */
|
||||||
// function handleUserSelectCancel() {
|
function handleUserSelectCancel() {
|
||||||
// selectedUsers.value = [];
|
selectedUsers.value = [];
|
||||||
// }
|
}
|
||||||
|
|
||||||
/** 处理发起人类型变化 */
|
/** 处理发起人类型变化 */
|
||||||
function handleStartUserTypeChange(value: number) {
|
function handleStartUserTypeChange(value: number) {
|
||||||
@@ -285,7 +288,12 @@ defineExpose({ validate });
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="流程图标">
|
<ElFormItem label="流程图标">
|
||||||
<ImageUpload v-model:value="modelData.icon" />
|
<ImageUpload
|
||||||
|
v-model:value="modelData.icon"
|
||||||
|
:show-description="false"
|
||||||
|
:width="120"
|
||||||
|
:height="120"
|
||||||
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="流程描述" prop="description">
|
<ElFormItem label="流程描述" prop="description">
|
||||||
<ElInput v-model="modelData.description" type="textarea" clearable />
|
<ElInput v-model="modelData.description" type="textarea" clearable />
|
||||||
@@ -424,19 +432,19 @@ defineExpose({ validate });
|
|||||||
</ElForm>
|
</ElForm>
|
||||||
|
|
||||||
<!-- 用户选择弹窗 -->
|
<!-- 用户选择弹窗 -->
|
||||||
<!-- <UserSelectModalComp
|
<UserSelectModalComp
|
||||||
class="w-3/5"
|
class="w-3/5"
|
||||||
v-model:value="selectedUsers"
|
v-model:value="selectedUsers"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
@confirm="handleUserSelectConfirm"
|
@confirm="handleUserSelectConfirm"
|
||||||
@closed="handleUserSelectClosed"
|
@closed="handleUserSelectClosed"
|
||||||
@cancel="handleUserSelectCancel"
|
@cancel="handleUserSelectCancel"
|
||||||
/> -->
|
/>
|
||||||
<!-- 部门选择对话框 -->
|
<!-- 部门选择对话框 -->
|
||||||
<!-- <DeptSelectModalComp
|
<DeptSelectModalComp
|
||||||
class="w-3/5"
|
class="w-3/5"
|
||||||
:check-strictly="true"
|
:check-strictly="true"
|
||||||
@confirm="handleDeptSelectConfirm"
|
@confirm="handleDeptSelectConfirm"
|
||||||
/> -->
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
// TODO @芋艿:是否有更好的组织形式?!
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemDeptApi } from '#/api/system/dept';
|
||||||
|
|
||||||
|
import { nextTick, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { handleTree } from '@vben/utils';
|
||||||
|
|
||||||
|
import { ElCard, ElCol, ElRow, ElTree } from 'element-plus';
|
||||||
|
|
||||||
|
import { getSimpleDeptList } from '#/api/system/dept';
|
||||||
|
|
||||||
|
defineOptions({ name: 'DeptSelectModal' });
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
// 取消按钮文本
|
||||||
|
cancelText?: string;
|
||||||
|
// checkable 状态下节点选择完全受控
|
||||||
|
checkStrictly?: boolean;
|
||||||
|
// 确认按钮文本
|
||||||
|
confirmText?: string;
|
||||||
|
// 是否支持多选
|
||||||
|
multiple?: boolean;
|
||||||
|
// 标题
|
||||||
|
title?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
cancelText: '取消',
|
||||||
|
checkStrictly: false,
|
||||||
|
confirmText: '确认',
|
||||||
|
multiple: true,
|
||||||
|
title: '部门选择',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
confirm: [deptList: SystemDeptApi.Dept[]];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 部门树形结构
|
||||||
|
const deptTree = ref<any[]>([]);
|
||||||
|
// 选中的部门 ID 列表
|
||||||
|
const selectedDeptIds = ref<number[]>([]);
|
||||||
|
// 部门数据
|
||||||
|
const deptData = ref<SystemDeptApi.Dept[]>([]);
|
||||||
|
// Tree 组件引用
|
||||||
|
const treeRef = ref();
|
||||||
|
|
||||||
|
// 对话框配置
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
// 获取选中的部门ID
|
||||||
|
const selectedIds: number[] = props.checkStrictly
|
||||||
|
? treeRef.value?.getCheckedKeys() || []
|
||||||
|
: selectedDeptIds.value;
|
||||||
|
|
||||||
|
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);
|
||||||
|
// 等待 DOM 更新后再设置选中的节点
|
||||||
|
await nextTick();
|
||||||
|
// 设置已选择的部门
|
||||||
|
if (data.selectedList?.length) {
|
||||||
|
const selectedIds = data.selectedList
|
||||||
|
.map((dept: SystemDeptApi.Dept) => dept.id)
|
||||||
|
.filter((id: number) => id !== undefined);
|
||||||
|
selectedDeptIds.value = selectedIds;
|
||||||
|
treeRef.value.setCheckedKeys(selectedIds);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 处理选中状态变化 */
|
||||||
|
function handleCheck(
|
||||||
|
_data: any,
|
||||||
|
{ checkedKeys }: { checkedKeys: (number | string)[] },
|
||||||
|
) {
|
||||||
|
// 确保 checkedKeys 都是 number 类型
|
||||||
|
const keys = checkedKeys.map((key) =>
|
||||||
|
typeof key === 'string' ? Number(key) : key,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (props.multiple) {
|
||||||
|
selectedDeptIds.value = keys;
|
||||||
|
} else {
|
||||||
|
// 单选模式下,只保留最后选择的节点
|
||||||
|
const lastSelectedId = keys[keys.length - 1];
|
||||||
|
if (lastSelectedId) {
|
||||||
|
selectedDeptIds.value = [lastSelectedId];
|
||||||
|
treeRef.value?.setCheckedKeys([lastSelectedId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Modal :title="title" key="dept-select-modal" class="w-3/5">
|
||||||
|
<ElRow class="h-full">
|
||||||
|
<ElCol :span="24">
|
||||||
|
<ElCard class="h-full">
|
||||||
|
<ElTree
|
||||||
|
v-if="deptTree.length > 0"
|
||||||
|
ref="treeRef"
|
||||||
|
:data="deptTree"
|
||||||
|
:props="{ label: 'name', children: 'children' }"
|
||||||
|
:check-strictly="checkStrictly"
|
||||||
|
:default-expand-all="true"
|
||||||
|
show-checkbox
|
||||||
|
check-on-click-node
|
||||||
|
node-key="id"
|
||||||
|
@check="handleCheck"
|
||||||
|
/>
|
||||||
|
</ElCard>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
2
apps/web-ele/src/views/system/dept/components/index.ts
Normal file
2
apps/web-ele/src/views/system/dept/components/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// TODO @xingyu:【待讨论】是不是把 user select 放到 user 目录的 components 下,dept select 放到 dept 目录的 components 下
|
||||||
|
export { default as DeptSelectModal } from './dept-select-modal.vue';
|
||||||
1
apps/web-ele/src/views/system/user/components/index.ts
Normal file
1
apps/web-ele/src/views/system/user/components/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as UserSelectModal } from './user-select-modal.vue';
|
||||||
@@ -0,0 +1,518 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
// TODO @芋艿:是否有更好的组织形式?!
|
||||||
|
// TODO @xingyu:你感觉,这个放到每个 system、infra 模块下,然后新建一个 components,表示每个模块,有一些共享的组件?然后,全局只放通用的(无业务含义的),可以哇?
|
||||||
|
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 {
|
||||||
|
ElButton,
|
||||||
|
ElCol,
|
||||||
|
ElInput,
|
||||||
|
ElMessage,
|
||||||
|
ElPagination,
|
||||||
|
ElRow,
|
||||||
|
ElTransfer,
|
||||||
|
ElTree,
|
||||||
|
} from 'element-plus';
|
||||||
|
|
||||||
|
import { getSimpleDeptList } from '#/api/system/dept';
|
||||||
|
import { getUserPage } from '#/api/system/user';
|
||||||
|
|
||||||
|
// 部门树节点接口
|
||||||
|
interface DeptTreeNode {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
children?: DeptTreeNode[];
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({ name: 'UserSelectModal' });
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
cancelText?: string;
|
||||||
|
confirmText?: string;
|
||||||
|
multiple?: boolean;
|
||||||
|
title?: string;
|
||||||
|
value?: number[];
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
title: '选择用户',
|
||||||
|
multiple: true,
|
||||||
|
value: () => [],
|
||||||
|
confirmText: '确定',
|
||||||
|
cancelText: '取消',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
cancel: [];
|
||||||
|
closed: [];
|
||||||
|
confirm: [value: SystemUserApi.User[]];
|
||||||
|
'update:value': [value: number[]];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 部门树数据
|
||||||
|
const deptTree = ref<any[]>([]);
|
||||||
|
const deptList = ref<SystemDeptApi.Dept[]>([]);
|
||||||
|
const expandedKeys = ref<string[]>([]);
|
||||||
|
const selectedDeptId = ref<number>();
|
||||||
|
const deptSearchKeys = ref('');
|
||||||
|
|
||||||
|
// 用户数据管理
|
||||||
|
const userList = ref<SystemUserApi.User[]>([]); // 存储所有已知用户
|
||||||
|
const selectedUserIds = ref<number[]>([]);
|
||||||
|
|
||||||
|
// 弹窗配置
|
||||||
|
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.id);
|
||||||
|
|
||||||
|
// 加载初始用户数据
|
||||||
|
await loadUserData(1, leftListState.value.pagination.pageSize);
|
||||||
|
|
||||||
|
// 设置已选用户
|
||||||
|
if (data.userIds?.length) {
|
||||||
|
selectedUserIds.value = data.userIds;
|
||||||
|
// 加载已选用户的完整信息 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({
|
||||||
|
searchValue: '',
|
||||||
|
dataSource: [] as SystemUserApi.User[],
|
||||||
|
pagination: {
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 右侧列表状态
|
||||||
|
const rightListState = ref({
|
||||||
|
searchValue: '',
|
||||||
|
dataSource: [] as SystemUserApi.User[],
|
||||||
|
pagination: {
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:Transfer 数据源
|
||||||
|
const transferDataSource = computed(() => {
|
||||||
|
// 使用 Map 来去重,确保每个用户只出现一次
|
||||||
|
const userMap = new Map<number, any>();
|
||||||
|
|
||||||
|
// 先添加左侧数据
|
||||||
|
for (const user of leftListState.value.dataSource) {
|
||||||
|
if (user.id) {
|
||||||
|
userMap.set(user.id, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再添加右侧数据(如果已存在则不会重复添加)
|
||||||
|
for (const user of rightListState.value.dataSource) {
|
||||||
|
if (user.id && !userMap.has(user.id)) {
|
||||||
|
userMap.set(user.id, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为 Transfer 需要的格式
|
||||||
|
return [...userMap.values()].map((user) => ({
|
||||||
|
key: user.id!,
|
||||||
|
label: `${user.nickname} (${user.username})`,
|
||||||
|
disabled: false,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 过滤部门树数据
|
||||||
|
const filteredDeptTree = computed(() => {
|
||||||
|
if (!deptSearchKeys.value) return deptTree.value;
|
||||||
|
|
||||||
|
const filterNode = (node: any, depth = 0): any => {
|
||||||
|
// 添加深度限制,防止过深的递归导致爆栈
|
||||||
|
if (depth > 100) return null;
|
||||||
|
|
||||||
|
// 按部门名称搜索
|
||||||
|
const name = node?.name?.toLowerCase();
|
||||||
|
const search = deptSearchKeys.value.toLowerCase();
|
||||||
|
|
||||||
|
// 如果当前节点匹配,直接返回节点,不处理子节点
|
||||||
|
if (name?.includes(search)) {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
children: node.children,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果当前节点不匹配,检查子节点
|
||||||
|
if (node.children) {
|
||||||
|
const filteredChildren = node.children
|
||||||
|
.map((child: any) => filterNode(child, depth + 1))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (filteredChildren.length > 0) {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
children: filteredChildren,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return deptTree.value.map((node: any) => filterNode(node)).filter(Boolean);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载用户数据
|
||||||
|
async function loadUserData(pageNo: number, pageSize: number) {
|
||||||
|
try {
|
||||||
|
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 {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新右侧列表数据
|
||||||
|
function updateRightListData() {
|
||||||
|
// 使用 Set 来去重选中的用户ID
|
||||||
|
const uniqueSelectedIds = new Set(selectedUserIds.value);
|
||||||
|
|
||||||
|
// 获取选中的用户,确保不重复
|
||||||
|
const selectedUsers = userList.value.filter((user) =>
|
||||||
|
uniqueSelectedIds.has(user.id!),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 应用搜索过滤
|
||||||
|
const filteredUsers = rightListState.value.searchValue
|
||||||
|
? selectedUsers.filter((user) =>
|
||||||
|
user.nickname
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(rightListState.value.searchValue.toLowerCase()),
|
||||||
|
)
|
||||||
|
: selectedUsers;
|
||||||
|
|
||||||
|
// 更新总数(使用 Set 确保唯一性)
|
||||||
|
rightListState.value.pagination.total = new Set(
|
||||||
|
filteredUsers.map((user) => user.id),
|
||||||
|
).size;
|
||||||
|
|
||||||
|
// 应用分页
|
||||||
|
const { current, pageSize } = rightListState.value.pagination;
|
||||||
|
const startIndex = (current - 1) * pageSize;
|
||||||
|
const endIndex = startIndex + pageSize;
|
||||||
|
|
||||||
|
rightListState.value.dataSource = filteredUsers.slice(startIndex, endIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理左侧分页变化
|
||||||
|
async function handleLeftPaginationChange(page: number) {
|
||||||
|
await loadUserData(page, leftListState.value.pagination.pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理右侧分页变化
|
||||||
|
function handleRightPaginationChange(page: number) {
|
||||||
|
rightListState.value.pagination.current = page;
|
||||||
|
updateRightListData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理用户选择变化
|
||||||
|
function handleUserChange(
|
||||||
|
value: (number | string)[],
|
||||||
|
_direction: string,
|
||||||
|
_movedKeys: (number | string)[],
|
||||||
|
) {
|
||||||
|
// 使用 Set 来去重选中的用户ID,并确保转换为 number 类型
|
||||||
|
selectedUserIds.value = [...new Set(value.map(Number))];
|
||||||
|
emit('update:value', selectedUserIds.value);
|
||||||
|
updateRightListData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置数据
|
||||||
|
function resetData() {
|
||||||
|
userList.value = [];
|
||||||
|
selectedUserIds.value = [];
|
||||||
|
|
||||||
|
// 取消部门选中
|
||||||
|
selectedDeptId.value = undefined;
|
||||||
|
|
||||||
|
// 取消选中的用户
|
||||||
|
selectedUserIds.value = [];
|
||||||
|
|
||||||
|
leftListState.value = {
|
||||||
|
searchValue: '',
|
||||||
|
dataSource: [],
|
||||||
|
pagination: {
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
rightListState.value = {
|
||||||
|
searchValue: '',
|
||||||
|
dataSource: [],
|
||||||
|
pagination: {
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理部门搜索
|
||||||
|
function handleDeptSearch(value: string) {
|
||||||
|
deptSearchKeys.value = value;
|
||||||
|
|
||||||
|
// 如果有搜索结果,自动展开所有节点
|
||||||
|
if (value) {
|
||||||
|
const getAllKeys = (nodes: any[]): string[] => {
|
||||||
|
const keys: string[] = [];
|
||||||
|
for (const node of nodes) {
|
||||||
|
keys.push(node.id);
|
||||||
|
if (node.children) {
|
||||||
|
keys.push(...getAllKeys(node.children));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
};
|
||||||
|
expandedKeys.value = getAllKeys(deptTree.value);
|
||||||
|
} else {
|
||||||
|
// 清空搜索时,只展开第一级节点
|
||||||
|
expandedKeys.value = deptTree.value.map((node) => node.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理部门选择
|
||||||
|
async function handleDeptSelect(node: any) {
|
||||||
|
// 更新选中的部门ID
|
||||||
|
const newDeptId = node.id ? Number(node.id) : undefined;
|
||||||
|
selectedDeptId.value =
|
||||||
|
newDeptId === selectedDeptId.value ? undefined : newDeptId;
|
||||||
|
|
||||||
|
// 重置分页并加载数据
|
||||||
|
const { pageSize } = leftListState.value.pagination;
|
||||||
|
leftListState.value.pagination.current = 1;
|
||||||
|
await loadUserData(1, pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认选择
|
||||||
|
function handleConfirm() {
|
||||||
|
if (selectedUserIds.value.length === 0) {
|
||||||
|
ElMessage.warning('请选择用户');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit(
|
||||||
|
'confirm',
|
||||||
|
userList.value.filter((user) => selectedUserIds.value.includes(user.id!)),
|
||||||
|
);
|
||||||
|
modalApi.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消选择
|
||||||
|
function handleCancel() {
|
||||||
|
emit('cancel');
|
||||||
|
modalApi.close();
|
||||||
|
// 确保在动画结束后再重置数据
|
||||||
|
setTimeout(() => {
|
||||||
|
resetData();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
function handleClosed() {
|
||||||
|
emit('closed');
|
||||||
|
resetData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归处理部门树节点
|
||||||
|
function processDeptNode(node: any): DeptTreeNode {
|
||||||
|
return {
|
||||||
|
id: String(node.id),
|
||||||
|
label: `${node.name} (${node.id})`,
|
||||||
|
name: node.name,
|
||||||
|
children: node.children?.map((child: any) => processDeptNode(child)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal key="user-select-modal" class="w-3/5" :title="title">
|
||||||
|
<ElRow :gutter="16">
|
||||||
|
<ElCol :span="6">
|
||||||
|
<div class="h-[500px] overflow-auto rounded border">
|
||||||
|
<div class="border-b p-2">
|
||||||
|
<ElInput
|
||||||
|
v-model="deptSearchKeys"
|
||||||
|
placeholder="搜索部门"
|
||||||
|
clearable
|
||||||
|
@input="handleDeptSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ElTree
|
||||||
|
:data="filteredDeptTree"
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
:default-expanded-keys="expandedKeys"
|
||||||
|
:current-node-key="
|
||||||
|
selectedDeptId ? String(selectedDeptId) : undefined
|
||||||
|
"
|
||||||
|
node-key="id"
|
||||||
|
highlight-current
|
||||||
|
@node-click="handleDeptSelect"
|
||||||
|
>
|
||||||
|
<template #default="{ node }">
|
||||||
|
<span>{{ node.label }}</span>
|
||||||
|
</template>
|
||||||
|
</ElTree>
|
||||||
|
</div>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="18">
|
||||||
|
<ElTransfer
|
||||||
|
v-model="selectedUserIds"
|
||||||
|
:data="transferDataSource"
|
||||||
|
:titles="['未选', '已选']"
|
||||||
|
filterable
|
||||||
|
filter-placeholder="搜索用户"
|
||||||
|
@change="handleUserChange"
|
||||||
|
>
|
||||||
|
<template #default="{ option }">
|
||||||
|
<span>{{ option.label }}</span>
|
||||||
|
</template>
|
||||||
|
</ElTransfer>
|
||||||
|
<div class="mt-2 flex justify-between">
|
||||||
|
<ElPagination
|
||||||
|
v-model:current-page="leftListState.pagination.current"
|
||||||
|
v-model:page-size="leftListState.pagination.pageSize"
|
||||||
|
:total="leftListState.pagination.total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
small
|
||||||
|
@current-change="handleLeftPaginationChange"
|
||||||
|
/>
|
||||||
|
<ElPagination
|
||||||
|
v-model:current-page="rightListState.pagination.current"
|
||||||
|
v-model:page-size="rightListState.pagination.pageSize"
|
||||||
|
:total="rightListState.pagination.total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
small
|
||||||
|
@current-change="handleRightPaginationChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="handleCancel">{{ cancelText }}</ElButton>
|
||||||
|
<ElButton
|
||||||
|
type="primary"
|
||||||
|
:disabled="selectedUserIds.length === 0"
|
||||||
|
@click="handleConfirm"
|
||||||
|
>
|
||||||
|
{{ confirmText }}
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-transfer) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-transfer-panel) {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-transfer-panel__header) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-transfer-panel__filter) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-transfer-panel__body) {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-transfer-panel__list) {
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-transfer__buttons) {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user