vue3 树形穿梭框, 子节点可内部排序

需要实现的效果


需要实现的效果

需求:

需要树形来回穿梭, 并且子节点可以在父节点内调换位置. 父节点可以上下箭头排序(没做)

父组件

<template>
    <div class="treeTransfer-box">
        <TreeTransfer ref="treeTransfer" :nodeKey="'id'" :fromData="menuList" :toData="ruleForm.menuIds" :defaultProps="transferProps" :leftTit="'已选检测点'" :rightTit="'未选检测点'" @checkVal="checkVal" :BaseFormdata="BaseFormdata" />
    </div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref, watch, nextTick, onMounted } from 'vue';
import TreeTransfer from './components/TreeTransfer.vue';
const treeTransfer = ref();
const menuList = ref([
	{
		id: 1,
		label: '出厂水质检测点',
		children: [
			{ id: '11', label: '二水厂出厂水质检测点', labelTypeId: 2, children: [], number: 0 },
			{ id: '12', label: '三水厂出厂水质检测点', labelTypeId: 2, children: [], number: 1 },
			{ id: '13', label: '四水厂出厂水质检测点', labelTypeId: 2, children: [], number: 0 },
		],
		disabled: false,
	},
	{
		id: 2,
		label: '管网(末梢)水质检测点',
		children: [
			{ id: '21', label: '管网二水厂出厂水质检测点', labelTypeId: 2, children: [], number: 3 },
			{ id: '22', label: '管网三水厂出厂水质检测点', labelTypeId: 2, children: [], number: 0 },
			{ id: '23', label: '管网四水厂出厂水质检测点', labelTypeId: 2, children: [], number: 0 },
		],
		disabled: false,
	},
	{
		id: 3,
		label: '二水厂1号井水质检测点',
		children: [
			{ id: '31', label: '二水厂出厂水质检测点', labelTypeId: 2, children: [], number: 0 },
			{ id: '32', label: '三水厂出厂水质检测点', labelTypeId: 2, children: [], number: 3 },
			{ id: '33', label: '四水厂出厂水质检测点', labelTypeId: 2, children: [], number: 0 },
		],
		disabled: false,
	},
	{
		id: 4,
		label: '二水厂1号井水质检测点',
		children: [
			{ id: '41', label: '二水厂1号井水质检测点', labelTypeId: 2, children: [], number: 0 },
			{ id: '42', label: '三水厂1号井水质检测点', labelTypeId: 2, children: [], number: 2 },
			{ id: '43', label: '四水厂1号井水质检测点', labelTypeId: 2, children: [], number: 0 },
		],
		disabled: false,
	},
	{
		id: 5,
		label: '二水厂1号井水质检测点',
		children: [
			{ id: '51', label: '二水厂1号井水质检测点', labelTypeId: 2, children: [], number: 1 },
			{ id: '52', label: '三水厂1号井水质检测点', labelTypeId: 2, children: [], number: 0 },
			{ id: '53', label: '四水厂1号井水质检测点', labelTypeId: 2, children: [], number: 0 },
		],
		disabled: false,
	},
	{
		id: 6,
		label: '二水厂1号井水质检测点',
		children: [
			{ id: '61', label: '二水厂1号井水质检测点', labelTypeId: 2, children: [], number: 0 },
			{ id: '62', label: '三水厂1号井水质检测点', labelTypeId: 2, children: [], number: 0 },
			{ id: '63', label: '四水厂1号井水质检测点', labelTypeId: 2, children: [], number: 0 },
		],
		disabled: false,
	},
]);
const detailsList = [
	{ key: '任务周期', value: '日检' },
	{ key: '期检次数 (已检/需检)', value: '0/1次' },
	{ key: '需检指标', value: '10项' },
	{ key: '闭单延长时间', value: '2天' },
	{ key: '备注', value: '检测对象包含4个水厂的出厂水质、以及从全部站点中抽出来的10个站点的水质' },
	{ key: '采样时间', value: '2024.06.01' },
];

