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.有问题 随时欢迎大家来交流