合作联系微信: w6668263      合作联系电话:177-9238-7426     

移动端tree组件父子组件联动。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

TreeList组件

 

 

<!--
 * @Author: yeminglong
 * @Date: 2024-09-27 10:14:30
 * @LastEditors: yeminglong
 * @LastEditTime: 2024-10-17 15:56:43
 * @Description:
 -->
<script>
import TreeItem from '@/components/treeList/TreeItem.vue'

export default {
	name: 'TreeList',
	components: { TreeItem },
	props: {
		// 默认勾选的节点的 key 的数组
		defaultCheckedKeys: Array,
		// 默认展开的节点的 key 的数组
		defaultExpandedKeys: Array,
		// 是否每次只打开一个同级树节点展开
		accordion: Boolean,
		// 数据源
		data: Array,
		// 配置属性
		props: {
			label: 'name',
			children: 'children'
		},
		// 节点标识
		nodeKey: String
	},

	// provide: {
	// 	// 当前选中的节点
	// 	currentNode: {
	// 		data: null
	// 	}
	// },
	watch: {
		data: {
			handler(data) {
				this.initData()
			},
			deep: true,
			immediate: true
		},
		defaultCheckedKeys: {
			handler(keys) {
				this.setCheckedKeys(keys)
			},
			deep: true,
			immediate: true
		},
		defaultExpandedKeys: {
			handler(keys) {
				this.setExpandedKeys(keys)
			},
			deep: true,
			immediate: true
		},
		config: {
			handler() {
				this.initData()
			},
			deep: true
		},
		props: {
			handler(props) {
				Object.assign(this.config, props)
			},
			deep: true,
			immediate: true
		}
	},
	data() {
		return {
			config: {
				label: 'name',
				children: 'children',
				...this.props
			},
			treeList: []
		}
	},
	methods: {
		initData() {
			const { config } = this
			const treeList = []
			const deep = (list = [], parent = null) => {
				for (const item of list) {
					const children = item?.[config.children] ?? []
					const node = {
						// 勾选状态
						checked: false,
						// 是否是半选
						isHalf: false,
						// 是否是叶子节点
						isLeaf: children.length === 0,
						// 层级默认是从0开始
						level: !parent ? 0 : parent.level + 1,
						label: item[config.label],
						disabled: item?.disabled ?? false,
						open: item?.open ?? true,
						// 数据
						data: item,
						// 子节点
						children: []
					}
					if (!parent) {
						treeList.push(node)
					} else {
						parent.children.push(node)
					}

					if (children && children.length) {
						node.isLeaf = false
						deep(item.children, node)
					}
				}
			}
			deep(this.data, null)
			this.treeList = treeList
			this.setCheckedKeys(this.defaultCheckedKeys)
		},
		/**
		 * 获取数据集合
		 * @returns {*[]}
		 */
		getData() {
			return this.treeList
		},
		/**
		 * 则返回目前被选中的节点的 key 所组成的数组
		 * @param leafOnly {Boolean} 是否只是叶子节点
		 * @returns {*[]}
		 */
		getCheckedKeys(leafOnly = false) {
			const { treeList, config, nodeKey } = this
			const checkedKeys = []
			const deep = (list = []) => {
				for (const item of list) {
					if (!leafOnly && item.checked) {
						// console.log(item.data[config.label])
						checkedKeys.push(item.data[nodeKey])
					}
					if (item.children && item.children.length) {
						deep(item.children)
					} else {
						if (leafOnly && item.checked) {
							// console.log(item.data[config.label])
							checkedKeys.push(item.data[nodeKey])
						}
					}
				}
			}
			deep(treeList)
			return checkedKeys
		},
		/**
		 * 返回目前被选中的节点所组成的数组
		 * @param leafOnly {Boolean} 是否只是叶子节点
		 * @param includeHalfChecked {Boolean} 是否包含半选节点
		 * @returns {*[]}
		 */
		getCheckedNodes(leafOnly = false, includeHalfChecked = false) {
			const { treeList, config } = this
			const checkedNodes = []
			const deep = (list = []) => {
				for (const item of list) {
					if (!leafOnly) {
						if (
							item.checked &&
							!(includeHalfChecked && item.isHalf)
						) {
							const node = { ...item.data }
							delete node[config.children]
							checkedNodes.push(node)
						}
					}

					if (item.children && item.children.length) {
						deep(item.children)
					} else {
						if (leafOnly) {
							if (
								item.checked &&
								!includeHalfChecked &&
								!item.isHalf
							) {
								const node = { ...item.data }
								delete node[config.children]
								checkedNodes.push(node)
							}
						}
					}
				}
			}
			deep(treeList)
			// console.log(JSON.stringify(checkedNodes, null, 4))
			return checkedNodes
		},
		/**
		 * 通过 keys 设置目前勾选的节点,使用此方法必须设置 node-key 属性
		 * @param keys
		 * @param leafOnly
		 */
		setCheckedKeys(keys = [], leafOnly = false) {
			const { config, nodeKey } = this
			// const isHalf = item => {
			// 	const children = item.children
			// 	let totalNodes = 0
			// 	let checkedNodes = 0
			// 	function isHalfSelected(nodes = []) {
			// 		for (const node of nodes) {
			// 			totalNodes++
			// 			if (node.checked) {
			// 				checkedNodes++
			// 			}
			// 			if (node.children.length) {
			// 				isHalfSelected(node.children)
			// 			}
			// 		}
			// 	}
			// 	isHalfSelected(children)
			// 	return checkedNodes > 0 && totalNodes !== checkedNodes
			// }
			const isHalfSelected = (nodes = []) => {
				let totalNodes = 0
				let checkedNodes = 0

				function traverse(node) {
					totalNodes++
					if (node.checked) {
						checkedNodes++
					}
					if (node.children.length) {
						node.children.forEach(traverse)
					}
				}

				nodes.forEach(traverse)
				return checkedNodes > 0 && totalNodes !== checkedNodes
			}

			const isHalf = item => {
				const children = item.children
				return isHalfSelected(children)
			}
			const deep = (list = [], parent = null) => {
				for (const item of list) {
					const children = item.children
					if (children?.length) {
						deep(children, item)
					}
					item.checked = keys.includes(item.data[nodeKey])
					const checkCount = children.filter(v => v.checked).length

					//  非叶子节点 判断选中和半选状态
					if (!item.isLeaf) {
						// 半选状态
						item.isHalf = isHalf(item)
						item.checked = children.length === checkCount
					}
				}
			}

			if (keys) {
				deep(this.treeList, null)
			}
		},
		/**
		 * 默认展开的节点的 key 的数组
		 * @param keys
		 */
		setExpandedKeys(keys = []) {
			const { nodeKey } = this

			const deep = (list = [], parent = null) => {
				for (const item of list) {
					const children = item.children
					if (children?.length) {
						deep(children, item)
					}
					item.open = keys.includes(item.data[nodeKey])
				}
			}

			if (keys.length) {
				deep(this.treeList, null)
			}
		}
	}
}
</script>

