基于vue3和elementPlus的el-tree组件,实现树结构穿梭框,支持数据回显和懒加载

一、功能

功能描述

  • 数据双向穿梭:支持从左侧向右侧转移数据,以及从右侧向左侧转移数据。
  • 懒加载支持:支持懒加载数据,适用于大数据量的情况。
  • 多种展示形式:右侧列表支持以树形结构或列表形式展示。
  • 全选与反选:支持全选和全不选操作,以及保持树形结构的层级关系。
  • 搜索过滤:支持对左侧和右侧数据进行搜索过滤。
  • 自定义节点内容:支持自定义右侧列表中每个节点的内容

配置选项:
nodeKey:节点的主键,用于唯一标识每个节点。
leftTitlerightTitle:左侧和右侧树形列表的标题。
lazy:是否开启懒加载,当设置为 true 时,需要通过 loadMethod 方法加载数据。
loadMethod:懒加载时,用于加载数据的方法。
defaultProps:树节点的默认属性,包括标签、子节点和禁用状态。
leftDatarightData:左侧和右侧树形列表的数据。
defaultSelectionKeys:默认选中的数据的 ID 列表。
isSort:是否对右侧数据进行排序。
defaultExpandAll:是否默认展开所有节点。
checkOnClickNode:是否在点击节点的时候选中节点,默认值为 false,即只有在点击复选框时才会选中节点。
expandOnClickNode:是否在点击节点时展开或收缩节点。
isToList:是否将右侧数据展示为列表形式。

事件:
checkVal:当选中数据发生变化时触发的事件,返回当前选中的数据。

二、使用

1、 tree to list

在这里插入图片描述
使用 :

<script lang="ts" setup>
import { ref } from 'vue'
const transferProps = ref({
	label: 'name',
	children: 'children',
	disabled: 'disabled',
	isLeaf: 'leaf',
})

const checkVal = (val: any) => {
	 console.log('checkVal  ; ', val)
}

const loadNode = async (pid = 0) => {
	return new Promise((resolve) => {
		// 模拟网络请求延迟
		setTimeout(() => {
			// 假数据,树结构
			let data

			if (pid === 0) {
				data = [{ pid: 0, id: 1, name: 'region' }]
			} else if (pid === 1) {
				data = [{ pid: 1, id: 2, name: 'region1-1' }, { pid: 1, id: 3, name: 'region1-2', leaf: true }]
			} else if (pid === 2) {
				data = [{ pid: 2, id: 4, name: 'region2-1' }, { pid: 2, id: 5, name: 'region2-2', leaf: true }, { pid: 2, id: 6, name: 'region2-3', leaf: true }]
			} else {
				data = []
			}
			// 返回对应父节点的子节点
			resolve(data || [])
		}, 300) // 模拟延迟
	})
}
</script>

<template>
	<div>
		<ZtTreeTransfer
			:default-props="transferProps"
			:load-method="loadNode"
			node-key="id"
			is-select-all-nodes
			is-sort
			is-to-list
			lazy
			@check-val="checkVal"
		/>
	</div>
</template>

2、 tree to tree

可以配置默认选中的数据的ids,显示在右侧列表,以实现数据回显
在这里插入图片描述
使用 :

<script lang="ts" setup>
import { ZtTreeTransfer } from '@zt-components/components'

import { ref } from 'vue'

const fromData = ref([
	{
		id: 1,
		label: '1Level one 1',
		children: [
			{
				id: 4,
				label: '1-1',
				children: [
					{
						id: 9,
						label: '1-1-1',
					},
					{
						id: 10,
						label: '1-1-2',
					},
				],

			},
		],
	},
	{
		id: 2,
		label: '2Level one 2',
		children: [
			{
				id: 5,
				label: '2-1',
			},
			{
				id: 6,
				label: '2-2',
			},
		],
	},
	{
		id: 3,
		label: '3Level one 31111111',
		children: [
			{
				id: 7,
				label: '3-111111111111111111',
				disabled: true,
			},
			{
				id: 8,
				label: 'Level two 3-21111111',
				disabled: true,
				children: [
					{
						id: 11,
						label: '4-111111111111111111111',
					},
					{
						id: 12,
						label: '4-211111111111111111111',
					},
				],
			},
		],
	},
]) // 树形数据
const toData = ref([9, 10]) // 选中的ids数据
const transferProps = ref({
	label: 'label',
	children: 'children',
	disabled: 'disabled',
})

const checkVal = (val: any) => {
	console.log(val)
}
</script>

<template>
	<div>
		<ZtTreeTransfer
			:default-props="transferProps"
			:default-selection-keys="toData"
			:left-data="fromData"
			node-key="id"
			default-expand-all
			is-select-all-nodes
			is-sort
			@check-val="checkVal"
		/>
	</div>
</template>

三、代码实现

<script lang="ts" setup>
import { computed, nextTick, ref, watch } from 'vue'
import { ArrowLeft, ArrowRight, Search } from '@element-plus/icons-vue'

