[组件库] V3 + AntD Tree 用户\组织列表选择组件

image

c-p-w [AddCommand.vue、OfficeUserSelector.vue、SelectorUserModal.vue、SelectOrgModal.vue]

选择成员

image

<!-- src/components/Distribute/components/SelectUserModal.vue -->
<!-- src/components/Distribute/components/SelectUserModal.vue -->
<template>
	<a-modal
		v-model:visible="visible"
		:width="1400"
		title="选择成员"
		@cancel="handleCancel"
		@ok="handleOk"
	>
		<a-tabs v-model:activeKey="activeKey" @change="onTabChange">
			<a-tab-pane key="1" tab="当前组织"></a-tab-pane>
			<a-tab-pane key="2"  tab="全部组织"></a-tab-pane>
		</a-tabs>
		<div class="user-selector-container">
			<!-- 组织树部分 -->
			<div class="org-tree-panel">
				<h3>组织列表</h3>
				<a-input-search
					v-model:value="searchKeyword"
					placeholder="输入关键字进行过滤"
					style="margin-bottom: 16px; width: 100%"
					@change="filterOrgs"
				/>
				<a-spin :spinning="spinning">
					<div class="content-tree">
						<a-tree
							v-model:selectedKeys="selectedOrgKeys"
							:check-strictly="true"
							:default-expand-all="false"
							:tree-data="filteredOrgs"
							@select="onOrgSelect"
						/>
					</div>
				</a-spin>
			</div>
			<!-- 成员列表部分 -->
			<div class="user-list-panel">
				<h3>选择成员</h3>
				<a-spin :spinning="userLoading">
					{{selectedUserIds}}
					<div v-if="selectedOrgCode" class="user-list">
						<a-checkbox-group
							v-model:value="selectedUserIds"
							@change="onUserChange"
						>
							<div class="user-grid">
								<a-checkbox
									v-for="user in userList"
									:key="user.userCode"
									:title="user.name"
									:value="user.userCode"
									class="grid-item"
								>
									<span :title="user.name">{{ user.name }}</span>
								</a-checkbox>
							</div>
						</a-checkbox-group>
					</div>
					<div v-else class="no-org-selected">请选择一个组织</div>
				</a-spin>
			</div>
		</div>
	</a-modal>
</template>

<script setup>
import { ref, watch } from 'vue'
import { isEmpty } from 'lodash'
import { getUserList } from '@/api/user'
const emit = defineEmits(['update:visible', 'select', 'onTabChange'])
const props = defineProps({
	visible: {
		type: Boolean,
		default: false
	},
	orgData: {
		type: Array,
		default: () => []
	},
	orgSpinning: {
		type: Boolean,
		default: false
	}
})

const activeKey = ref('1')
const onTabChange = (key) => {
	activeKey.value = key
	emit('onTabChange', key === '1')
}


const visible = ref(false)
const searchKeyword = ref('')
const selectedOrgKeys = ref([]) // 使用 selectedKeys
const filteredOrgs = ref([])
const spinning = ref(true)
const selectedOrgCode = ref(null) // 当前选中的组织编码
const userList = ref([]) // 成员列表
const allSelectedUsers = ref([]) // 存储所有选中的用户,不随组织切换清空
const allSelectedUsersData = ref([])
const selectedUserIds = ref([]) // 选中的成员ID
// 保存上一次的选中值
const prevSelectedIds = ref([])