<template>
	<div class="tree-list">
		<div
			class="root-item"
			v-for="(item, index) in treeList"
			:key="`children-${(nodeKey && item.data[nodeKey]) || index}`"
		>
			<tree-item
				:accordion="accordion"
				:half-value="item.isHalf"
				@half-change="
					val => {
						treeList[index].isHalf = val
						$set(treeList[index], 'isHalf', val)
					}
				"
				:disabled="item.disabled"
				:open="item.open"
				@open-change="
					open => {
						if (accordion) {
							treeList.forEach((child, i) => {
								child.open = false
								$set(treeList[i], 'open', false)
							})
							treeList[index].open = true
							$set(treeList[index], 'open', true)
						} else {
							treeList[index].open = open
							$set(treeList[index], 'open', open)
						}
					}
				"
				:is-leaf="treeList[index].isLeaf"
				v-model="treeList[index].checked"
				:node-key="nodeKey"
				:path="[item.data?.[nodeKey] ?? item.label]"
				:props="props"
				:data="item.data"
				:label="item.label"
				:children="item.children"
				@check="$emit('check', $event)"
			>
				<template
					#label="{ data, label, value, checked, path, isHalf }"
				>
					<slot
						name="label"
						v-bind="{
							data,
							label,
							value,
							checked,
							path,
							isHalf
						}"
					>
						{{ label }}
					</slot>
				</template>
			</tree-item>
		</div>
	</div>
</template>

<style scoped lang="scss">
.root-item {
	//background: #f5f8fd;
	//border-radius: 12px;
	//padding: 38px 50px;
	//margin: 20px 0 20px 0;
}
</style>

 

 

 

 

TreeItem组件

<!--
 * @Author: yeminglong
 * @Date: 2024-09-25 09:57:50
 * @LastEditors: yeminglong
 * @LastEditTime: 2024-10-17 15:57:34
 * @Description:
 -->
<script>
import CheckBox from '@/components/treeList/CheckBox.vue'