/* 定义props */
const props: TreeTransferProps = defineProps({
	// 主键
	nodeKey: {
		type: String,
		default: 'id',
	},
	// 左侧标题
	leftTitle: {
		type: String,
		default: () => {
			return '全部列表'
		},
	},
	// 右侧标题
	rightTitle: {
		type: String,
		default: () => {
			return '已选列表'
		},
	},
	// 是否开启懒加载
	lazy: { type: Boolean, default: false },
	// 懒加载时,加载数据的方法
	loadMethod: { type: Function, required: false },
	// tree绑定的props
	defaultProps: {
		type: Object,
		default: () => ({
			label: 'label',
			children: 'children',
			disabled: 'disabled',
		}),
	},
	// 左侧树结构数据
	leftData: {
		type: Array,
		default: () => {
			return []
		},
	},
	// 默认选中的数据的ids,显示在右侧列表
	defaultSelectionKeys: {
		type: Array,
		default: () => {
			return []
		},
	},
	// 右侧数据是否按顺序排序 仅在平铺展开是有效  只支持按住键正序排序
	isSort: {
		type: Boolean,
	},
	defaultExpandAll: {
		type: Boolean,
		default: false,
	},
	// 是否在点击节点的时候选中节点,默认值为 false,即只有在点击复选框时才会选中节点。
	checkOnClickNode: {
		type: Boolean,
		default: false,
	},
	// 是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。
	expandOnClickNode: {
		type: Boolean,
		default: true,
	},
	// 选择右侧所选数据的展示类型,默认是tree,true时为list
	isToList: {
		type: Boolean,
		default: false,
	},
}) // 又侧筛选条件

/* 定义emit */
const emit = defineEmits(['checkVal'])

/**
 * 定义props类型
 */
export interface TreeTransferProps {
	nodeKey: any
	leftTitle: any
	rightTitle: any
	defaultProps: any
	leftData: any
	defaultSelectionKeys: any
	isSort: boolean
	defaultExpandAll: Array<any>
	checkOnClickNode: boolean
	expandOnClickNode: boolean
	isToList: any
	loadMethod: Function
	lazy: boolean
}

const isCheckedAllLeft = ref(false) // 左侧全选框是否选中
const isCheckedAllRight = ref(false) // 右侧全选框是否选中

const isLeftCheckAllBoxDisabled = ref(false) // 左侧全选框是否禁用
const isRightCheckAllBoxDisabled = ref(false) // 右侧全选框是否禁用

const leftTreeRef = ref() // 左侧树ref
const rightTreeRef = ref() // 右侧树ref

const leftFilterText = ref('') // 左侧筛选条件
const rightFilterText = ref('')
const leftTreeData = ref([]) // 左侧tree数据
// 用于在右侧显示的数据列表
const rightData = ref([]) // 右侧列表结构数据
const rightTreeData = ref([]) // 右侧树结构数据

// 数组打平
const flattenTree = (treeData: any[], defaultProps: any): any[] => {
	let flatData: any[] = []
	treeData.forEach((node) => {
		flatData.push(node)
		if (node[defaultProps.children] && node[defaultProps.children].length) {
			flatData = flatData.concat(flattenTree(node[defaultProps.children], defaultProps))
		}
	})
	return flatData
}

// 校验树是否全选
const checkedAllTrue = (treeRef: any, treeData: any[], nodeKey: any, defaultProps: any): boolean => {
	// 校验是否全选
	const allKeys: string[] = treeRef.getCheckedKeys()
	const allNodes: any[] = flattenTree(treeData, defaultProps)
	const allKeysSet: Set<string> = new Set(allKeys)
	const allNodesSet: Set<string> = new Set(allNodes.map(node => node[nodeKey]))

	return allKeysSet.size === allNodesSet.size && [...allKeysSet].every(key => allNodesSet.has(key))
}

// 深拷贝
const deepClone = (obj: any): any => {
	// 判断拷贝的obj是对象还是数组
	const objClone: any = Array.isArray(obj) ? [] : {}
	if (obj && typeof obj === 'object') {
		// obj不能为空,并且是对象或者是数组 因为null也是object
		for (const key in obj) {
			if (obj.hasOwnProperty(key)) {
				if (obj[key] && typeof obj[key] === 'object') {
					// obj里面属性值不为空并且还是对象,进行深度拷贝
					objClone[key] = deepClone(obj[key]) // 递归进行深度的拷贝
				} else {
					objClone[key] = obj[key] // 直接拷贝
				}
			}
		}
	}
	return objClone
}

// 校验是否树节点是否全部禁用 nodes: []
const checkAllDisabled = (nodes: any[], defaultProps: any): boolean => {
	if (!(nodes && Array.isArray(nodes))) return false

	for (const node of nodes) {
		// 如果当前节点的disabled不是true,则直接返回false
		if (!node[defaultProps.disabled]) {
			return false
		}
		// 如果当前节点有子节点,则递归检查子节点
		if (node[defaultProps.children]?.length) {
			const childrenAreDisabled = checkAllDisabled(node[defaultProps.children], defaultProps)
			// 如果子节点中有任何disabled不是true,则返回false
			if (!childrenAreDisabled) {
				return false
			}
		}
	}
	// 如果所有节点的disabled都是true,则返回true
	return true
}

