Vue3 + TS + Element-Plus 封装Tree组件 《亲测可用》

9a69fede8b2044a79dd834e3e48f20b4.png前期回顾f8e3cc1a0f694ac2b665ca2ad14c49d7.png

Vite + Vue3 + Ts 《企业级项目》二次封装 el-table、el-pagination、el-tooltip、el-dialog_vue后台管理系统需要二次封装的组件有哪些_彩色之外的博客-CSDN博客封装的功能有哪些?分页、表格排序、文字居中、溢出隐藏、操作列、开关、宽、最小宽、type类型(selection/index/expand)、格式化 、不同页面不同操作列、vuex、vue持久化插件、(此处没有接口所以用到,还涉及了query与params传值区别)子组件说思路:data数据请求接口拿到,表头数据一般也是后台接口,如没,前台可自定义自己写......_vue后台管理系统需要二次封装的组件有哪些https://blog.csdn.net/m0_57904695/article/details/125613767?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168906559516800227493706%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=168906559516800227493706&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-5-125613767-null-null.268%5Ev1%5Ekoosearch&utm_term=%E5%B0%81%E8%A3%85&spm=1018.2226.3001.4450

目录

😷  子组件:

😎  父组件:

🥰😉  谢谢观看


😷  子组件:

<template>
	<template v-if="isOperator">
		<el-button size="small" @click="addNode" type="primary" style="margin-left: 2px">
			<SvgIcon name="ant-PlusOutlined"></SvgIcon>
			添加节点</el-button
		>
		<el-button size="small" @click="removeNode" type="danger" style="margin-left: 72px">
			<SvgIcon name="ant-ClearOutlined"></SvgIcon>
			删除节点</el-button
		>
		<el-input
			ref="searchInputRef"
			v-model="searchQuery"
			size="small"
			placeholder="搜索菜单"
			class="mb5 mt5"
			style="width: 280px; display: block"
			:maxlength="20"
			show-word-limit
			clearable
		></el-input>
	</template>

	<el-tree
		class="tree"
		ref="treeRef"
		:indent="0"
		node-key="id"
		:data="props.treeData"
		:props="props.defaultProps"
		:check-strictly="checkStrictly"
		:show-checkbox="isShowCheckbox"
		:check-on-click-node="checkOnClickNode"
		:default-expand-all="defaultExpandAll"
		:allow-drag="allowDrag"
		:draggable="isDraggable"
		:allow-drop="allowDrop"
		@node-drag-end="handleDragEnd"
		@node-click="handleNodeClick"
		@node-contextmenu="editNode"
		@check-change="getCheckedAllNodes"
		:filter-node-method="filterNode"
		v-bind="$attrs"
	>
		<template #default="{ node }">
			<SvgIcon :name="checkIconByNodeLevel(node)" class="mr5"></SvgIcon>
			<input
				v-if="showIpt && node.label === curNodLabel && isEditNode"
				ref="inputRef"
				type="text"
				:value="node.label"
				@blur="showIpt = false"
				@keyup.enter="updateNodeLabel($event, node)"
			/>
			<span v-else>
				<el-tooltip
					:content="node.label"
					placement="right"
					v-if="node.label.length > ellipsisLen"
				>
					<span :class="compNodeCustomized(node)">
						{{ ellipsis(node.label, ellipsisLen) }}
					</span>
				</el-tooltip>
				<span :class="compNodeCustomized(node)" v-else>
					{{ node.label }}
				</span>
			</span>
		</template>
	</el-tree>
</template>

<script setup lang="ts">
import { nextTick, ref, computed, watch, onMounted } from 'vue';
import type Node from 'element-plus/es/components/tree/src/model/node';
const showIpt = ref<boolean>(false); // 是否显示输入框
const curNodLabel = ref<string>(); // 记录右键点击的节点
const inputRef = ref(); // 输入框实例
const treeRef = ref(); // 树实例
const searchInputRef = ref(); // 树搜索实例

// 判断节点能否被放置 如果返回 false ,节点不能被放置
const allowDrop = () => true;
// 判断节点能否被拖拽 如果返回 false ,节点不能被拖动
const allowDrag = () => true;