export default {
	name: 'TreeItem',
	components: { CheckBox },
	// inject: ['currentNode'],
	props: {
		// 是否每次只打开一个同级树节点展开
		accordion: Boolean,
		level: {
			type: Number,
			default: 0
		},
		value: Boolean,
		halfValue: Boolean,
		isLeaf: Boolean,
		disabled: Boolean,
		open: { type: Boolean, default: true },
		nodeKey: String,
		path: Array,
		props: {
			label: 'name',
			children: 'children'
		},
		data: Object,
		label: String,
		children: Array
	},
	computed: {
		isHalf() {
			const children = this.children
			let totalNodes = 0
			let checkedNodes = 0
			function isHalfSelected(nodes = []) {
				for (const node of nodes) {
					totalNodes++
					if (node.checked) {
						checkedNodes++
					}
					if (node.children.length) {
						isHalfSelected(node.children)
					}
				}
			}
			isHalfSelected(children)
			return checkedNodes > 0 && totalNodes !== checkedNodes
		},
		nodeData() {
			// 派发节点check事件
			const node = { ...this.data }
			delete node[this.props.children]
			return node
		}
	},
	watch: {
		value: {
			handler(checked) {
				this.checked = checked
			},
			immediate: true
		},
		// checked(checked) {
		// 	if (this.level >= this.currentNode.data?.level) {
		// 		this.checkedChildren(checked)
		// 	}
		// },

		isHalf(halfValue) {
			this.$emit('half-change', halfValue)
		},
		open: {
			handler(open) {
				this.expanded = open
			},
			immediate: true
		},
		expanded(open) {}
	},
	data() {
		return {
			checked: false,
			expanded: true
		}
	},
	methods: {
		/**
		 * 点击节点
		 */
		handleClick(checked) {
			this.checkedChildren(this.children, checked)
			// if (this.disabled) {
			// 	return false
			// }
			// const checked = !this.checked
			// this.currentNode.data = { checked, ...this.$props }
			this.checked = checked
			this.$emit('input', checked)
			this.$emit('node-change', { checked, data: this.nodeData })
			this.$nextTick(() => {
				this.$emit('check', {
					checked,
					data: this.nodeData,
					label: this.label,
					path: this.path,
					isHalf: this.isHalf
				})
			})
		},
		/**
		 * children 全选 反选
		 * @param children
		 * @param checked
		 */
		checkedChildren(children = [], checked = false) {
			// const children = this?.children
			if (children && children.length) {
				for (const child of children) {
					if (child.children.length) {
						this.checkedChildren(child.children, checked)
					}

					child.checked = checked
				}
			}
		},
		/**
		 * 父级节点处理node-change事件
		 */
		nodeChange() {
			const children = this.children
			const checkCount = children.filter(v => v.checked).length
			// this.checked = count > 0
			this.checked = children.length === checkCount

			this.$emit('input', this.checked)
			this.$emit('node-change', {
				checked: this.checked,
				data: this.nodeData
			})
		},
		openChange({ open, index }) {
			if (this.accordion) {
				this.children.forEach((child, i) => {
					child.open = false
					this.$set(this.children[i], 'open', false)
				})
				this.children[index].open = true
				this.$set(this.children[index], 'open', true)
			} else {
				this.children[index].open = open
				this.$set(this.children[index], 'open', open)
			}
		}
	}
}
</script>

