uni-app用css编写族谱树家谱树

发布于:2025-08-01 ⋅ 阅读:(27) ⋅ 点赞:(0)

需求背景:公司接到一个项目,是需要做一个族谱微信小程序,需要有族谱树,且可以添加家族人员。

灵感来源:在插件市场中下载了作者 羊羊不想写代码 的插件tree-list族谱,树形列表,可缩放滑动 - DCloud 插件市场,根据作者的代码逻辑增加了横向的树结构,使用简单。

父组件引用:(数据在用的时候从后端请求)

<template>
	<view class="">
		<view class="switch-btn" @click="switchRow">
			{{switchName}}
		</view>
		<!-- 人物关系图 -->
		<template>
			<view class="genealogy-tree">
				<movable-area :style="{height: '100vh',width: '100vw'}">
					<movable-view @scale="changeScale" :scale="true" :scale-max="1" :scale-min="0.5" class="max" direction="all"
						:style="{width: `${treeConfig.width}px`,height: `${treeConfig.height}px`}">
						<div class="tree-content">
							<columnTreeList v-if="switchName == '竖向'" :tree-data="treeData" :tree-first="true" />
							<rowTreeList v-else :tree-data="treeData" :tree-first="true" />
						</div>
					</movable-view>
				</movable-area>
			</view>
		</template>
	</view>
</template>

<script setup>
	import {
		ref,
		provide,
		nextTick,
		watch,
		getCurrentInstance,
		onMounted
	} from 'vue';
	import columnTreeList from '@/components/column-tree-list.vue'
	import rowTreeList from '@/components/row-tree-list.vue';
	
	const switchName = ref('横向')
	
	let scale = ref(1); // 缩放倍率
	const instance = getCurrentInstance(); // 获取组件实例
	let treeConfig = ref({ // movable-view移动区域大小
		width: 0,
		height: 800
	})
	/**
	 * 获取元素信息
	 * @param {String} domID dom元素id
	 * */
	const getDomInfo = (domID) => {
		return new Promise((resolve, reject) => {
			const bar = uni.createSelectorQuery().in(instance);
			bar.select(domID).boundingClientRect(res => {
				if (res) resolve(res);
				else reject()
			}).exec();
		})
	}
	// 树形结构数据
	let treeData = ref([{
		id: 1,
		name: '祖宗',
		child: [{
				id: 2,
				name: '爷爷',
				spouse: {
					id: 2001,
					name: '奶奶',
				},
				child: [{
						id: 3,
						name: '父亲',
						spouse: {
							id: 3001,
							name: '妈妈',
						},
						child: [{
								id: 4,
								name: '自己',
							},
							{
								id: 9,
								name: '妹妹',
							}
						]
					},
					{
						id: 5,
						name: '二伯',
					},
				]
			},
			{
				id: 6,
				name: '二大爷',
				child: [{
						id: 7,
						name: '大叔',
					},
					{
						id: 8,
						name: '二叔',
					},
				]
			}
		]
	}])
	// 删除
	provide('delItem', (item) => {
		treeData.value = deleteNodeById(treeData.value, item.id);
	})
	/**
	 * 添加子级
	 * @param { object } item 当前点击的对象
	 * */
	provide('addItem', (item) => {
		const data = {
			id: Math.floor(Math.random() * 1000), // 唯一键后续自行设置
			name: '新增子级',
		}
		handleData(item.id, treeData.value, data, 1)
	})
	/**
	 * 添加配偶
	 * @param { object } item 当前点击的对象
	 * */
	provide('addSpouse', (item) => {
		console.log(31231, item);
		const data = {
			id: Math.floor(Math.random() * 100), // 唯一键后续自行设置
			name: '配偶',
		}
		handleData(item.id, treeData.value, data, 2)
	})
	/**
	 * 递归对树形结构添加节点
	 * @param {number | string} id 唯一键
	 * @param { Array } 树形结构数组
	 * @param { Object } obj 添加的数据 {id,name,...}
	 * @param { number } type 添加的类型 1:子级,2:配偶
	 * */
	const handleData = (id, data, obj, type) => {
		data.forEach(item => {
			if (item.id === id) {
				// 在这里处理新增子级还是配偶
				if (type === 1) {
					item.child ? item.child.push(obj) : item.child = [obj]
				} else if (type === 2) {
					// 如果存在配偶这里赋值将进行替换
					// 如需多配偶需自行改为数组形式(tree-list里的spouse也需要同步修改为数组)
					item.spouse = obj;
				}
			} else {
				if (item.child) {
					handleData(id, item.child, obj, type)
				}
			}
		})
		return data
	}
	/**
	 * 递归删除树形结构元素
	 * @param { Array } tree 树形结构数据
	 * @param { number | string } id 唯一键
	 * */
	const deleteNodeById = (tree, targetId) => {
		for (let i = 0; i < tree.length; i++) {
			const node = tree[i];
			if (node.id === targetId) {
				console.log('找到了', node);
				// 使用 splice 删除节点
				tree.splice(i, 1);
				return tree; // 返回新的数组
			}
			if (node.child && node.child.length > 0) {
				// 递归查找子节点
				node.child = deleteNodeById(node.child, targetId);
			}
		}
		return tree; // 没有找到目标节点,返回原数组
	}

	// 设置移动缩放大小
	const setTreeConfig = () => {
		nextTick(() => {
			setTimeout(() => {
				getDomInfo('.tree-content').then(res => {
					treeConfig.value = {
						width: res.width / scale.value,
						height: res.height / scale.value
					}
					console.log('返回值',res);
				})
			}, 200)
		})
	}

	const changeScale = (e) => {
		scale.value = e.detail.scale;
		console.log('缩放',e.detail)
	}
	
	// 转换
	const switchRow = () => {
		switchName.value = switchName.value == '横向' ? '竖向' : '横向'
	}

	// 监听树形结构数据变化
	watch(treeData.value, (newVal, oldVal) => {
		setTreeConfig()
	})

	onMounted(() => {
		setTreeConfig()
	})
