当前位置: 首页 > news >正文

Vue3+Ant-design-vue 实现树形穿梭框

1.需求:实现树形结构的穿梭框,并且可以左右来回穿梭,穿梭箭头也是跟着左右俩侧树形结构选中状态而高亮(也就是左侧树形结构选完后 穿梭向右箭头要高亮 相反 右侧树形结构选完后 穿梭左箭头要高亮),左侧树形结构穿梭后 左侧选中节点置灰

2.数据格式 与后端同学确认好 可以以我这个为例子

{"code": 0,"level": null,"msg": "操作成功","ok": true,"data": [{"departmentId": 4237,"departmentName": "最外层一级","parentDepartmentId": 157,"departmentType": "1001","haveFlag": true,"children": [{"departmentId": 4245,"departmentName": "里层一级","parentDepartmentId": 4237,"departmentType": "1002","haveFlag": true,"children": [{"departmentId": 4116,"departmentName": "里层二级","parentDepartmentId": 4245,"departmentType": "1","haveFlag": true,"children": []}]}]}],"dataType": 1
}

3.具体代码

现在我是把这个封装成组件了 以下会介绍具体封装及使用1.先介绍具体组件封装1-1 先上代码 完了再拆开细讲里面逻辑包括数据封装后端返回的格式应该是不满足组件自带的数据格式 所以需要对数据进行封装如果后端返回的满足组件自带数据格式 那就不需要封装1-2 代码<template><div ref="roleDataScope"><a-transferv-model:target-keys="targetKeys"@update:target-keys="handleTargetKeysChange"class="tree-transfer":data-source="dataSource":render="(item) => item.title":show-select-all="false"><template #children="{ direction, selectedKeys, onItemSelect }"><div class="look_css" v-if="direction === 'left'"><div class="txt_css">查看数据</div><a-input-search v-model:value="value" class="search_css" @search="handleSearch" placeholder="点击搜索有结果" /></div><template v-if="direction === 'left'"><template v-if="filteredTreeData.length > 0"><a-treeblock-nodecheckablecheck-strictlystyle="height: 500px; overflow-y: scroll":checked-keys="[...selectedKeys, ...targetKeys]":tree-data="filteredTreeData"@check="(_, props) => {onChecked(props, [...selectedKeys, ...targetKeys], onItemSelect);}"@select="(_, props) => {onChecked(props, [...selectedKeys, ...targetKeys], onItemSelect);}"/></template><a-empty v-else description="未找到相关数据" /></template><a-treev-else-if="targetKeys.length > 0"block-nodecheckablecheck-strictly:checked-keys="[...selectedKeys]":tree-data="findSelectedNodes(tData, targetKeys)"@check="(_, props) => {onChecked(props, [...selectedKeys], onItemSelect);}"@select="(_, props) => {onChecked(props, [...selectedKeys], onItemSelect);}"/></template></a-transfer></div>
</template>
<script setup>import { computed, watch, ref, inject, onMounted } from 'vue';import { departmentApi } from '/@/api/system/department-api';const selectRoleId = inject('selectRoleId');const emit = defineEmits(['update:selectedData']);// 转换接口数据到树形组件需要的格式function convertDepartmentData(data, selectedKeys = []) {return data.map((item) => {const newNode = {key: item.departmentId.toString(),title: item.departmentName,type: item.departmentType,...(item.haveFlag && { checked: true }),};if (item.haveFlag) {selectedKeys.push(item.departmentId.toString());}if (item.children && item.children.length > 0) {newNode.children = convertDepartmentData(item.children, selectedKeys);}return newNode;});}const value = ref('');const tData = ref([]);const transferDataSource = ref([]);const targetKeys = ref([]);function flatten(list = []) {list.forEach((item) => {transferDataSource.value.push(item);flatten(item.children);});}function isChecked(selectedKeys, eventKey) {return selectedKeys.indexOf(eventKey) !== -1;}function handleTreeData(treeNodes, targetKeys = []) {return treeNodes.map(({ children, ...props }) => ({...props,disabled: targetKeys.includes(props.key),children: handleTreeData(children ?? [], targetKeys),}));}// 查找已选节点并构建树形数据function findSelectedNodes(nodes, selectedKeys) {return nodes.map((node) => {const newNode = { ...node };if (node.children && node.children.length > 0) {newNode.children = findSelectedNodes(node.children, selectedKeys);}const isNodeSelected = selectedKeys.includes(node.key);const hasSelectedChild = newNode.children && newNode.children.length > 0;if (isNodeSelected || hasSelectedChild) {return newNode;}return null;}).filter(Boolean);}const dataSource = ref(transferDataSource.value);const treeData = computed(() => {return handleTreeData(tData.value, targetKeys.value);});// 过滤树形数据的函数function filterTree(nodes, keyword) {if (!keyword) return nodes;return nodes.map((node) => {const newNode = { ...node };if (node.children) {newNode.children = filterTree(node.children, keyword);}if (node.title.includes(keyword) || (newNode.children && newNode.children.length > 0)) {return newNode;}return null;}).filter(Boolean);}function handleSearch(val) {value.value = val;}// 计算属性,用于获取过滤后的树形数据const filteredTreeData = computed(() => {return filterTree(treeData.value, value.value);});// 收集所有子节点的keyfunction collectChildKeys(node, keys = []) {if (node.key) {keys.push(node.key);}if (node.children && node.children.length > 0) {node.children.forEach((child) => collectChildKeys(child, keys));}return keys;}const onChecked = (e, checkedKeys, onItemSelect) => {const { node } = e;const isChecked = !checkedKeys.includes(node.key);// 收集当前节点及其所有子节点的keyconst allKeys = collectChildKeys(node);// 批量更新选中状态allKeys.forEach((key) => {onItemSelect(key, isChecked);});};function collectParentKeys(nodes, targetKeys, parentKeys = []) {for (const node of nodes) {if (targetKeys.includes(node.key)) {parentKeys.push(node.key);}if (node.children && node.children.length > 0) {const childParentKeys = collectParentKeys(node.children, targetKeys, [...parentKeys]);if (childParentKeys.length > parentKeys.length) {if (!parentKeys.includes(node.key)) {parentKeys.push(node.key);}}}}return parentKeys;}// 辅助函数:构建目标格式数据function buildTargetFormat(selectedNodes) {console.log('selectedNodes:', selectedNodes);// 收集所有节点的 key 映射const nodeMap = {};selectedNodes.forEach((node) => {nodeMap[node.key] = node;});// 将 collectChildKeys 函数声明移动到函数体根位置function collectChildKeys(children, unitIdList) {children.forEach((child) => {if (selectedNodes.some((n) => n.key === child.key)) {// console.log('child:', child);if (child.type != '1002') {unitIdList.push(child.key);}if (child.children && child.children.length > 0) {collectChildKeys(child.children, unitIdList);}}});}const result = [];selectedNodes.forEach((node) => {if (node.children && node.children.length > 0) {// 有子级的节点,收集子级 keyconst unitIdList = [];collectChildKeys(node.children, unitIdList);console.log('node', node);if (node.type == '1001') {result.push({companyId: node.key,unitIdList: [...new Set(unitIdList)], // 去重});}} else if (!selectedNodes.some((n) => n.children && n.children.some((c) => c.key === node.key))) {// 没有父级引用的叶子节点result.push({companyId: node.key,unitIdList: [],});}});return result;}const handleTargetKeysChange = (newTargetKeys) => {targetKeys.value = newTargetKeys;// 收集所有父级节点的 keyconst allKeys = [...new Set([...newTargetKeys, ...collectParentKeys(tData.value, newTargetKeys)])];// 筛选出包含父级节点的数据const selectedData = dataSource.value.filter((item) => allKeys.includes(item.key));// 转换为目标格式const formattedData = buildTargetFormat(selectedData);emit('update:selectedData', formattedData);};// 初始化数据function getInit() {const formData = new FormData();formData.append('roleId', selectRoleId.value);departmentApi.queryDepartmentDataPermissionTree(formData).then((res) => {if (res.ok) {const selectedKeys = [];tData.value = convertDepartmentData(res.data, selectedKeys);targetKeys.value = selectedKeys;transferDataSource.value = [];flatten(tData.value);dataSource.value = transferDataSource.value;}});}watch(() => selectRoleId.value,() => getInit());onMounted(() => {getInit();handleTargetKeysChange(targetKeys.value);});
</script>
<style scoped>.tree-transfer .ant-transfer-list:first-child {width: 50%;flex: none;}.look_css {display: flex;flex-direction: row;justify-content: space-between;align-items: center;margin-top: 10px;.txt_css {margin-left: 20px;}.search_css {width: 200px;margin-right: 20px;}}
</style>2.再介绍如何具体使用
<RoleDataScope ref="roleDataScopeRef" v-model:selectedData="rightSideData" />
import RoleDataScope from '../role-data-scope/index.vue';
const rightSideData = ref([]);
因为我这个保存按钮是在父组件里面 所以要把对应的值传过来
这个根据实际情况而定 如果你的保存按钮就在子组件里写 那就不用传了