<template>
	<div class="tree-item" :class="`tree-item-${level}`">
		<div class="tree-item-header">
			<div class="checkbox-box">
				<div
					class="checkbox-box-toggle"
					:class="{
						'is-expanded': expanded,
						'is-leaf': !children.length
					}"
				>
					<div
						v-if="children.length"
						@click="
							() => {
								expanded = !expanded
								$emit('open-change', expanded)
							}
						"
					>
						<svg-icon
							icon-class="common-arrow"
							class="toggle-arrow"
						/>
					</div>
				</div>
				<check-box
					v-model="checked"
					@change="handleClick"
					:disabled="disabled"
					:half="isHalf"
				>
					<!--<div-->
					<!--	class="checkbox-box-input"-->
					<!--	:class="{-->
					<!--		'is-checked': checked,-->
					<!--		'is-half': isHalf,-->
					<!--		'is-disabled': disabled-->
					<!--	}"-->
					<!--&gt;-->
					<!--	<template v-if="checked">-->
					<!--		<svg-icon-->
					<!--			class="checkbox-box-input-icon"-->
					<!--			icon-class="common-checked"-->
					<!--		/>-->
					<!--	</template>-->
					<!--	<template v-else>-->
					<!--		<span class="half-line" v-if="isHalf">&nbsp;</span>-->
					<!--	</template>-->
					<!--</div>-->
					<div class="checkbox-box-label">
						<slot
							name="label"
							v-bind="{
								data,
								label,
								value,
								checked,
								path,
								isHalf
							}"
						>
							{{ label }}
						</slot>
					</div>
				</check-box>
			</div>
		</div>

		<div
			class="tree-item-sub"
			:class="[`tree-item-sub-${level}`, !expanded && `sub-is-expanded`]"
			v-if="children && children.length"
		>
			<!--:value="item.checked"-->
			<!--@input="value => (children[index].checked = value)"-->
			<tree-item
				v-for="(item, index) in children"
				:key="`children-${(nodeKey && item.data[nodeKey]) || index}`"
				:level="level + 1"
				:accordion="accordion"
				:half-value="item.isHalf"
				@half-change="
					val => {
						children[index].isHalf = val
						$set(children[index], 'isHalf', val)
					}
				"
				:is-leaf="children[index].isLeaf"
				:disabled="item.disabled"
				:open="item.open"
				@open-change="
					open => {
						openChange({ open, index })
					}
				"
				v-model="children[index].checked"
				@node-change="nodeChange"
				:node-key="nodeKey"
				:path="[...path, item.data?.[nodeKey] ?? item.label]"
				:props="props"
				:data="item.data"
				:label="item.label"
				:children="item.children"
				@check="$emit('check', $event)"
			>
				<template
					#label="{ data, label, value, checked, path, isHalf }"
				>
					<slot
						name="label"
						v-bind="{
							data,
							label,
							value,
							checked,
							path,
							isHalf
						}"
					>
						{{ label }}
					</slot>
				</template>
			</tree-item>
		</div>
	</div>
</template>

<style scoped lang="scss">
.tree-item {
	//margin: 5px 0 5px 0;
}

.tree-item-header {
	display: flex;
	align-items: center;
	padding: 15px 0;
	//margin-bottom: 20px;
}

.checkbox-box {
	display: flex;
	//align-items: center;
	align-items: flex-start;
	font-size: 38px;
	color: #323334;

	> div {
		display: flex;
		align-items: center;

		&:last-of-type {
			flex: 1;
			display: flex;
			align-items: flex-start;
			//border: 1px solid red;
		}
	}

	.checkbox-box-toggle {
		width: 50px;
		height: 50px;
		display: inline-flex;
		align-items: center;
		justify-content: center;
		cursor: pointer;
		font-size: 45px;
		transform: rotate(0deg);
		transition: transform 0.3s ease-in-out;
		transform-origin: 50% 50%;
		margin-right: 15px;
		overflow: hidden;
		color: #c0c4cc;

		&.is-expanded {
			transform: rotate(90deg);
		}

		&.is-leaf {
			//visibility: hidden;
		}

		//:deep(.toggle-arrow) {
		//	color: #c0c4cc;
		//}
	}

	.checkbox-box-input {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		width: 50px;
		height: 50px;
		border-radius: 10px;
		border: 2px solid rgb(200, 201, 204);
		background: #fff;
		color: #fff;
		margin-right: 10px;
		cursor: pointer;
		overflow: hidden;

		:deep(.checkbox-box-input-icon) {
			width: 45px;
			height: 45px;
			overflow: hidden;
		}

		&:hover {
			border: 2px solid #0262f1;
		}

		&.is-half,
		&.is-checked {
			border: 2px solid #0262f1;
			background: #0262f1;
			overflow: hidden;
		}
		&.is-disabled {
			background-color: #ebf0fa;
			border-color: rgb(220, 223, 230);
			cursor: not-allowed;
			overflow: hidden;
			color: rgb(190, 194, 202);

			&:hover {
				border: 2px solid #dcdfe6;
			}
		}
		.half-line {
			border-radius: 4px;
			height: 4px;
			width: 60%;
			background: #fff;
		}
	}

	.checkbox-box-label {
		flex: 1;
	}
}

.tree-item-sub {
	padding-left: 50px;
	height: auto;
	opacity: 1;
	overflow: hidden;

	&.sub-is-expanded {
		opacity: 0;
		height: 0;
		transition: opacity 0.5s ease, height 0.5s ease;
	}
}
</style>

 

checkbox组件

<!--
 * @Author: yeminglong
 * @Date: 2024-09-25 09:57:50
 * @LastEditors: yeminglong
 * @LastEditTime: 2024-10-17 15:57:34
 * @Description:
 -->
<script>
export default {
	name: 'CheckBox',
	props: {
		disabled: Boolean,
		value: Boolean,
		half: Boolean
	},
	watch: {
		value: {
			handler(checked) {
				this.checked = checked
			},
			immediate: true
		},
		checked: {
			handler(checked) {
				this.$emit('input', checked)
			},
			immediate: true
		}
	},
	data() {
		return {
			checked: this.value
		}
	},
	methods: {
		handleClick() {
			if (this.disabled) {
				return
			}
			this.checked = !this.checked
			this.$emit('input', this.checked)
			this.$emit('change', this.checked)
		}
	}
}
</script>