const userLoading = ref(false)
watch(
	() => allSelectedUsers.value,
	(newVal, oldVal) => {
		if (!isEmpty(newVal) && !isEmpty(userList.value)) {
			// 计算新增的用户
			const newlyAdded = newVal.filter(
				(userCode) => !oldVal || !oldVal.includes(userCode)
			)
			// 计算移除的用户
			const newlyRemoved =
				oldVal && oldVal.filter((userCode) => !newVal.includes(userCode))

			// 添加新选中的用户数据
			newlyAdded.forEach((userCode) => {
				const userData = userList.value.find(
					(item) => item.userCode === userCode
				)
				if (
					userData &&
					!allSelectedUsersData.value.some((user) => user.userCode === userCode)
				) {
					allSelectedUsersData.value.push({
						userCode: userData.userCode,
						userName: userData.name
					})
				}
			})

			// 移除取消选中的用户数据
			if (newlyRemoved) {
				newlyRemoved.forEach((userCode) => {
					const index = allSelectedUsersData.value.findIndex(
						(user) => user.userCode === userCode
					)
					if (index > -1) {
						allSelectedUsersData.value.splice(index, 1)
					}
				})
			}
		} else {
			allSelectedUsersData.value = []
		}
	},
	{ deep: true, immediate: true }
)

watch(
	[() => filteredOrgs.value, () => props.orgSpinning],
	([newFilteredOrgs, newOrgSpinning]) => {
		// 如果外部传入了orgSpinning,优先使用外部的值
		if (newOrgSpinning !== undefined) {
			spinning.value = newOrgSpinning
		} else {
			// 否则根据filteredOrgs是否为空来设置
			spinning.value = isEmpty(newFilteredOrgs)
		}
	},
	{ deep: true, immediate: true }
)
// 格式化组织树结构
const formatOrgTree = (orgs) => {
	return orgs.map((org) => {
		const children = org.subs
			? org.subs.map((child) => ({
					key: child.officeCode,
					title: child.officeName,
					value: child.officeCode,
					children: child.subs
						? child.subs.map((grandchild) => ({
								key: grandchild.officeCode,
								title: grandchild.officeName,
								value: grandchild.officeCode
						  }))
						: []
			  }))
			: []

		return {
			key: org.officeCode,
			title: org.officeName,
			value: org.officeCode,
			children: children
		}
	})
}
// 初始化数据
watch(
	() => props.orgData,
	(newVal) => {
		if (newVal && newVal.length > 0) {
			filteredOrgs.value = formatOrgTree(newVal)
		}
	},
	{ immediate: true }
)
// 过滤组织
const filterOrgs = () => {
	if (!searchKeyword.value) {
		filteredOrgs.value = formatOrgTree(props.orgData)
		return
	}

	const filtered = props.orgData
		.map((org) => {
			const matchedChildren =
				org.subs?.filter((child) =>
					child.officeName.includes(searchKeyword.value)
				) || []

			if (
				org.officeName.includes(searchKeyword.value) ||
				matchedChildren.length > 0
			) {
				return {
					key: org.officeCode,
					title: org.officeName,
					value: org.officeCode,
					children: matchedChildren.map((child) => ({
						key: child.officeCode,
						title: child.officeName,
						value: child.officeCode,
						children:
							child.subs?.map((grandchild) => ({
								key: grandchild.officeCode,
								title: grandchild.officeName,
								value: grandchild.officeCode
							})) || []
					}))
				}
			}
			return null
		})
		.filter((item) => item !== null)

	filteredOrgs.value = filtered
}

// 组织选择事件
const onOrgSelect = (selectedKeys, info) => {
	selectedOrgKeys.value = selectedKeys
	// 获取选中的组织编码(只取第一个,因为是单选)
	if (selectedKeys.length > 0) {
		const orgCode = selectedKeys[0]
		selectedOrgCode.value = orgCode
		loadUsers(orgCode)
	} else {
		// 当没有选中任何组织时
		selectedOrgCode.value = null
		userList.value = []
		selectedUserIds.value = []
	}
}
// 加载成员列表
const loadUsers = async (officeCode) => {
	userLoading.value = true
	try {
		const response = await getUserList({ officeCode })
		userList.value = response.data.data || []
		// 根据全局选中状态更新当前视图的选中状态
		selectedUserIds.value = userList.value
			.filter((user) => allSelectedUsers.value.includes(user.userCode))
			.map((user) => user.userCode)
	} catch (error) {
		console.error('加载成员列表失败:', error)
		userList.value = []
		selectedUserIds.value = []
	} finally {
		userLoading.value = false
	}
}