// 设置数组的某个字段值为某个参数
const setFieldValue = (array: any[], field: string, value: any, defaultProps: any) => {
	// 遍历数组中的每个元素
	array.forEach((item) => {
		// 如果元素是对象且有属性,则设置字段值
		if (typeof item === 'object' && item !== null) {
			item[field] = value
			// 如果元素有子数组,递归调用函数
			if (Array.isArray(item[defaultProps.children])) {
				setFieldValue(item[defaultProps.children], field, value, defaultProps)
			}
		}
	})
}

// 设置禁用
const setTreeIsDisabled = (data: any[], nodeKeysToDisable: string[], nodeKey: string, defaultProps: any, flag = true) => {
	if (!data || !data.length) return
	data.forEach((item) => {
		if (nodeKeysToDisable && nodeKeysToDisable.length && nodeKeysToDisable.includes(item[nodeKey])) {
			// 如果当前节点的id主键在要禁用的id主键列表中,设置disabled为true
			item[defaultProps.disabled] = flag
		}
		// 如果当前节点有children,递归调用函数
		const itemChildren = item[defaultProps.children]
		if (itemChildren && Array.isArray(itemChildren)) {
			setTreeIsDisabled(itemChildren, nodeKeysToDisable, nodeKey, defaultProps, flag)
		}
	})
}

// 获取数组中disabled的节点的Ids
const getDisabledNodeIds = (nodes: any[], nodeKey: string, defaultProps: any): string[] => {
	const disabledIds: string[] = []

	function traverse(node: any) {
		if (node.disabled) {
			disabledIds.push(node[nodeKey])
		}
		if (node[defaultProps.children]?.length) {
			node[defaultProps.children].forEach((child: any) => traverse(child))
		}
	}

	nodes.forEach(node => traverse(node))
	return disabledIds
}

// 递归校验 当子节点全部被禁用时 ,则设置其父节点也禁用
const updateDisabledStatus = (nodes: any[], defaultProps: any) => {
	nodes.forEach((node) => {
		// 首先检查当前节点是否有子节点
		if (node[defaultProps.children]?.length) {
			// 假设当前节点的所有子节点都是禁用的
			let allChildrenDisabled = true

			// 递归检查所有子节点的disabled状态
			node[defaultProps.children].forEach((child: any) => {
				// 如果子节点有子节点,递归调用
				if (child[defaultProps.children]?.length) {
					updateDisabledStatus([child], defaultProps) // 递归更新子节点状态
				}
				// 更新子节点的disabled状态
				child[defaultProps.disabled] = child[defaultProps.children].length > 0
					? child[defaultProps.children].every((c: any) => c[defaultProps.disabled])
					: child[defaultProps.disabled]

				// 如果发现任何一个子节点没有被禁用,更新假设
				if (!child[defaultProps.disabled]) {
					allChildrenDisabled = false
				}
			})

			// 更新当前节点的disabled状态
			node[defaultProps.disabled] = allChildrenDisabled
		}
	})
}

// 左侧输入框过滤事件
const filterLeftNode = (value, data) => {
	if (!value) return true
	return data[props.defaultProps.label].includes(value)
}

// 右侧输入框过滤事件
const filterRightNode = (value, data) => {
	if (!value) return true
	return data[props.defaultProps.label].includes(value)
}

// 右侧数据按顺序排序
const sortRightListByKey = () => {
	if (!props.isSort) return rightData.value
	return rightData.value.sort((a, b) => a[props.nodeKey] - b[props.nodeKey])
}

// 递归函数,用于构建只包含 ids 数组中 id 的树结构
const filterTreeByIds = (treeData, ids) => {
	return treeData
		.map((node) => {
			// 创建一个新节点对象,避免直接修改原始数据
			const newNode = { ...node }
			newNode[props.defaultProps.disabled] = false

			// 如果当前节点的 id 在 ids 中,保留这个节点及其子节点
			if (ids.includes(node[props.nodeKey])) {
				// 递归地过滤子节点
				newNode[props.defaultProps.children] = filterTreeByIds(node[props.defaultProps.children] || [], ids)
			} else {
				// 如果当前节点的 id 不在 ids 中,但有子节点,递归地过滤子节点
				// 同时,如果子节点中有至少一个节点的 id 在 ids 中,保留当前节点
				newNode[props.defaultProps.children] = filterTreeByIds(node[props.defaultProps.children] || [], ids).filter(child => child !== null)
			}

			// 如果当前节点的 id 不在 ids 中,且没有子节点或子节点都不在 ids 中,则不保留这个节点
			if (!ids.includes(node[props.nodeKey]) && (!newNode[props.defaultProps.children] || newNode[props.defaultProps.children].length === 0)) {
				return null
			}

			// 返回新的节点对象
			return newNode
		})
		.filter(node => node !== null) // 过滤掉 null 节点
}

