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

c-p-w [AddCommand.vue、OfficeUserSelector.vue、SelectorUserModal.vue、SelectOrgModal.vue]
选择成员

<!-- 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>
选择组织

<!-- 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
}
}
学而不思则罔,思而不学则殆!

浙公网安备 33010602011771号