<template>
	<div @click="handleClick">
		<div
			class="checkbox-box-input"
			:class="{
				'is-checked': checked,
				'is-half': half,
				'is-disabled': disabled
			}"
		>
			<template v-if="checked">
				<svg-icon
					class="checkbox-box-input-icon"
					icon-class="common-checked"
				/>
			</template>
			<template v-else>
				<span class="half-line" v-if="half">&nbsp;</span>
			</template>
		</div>
		<slot> </slot>
	</div>
</template>

<style scoped lang="scss">
.checkbox-box-input {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 50px;
	height: 50px;
	border-radius: 10px;
	border: 2px solid rgb(200, 201, 204);
	background: #fff;
	color: #fff;
	margin-right: 10px;
	cursor: pointer;
	overflow: hidden;

	:deep(.checkbox-box-input-icon) {
		width: 45px;
		height: 45px;
		overflow: hidden;
	}

	&:hover {
		border: 2px solid #0262f1;
	}

	&.is-half,
	&.is-checked {
		border: 2px solid #0262f1;
		background: #0262f1;
		overflow: hidden;
	}

	&.is-disabled {
		background-color: #ebf0fa;
		border-color: rgb(220, 223, 230);
		cursor: not-allowed;
		overflow: hidden;
		color: rgb(190, 194, 202);

		&:hover {
			border: 2px solid #dcdfe6;
		}
	}

	.half-line {
		border-radius: 4px;
		height: 4px;
		width: 60%;
		background: #fff;
	}
}
</style>

 

 

 

调用TreeList组件示例:

<!--
 * @Author: yeminglong
 * @Date: 2024-09-25 09:46:34
 * @LastEditors: yeminglong
 * @LastEditTime: 2024-10-15 13:48:28
 * @Description:
 -->
<script>
import TreeList from '@/components/treeList/TreeList.vue'

export default {
	name: 'index',
	components: { TreeList },
	computed: {},
	data() {
		const treeData = [
			{
				id: 1,
				label: '一级 1',
				children: [
					{
						id: 4,
						label: '二级 1-1',
						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'
					}
				]
			}
		]
		return {
			treeData,
			defaultProps: {
				children: 'children',
				label: 'label'
			},
			defaultCheckedKeys: []
		}
	},
	methods: {
		check({ checked, data, path }) {
			console.log(path)
			// alert(JSON.stringify({ checked, data }))
		},
		getCheckedKeys() {
			console.log(this.$refs.treeListRef.getCheckedKeys(true))
		},
		getCheckedNodes() {
			console.log(this.$refs.treeListRef.getCheckedNodes(false, false))
		}
	}
}
</script>

<template>
	<div class="root-flex">
		<div>
			<button @click="getCheckedKeys">getCheckedKeys()</button>
			&nbsp;&nbsp;
			<button @click="getCheckedNodes">getCheckedNodes()</button>
			<tree-list
				ref="treeListRef"
				:data="treeData"
				node-key="id"
				:props="defaultProps"
				@check="check"
				:default-checked-keys="defaultCheckedKeys"
			>
				<template
					#label="{ data, value, checked, path, isHalf, label }"
				>
					{{ label }}
					<!--({{ data.count }}) &#45;&#45;checked:{{-->
					<!--	checked-->
					<!--}}-->
					<!--&#45;&#45;halfValue:{{ halfValue }}&#45;&#45;isHalf:{{ isHalf }}-->
				</template>
			</tree-list>
		</div>
		<!--<div>-->
		<!--	<textarea-->
		<!--		style="width: 100%"-->
		<!--		:rows="6"-->
		<!--		readonly-->
		<!--		:value="JSON.stringify(newTreeData, null, 4)"-->
		<!--	/>-->
		<!--</div>-->
	</div>
</template>

<style scoped lang="scss">
.root-flex {
	width: 100%;
	height: 100%;
	display: flex;
	overflow-y: auto;
	background: #fff;
	font-size: 20px;

	> div {
		height: 100%;
		width: 100%;
	}

	//:deep(.tree-item-1) {
	//	background: #f5f8fd;
	//	border-radius: 12px;
	//	overflow: hidden;
	//	margin-top: 10px;
	//	padding: 20px;
	//}
}
</style>

 

 

用到的图标资源

 

 

posted on 2024-09-27 19:13  草率的龙果果  阅读(114)  评论(0)    收藏  举报

导航