const ruleForm = ref({
	menuIds: [],
});
const transferProps = ref({ children: 'children', label: 'label', disabled: 'disabled', number: 'number' });
const BaseFormdata = ref<any>({});
const BaseData = ref<{ visible: boolean; title: string }>({
	visible: false,
	title: '',
});

watch(
	BaseData,
	(val) => {
		if (val.visible) {
			nextTick(() => {
				treeTransfer.value.initData();
			});
		}
	},
	{
		deep: true,
	}
);
onMounted(() => {
	Init('新增', {});
});
// 初始化
const Init = (title: string, data: any) => {
	BaseFormdata.value = data ? JSON.parse(JSON.stringify(data)) : {};
	console.log('BaseFormdata.value', BaseFormdata.value);
	BaseData.value.title = title;
	BaseData.value.visible = true;
};
const checkVal = (val: any) => {
	const arr = [];
	for (const i in val) {
		arr.push(val[i].id);
	}
	console.log('arrr', arr);
	console.log('ruleForm', ruleForm);
	ruleForm.menuIds = arr;
};
</script>
<style lang="scss" scoped>
.treeTransfer-box {
	width: 50vw;
	min-height: 40vh;
}
</style>

子组件

<template>
    <div class='tree-transfer'>
        <!-- 左侧框 -->
        <div class="left-tree ">
            <div class="header">
                <div class="header-left">
                    <el-checkbox v-model="checkedLeft" :disabled="leftData.length < 1" label="" size="large" @change="leftAllCheck" />
                    <div class="tree-tit">{{leftTit || '左侧栏'}}</div>
                </div>
                <span class="header-right">
                    0/10
                </span>
            </div>
            <div class="content">
                <!-- 搜索框 -->
                <div class="seacrh-box">
                    <el-input v-model.trem="leftFilterText" placeholder="请输入" clearable @clear="onSearchLeft" @keyup.enter="onSearchLeft" @change="onSearchLeft"></el-input>
                    <el-icon>
                        <Search />
                    </el-icon>
                </div>
                <!-- 数据区域 -->
                <div class="tree-box">
                    <el-tree ref="treeRefL" v-if="reLoad" :data="leftData" show-checkbox default-expand-all :node-key="nodeKey" highlight-current :props="defaultProps" :allow-drop="allowDrop" draggable :allow-drag="allowDrag" :filter-node-method="filterNode">
                        <template #default="scope">
                            <div class=" el-tree-node">
                                <div class=" el-tree-node__content">
                                    <div class="custom-tree-node">
                                        <div class="custom-tree-node-text-box" style="margin-left: 10px;">
                                            <span class="custom-tree-node-title">
                                                {{ `${scope.data.label} `}}
                                            </span>
                                            <div v-if="scope.node.level == 1" class="custom-tree-node-icon" style="text-align: right;">
                                                <span></span>
                                                <span></span>
                                                <span class="line">|</span>
                                                <span class="number">2</span>
                                            </div>
                                            <div v-if="scope.node.level == 2" class="custom-right">
                                                {{ scope.node.number}}
                                                <div v-if="props.BaseFormdata.date =='month'">
                                                    当年理化<span :style="{color:scope.data.number > 0?'#1FC26B':'#F23061'}">{{scope.data.number}}</span></div>
                                                <div>
                                                    <img src="../../../../assets/icon/list.png" width="20" alt="">
                                                </div>
                                            </div>

                                        </div>
                                    </div>
                                </div>

                            </div>
                        </template>

                    </el-tree>
                </div>
            </div>
        </div>

        <div class="btn-div">
            <el-button :icon="ArrowLeftBold" type="info" :disabled="disabled" @click="toLeft()" />
            <el-button :icon="ArrowRightBold" type="info" :disabled="disabled" @click="toRight()" />
        </div>
        <div class="right-tree">
            <div class="header">
                <div class="header-left">
                    <el-checkbox v-model="checkedRight" :disabled="rightData.length < 1" label="" size="large" @change="rightAllCheck" />
                    <div class="tree-tit">{{rightTit || '右侧栏'}}</div>
                </div>
                <span class="header-right">
                    0/10
                </span>
            </div>
            <div class="content">
                <!-- 搜索框 -->
                <div class="seacrh-box">
                    <el-input v-model.trem="rightFilterText" placeholder="请输入" clearable @clear="onSearchRight" @keyup.enter="onSearchRight" @change="onSearchRight"></el-input>
                    <el-icon>
                        <Search />
                    </el-icon>
                </div>
                <!-- 数据区域 -->
                <div class="tree-box">
                    <el-tree ref="treeRefR" v-if="reLoad" :data="rightData" show-checkbox default-expand-all :node-key="nodeKey" highlight-current :props="defaultProps" :filter-node-method="filterNode" />

                </div>
            </div>

        </div>
    </div>