// 去右边
const toRight = () => {
	/*  右侧显示的数据获取 */
	rightTreeData.value = getRightTreeData()
	rightData.value = getRightListData()

	// 给父组件抛出已选择的数据
	checkVal()

	/*
	 *  更新移动后的左侧树的节点状态 和全选按钮状态
	 *    先给所有已右移的节点设置禁用
	 *    再通过递归计算是否将子节点的父节点也设置禁用(子节点全部禁用时,将其父节点也禁用)
	 *
	 * */
	const rids = rightData.value.map(item => item[props.nodeKey])
	setTreeIsDisabled(leftTreeData.value, rids, props.nodeKey, props.defaultProps)
	updateDisabledStatus(leftTreeData.value, props.defaultProps)
	isLeftCheckAllBoxDisabled.value = checkAllDisabled(leftTreeData.value, props.defaultProps)
}
// 去左边
const toLeft = async () => {
	if (props.isToList) {
		// 获取当前右侧选中的数据,没有就return
		const listToLeftIds = rightData.value.filter(item => item.checked).map(item => item[props.nodeKey])
		if (!listToLeftIds.length) return

		// 从右侧去掉选中的数据,并将所有数据的checked设为false,避免由索引变更导致的异常选中
		const unselectedList = rightData.value.filter(item => !item.checked)
		rightData.value.map(item => (item.checked = false))
		rightData.value = unselectedList

		// 恢复选中数据在左侧的可选状态,并清除选中状态
		listToLeftIds.forEach(item => leftTreeRef.value.setChecked(item, false))
		setTreeIsDisabled(leftTreeData.value, listToLeftIds, props.nodeKey, props.defaultProps, false)
		updateDisabledStatus(leftTreeData.value, props.defaultProps)

		checkVal()
		isLeftCheckAllBoxDisabled.value = checkAllDisabled(leftTreeData.value, props.defaultProps)
	} else {
		// 获取当前右侧选中的数据,没有就return
		const treeToLeftIds = getRightTReeCheckedNodeIds()
		if (!treeToLeftIds.length) return

		// 恢复选中数据在左侧的可选状态,并清除选中状态
		setTreeIsDisabled(leftTreeData.value, treeToLeftIds, props.nodeKey, props.defaultProps, false)
		treeToLeftIds.forEach(item => leftTreeRef.value.setChecked(item, false))
		updateDisabledStatus(leftTreeData.value, props.defaultProps)

		checkVal()
		isLeftCheckAllBoxDisabled.value = checkAllDisabled(leftTreeData.value, props.defaultProps)

		rightTreeData.value = []
		rightTreeData.value = getRightTreeData()
		isCheckedAllRight.value = checkedAllTrue(rightTreeRef.value, rightTreeData.value, props.nodeKey, props.defaultProps)
	}
}

// 获取右侧树中选中节点的Ids
const getRightTReeCheckedNodeIds = () => {
	// 返回全部节点填false, false ;返回叶子结点填true,true
	const checkNodeIds = rightTreeRef.value.getCheckedKeys(true)
	if (!checkNodeIds.length) return []

	return checkNodeIds
}

// 左侧数据全选操作(全不选)
const handleLeftAllCheck = () => {
	const leftTree = leftTreeRef.value
	const disabledIds = getDisabledNodeIds(leftTreeData.value, props.nodeKey, props.defaultProps)

	if (isCheckedAllLeft.value) {
		/*
		 * 操作 : 设置全选
		 * 逻辑 : 已经设置了disable的节点无法编辑选中,所以先获取所有设置了disable的节点的ids,然后将所有数据放开disable,设置全部选中,选中后再将ids中的节点设置禁用
		 * */
		setFieldValue(leftTreeData.value, props.defaultProps.disabled, false, props.defaultProps)
		leftTree?.setCheckedNodes(leftTreeData.value)
		setTreeIsDisabled(leftTreeData.value, disabledIds, props.nodeKey, props.defaultProps)
		isCheckedAllLeft.value = true
	} else {
		/*
		 * 操作 : 设置全不选
		 * 逻辑 : 已经设置disabled的节点不应该改变其选中和禁用状态 ,所以先获取所有禁用数据的ids(也就是checked=true的所有当前选中状态的数据),然后取消全部的选中状态,再将ids中的节点设置为选中状态
		 * */
		leftTree?.setCheckedNodes([])
		disabledIds.forEach(item => leftTreeRef.value.setChecked(item, true))
		isCheckedAllLeft.value = false
	}
}
// 左侧树节点checkbox被点击
const handleLeftCheckChange = () => {
	isCheckedAllLeft.value = checkedAllTrue(leftTreeRef.value, leftTreeData.value, props.nodeKey, props.defaultProps)
}

// 右侧树节点checkbox被点击
const handleRightCheckChange = () => {
	isCheckedAllRight.value = checkedAllTrue(rightTreeRef.value, rightTreeData.value, props.nodeKey, props.defaultProps)
}

// 右侧数据全选操作(全不选)
const handleRightAllCheck = () => {
	// list
	setFieldValue(rightData.value, 'checked', isCheckedAllRight.value, props.defaultProps)
	// tree
	rightTreeRef.value.setCheckedNodes(isCheckedAllRight.value ? rightTreeData.value : [])
}

