利用AntDesign中a-tree和checkbox构造组织单位人员树选择组件
业务效果图
核心代码
<template> <div class="select-container"> <a-modal v-model:visible="visible" @ok="handleOk" @cancel="handleCancel" width="1500px"> <template #title> <div style="font-weight: 600; font-size: 16px"> 选择任务协作者 </div> </template> <div class="tree-container" ref="treeContainerNodeRef"> <div class="tree-section01"> <div class="section01-left"> <a-input-search v-model:value="searchValue" style="margin-bottom: 8px" placeholder="搜索" @search="onSearch" /> <div class="tree-title">选择组织</div> <a-tree :tree-data="treeData" v-model:expandedKeys="expandedKeys" v-model:selectedKeys="selectedKeys" v-model:checkedKeys="checkedKeys" @select="selectOrgTreeNode" > </a-tree> </div> <div class="section01-right"> <a-divider type="vertical" style="height: 100%;" /> </div> </div> <div class="tree-section02" ref="treeSection02ScrollRef"> <div class="section-left"> <div class="tree-title">选择人员</div> <a-checkbox-group v-model:value="selectedUserData" name="checkboxgroup" :options="userDataOptions" @change="checkUserData" /> </div> <div class="section-right"> <a-divider type="vertical" style="height: 100%;" /> </div> </div> <div class="tree-section03"> <div class="tag-container"> <div style="font-weight: 600;height: 22px;line-height: 22px; position: relative; top: 3px; margin-bottom: 15px;"> 已选人员 </div> <div class="tag-content"> <a-tag closable class="tag-item" @close="closeTag(tag)" v-for="(tag) in tags" :key="nanoid()"> {{ tag.label }} </a-tag> </div> </div> </div> </div> </a-modal> </div> </template> <script setup> import {onMounted, ref, defineExpose, defineEmits, onBeforeUnmount, toRaw, watch, nextTick} from 'vue'; import {useStore} from 'vuex' import {nanoid} from 'nanoid' import mitt from '@/utils/mitt' import {getOfficeTree, getUserTree} from "@/api/user"; import lodash from 'lodash' const emit = defineEmits(); const store = useStore() const visible = ref(false); // 组织树数据源 const treeData = ref([]); // 用户树数据源 const userData = ref([]); // 所有用户数据 const globalUserDataOptions = ref([]); const userDataOptions = ref([]); // checkBoxUserData; const selectedUserData = ref([]); // 缓存数据 const cacheUserOptions = ref([]); const curTreeData = ref([]); const expandedKeys = ref([]); // 全局selectedKeys const globalUserData = ref([]); const selectedKeys = ref([]); const checkedKeys = ref([]); const tags = ref([]) const treeSection02ScrollRef = ref(null); const treeContainerNodeRef = ref(null); const executeFlag = ref(false); onMounted(() => { mitt.on('selectedUserDataCode', (data) => { selectedUserData.value = data; globalUserData.value = data; // 从缓存中搜寻 start let cacheUserOptionsData = JSON.parse(localStorage.getItem('cacheGlobalUserDataOptions')); if (cacheUserOptionsData !== null) { nextTick(() => { tags.value = cacheUserOptionsData.filter(v => globalUserData.value.some(val => val === v.value)) }) } // 从缓存中搜索 end }) mitt.on('deselectTag', (deselectTagValue) => { tags.value = tags.value.filter(item => item.value !== deselectTagValue); }) queryData(); }) // 滚动到容器顶部 const scrollToEditflag = (domNode, height) => { for (let i = 0; i < height + 1000; i++) { setTimeout(() => { domNode.value.scrollTo(0, i); }, 100); } }; // 组织架构树搜索 const searchValue = ref(''); const onSearch = (searchInputValue) => { if ('' === searchInputValue) { treeData.value = curTreeData.value; return; } treeData.value = mapTreeData(searchInputValue, curTreeData.value) }; const mapTreeData = (value, arr) => { const newarr = []; const keys = []; arr.forEach((element) => { if (element.title.indexOf(value) > -1) { newarr.push(element) keys.push(element.key) } else { // 遍历子层 if (element.children && element.children.length > 0) { const redata = mapTreeData(value, element.children); if (redata && redata.length > 0) { const obj = { ...element, children: redata }; keys.push(obj.key) newarr.push(obj); } } } }) expandedKeys.value = keys; return newarr } // 点击左侧组织架构树根据组织code获取所属用户 const selectOrgTreeNode = async (selectedKeys) => { // 重新得到selectUserData executeFlag.value = true; let officeCode = selectedKeys[0] let res = await getUserTree({officeCode: officeCode}); if (200 === res.status) { userDataOptions.value = []; userData.value = res.data.data; userData.value.forEach((user) => { let option = { label: user.userName, value: user.userCode, } userDataOptions.value.push(option) globalUserDataOptions.value.push(option); }) // selectUserData 是根据当前userDataOptions过滤globalUserData得到的 selectedUserData.value = userDataOptions.value.filter(v => globalUserData.value.some(val => val === v.value)).map(item => item.value); globalUserDataOptions.value = lodash.unionBy(globalUserDataOptions.value, "value"); // start 更新缓存 cacheGlobalUserDataOptions let cacheGlobalUserDataOptions = JSON.parse(localStorage.getItem('cacheGlobalUserDataOptions')); if (cacheGlobalUserDataOptions !== null) { let cacheData = cacheGlobalUserDataOptions; // 更新缓存数据并去重 cacheData = [...cacheData, ...globalUserDataOptions.value]; cacheData = lodash.unionBy(cacheData, "value"); localStorage.setItem('cacheGlobalUserDataOptions', JSON.stringify(cacheData)); } else { localStorage.setItem('cacheGlobalUserDataOptions', JSON.stringify(globalUserDataOptions.value)); } // end 更新缓存 } } // closeTag const closeTag = (tag) => { selectedUserData.value = selectedUserData.value.filter((key) => (key !== tag.value)); } // 左侧组织架构树 const queryData = async () => { let officeUserData = []; if (localStorage.getItem('officeUserData') === null) { let res = await getOfficeTree(); if (res.status === 200) { officeUserData = res.data.data; } localStorage.setItem('officeUserData', JSON.stringify(officeUserData)); } else { officeUserData = JSON.parse(localStorage.getItem('officeUserData')); } recursive(officeUserData); } // 根据数据源构建架构树 const recursive = (newList) => { newList.map((item => { item.title = item.officeName; item.key = item.officeCode; if (item.children !== undefined) { recursive(item.children) } })) treeData.value = newList; curTreeData.value = newList; }; const searchOfficeInfo = (userCode, treeDataParam) => { for (let i = 0; i < treeDataParam.length; i++) { if (treeDataParam[i].userCode === userCode) { return { label: treeDataParam[i].userName, value: treeDataParam[i].userCode, }; } else { let res = searchOfficeInfo(officeCode, treeDataParam[i].userVos); if (res != null) { return res; } } } } const checkUserData = (checkedValue) => { } watch(() => selectedUserData.value, (newVal, oldVal) => { // 会记录之前的selectedUserData let diff = newVal.concat(oldVal) .filter(v => !newVal.includes(v) || !oldVal.includes(v)); let resGlobalUserData = [] if(newVal.length === 0 && oldVal.length === 0) { return; } if (newVal.length > oldVal.length) { // 新增 resGlobalUserData = [...new Set([...globalUserData.value, ...diff])]; } if (newVal.length < oldVal.length) { if(executeFlag.value) { executeFlag.value = false; let cacheData = JSON.parse(localStorage.getItem("cacheGlobalUserDataOptions")); tags.value = cacheData.filter(v => globalUserData.value.some(val => val === v.value)) return; } resGlobalUserData = globalUserData.value.filter(item => item !== diff[0]); } globalUserData.value = resGlobalUserData; // 缓存 let cacheData = JSON.parse(localStorage.getItem("cacheGlobalUserDataOptions")); tags.value = cacheData.filter(v => globalUserData.value.some(val => val === v.value)) }, {deep: true}) // 显示弹窗 const showModal = () => { visible.value = true; }; const handleCancel = (e) => { visible.value = false; } const handleOk = (e) => { visible.value = false; emit('tagNodeData', tags.value); }; onBeforeUnmount(() => { mitt.off(); }) defineExpose({ showModal }); </script> <style lang="scss" scoped> .tree-container { display: flex; flex-direction: row; min-height: 400px; max-height: 650px; .tree-section01 { overflow-y: scroll; margin-right: 20px; display: flex; } .tree-section02 { display: flex; overflow-y: scroll; margin-right: 20px; padding-top: 35px; flex: 2; & :deep(.ant-checkbox-group) { display: flex !important; flex-direction: column !important; } } .tree-section03 { padding-top: 30px; flex: 3; } .section-left { width: 100%; } .tree-title { font-size: 14px; height: 22px; line-height: 22px; font-weight: 600; color: rgba(21, 22, 24, 0.92); } .tag-container { .tag-content { width: 100%; display: flex; flex-wrap: wrap; .tag-title { font-size: 14px; height: 22px; line-height: 22px; font-weight: 600; color: red; } .tag-item { margin: 2px 2px; } } } } </style>
学而不思则罔,思而不学则殆!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具