</template>

<script setup lang="ts">
import { ref, nextTick, defineExpose, watch } from 'vue';
import { ArrowLeftBold, ArrowRightBold, Back, Search } from '@element-plus/icons-vue';
import { array } from 'js-md5';
import lodash from 'lodash';

const props = defineProps({
	nodeKey: String,
	fromData: Array,
	toData: Array,
	defaultProps: {},
	leftTit: String,
	rightTit: String,
	disabled: {
		type: Boolean,
		default: false,
	},
	BaseFormdata: Object,
});
//定义emit
const emit = defineEmits(['checkVal']);
const treeRefL = ref([]);
const treeRefR = ref([]);
const leftData = ref([]);
const rightData = ref([]);
const reLoad = ref(true);

//右侧数据
const toData = ref([]);
// 右侧需要移除的数据
const removeData = ref([]);

const checkedLeft = ref(false);
const checkedRight = ref(false);
const leftFilterText = ref<string>('');
const rightFilterText = ref<string>('');

watch(rightFilterText, (val) => {
	treeRefR.value!.filter(val);
});
watch(leftFilterText, (val) => {
	treeRefL.value!.filter(val);
});

/**
 * 清空数据
 */
const clearData = () => {
	toData.value = [];
};
/**
 * 初始化数据
 */
const initData = () => {
	reLoad.value = true;
	const originalLeft = JSON.parse(JSON.stringify(props.fromData));
	const originalRight = JSON.parse(JSON.stringify(props.fromData));
	if (props.toData.length > 0) {
		leftData.value = sortData(originalLeft, props.toData, 'left');
		rightData.value = sortData(originalRight, props.toData, 'right');
	} else {
		leftData.value = originalLeft;
		rightData.value = [];
	}
};

