1.需求:实现树形结构的穿梭框,并且可以左右来回穿梭,穿梭箭头也是跟着左右俩侧树形结构选中状态而高亮(也就是左侧树形结构选完后 穿梭向右箭头要高亮 相反 右侧树形结构选完后 穿梭左箭头要高亮),左侧树形结构穿梭后 左侧选中节点置灰
2.数据格式 与后端同学确认好 可以以我这个为例子
javascript
{
"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.具体代码
javascript
现在我是把这个封装成组件了 以下会介绍具体封装及使用
1.先介绍具体组件封装
1-1 先上代码 完了再拆开细讲里面逻辑包括数据封装
后端返回的格式应该是不满足组件自带的数据格式 所以需要对数据进行封装
如果后端返回的满足组件自带数据格式 那就不需要封装
1-2 代码
<template>
<div ref="roleDataScope">
<a-transfer
v-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-tree
block-node
checkable
check-strictly
style="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-tree
v-else-if="targetKeys.length > 0"
block-node
checkable
check-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);
});
// 收集所有子节点的key
function 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);
// 收集当前节点及其所有子节点的key
const 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) {
// 有子级的节点,收集子级 key
const 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;
// 收集所有父级节点的 key
const 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.有问题 随时欢迎大家来交流