// 返回已选数据给父组件
const checkVal = () => {
	emit('checkVal', props.isToList ? rightData.value : leftTreeRef.value.getCheckedNodes(true))
}

const walkTreeData = (nodes, selectedKeys) => {
	const ret = []
	nodes.forEach((node) => {
		const newNode = { ...node }
		newNode[props.defaultProps.disabled] = false

		delete newNode[props.defaultProps.children]
		node[props.defaultProps.children] && (newNode[props.defaultProps.children] = walkTreeData(node[props.defaultProps.children], selectedKeys))
		if (selectedKeys.includes(newNode[props.nodeKey]) || (newNode[props.defaultProps.children] && newNode[props.defaultProps.children].length)) {
			ret.push(newNode)
		}
	})

	return ret
}

// 获取右侧list结构数据
const getRightListData = () => {
	/*  右侧list结构数据获取 */
	if (!currentLeftUseableNodes.value.length) return []

	const newArr = rightData.value.concat(currentLeftUseableNodes.value)
	const obj: any = {}
	// 去重
	const peon: any = newArr.reduce((cur, next) => {
		obj[next[props.nodeKey]] ? '' : (obj[next[props.nodeKey]] = true && cur.push(next))
		cur.checked = false
		return cur
	}, []) // 设置cur默认类型为数组,并且初始值为空的数组

	return peon
}

// 获取右侧树结构数据
const getRightTreeData = () => {
	if (!leftTreeRef.value || !rightTreeRef.value) return []

	const checkedKeys = leftTreeRef.value.getCheckedKeys(false) // 当前选中节点 key 的数组
	const halfCheckedKeys = leftTreeRef.value.getHalfCheckedKeys() // 目前半选中的节点的 key 所组成的数组
	const allCheckedKeys = halfCheckedKeys.concat(checkedKeys)
	if (allCheckedKeys && allCheckedKeys.length) {
		return walkTreeData(leftTreeData.value, allCheckedKeys)
	} else {
		return []
	}
}

// 获取左侧树当前所选的可进行右移操作的数据
const currentLeftUseableNodes = computed(() => {
	if (!leftTreeRef.value) return []

	// 返回全部节点填false ;返回叶子结点填true
	const checkNodes = leftTreeRef.value.getCheckedNodes(true) // 将返回当前选中节点的节点数组
	if (!checkNodes.length) return []

	// 过滤当前已选,如果没有选择新的数据就return
	const useableNodes = checkNodes.filter(item => !item[props.defaultProps.disabled])
	if (!useableNodes.length) return []

	return useableNodes
})

// 左移按钮disabled计算
const isToLeftBtnDisabled = computed(() => {
	let checkNodes = []
	rightTreeRef.value && (checkNodes = rightTreeRef.value.getCheckedNodes(false, false)) // tree选择的节点
	const listToLeftIds = rightData.value.filter(item => item.checked).map(item => item[props.nodeKey]) // list选择的节点

	return !(listToLeftIds.length || checkNodes.length)
})

// 更新 treeData 中的指定节点,添加子节点
const updateTreeData = (targetNode: any, childNodes: any) => {
	const recursiveUpdate = (nodes: any) => {
		for (const node of nodes) {
			if (node[props.nodeKey] === targetNode[props.nodeKey]) {
				node[props.defaultProps.children] = childNodes // 将子节点添加到目标节点
			} else if (node[props.defaultProps.children]) {
				recursiveUpdate(node[props.defaultProps.children]) // 递归查找目标节点
			}
		}
	}

	if (!Object.keys(leftTreeData.value).length) {
		leftTreeData.value = childNodes
		return
	}

	recursiveUpdate(leftTreeData.value)
}

//  懒加载方法
const handleLoadNode = (node: any, resolve: any) => {
	if (props.lazy) {
		const pid = node.level === 0 ? 0 : node.data[props.nodeKey]
		props
			.loadMethod(pid)
			.then((res: any) => {
				if (res || Array.isArray(res)) {
					// 更新 treeData,确保包含懒加载的节点
					// 在节点展开时,确保 treeData 是最新的完整结构
					resolve(res)
				} else {
					resolve([])
				}
				updateTreeData(node.data, res)
			})
			.catch((err: any) => {
				console.error('Failed to load node data:', err)
				resolve([])
			})
	} else {
		resolve(node.data[props.defaultProps.children] || [])
	}
}

// 监听右侧数据变化,判断右侧全选框是否选中
watch(
	() => rightData.value,
	(newData) => {
		if (!newData || !props.isToList) return
		isCheckedAllRight.value = newData.length && newData.every(item => item.checked)
	},
	{
		deep: true,
		immediate: true,
	},
)

// 初始化操作,将传参的默认选中节点传递并显示到右侧
watch(
	() => props.defaultSelectionKeys,
	(newKeys) => {
		if (props.lazy && props.loadMethod) return
		if (!newKeys?.length) return

		nextTick(async () => {
			// 设置目前选中的节点
			await leftTreeRef.value.setCheckedKeys(newKeys)
			toRight()
		})
	},
	{
		deep: true,
		immediate: true,
	},
)