//方法
//去右边
const toRight = () => {
	// 将勾选中的数据保存到toData中
	const checkNodes = treeRefL.value.getCheckedNodes(false, false);
	const newArr = toData.value.concat(checkNodes);
	const obj = {};
	const peon = newArr.reduce((cur, next) => {
		obj[next[props.nodeKey]] ? '' : (obj[next[props.nodeKey]] = true && cur.push(next));
		return cur;
	}, []); //设置cur默认类型为数组,并且初始值为空的数组
	toData.value = peon;
	reLoad.value = false;
	const originalLeft = JSON.parse(JSON.stringify(props.fromData));
	const originalRight = JSON.parse(JSON.stringify(props.fromData));
	// 抽离出选中数据中的id
	const ids = extractId(toData.value);
	// 重新整理两侧树中数据
	leftData.value = sortData(originalLeft, ids, 'left');
	rightData.value = sortData(originalRight, ids, 'right');
	nextTick(() => {
		reLoad.value = true;
	});
	checkVal();
};
//去左边
const toLeft = () => {
	// 将勾选中的数据保存到toData中
	const checkNodes = treeRefR.value.getCheckedNodes(false, false);

	const newArr = removeData.value.concat(checkNodes);
	const obj = {};
	const peon = newArr.reduce((cur, next) => {
		obj[next[props.nodeKey]] ? '' : (obj[next[props.nodeKey]] = true && cur.push(next));
		return cur;
	}, []); //设置cur默认类型为数组,并且初始值为空的数组
	const dataNeedRemove = peon;
	reLoad.value = false;
	const originalLeft = JSON.parse(JSON.stringify(props.fromData));
	const originalRight = JSON.parse(JSON.stringify(props.fromData));
	// 抽离出选中数据中的id
	const idsNeedRemove = extractId(dataNeedRemove);
	// 删除相同id
	const oldData = removeId(toData.value, idsNeedRemove);
	toData.value = oldData;
	// 右侧列表需要保留的数据的id
	const ids = extractId(oldData);
	// 重新整理两侧树中数据
	leftData.value = sortData(originalLeft, ids, 'left');
	rightData.value = sortData(originalRight, ids, 'right');
	nextTick(() => {
		reLoad.value = true;
	});
	checkVal();
};
/**
 * 将tree中的整理进行整理,判断数据是否再tree中显示
 * @param data tree数据
 * @param condition 被选中的数据
 * @param leftRight 整理左侧tree中的数据还是整理右侧tree中的数据
 */
const sortData = (data: any, condition: Array<string>, leftRight: string) => {
	if (leftRight === 'left') {
		const result = [];
		for (const item of data) {
			// 判断item的id是否在condition中,如果不在,说明不需要删除
			if (!condition.includes(item.id)) {
				// 如果item有children属性,递归调用本函数,传入item的children和condition
				if (item.children) {
					item.children = sortData(item.children, condition, leftRight);
				}
				// 如果item的children为空数组,删除item的children属性
				if (item.children && item.children.length === 0) {
					delete item.children;
				}
				result.push(item);
			} else {
				// 否则,判断item是否有children属性
				if (item.children) {
					const subResult = sortData(item.children, condition, leftRight);
					// 如果返回的结果数组不为空,说明有符合条件的子数据
					if (subResult.length > 0) {
						// 将item的children属性更新为返回的结果数组
						item.children = subResult;
						result.push(item);
					}
				}
			}
		}
		return result;
	} else {
		const result = [];
		for (const item of data) {
			// 如果item的id在condition中,说明该数据需要保留
			if (condition.includes(item.id)) {
				if (item.children) {
					item.children = sortData(item.children, condition, leftRight);
				}
				// 如果item的children为空数组,删除item的children属性
				if (item.children && item.children.length === 0) {
					delete item.children;
				}
				result.push(item);
			} else {
				// 否则,判断item是否有children属性
				if (item.children) {
					const subResult = sortData(item.children, condition, leftRight);
					// 如果返回的结果数组不为空,说明有符合条件的子数据
					if (subResult.length > 0) {
						// 将item的children属性更新为返回的结果数组
						item.children = subResult;
						result.push(item);
					}
				}
			}
		}
		return result;
	}
};
/**
 * 如果新数组中的id再旧数组中存在则删除原始数组中的id
 * @param oldIds 原始id
 * @param newIds 新id
 */
const removeId = (data: any, newIds: Array<string>) => {
	const ids = [];
	for (const item of data) {
		if (!newIds.includes(item.id)) {
			ids.push(item);
		}
	}
	return ids;
};
/**
 * 将id从备选中的数据取出
 * @param arr tree中被选中的数据
 */