const ellipsis = (value: string, len: number) => {
	if (!value) return '';
	if (value.length > len) return value.slice(0, len) + '...';
};
interface DefaultProps {
	id: string;
	children: string;
	label: string;
	disabled: string;
}
const emits = defineEmits(['eCurNode', 'eCheckedNodes', 'eSaveNodes']);
interface Props {
	treeData?: any[]; // 树数据
	checkStrictly?: boolean; // 是否严格模式
	isShowCheckbox?: boolean; // 是否显示复选框
	checkOnClickNode?: boolean; // 点击节点时是否选中复选框
	defaultExpandAll?: boolean; // 是否默认展开所有
	isDraggable?: boolean; // 是否可拖拽
	isOperator?: boolean; // 是否有操作
	isEditNode?: boolean; // 是否可编辑节点
	ellipsisLen?: number; // 文本超出长度
	defaultProps?: DefaultProps; // 默认属性
	treeIcon?: string[]; // 树图标 [0]:最后一级图标;[1]:展开图标;[2]:折叠图标;
}

const props = withDefaults(defineProps<Props>(), {
	treeData: () => [],
	checkStrictly: true,
	isShowCheckbox: true,
	checkOnClickNode: false, // true:不关联,false:关联 (line:105 handleNodeClick做了处理)
	defaultExpandAll: true,
	isDraggable: false,
	isOperator: false,
	isEditNode: false,
	ellipsisLen: 10,
	defaultProps: (): DefaultProps => ({
		id: 'strategyId',
		children: 'children',
		label: 'label',
		disabled: 'disabled',
	}),
	treeIcon: () => ['FileOutlined', 'FolderOpenOutlined', 'MinusSquareOutlined'],
});

const searchQuery = ref('');

// 点击节点时触发
const handleNodeClick = (data: Tree, node: { data: { disabled: boolean }; checked: boolean }) => {
	emits('eCurNode', data);
	// 点击该行 如果不是复选框或是禁用的,不勾选
	if (!props.isShowCheckbox || node.data.disabled) return;
	node.checked = !node.checked;
};

// 删除节点
const removeNode = () => {
	const checkedNodes = treeRef.value.getCheckedNodes();
	if (checkedNodes.length === 0) return alert('请至少勾选一项才能删除节点');
	for (const node of checkedNodes) {
		nextTick(() => {
			treeRef.value.remove(node.id, false);
		});
	}
};

// 右击节点时触发
const editNode = (event: MouseEvent, node: Node & DefaultProps) => {
	event.preventDefault();
	curNodLabel.value = node[props.defaultProps.label];
	showIpt.value = true;
	nextTick(() => {
		inputRef.value?.focus();
	});
};

// 更新节点的label
const updateNodeLabel = (e: Event, node: Tree) => {
	const target = e.target as HTMLInputElement;
	if (isValueInTree(props.treeData, target.value)) return alert('该节点已存在');
	node = Object.assign({}, node);
	node.data.label = target.value;
	showIpt.value = false;
};

// 使用非递归方式查找树中是否存在某个值
function isValueInTree(data: string | any[], value: string) {
	const stack = [...data];
	while (stack.length) {
		const node = stack.pop();
		if (node.label === value) return true;
		if (Array.isArray(node.children)) {
			stack.push(...node.children);
		}
	}
	return false;
}

// 新增节点
const addNode = () => {
	const checkedNodes = treeRef.value.getCheckedNodes();
	if (checkedNodes.length === 0) return alert('请至少勾选一项才能添加节点');
	const nodeName = prompt('请输入节点名称');
	if (!nodeName) return;

	if (isValueInTree(props.treeData, nodeName)) return alert('该节点已存在');
	for (const parentNode of checkedNodes) {
		const newNode = {
			id: props.treeData.length + 1,
			label: nodeName,
		};
		if (!parentNode.children) {
			parentNode.children = [];
		}
		parentNode.children.push(newNode);
	}
};

// 结束拖拽
const handleDragEnd = (dropNode: Node) => {
	if (!dropNode) return;
	if (props.isDraggable === false) return;
	saveNode();
};

// 保存节点
function saveNode() {
	emits('eSaveNodes', props.treeData);
}

