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