const extractId = (arr: any) => {
	const newArr = [];
	for (const i in arr) {
		newArr.push(arr[i].id);
	}
	return newArr;
};
//返回父组件
const checkVal = () => {
	emit('checkVal', toData.value);
};
// 判断节点是否可拖拽
const allowDrag = (draggingNode) => {
	return draggingNode.data.labelTypeId; //唯一键,最外层父节点无labelTypeId属性,则不可拓展
};
// 拖拽时判定目标节点能否被放置
// 'prev'、'inner' 和 'next',分前、插入、后
const allowDrop = (moveNode, inNode, type: string) => {
	// allow-drop属性是用来限制树节点拖拽后是否可以放置在当前位置,属性值为true时可以,为false时不可以。
	// 1.子节点禁止外托
	// 2.禁止拖进有子节点的层级
	// 3.没有设置一级节点判断 不可拖拽在allowDrag设置
	// 二级拖动到二级
	if (moveNode.level == 2 && inNode.level == 2 && moveNode.parent.id == inNode.parent.id) {
		// 四种情况
		if (moveNode.nextSibling == undefined) {
			return type == 'prev';
		} else if (inNode.nextSibling == undefined) {
			return type == 'next';
		} else if (moveNode.nextSibling.id !== inNode.id) {
			return type == 'prev';
		} else {
			return type == 'next';
		}
	}
};
//左侧头部全选
const leftAllCheck = () => {
	const leftTree = treeRefL.value;
	if (checkedLeft.value) {
		leftTree?.setCheckedNodes(leftData.value);
		checkedLeft.value = true;
	} else {
		leftTree?.setCheckedNodes([]);
		checkedLeft.value = false;
	}
};
//右侧头部全选
const rightAllCheck = () => {
	const rightTree = treeRefR.value;
	if (checkedRight.value) {
		rightTree?.setCheckedNodes(rightData.value);
		checkedRight.value = true;
	} else {
		rightTree?.setCheckedNodes([]);
		checkedRight.value = false;
	}
};
// 左侧搜素
const onSearchLeft = () => {
	treeRefL.value!.filter(leftFilterText.value);
};
// 右侧搜素
const onSearchRight = () => {
	treeRefR.value!.filter(rightFilterText.value);
};
//tree 过滤
const filterNode = (value: string, data: any) => {
	if (!value) return true;
	return data.label.includes(value);
};

defineExpose({
	clearData,
	initData,
});
</script>


<style lang="scss" scoped>
.tree-transfer {
	width: 100%;
	height: 100%;
	display: flex;
	justify-content: space-between;
	.left-tree,
	.right-tree {
		flex-grow: 1;
		width: calc((100% - 60px) / 2);
		border-radius: 4px;
		box-sizing: border-box;
		border: 1px solid #d1d9e5;
		.header {
			display: flex;
			align-items: center;
			justify-content: space-between;
			height: 40px;
			background: #f0f2f5;
			padding: 0 12px 0 14px;
			.header-left {
				display: flex;
				align-items: center;
				width: 80%;
				.tree-tit {
					font-family: MiSans;
					font-size: 14px;
					font-weight: bold;
					line-height: 14px;
					letter-spacing: 0em;
					font-variation-settings: 'opsz' auto;
					color: #01193d;
				}
			}
		}
		.content {
			.seacrh-box {
				display: flex;
				align-items: center;
				height: 56px;
				background: #f0f2f5;
				padding: 12px;
				box-sizing: border-box;
				border: 1px solid #d1d9e5;
				.el-input {
					height: 32px;
					border-radius: 4px;
					background: #ffffff;
					box-sizing: border-box;
					border: 1px solid #d1d9e5;
					margin-right: 7px;
				}
				.el-icon {
					cursor: pointer;
				}
			}
			.tree-box {
				overflow: auto;
				min-height: 300px;
				height: 400px;
				border: 1px solid #ddd;
				border-radius: 4px;
				.item {
					padding: 0 10px;
					font-size: 14px;
					line-height: 26px;
					cursor: pointer;
					&.active {
						background: #b9d7fa;
					}
				}
				.item-checkbox {
					height: 26px;
					padding: 0 10px;
					font-size: 14px;
					line-height: 26px;
					& > .el-checkbox {
						height: 26px;
					}
				}

				.el-tree-node {
					width: 100%;
					.el-tree-node > .el-tree-node__content {
						height: 28px;
						width: 100%;
						.custom-tree-node {
							width: 100%;
							.custom-tree-node-text-box {
								display: flex;
								justify-content: space-between;
								.custom-tree-node-title {
								}
								.custom-tree-node-icon {
									span {
										width: 16px;
										height: 16px;
										opacity: 0.4;
										margin-right: 4px;
									}
									span.line {
										margin-left: 8px;
									}
									span.number {
										margin-left: 17px;
										margin-right: 12px;
									}
								}
								.custom-right {
									display: flex;
									div:first-child {
										margin-right: 5px;
									}
								}
							}
						}
					}
				}
				.el-tree {
					.el-tree-node.is-expanded.is-focusable > .el-tree-node__content {
						background: #f0f2f5;
					}
				}
			}
		}
	}
	.btn-div {
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		width: 64px;
		.el-button {
			width: 40px;
			height: 40px;
			border-radius: 4px;
			color: #4e5766;
			background: rgba(191, 200, 217, 0.2);
			box-sizing: border-box;
			border: 1px solid #bfc8d9;
			margin-bottom: 8px;
		}
		.el-button + .el-button {
			margin-left: 0;
		}
	}
	.el-checkbox__input.is-disabled .el-checkbox__inner {
		display: none;
	}
}
.el-table .rowStyle {
	background-color: #f8f8f8;
}