// 复选框改变
const getCheckedAllNodes = (data: Tree, isSelected: boolean) => {
	if (!props.isShowCheckbox) return;
	const checkedNodes = treeRef.value.getCheckedNodes();
	const halfCheckedNodes = treeRef.value.getHalfCheckedNodes();
	// data: 当前节点的数据
	// isSelected: 当前节点是否被选中
	// checkedNodes: 所有选中的节点
	// halfCheckedNodes: 所有半选中的节点
	emits('eCheckedNodes', data, isSelected, checkedNodes, halfCheckedNodes);
};

// 根据节点层级显示不同的图标
const checkIconByNodeLevel = computed(() => {
	return (node: { childNodes: []; expanded: boolean }) => {
		if (node.childNodes.length === 0) return `ant-${props.treeIcon[0]}`;
		return node.expanded ? `ant-${props.treeIcon[1]}` : `ant-${props.treeIcon[2]}`;
	};
});

// 根据id || disabled 控制节点颜色变化
const compNodeCustomized = computed(() => {
	// const customizedIds = [1]; // 指定需要自定义的节点id数组

	return (node: { data: { id: number; disabled: boolean } }) => {
		if (node.data.disabled) return 'node-customized';
		// return customizedIds.includes(node.data.id) ? 'node-customized' : '';
	};
});

// 数据搜索
const filterNode = (value: string, data: any) => {
	if (!value) return true;
	return data.label.includes(value);
};
watch(searchQuery, (newVal) => {
	treeRef.value?.filter(newVal);
});
onMounted(() => setTimeout(() => searchInputRef.value?.focus(), 1000));
defineExpose({
	treeRef,
	removeNode,
	addNode,
});
</script>

<style lang="scss" scoped>
.tree {
	border-radius: 5px;
	// background-color: #d30808;
	padding: 15px;
	width: 280px;
	height: 100%;

	// height: calc(100vh - 115px);
	// min-height: 750px;
	padding-bottom: 10px;
	overflow-x: hidden;
	overflow-y: auto;
	border: 1px solid #e6e6e6;

	&:hover {
		&:hover {
			box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
		}
	}
	transition: box-shadow 0.3s ease-in-out;

	:deep(.el-tree-node) {
		position: relative;
	}
	:deep(.el-tree-node__children) {
		padding-left: 16px;
	}
	:deep(.el-tree-node::before) {
		content: '';
		width: 22px;
		height: 20px;
		position: absolute;
		left: -12px;
		top: 1px;
		border-width: 1px;
		border-left: 1px dashed #ccc;
	}
	:deep(.el-tree-node:last-child::before) {
		height: 38px;
	}
	:deep(.el-tree-node::after) {
		content: '';
		width: 22px;
		height: 20px;
		position: absolute;
		left: -3px;
		top: 11px;
		border-width: 1px;
		border-top: 1px dashed #ccc;
	}
	& > :deep(.el-tree-node::after) {
		border-top: none;
	}
	& > :deep(.el-tree-node::before) {
		border-left: none;
	}
	:deep(.el-tree-node__expand-icon) {
		font-size: 16px;
		:deep(&.is-leaf) {
			color: transparent;
			display: none;
		}
	}
	.node-customized {
		color: #c4bbb3;
	}
	// 一级不显示复选框其余内层都显示
	// :deep(.el-checkbox .el-checkbox__inner) {
	// 	display: none;
	// }

	// :deep(div[role='group']) {
	// 	.el-checkbox .el-checkbox__inner {
	// 		display: inline-block;
	// 	}
	// }

	// :deep(.el-tree-node__content) {
	// 	:deep(.el-checkbox .el-checkbox__inner) {
	// 		display: none;
	// 	}
	// }

	// 只有最后一级显示复选框
	// :deep(.el-tree-node) {
	// 	.is-leaf + .el-checkbox .el-checkbox__inner {
	// 		display: inline-block;
	// 	}
	// 	.el-checkbox .el-checkbox__inner {
	// 		display: none;
	// 	}
	// }
	/* 隐藏所有复选框 
	-----------------------------------------------------------*/
	// :deep(.el-checkbox .el-checkbox__inner) {
	// 	display: none;
	// }

	/* 只在三级菜单中显示复选框 
	-----------------------------------------------------------*/
	// :deep(div[role='group'] div[role='group'] .el-checkbox .el-checkbox__inner) {
	// 	display: inline-block;
	// }
}
:deep(.el-input__wrapper, .el-input__wrapper.is-focus) {
	width: 100%;
}
</style>

