需求背景:公司接到一个项目,是需要做一个族谱微信小程序,需要有族谱树,且可以添加家族人员。
灵感来源:在插件市场中下载了作者 羊羊不想写代码 的插件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 插件市场 中的代码,只是在横向树结构中修改了部分代码。