:deep(.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content) {
	padding-left: 0px;
	background: #f0f2f5;
}
:deep(.el-tree--highlight-current) {
	.el-tree-node.is-expanded.is-focusable {
		.el-tree-node__content {
			background-color: #f0f2f5;
		}
		.el-tree-node__children {
			.el-tree-node__content {
				background-color: #fff;
			}
		}
	}
}
</style>
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue3中,Ant-Design-Vue的Tree组件的用法与Vue2有些不同。以下是示例代码: ```html <template> <a-tree :tree-data="treeData" :draggable="true" :block-node="true" :show-line="true" :default-expanded-keys="defaultExpandedKeys" v-model:selected-keys="selectedKeys" @select="onSelect"> <template #title="{ key, title }"> <span> {{ title }} <a @click.stop="addNode(key)">Add</a> <a @click.stop="removeNode(key)">Delete</a> </span> </template> </a-tree> </template> <script> import { ref } from 'vue' export default { setup() { const treeData = ref([ { title: 'Parent 1', key: '0-0', children: [ { title: 'Child 1', key: '0-0-0' }, { title: 'Child 2', key: '0-0-1' } ] } ]) const defaultExpandedKeys = ref(['0-0']) const selectedKeys = ref([]) const addNode = (parentKey) => { const newNode = { title: 'New Node', key: `${parentKey}-${treeData.value.length}` } const parentNode = treeData.value.find(node => node.key === parentKey) if (!parentNode.children) { parentNode.children = [] } parentNode.children.push(newNode) treeData.value = [...treeData.value] } const removeNode = (key) => { const parentKey = key.split('-').slice(0, -1).join('-') const parentNode = treeData.value.find(node => node.key === parentKey) parentNode.children = parentNode.children.filter(node => node.key !== key) treeData.value = [...treeData.value] } const onSelect = (selectedKeys) => { console.log(selectedKeys) } return { treeData, defaultExpandedKeys, selectedKeys, addNode, removeNode, onSelect } } } </script> ``` 这个示例与Vue2中的示例类似,只是我们使用了Vue3的Composition API来编写代码。我们使用了ref函数来创建响应式变量。在addNode和removeNode方法中,我们使用了Vue3的响应式API来更新数据。当我们改变treeData的值时,我们必须通过解构赋值来创建一个新的数组来触发更新。注意,在Vue3中,我们使用v-model来绑定selected-keys。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值