😎  父组件:

<template>
	<div class="con">
		<zw-tree @eSaveNodes="onSaveNodes" v-bind="state" />
	</div>
</template>

<script setup lang="ts">
import { reactive } from 'vue';
const state = reactive({
	treeData: [
		{
			id: 1,
			label: '一级 1一级一级一级一级一级一级一级一级一级一级一级一级',
			disabled: true,
			children: [
				{
					id: 4,
					label: '二级 1-1',
					disabled: null,

					children: [
						{
							id: 9,
							label: '三级 1-1-1',
						},
						{
							id: 10,
							label: '三级 1-1-2',
						},
					],
				},
			],
		},
		{
			id: 2,
			label: '一级 2',
			children: [
				{
					id: 5,
					label: '二级 2-1',
				},
				{
					id: 6,
					label: '二级 2-2',
				},
			],
		},
		{
			id: 3,
			label: '一级 3',
			children: [
				{
					id: 7,
					label: '二级 3-1',
				},
				{
					id: 8,
					label: '二级 3-2',
				},
			],
		},
		{
			id: 1,
			label: '一级 1一级一级一级一级一级一级一级一级一级一级一级一级',
			disabled: true,
			children: [
				{
					id: 4,
					label: '二级 1-1',
					disabled: null,

					children: [
						{
							id: 9,
							label: '三级 1-1-1',
						},
						{
							id: 10,
							label: '三级 1-1-2',
						},
					],
				},
			],
		},
		{
			id: 2,
			label: '一级 2',
			children: [
				{
					id: 5,
					label: '二级 2-1',
				},
				{
					id: 6,
					label: '二级 2-2',
				},
			],
		},
		{
			id: 3,
			label: '一级 3',
			children: [
				{
					id: 7,
					label: '二级 3-1',
				},
				{
					id: 8,
					label: '000000000000000000000',
				},
			],
		},
		{
			id: 4,
			label: '一级 3',
			children: [
				{
					id: 7,
					label: '二级 3-1',
				},
				{
					id: 8,
					label: '222222',
				},
			],
		},
		{
			id: 5,
			label: '一级 3',
			children: [
				{
					id: 7,
					label: '二级 3-1',
				},
				{
					id: 8,
					label: '注意不要遮挡数据,底部要展示出来-----------------------------------------',
				},
			],
		},
	],
	isOperator: true,
});
function onSaveNodes(data: Tree) {
	console.log(data);
}
</script>

<style>
.con {
	padding: 10px;
	/* 【重要】tree的高度根据外层容器,tree默认100% */
	width: 280px;
	/* height: 500px; */
	height: 100%;
}
</style>

更多的el-tree看这里 🤺👉  自定义《element-UI》el-tree 的样式 、亲测管用_自定义《element-UI》el-tree 的样式 、亲测管用_https://blog.csdn.net/m0_57904695/article/details/123514519?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168906618216800211567162%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=168906618216800211567162&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-123514519-null-null.268%5Ev1%5Ekoosearch&utm_term=el-tree&spm=1018.2226.3001.4450彩色之外的博客-csdn博客<>

更多的el-table看这里  😂 

点击《el-table》让选中的行变色,亲测实用_彩色之外的博客-CSDN博客公司各种需求又来了,直接看下面文吧,一看就懂就不在说需求了,此时我觉得我的表情包是【我就像是一个小朋友站在路标下满头的问号】亲测管用,希望可以帮助到大家https://blog.csdn.net/m0_57904695/article/details/123722382?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168906621616782425128470%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=168906621616782425128470&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-9-123722382-null-null.268%5Ev1%5Ekoosearch&utm_term=%E8%A1%A8%E6%A0%BC&spm=1018.2226.3001.4450

🥰😉  谢谢观看

7730e2bd39d64179909767e1967da702.jpeg

 _______________________________  期待再见  _______________________________ 

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

彩色之外

你的打赏是我创作的氮气加速动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值