// 成员选择变化事件
const onUserChange = (value) => {
	// 找出取消选中的项(之前在选中状态,现在不在)
	const newlyUnchecked = prevSelectedIds.value.filter(
		val => !value.includes(val)
	)

	// 获取当前组织中的用户ID
	const currentOrgUserIds = userList.value.map((user) => user.userCode)
	// 找出新增的选中项
	const newlyChecked = value.filter(
		(userCode) => !allSelectedUsers.value.includes(userCode)
	)
	// 更新全局选中用户列表
	allSelectedUsers.value = allSelectedUsers.value
		.filter((userCode) => !newlyUnchecked.includes(userCode)) // 移除取消选中的
		.concat(newlyChecked) // 添加新增选中的

	// 更新上一次的选中值
	prevSelectedIds.value = [...value]
	// 更新当前视图的选中状态
	selectedUserIds.value = [...value]
}

// 确认选择
const handleOk = () => {
	emit('select', {
		userCode: allSelectedUsers.value, // 返回全局选中的用户,
		userData: allSelectedUsersData.value // 返回全局选中的用户数据
	})
	visible.value = false
}

// 取消选择
const handleCancel = () => {
	visible.value = false
}

// 暴露给父组件的方法
defineExpose({
	open: () => {
		visible.value = true
	},
	updateCheckedKeys: (keys) => {
		if (isEmpty(keys)) {
			selectedUserIds.value = []
			allSelectedUsers.value = []
			allSelectedUsersData.value = []
		} else {
			let removeUserCode = keys[0]
			selectedUserIds.value = selectedUserIds.value.filter(
				(item) => item !== removeUserCode
			)
			allSelectedUsers.value = selectedUserIds.value.filter(
				(item) => item !== removeUserCode
			)
			allSelectedUsersData.value = allSelectedUsersData.value.filter(
				(item) => item.userCode !== removeUserCode
			)
		}
	}
})
</script>

<style lang="scss" scoped>
.user-selector-container {
	display: flex;
	gap: 20px;
	height: 500px;
}
:deep(.ant-checkbox-wrapper) {
	margin: 0 !important;
}
.org-tree-panel,
.user-list-panel {
	flex: 1;
	background: #fff;
	padding: 16px;
	border: 1px solid #e8e8e8;
	border-radius: 4px;
	overflow: hidden;
}

.content-tree {
	min-height: 300px;
	max-height: 400px;
	overflow-y: auto;
}

.user-list {
	max-height: 400px;
	overflow-y: auto;
}

.no-org-selected {
	text-align: center;
	padding: 20px;
	color: #999;
}

h3 {
	margin-top: 0;
	margin-bottom: 16px;
	font-size: 16px;
	font-weight: bold;
}
.user-grid {
	display: grid;
	grid-template-columns: repeat(4, 1fr);
	gap: 4px;
}