</script>

<style lang="scss" scoped>
	.genealogy-tree {
		min-height: 100%;
		min-width: 100vw;
		position: relative;
		overflow-x: scroll;
		// overflow: hidden;

		.tree-content {
			position: absolute;
			top: 0;
			left: 0;
			transition: all .3s;
		}
	}

	::v-deep .uni-table-th {
		color: #000 !important;
	}

	::v-deep .uni-table-td {
		color: #000 !important;
	}

	.th-bg {
		background-color: #d9d9d9;
	}
	.switch-btn {
		position: fixed;
		top: 30rpx;
		right: 30rpx;
		height: 50rpx;
		width: 100rpx;
		border-radius: 20rpx;
		background-color: #f8f8f8;
		box-shadow: 0 6rpx 0rpx 4rpx #00000080;
		font-size: 20rpx;
		line-height: 50rpx;
		text-align: center;
		z-index: 99;
	}
</style>

子组件:子组件自我递归调用(原作者代码---竖向树结构)

<template>
	<view class="card">
		<view class="ul">
			<view class="li" v-for="(item,index) in treeData" :key="index">
				<view class="item" :class="{'line-left': index !== 0, 'line-right': index != treeData.length - 1}">
					<view class="item-name" :class="{'line-bottom':item.child && item.child.length > 0,'line-top':!treeFirst}">
						<view class="content">
							<image src="@/static/logo.png" mode="widthFix" style="width: 40rpx;height: auto;border-radius: 50%;">
							</image>
							<text class="name">{{item.name}}</text>
							<button class="btn" @click="addItem(item)">添加子级</button>
							<button class="btn" @click="addSpouse(item)">添加配偶</button>
							<button class="btn" @click="delItem(item)">删除当前</button>
						</view>
						<!-- 配偶 -->
						<view class="content-2" v-if="item.spouse">
							<image src="@/static/logo.png" mode="widthFix" style="width: 40rpx;height: auto;border-radius: 50%;">
							</image>
							<text class="name">{{item.spouse.name}}</text>
						</view>
					</view>
				</view>
				<column-tree-list v-if="item.child && item.child.length > 0" :tree-data="item.child"></column-tree-list>
			</view>
		</view>
	</view>
</template>

<script setup name="column-tree-list">
	import columnTreeList from '@/components/column-tree-list.vue'
	import {
		inject
	} from 'vue'
	const delItem = inject('delItem')
	const addItem = inject('addItem')
	const addSpouse = inject('addSpouse')
	defineProps(['treeData', 'treeFirst'])
</script>