4.有问题 随时欢迎大家来交流

http://www.xdnf.cn/news/1392049.html

相关文章:

  • BlueKing-ci
  • 币安创始人赵长鹏:香港需要更广泛的加密货币产品来与美国和阿联酋竞争
  • docker-相关笔记
  • Cesium 入门教程(十三):粒子系统实例
  • 2025年03月 Scratch 图形化(一级)真题解析#中国电子学会#全国青少年软件编程等级考试
  • springboot中循环依赖的解决方法-使用反射
  • mysql双机热备(主主模式)
  • Java项目实现【记录系统操作日志】功能
  • 基于FPGA的DDR3读写实验学习
  • 《ArkUI 记账本开发:状态管理与数据持久化实现》
  • el-table合并列实例
  • 光谱相机多层镀膜技术如何提高透过率
  • (二)Python语法基础(下)
  • 响应式编程框架Reactor【2】
  • Redis开发06:使用stackexchange.redis库结合WebAPI对redis进行增删改查
  • Vue3 全面介绍
  • 技术SEO修复ROI最大化:有限资源下的优先排序策略
  • 【笔记】Linux高性能网络详解之DPDK
  • uni-app 常用钩子函数:从场景到实战,掌握开发核心
  • 算法题打卡力扣第169题:多数元素(easy)
  • 单点登录(SSO)前端(Vue2.X)改造
  • MYSQL-索引(上)
  • week5-[二维数组]对角线
  • 平安健康平安芯医AI解析:7×24小时问诊+95%诊断准确率,人文温度短板与医生效能提升引热议
  • DNS域名系统
  • Less嵌套写法
  • 无人机中的坐标系理解:机体坐标系,东北天(NED)坐标系,世界大地(WGS84)坐标系
  • 换公司如何快速切入软件项目工程
  • 在 Ubuntu 24.04 Linux 上安装 Basemark GPU Benchmark 的步骤
  • PCIe 6.0配置与地址空间架构:深入解析设备初始化的核心机制