// 初始化操作,将传参的默认选中节点传递并显示到右侧
watch(
	() => props.leftData,
	(newData) => {
		// 如果是懒加载,并且有loadMethod方法,直接return
		if (props.lazy && props.loadMethod) return
		// 没有数据就return
		if (!newData?.length) return
		leftTreeData.value = deepClone(newData)
		setFieldValue(leftTreeData.value, props.defaultProps.disabled, false, props.defaultProps)
	},
	{
		deep: true,
		immediate: true,
	},
)

watch(leftFilterText, (val) => {
	leftTreeRef.value!.filter(val)
})
</script>

<template>
	<div class="zt-tree-transfer">
		<!-- 左边 -->
		<div class="left-content">
			<div class="list">
				<div class="left_lowline">
					<el-checkbox
						v-model="isCheckedAllLeft"
						:disabled="isLeftCheckAllBoxDisabled"
						label=""
						size="large"
						@change="handleLeftAllCheck"
					/>
					<p class="left_title">
						{{ leftTitle }}
					</p>
				</div>
				<!-- 搜索 -->
				<div class="left_input">
					<el-input
						v-model="leftFilterText"
						:prefix-icon="Search"
						class="w-50 m-2"
						placeholder="搜索"
						clearable
					/>
				</div>
				<div class="left-tree">
					<el-tree
						ref="leftTreeRef"
						v-slot="{ node, data }"
						:check-on-click-node="checkOnClickNode"
						:data="leftTreeData"
						:default-expand-all="defaultExpandAll"
						:expand-on-click-node="expandOnClickNode"
						:filter-node-method="filterLeftNode"
						:lazy="lazy"
						:load="handleLoadNode"
						:node-key="nodeKey"
						:props="defaultProps"
						highlight-current
						show-checkbox
						@check-change="handleLeftCheckChange"
					/>
				</div>
			</div>
		</div>
		<!-- 中间按钮 -->
		<div class="btn-div">
			<div class="btn-item" @click="toRight()">
				<el-button
					:disabled="!currentLeftUseableNodes.length"
					:icon="ArrowRight"
					size="large"
					type="primary"
				/>
			</div>
			<div class="btn-item" @click="toLeft()">
				<el-button
					:disabled="isToLeftBtnDisabled"
					:icon="ArrowLeft"
					size="large"
					type="primary"
				/>
			</div>
		</div>
		<!-- 右边 -->
		<div class="righ-content">
			<div class="list">
				<div class="left_lowline">
					<el-checkbox
						v-model="isCheckedAllRight"
						:disabled="isRightCheckAllBoxDisabled"
						label=""
						size="large"
						@change="handleRightAllCheck"
					/>
					<p class="left_title">
						{{ rightTitle }}
					</p>
				</div>
				<!-- 搜索 -->
				<div class="left_input">
					<el-input
						v-model="rightFilterText"
						:prefix-icon="Search"
						class="w-50 m-2"
						placeholder="搜索"
						clearable
					/>
				</div>

				<!--    右侧数据展示格式为list时    -->
				<div v-if="isToList">
					<!--   根据[props.nodeKey]排序  ;  根据rightFilterText进行过滤显示    -->
					<div
						v-for="(item, index) in sortRightListByKey().filter((item) => item[defaultProps.label].includes(rightFilterText))"
						v-if="sortRightListByKey().filter((item) => item[defaultProps.label].includes(rightFilterText)).length"
						:key="index"
						class="right_item"
					>
						<!-- 检查是否有名为 "right-item" 的插槽内容 -->
						<slot
							v-if="$slots['right-item']"
							:index="index"
							:item="item"
							name="right-item"
						></slot>
						<!-- 如果没有,则显示默认内容 -->
						<div v-else>
							<el-checkbox
								v-model="item.checked"
								:false-label="false"
								:true-label="true"
								:value="item[nodeKey]"
							>
								{{ item[defaultProps.label] }}
							</el-checkbox>
						</div>
					</div>

					<div v-else style="padding: 10px">
						<el-text type="info">
							暂无数据
						</el-text>
					</div>
				</div>

				<!--    右侧数据展示格式为tree时    -->
				<div v-else class="right-tree">
					<el-tree
						ref="rightTreeRef"
						v-slot="{ node, data }"
						:check-on-click-node="checkOnClickNode"
						:data="rightTreeData"
						:default-expand-all="defaultExpandAll"
						:expand-on-click-node="expandOnClickNode"
						:filter-node-method="filterRightNode"
						:node-key="nodeKey"
						:props="defaultProps"
						highlight-current
						show-checkbox
						@check-change="handleRightCheckChange"
					/>
				</div>
			</div>
		</div>
	</div>
</template>