.grid-item {
	max-width: 150px;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.grid-item:hover {
	background-color: #e6f7ff;
}
</style>

选择组织

image

<!-- src/components/Distribute/SelectOrgModal.vue -->
<!-- src/components/Distribute/SelectOrgModal.vue -->
<template>
	<a-modal
		v-model:visible="visible"
		title="组织列表"
		:width="700"
		@ok="handleOk"
		@cancel="handleCancel"
	>
		<a-tabs v-model:activeKey="activeKey" @change="onTabChange">
			<a-tab-pane key="1" tab="当前组织"></a-tab-pane>
			<a-tab-pane key="2" force-render tab="全部组织"></a-tab-pane>
		</a-tabs>
		<a-input-search
			v-model:value="searchKeyword"
			placeholder="输入关键字进行过滤"
			@change="filterOrgs"
			style="margin-bottom: 16px; width: 100%"
		/>
		<a-spin :spinning="spinning">
		<div class="content-tree">
			<a-tree
				v-model:checkedKeys="checkedKeys"
				checkable
				:tree-data="filteredOrgs"
				:default-expand-all="false"
				@check="onCheck"
				:check-strictly="true"
			/>
		</div>
		</a-spin>
	</a-modal>
</template>

<script setup>
import { ref, computed, watch } from 'vue'
import { isEmpty } from 'lodash'

const props = defineProps({
	visible: {
		type: Boolean,
		default: false
	},
	orgData: {
		type: Array,
		default: () => []
	},
	orgSpinning: {
		type: Boolean,
		default: false
	}
})

const emit = defineEmits(['update:visible', 'select', 'onTabChange'])

const visible = ref(false)
const searchKeyword = ref('')
const checkedKeys = ref([])
const filteredOrgs = ref([])
const spinning = ref(true)
// 初始化数据
watch(() => props.orgData, (newVal) => {
	if (newVal && newVal.length > 0) {
		filteredOrgs.value = formatOrgTree(newVal)
	}
}, { immediate: true })
watch(
	[() => filteredOrgs.value, () => props.orgSpinning],
	([newFilteredOrgs, newOrgSpinning]) => {
		// 如果外部传入了orgSpinning,优先使用外部的值
		if (newOrgSpinning !== undefined) {
			spinning.value = newOrgSpinning
		} else {
			// 否则根据filteredOrgs是否为空来设置
			spinning.value = isEmpty(newFilteredOrgs)
		}
	},
	{ deep: true, immediate: true }
)

const activeKey = ref('1')
const onTabChange = (key) => {
	activeKey.value = key
	emit('onTabChange', key === '1')
}
// 格式化组织树结构
const formatOrgTree = (orgs) => {
	return orgs.map(org => {
		const children = org.subs ? org.subs.map(child => ({
			key: child.officeCode,
			title: child.officeName,
			value: child.officeCode,
			children: child.subs ? child.subs.map(grandchild => ({
				key: grandchild.officeCode,
				title: grandchild.officeName,
				value: grandchild.officeCode
			})) : []
		})) : []

		return {
			key: org.officeCode,
			title: org.officeName,
			value: org.officeCode,
			children: children
		}
	})
}

// 过滤组织
const filterOrgs = () => {
	if (!searchKeyword.value) {
		filteredOrgs.value = formatOrgTree(props.orgData)
		return
	}

	const filtered = props.orgData.map(org => {
		const matchedChildren = org.subs?.filter(child =>
			child.officeName.includes(searchKeyword.value)
		) || []

		if (org.officeName.includes(searchKeyword.value) || matchedChildren.length > 0) {
			return {
				key: org.officeCode,
				title: org.officeName,
				value: org.officeCode,
				children: matchedChildren.map(child => ({
					key: child.officeCode,
					title: child.officeName,
					value: child.officeCode,
					children: child.subs?.map(grandchild => ({
						key: grandchild.officeCode,
						title: grandchild.officeName,
						value: grandchild.officeCode
					})) || []
				}))
			}
		}
		return null
	}).filter(item => item !== null)

	filteredOrgs.value = filtered
}

// 选择事件
const onCheck = (checkedKeysValue) => {
	checkedKeys.value = checkedKeysValue.checked
}

// 确认选择
const handleOk = () => {
	emit('select', checkedKeys.value)
	visible.value = false
}

// 取消选择
const handleCancel = () => {
	activeKey.value = '1'
	visible.value = false
}

// 暴露给父组件的方法
defineExpose({
	checkedKeys,
	open: () => {
		visible.value = true
	},
	updateCheckedKeys: (keys) => {
		checkedKeys.value = keys
		emit('select', checkedKeys.value)
	}
})
</script>
<style>
.content-tree {
	min-height: 300px;
	max-height: 612px;
	overflow-y: auto;
}
</style>

Hook

* useOrgSelector.js
// src/hooks/useOrgSelector.js
import { ref, computed } from 'vue'

export function useOrgSelector(orgData) {
	const selectedOrgs = ref([]) // 存储选中的组织名称
	const orgSelectorRef = ref(null) // 组织选择器引用

	// 打开组织选择器
	const openOrgSelector = () => {
		orgSelectorRef.value.open()
	}

	const removeAllOrg = () => {
		selectedOrgs.value = []
		orgSelectorRef.value.updateCheckedKeys([])
	}
	// 移除已选组织
	const removeOrg = (officeCode, index) => {
		selectedOrgs.value = selectedOrgs.value.filter(item => item.officeCode !== officeCode)
		let filteredKeys = orgSelectorRef.value.checkedKeys.filter(key => key !== officeCode)
		orgSelectorRef.value.updateCheckedKeys(filteredKeys)
	}


	// 处理组织选择结果
	const handleOrgSelect = (selectedKeys) => {
		// 获取选中的组织名称
		const selectedNames = getSelectedOrgNames(selectedKeys)
		selectedOrgs.value = [...selectedNames]
	}

	// 根据选中的 key 获取组织名称
	const getSelectedOrgNames = (keys) => {
		const names = []

		keys.forEach(key => {
			const org = findOrgById(key, orgData.value)
			if (org) {
				names.push({
					officeName: org.officeName,
					officeCode: org.officeCode
				})
			}
		})
		return names
	}

	// 递归查找组织
	const findOrgById = (officeCode, orgs) => {
		for (let org of orgs) {
			if (org.officeCode === officeCode) {
				return org
			}
			if (org.subs) {
				const found = findOrgById(officeCode, org.subs)
				if (found) {
					return found
				}
			}
		}
		return null
	}

	return {
		selectedOrgs,
		orgSelectorRef,
		openOrgSelector,
		removeOrg,
		handleOrgSelect,
		getSelectedOrgNames,
		findOrgById,
		removeAllOrg
	}
}

* useUserSelector.js
// src/components/Distribute/extends/useUserSelector.js
import { ref, computed } from 'vue'

export function useUserSelector(orgData) {
	const selectedUsers = ref([]) // 存储选中的成员
	const selectedMembers = ref([])
	const userSelectorRef = ref(null) // 成员选择器引用

	// 打开成员选择器
	const openUserSelector = () => {
		userSelectorRef.value.open()
	}

	// 移除已选成员
	const removeUser = (userInfo, index) => {
		userSelectorRef.value.updateCheckedKeys([userInfo.userCode])
		selectedMembers.value = selectedMembers.value.filter(item => item.userCode !== userInfo.userCode)
		selectedUsers.value = selectedUsers.value.filter(item => item !== userInfo.userCode)
	}

	// 处理成员选择结果
	const handleUserSelect = (selectedData) => {
		// 获取选中的成员信息
		selectedMembers.value = selectedData.userData
		selectedUsers.value = [... selectedData.userCode]
	}



	// 根据用户ID获取用户信息(需要根据实际情况实现)
	const getUserById = (userId) => {
		// 这里需要根据实际的用户数据源实现
		// 可以从全局用户列表中查找,或者调用API
		return {
			id: userId,
			name: `用户${userId}`,
			// 其他用户属性...
		}
	}

	const removeAllUser = () => {
		selectedUsers.value = []
		selectedMembers.value = []
		userSelectorRef.value.updateCheckedKeys([])
	}

	// 暴露给父组件的方法
	return {
		selectedUsers,
		selectedMembers,
		userSelectorRef,
		openUserSelector,
		removeUser,
		handleUserSelect,
		getUserById,
		removeAllUser
	}
}


posted @ 2026-03-02 11:32  Felix_Openmind  阅读(7)  评论(0)    收藏  举报
*{cursor: url(https://files-cdn.cnblogs.com/files/morango/fish-cursor.ico),auto;}