<style lang="scss" scoped>
	$line-length: 20px; //线长
	$spacing: 20px; //间距
	$extend: calc(#{$spacing}); //延长线

	// 线样式
	@mixin line {
		content: "";
		display: block;
		width: 1px;
		height: $line-length;
		position: absolute;
		left: 0;
		right: 0;
		margin: auto;
		background: #e43934;
	}

	.card {

		.ul {
			display: flex;
			justify-content: center;

			.li {

				.item {
					display: flex;
					justify-content: center;
					align-items: center;
					position: relative;

					&-name {
						position: relative;
						display: flex;
						justify-content: center;
						align-items: center;
						margin: $spacing 10rpx;

						.content,
						.content-2 {
							display: flex;
							flex-direction: column;
							align-items: center;
							background: #fff;
							padding: 20rpx;
							border-radius: 16rpx;
							box-sizing: border-box;

							box-shadow: 0px 5rpx 30rpx 5rpx rgba(0, 0, 0, 0.08);

							.name {
								margin: 10rpx 0 18rpx;
								color: #222;
								font-size: 20rpx;
							}
						}

						.content-2 {
							display: flex;
							flex-direction: column;
							align-self: flex-start;
							margin-left: 10rpx;
						}
					}
				}
			}

		}

		// 向下的线
		.line-bottom {
			&::after {
				@include line();
				bottom: -$line-length;
			}
		}

		// 向上的线
		.line-top {
			&::before {
				@include line();
				top: -$line-length;
			}
		}

		// 向左的线
		.line-left {
			&::after {
				@include line();
				width: calc(50% + #{$spacing});
				height: 1px;
				left: calc(-50% - #{$extend});
				top: 0;
			}
		}

		// 向右的线
		.line-right {
			&::before {
				@include line();
				width: calc(50% + #{$spacing});
				height: 1px;
				right: calc(-50% - #{$extend});
				top: 0;
			}
		}
	}

	.btn {
		font-size: 18rpx;
		width: 116rpx;
		height: 45rpx;
	}
</style>

横向树结构:

<template>
	<view class="vmPage">
		<view class="sub-branch" v-for="(item,index) in treeData" :key="index">
			<view class="item" :class="{'line-top': index !== 0, 'line-bottom': index !== treeData.length - 1}">
				<view class="item-name" :class="{'line-right':item.child && item.child.length > 0,'line-left':!treeFirst}">
					<view class="content">
						<image src="@/static/logo.png" mode="widthFix" style="width: 40rpx;height: auto;border-radius: 50%;">
						</image>
						<text class="name">{{item.name}}</text>
						<view class="btn" @click="addItem(item)">添加子级</view>
						<view class="btn" @click="addSpouse(item)">添加配偶</view>
						<view class="btn" @click="delItem(item)">删除当前</view>
					</view>
					<!-- 配偶 -->
					<view class="content-2" v-if="item.spouse">
						<image src="@/static/logo.png" mode="widthFix" style="width: 40rpx;height: auto;border-radius: 50%;">
						</image>
						<text class="name">{{item.spouse.name}}</text>
					</view>
				</view>
			</view>
			<row-tree-list v-if="item.child && item.child.length > 0" :tree-data="item.child"></row-tree-list>
		</view>
	</view>
</template>

<script setup name="row-tree-list">
	import {
		ref,
		inject
	} from 'vue'
	import rowTreeList from '@/components/row-tree-list.vue';

	const delItem = inject('delItem')
	const addItem = inject('addItem')
	const addSpouse = inject('addSpouse')
	defineProps(['treeData', 'treeFirst'])
</script>

<style lang="scss" scoped>
	$line-length: 30rpx;
	$spacing: 30rpx;
	$extend: calc(#{$spacing});
	$line-color: #e43934;

	// 线样式
	@mixin line {
		content: "";
		display: block;
		width: 1rpx;
		height: $line-length;
		position: absolute;
		top: 0;
		bottom: 0;
		margin: auto;
		background: #e43934;
	}

	@mixin flex-center {
		display: flex;
		justify-content: center;
		align-items: center;
	}

	.vmPage {
		display: flex;
		justify-content: center;
		flex-direction: column;

		.sub-branch {
			display: flex;
			.item {
				@include flex-center();
				position: relative;

				.item-name {
					position: relative;
					flex-direction: column;
					@include flex-center();
					align-items: flex-start;
					margin: 10rpx $spacing;
				}
			}
		}
	}

	.content,.content-2 {
		@include flex-center();
		background: #fff;
		padding: 20rpx;
		box-sizing: border-box;
		border-radius: 16rpx;
		box-shadow: 0px 5rpx 30rpx 5rpx rgba(0, 0, 0, 0.08);

		.name {
			display: inline-block;
			font-size: 20rpx;
			margin: 0 8rpx 0 20rpx;
			width: 30rpx;
		}

		.btn {
			font-size: 18rpx;
			width: 30rpx;
			text-align: center;
			padding: 8rpx;
			background-color: #f8f8f8;
			border: 1px solid rgba(0, 0, 0, .2);
			border-radius: 6rpx;
		}
	}

	// 向右的线
	.line-right {
		&::after {
			@include line();
			right: - $line-length;
			width: $line-length;
			height: 1rpx;
		}
	}

	// 向左的线
	.line-left {
		&::before {
			@include line();
			left: - $line-length;
			width: $line-length;
			height: 1rpx;
		}
	}

	// 向上的线
	.line-top {
		&::after {
			@include line();
			height: calc(50% + $line-length);
			left: 0;
			top: calc(-50% - $line-length);
		}
	}

	// 向下的线
	.line-bottom {
		&::before {
			@include line();
			height: calc(50% + $line-length);
			left: 0;
			bottom: calc(-50% - $line-length);
		}
	}
</style>

注:该文章所用代码多是复用原作者羊羊不想写代码 的个人主页 - DCloud问答 的插件tree-list族谱,树形列表,可缩放滑动 - DCloud 插件市场 中的代码,只是在横向树结构中修改了部分代码。