<style lang="less" scoped>
.zt-tree-transfer {
	display: flex;
	height: 500px;
	width: 800px;
	box-sizing: border-box;

	.btn-div {
		flex: 1;
		height: 60%;
		margin: auto;
		display: flex;
		flex-direction: column;
		justify-content: space-evenly;
		align-items: center;

		.btn-item {
			:deep(svg),
			:deep(.el-icon) {
				height: 1.6em !important;
				width: 1.6em !important;
			}
		}
	}

	.left-content {
		width: 45%;
		border: 1px solid #dcdfe6;
		box-sizing: border-box;
		padding: 5px 10px;

		.list {
			width: 100%;
			height: 100%;
			display: flex;
			flex-direction: column;
			overflow: hidden;

			.left-tree {
				width: calc(100% - 5px);
				height: 100%;
				overflow: auto;
				margin-top: 10px;
				padding-right: 5px;
			}
		}
	}

	.righ-content {
		box-sizing: border-box;
		border: 1px solid #dcdfe6;
		padding: 5px 10px;
		width: 45%;
		overflow: auto;

		.right_item {
			text-align: left;
		}

		.list {
			height: 100%;
			display: flex;
			flex-direction: column;
		}
	}

	.left_lowline {
		display: flex;
		align-items: center;
	}

	.right_lowline {
		display: flex;
		align-items: center;
	}

	:deep(.el-input__wrapper) {
		position: relative;

		.el-input__inner {
			padding-right: 18px;
		}

		.el-input__suffix {
			position: absolute;
			right: 8px;
			top: 50%;
			transform: translateY(-50%);
		}
	}

	// 滚动条宽度
	::-webkit-scrollbar {
		width: 6px;
		height: 6px;
	}

	// 滚动条轨道
	::-webkit-scrollbar-track {
		background: rgb(239, 239, 239);
		border-radius: 2px;
	}

	// 小滑块
	::-webkit-scrollbar-thumb {
		background: #40a0ff49;
		border-radius: 2px;
	}

	::-webkit-scrollbar-thumb:hover {
		background: #40a0ff;
	}

	:deep(.el-button:focus) {
		outline: none;
	}

	:deep(.el-tree) {
		display: inline-block;
		min-width: 100%;

		.el-tree-node__content {
			//margin-right: 5px;
		}
	}
}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/907515.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python数据分析案例61——信贷风控评分卡模型(A卡)(scorecardpy 全面解析)

案例背景 虽然在效果上&#xff0c;传统的逻辑回归模型通常不如现代的机器学习模型&#xff0c;但在风控领域&#xff0c;解释性至关重要。逻辑回归的解释性是这些“黑箱”模型所无法比拟的&#xff0c;因此&#xff0c;研究传统的评分卡模型依然是有意义的。 传统的评分卡模型…

Canvas 教程(一)

目录 一、初体验 二、通过js的方式创建canvas 三、为什么推荐属性的方式设置canvas的宽高&#xff1f; 四、常见画笔API 4.1 画直线 &#x1f536; 步骤 &#x1fae2; 小练习 4.2 线条的样式 4.2.1 线条的宽度设置 &#x1f536; API 4.2.2 线条的颜色设置 &#…

[CISCN 2019华北]PWN1-好久不见7

Partial RELRO 表示部分 RELRO 保护已启用。在这种情况下&#xff0c;只有某些部分&#xff08;如 GOT 中的只读部分&#xff09;是只读的。 NX enabled 表示这个二进制文件启用了 NX 保护&#xff0c;数据段是不可执行的。这可以防止某些类型的代码注入攻击。 这里是ida识别…

Leetcode 64. 最小路径和 动态规划+空间优化

原题链接&#xff1a;Leetcode 64. 最小路径和 二维数据 class Solution { public:int minPathSum(vector<vector<int>>& grid) {int m grid.size();int n grid[0].size();int dp[m][n];dp[0][0] grid[0][0];for (int j 1; j < n; j)dp[0][j] dp[0][…

教你一个免费把PDF产品宣传册转化为翻页电子产品宣传册的方法

在这个数字化时代&#xff0c;电子宣传册已经成为企业推广产品的重要手段。相比于传统的纸质宣传册&#xff0c;电子宣传册具有更高的互动性和传播效率。那么&#xff0c;如何将现有的PDF产品宣传册转化为具有翻页效果的电子宣传册呢&#xff1f;本文将为您详细介绍一种免费的方…

大数阶乘求末尾0的个数

题目&#xff1a;求123…n所得数的末尾有多少个0&#xff1f;n由键盘输入&#xff0c;且1000<n<10000。 答&#xff1a;思路1&#xff1a;考虑末尾0的来源。阶乘结果末尾的零是由于因数10的存在&#xff0c;10分解质因数&#xff1a;25。在阶乘的计算中&#xff0c;偶数&…

2024最新的开源博客系统:vue3.x+SpringBoot 3.x 前后端分离

本文转载自&#xff1a;https://fangcaicoding.cn/article/54 大家好&#xff01;我是方才&#xff0c;目前是8人后端研发团队的负责人&#xff0c;拥有6年后端经验&3年团队管理经验&#xff0c;截止目前面试过近200位候选人&#xff0c;主导过单表上10亿、累计上100亿数据…

鸿蒙生态下开发挑战-鸿蒙低代码开发工具展望及优势

