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

news2024/11/5 17:30:08

一、功能

功能描述

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

配置选项:
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>

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

leetcode-21-合并两个有序链表

题解&#xff1a; 1、初始化哑节点dum 2、 3、 代码&#xff1a; 参考&#xff1a;leetcode-88-合并两个有序数组

WPF怎么通过RestSharp向后端发请求

1.下载RestSharpNuGet包 2.请求类和响应类 public class ApiRequest {/// <summary>/// 请求地址/// </summary>public string Route { get; set; }/// <summary>/// 请求方式/// </summary>public Method Method { get; set; }/// <summary>//…

指派问题的求解

实验类型&#xff1a;◆验证性实验 ◇综合性实验 ◇设计性实验 实验目的&#xff1a;学会使用Matlab求解指派问题。 实验内容&#xff1a;利用Matlab编程实现枚举法求解指派问题。 实验例题&#xff1a;有5人分别对应完成5项工作&#xff0c;其各自的耗费如下表所示&#…

vue3 gsap 基于侦听器的动画

1、gsap实现动画 https://gsap.com/ .以上来自baidu ai 2、代码&#xff1a; 安装gsap&#xff1a;pnpm install gsap <script setup> import { ref, reactive, watch } from vue import gsap from gsapconst number ref(0) const tweened reactive({number: 0 })wat…

Flutter CustomScrollView 效果-顶栏透明与标签栏吸顶

CustomScrollView 效果 1. 关键组件 CustomScrollView, SliverOverlapAbsorber, SliverPersistentHeader 2. 关键内容 TLDR SliverOverlapAbsorber 包住 pinned为 true 的组件 可以被CustomScrollView 忽略高度。 以下的全部内容的都为了阐述上面这句话。初阶 Flutter 开发知…

江协科技STM32学习- P29 实验- 串口收发HEX数据包/文本数据包

&#x1f680;write in front&#x1f680; &#x1f50e;大家好&#xff0c;我是黄桃罐头&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd;​…

4.1 进程管理

在异步通信中&#xff0c;每个字符包含1位起始位、7位数据位和2位终止位&#xff0c;若每秒钟传送500个字符&#xff0c;则有效数据速率为&#xff08; &#xff09;。 A. 500b/s B. 700b/s C. 3500b/s D. 5000b/s 正确答案是 C。 解析 本题考查异步传输协议基础知识。 根据题目…

[进阶]集合的进阶(1)泛型

文章目录 泛型的深入泛型的细节泛型可以在很多地方定义泛型的继承和通配符总结 泛型的深入 泛型:是JDK5中引入的特性&#xff0c;可以在编译阶段约束操作的数据类型&#xff0c;并进行检查 泛型的格式:<数据类型> 注意:泛型只能引用数据类型 泛型的好处 统一了数据类型…

GB/T 28046.3-2011 道路车辆 电气及电子设备的环境条件和试验 第3部分:机械负荷(4)

写在前面 本系列文章主要讲解道路车辆电气及电子设备的环境条件和试验GB/T 28046标准的相关知识&#xff0c;希望能帮助更多的同学认识和了解GB/T 28046标准。 若有相关问题&#xff0c;欢迎评论沟通&#xff0c;共同进步。(*^▽^*) 第3部分&#xff1a;机械负荷 4.1 振动 …

【案例】旗帜飘动

开发平台&#xff1a;Unity 6.0 开发工具&#xff1a;Shader Graph 参考视频&#xff1a;Unity Shader Graph 旗帜飘动特效   一、效果图 二、Shader Graph 路线图 三、案例分析 核心思路&#xff1a;顶点偏移计算 与 顶点偏移忽略 3.1 纹理偏移 视觉上让旗帜保持动态飘动&a…

小白从零开始配置pytorch环境

一、下载ANACONDA 官方网址Anaconda Installers and Packages 笔者选择的是Anaconda3-5.3.0-Windows-x86_64.exe版本。全程安装可以手机开热点&#xff0c;会快一点。 二、查看电脑是否有显卡 1、打开任务管理器 2、查看电脑CUBA版本&#xff0c;如上篇文章所提到查看CUDA-V…

11.1 网络编程-套接字

练习&#xff1a; 使用搭建好的服务器和客户端&#xff0c;实现一个完整的注册&#xff0c;登录功能 服务器使用链表 文件IO的形式去记录账号和密码 代码实现&#xff1a; 服务器端&#xff1a; #include <myhead.h> struct Pack{char flags;char na…

基于MATLAB的战术手势识别

手势识别的研究起步于20世纪末&#xff0c;由于计算机技术的发展&#xff0c;特别是近年来虚拟现实技术的发展&#xff0c;手势识别的研究也到达一个新的高度。熵分析法是韩国的李金石、李振恩等人通过从背景复杂的视频数据中分割出人的手势形状&#xff0c;然后计算手型的质心…

面试题整理 1

实际参与的某公司面试&#xff0c;总结了遇到的值得整理记录的面试题。 目录 相对路径 正序判断 倒序判断 输出部门负责人及下级 代码实现 最终效果 科目平均分 SQL筛选 代码实现 分组错误 原因 查看版本 确认模式 设置模式 相遇洞穴 代码实现 方式一&#xf…

mysql查表相关练习

作业要求&#xff1a; 单表练习&#xff1a; 1 . 查询出部门编号为 D2019060011 的所有员工 2 . 所有财务总监的姓名、编号和部门编号。 3 . 找出奖金高于工资的员工。 4 . 找出奖金高于工资 40% 的员工。 5 找出部门编号为 D2019090011 中所有财务总监&#xff0c;和…

基于yolov5的输电线,电缆检测系统,支持图像检测,视频检测和实时摄像检测功能(pytorch框架,python源码)

更多目标检测和图像分类识别项目可看我主页其他文章 功能演示&#xff1a; yolov5&#xff0c;输电线(线缆)检测系统&#xff0c;系统既支持图像检测&#xff0c;也支持视频和摄像实时检测【pytorch框架】_哔哩哔哩_bilibili &#xff08;一&#xff09;简介 基于yolov5的输…

C语言另一种编码方式开发状态机(无switch)

目录 概述 一、开发环境 二、coding 三、运行结果 四、总结 概述 状态机编码思想&#xff0c;在很多领域都随处可见&#xff0c;传统的coding方式使用switch case来实现&#xff0c;状态跳转可读性差&#xff0c;咋们换一种思路来实现状态机开发&#xff0c;该方式 拓展好…

录屏天花板,录课新玩法,人像+一切,PPT/PDF/视频/网页,也可即可录

上新啦 &#x1f4f1;&#x1f4bb; 录屏也能录课的万能神器——超级推荐&#xff01; 你是不是也在找一款能高效录屏、录课、轻松剪辑的小工具&#xff1f;作为一名需要频繁录制屏幕和课程内容的老师&#xff08;或内容创作者&#xff09;&#xff0c;我找到了这个宝藏App&…

vscode php Launch built-in server and debug, PHP内置服务xdebug调试,自定义启动参数配置使用示例

在vscode中&#xff0c;当我们安装了插件 PHP Debug&#xff08;xdebug.php-debug&#xff09;或者 xdebug.php-pack 后 我们通过内置默认的 php xdebug配置启动php项目后&#xff0c;默认情况下我们在vscode中设置断点是不会生效的&#xff0c;因为我们的内置php服务默认启动时…

Linux入门(2)

林纳斯托瓦兹 Linux之父 1. echo echo是向指定文件打印内容 ehco要打印的内容&#xff0c;不加任何操作就默认打印到显示器文件上。 知识点 在Linux下&#xff0c;一切皆文件。 打印到显示器&#xff0c;显示器也是文件。 2.重定向 >重定向操作&#xff0c;>指向的…