鸿蒙生态下开发挑战 在鸿蒙生态下开发时&#xff0c;开发者可能会遇到多方面的挑战&#xff0c;这些挑战主要涉及开发工具、技术难度、生态竞争以及市场定位等方面。以下是对这些挑战的详细分析&#xff1a; 一、开发工具不完善 尽管鸿蒙系统的开发工具DevEco Studio在逐步完…

JavaScript的迭代器和生成器

1. 迭代器Iterator 1. 基本概念 JavaScript 表示集合的对象大致有Object&#xff0c;Array&#xff0c;Map&#xff0c;Set四种&#xff0c;并且这四种类型的数据之间可以相互以成员嵌套&#xff08;如Array的成员可以是Object&#xff0c;而Map又可以嵌入Object的成员中&am…

深度学习常用开源数据集介绍【持续更新】

DIV2K 介绍&#xff1a;DIV2K是一个专为 图像超分辨率&#xff08;SR&#xff09; 任务设计的高质量数据集&#xff0c;广泛应用于计算机视觉领域的研究和开发。它包含800张高分辨率&#xff08;HR&#xff09;训练图像和100张高分辨率验证图像&#xff0c;每张图像都具有极高…

Pinia-状态管理

Pinia-状态管理 特点&#xff1a; 1. 轻量和模块化 Pinia 是一个轻量级的状态管理库&#xff0c;支持模块化管理&#xff0c;即可以将应用的状态分成多个 store 以实现更好的组织。使用 Pinia&#xff0c;可以定义多个 store&#xff0c;每个 store 都是一个独立的模块&#x…

向量模型Jina Embedding: 从v1到v3论文笔记

文章目录 Jina Embedding: 从v1到v3Jina Embedding v1数据集准备训练过程 Jina Embedding v2预训练修改版BERT在文本对上微调在Hard Negatives上微调 Jina Embedding v2 双语言预训练修改版BERT在文本对上微调用多任务目标微调 Jina Embedding v3预训练在文本对上微调训练任务相…

修改HarmonyOS鸿蒙图标和名字,打包后安装到真机,应用图标丢失变成透明,修改名字也不生效,还是默认的labeL解决方案教程

HarmonyOS鸿蒙打包hap 安装应用到桌面没有图标&#xff0c;用hdc安装到真机&#xff0c;打包后应用图标丢失变成透明&#xff0c;名字也还是默认的label的bug&#xff0c;以下是解决方案 以下是修改方案&#xff1a; 1、修改应用名字&#xff1a; 2、修改应用图标&#xff1a…

3个模型的交互式多模型IMM,基于EKF的目标跟踪实例(附MATLAB代码)

文章目录 3个模型的IMM源代码运行结果代码介绍总结 3个模型的IMM 代码实现了基于 I M M IMM IMM&#xff08;Interacting Multiple Model&#xff09;算法的目标跟踪。它使用三种不同的运动模型&#xff08;匀速直线运动、左转弯和右转弯&#xff09;来预测目标的位置&#x…

Webservice 客户端 生成代码 cxf方式 jdk方式 wsdl保存到本地后,生成客户端代码

详解视频&#xff0c;如果看不懂图片&#xff0c;请看这个视频 客户端三种方式 jdk cxf 客户单 wsdl保存到本地后&#xff0c;生成客户端代码

轮廓图【HTML+CSS+JavaScript】

给大家分享一个很好看的轮播图&#xff0c;这个也是之前看到别人写的效果感觉很好看&#xff0c;所以后面也自己实现了一下&#xff0c;在这里分享给大家&#xff0c;希望大家也可以有所收获 轮播图效果&#xff1a; 视频效果有点浑浊&#xff0c;大家凑合着看&#xff0c;大家…

Windows上安装Redis

1.下载Redis 下载有2中选择&#xff1a; 官方redis官方下载地址&#xff1a; https://redis.io/download&#xff0c; 选择适合Windows的版本下载。 redis 64位下载地址&#xff1a; https://github.com/ServiceStack/rediswindows/tree/master/downloads&#xff0c; 我们下…

计算机视觉实验一:图像基础处理

1. 图像的直方图均衡 1.1 实验目的与要求 (1)理解直方图均衡的原理与作用; (2)掌握统计图像直方图的方法; (3)掌握图像直方图均衡的方法。 1.2 实验原理及知识点 直方图均衡化是通过灰度变换将一幅图象转换为另一幅均衡直方图&#xff0c;即在每个灰度级上都具有相同的象素…

第8章 利用CSS制作导航菜单作业

1.利用CSS技术&#xff0c;结合链接和列表&#xff0c;设计并实现“山水之间”页面。 浏览效果如下&#xff1a; HTML代码如下&#xff1a; <!DOCTYPE html> <html><head><meta charset"utf-8" /><title>山水之间</title><…

dhcp池没有空闲ip导致手机无法获得ip

得到用户反馈&#xff0c;一个高速项目部的wifi无法接入&#xff0c;让排查原因。 反馈有的手机能接入&#xff0c;有的接入不了。查看ac界面发现有个终端获得的ip是169.254.xxx.xxx。 ip地址是169.254.96.17显然是手机打开wlan开关后&#xff0c;鉴权通过后dhcp过程&#xff0…