项目中经常会碰到分销功能,用户分享邀请下级用户。会员树能直观的显示各级会员之间的关系。整个树形结构核心代码是用vue写的。我的后台代码是用的layui前端框架。话不多说先看效果图
html代码:
<div class="nav-tabs-custom">
<div class="tab-content" id="root">
<div class="box box-success">
<div class="layui-fluid">
<div class="layui-row layui-col-space15">
<div class="layui-col-md12">
<div class="layui-card">
<div class="layui-card-header">会员关系树</div>
<div class="layui-card-body">
<div class="demoTable layui-form" style="margin-bottom:10px">
<div class="layui-inline">
<input class="layui-input" v-model="keyword" placeholder="请输入UID或者手机号或用户昵称搜索" name="id" id="test-table-demoReload" autocomplete="off">
</div>
<button @click="searchByKeyWord" class="layui-btn" id="searchByKeyWord">搜索</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div style="padding:0 20px 20px 20px;width:100%;overflow-x: scroll;">
<template>
<a-tabs v-model="activeKey">
<a-tab-pane key="1" tab="会员关系树">
<a-spin :spinning="spinning" style="width: 100%; min-height: 50px">
<a-tree v-show="treeData.length > 0" class="tree-wrap"
:load-data="onLoadData"
:show-line="true"
:multiple="true"
:tree-data="treeData"
:replaceFields="{children:'children', title: 'is_fans', key: 'uid'}">
<template slot="custom" slot-scope="item">
<p>
<span>
<a-tag class="tags-status" color="#108ee9" v-if="item.status == 0">正常</a-tag>
<a-tag class="tags-status disabled" style="background-color: red;color: #fff;" v-else>禁用</a-tag>
</span>
<span class="vips-name" style="color:#808080">
<span>{{item.nickname ? `(${item.nickname})` : `(${item.realname})`}}</span>
</span>
<span>· 会员ID:<{{item.uid}}> </span>
<span>· 联系电话:<{{item.telphone}}> </span>
<span>· 注册时间:<{{item.created_at}}> </span>
</p>
</template>
<template slot="operate" slot-scope="item">
<a-button
type="link"
size="small"
:disabled="item.finished"
:loading="item.btnLoading"
@click="loadMore(item)">
{{item.title}}
</a-button>
</template>
</a-tree>
<a-empty v-show="treeData.length == 0" />
</a-spin>
</a-tab-pane>
<a-tab-pane key="2" tab="搜索结果" v-if="searchResult">
<a-spin :spinning="spinning1">
<a-tree v-if="treeData1.length > 0" class="tree-wrap"
:default-expanded-keys="defaultExpandedKeys"
:default-selected-keys="defaultSelectedKeys"
:replaceFields="{children:'children', title: 'is_fans', key: 'uid'}"
:load-data="onLoadData1"
:multiple="multiple"
:show-line="true"
:tree-data="treeData1">
<template slot="custom" slot-scope="item">
<p>
<span>
<a-tag class="tags-status" color="#108ee9" v-if="item.status == 0">正常</a-tag>
<a-tag class="tags-status disabled" style="background-color: red;color: #fff;" v-else>禁用</a-tag>
</span>
<span class="vips-name" style="color:#808080">
<span>{{item.nickname ? `(${item.nickname})` : `(${item.realname})`}}</span>
</span>
<span>· 会员ID:<{{item.uid}}> </span>
<span>· 联系电话:<{{item.telphone}}> </span>
<span>· 注册时间:<{{item.created_at}}> </span>
</p>
</template>
<template slot="operate" slot-scope="item">
<a-button
type="link"
size="small"
:disabled="item.finished"
:loading="item.btnLoading"
@click="loadMore1(item)">
{{item.title}}
</a-button>
</template>
</a-tree>
<a-empty v-else />
</a-spin>
</a-tab-pane>
</a-tabs>
</template>
</div>
</div>
</div>
</div>
js:
<!-- 引入样式 -->
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>-->
<script src="/static/js/vue.js"></script>
<!--<script src="https://cdn.jsdelivr.net/npm/ant-design-vue@1.4.10/dist/antd.min.js"></script>-->
<script src="/static/js/antd.min.js"></script>
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.js"></script>-->
<script src="/static/js/moment.js"></script>
<!--<link href="https://cdn.jsdelivr.net/npm/ant-design-vue@1.4.10/dist/antd.min.css" rel="stylesheet"-->
<link href="/static/css/antd.min.css" rel="stylesheet"
type="text/css" />
<script src="/static/layuiadmin/layui/layui.js"></script>
<script>
layui.config({
base: '/static/layuiadmin/' //静态资源所在路径
}).extend({
index: 'lib/index' //主入口模块
}).use(['jquery','index', 'table'], function(){
var $ = layui.$
new Vue({
el: '#root',
data(){
return {
activeKey:'1',
keyword: "",
searchResult:false,
spinning: false,
treeData: [],
spinning1: false,
treeData1: [],
pagination1: {
current: 1,
pageSize: 20,
},
defaultExpandedKeys: [],
defaultSelectedKeys: [],
multiple: true,
pagination: {
current: 1,
pageSize: 20,
},
}
},
created(){
this.spinning = true;
this.initGetUserRelationList(this.pagination);
},
methods: {
// 判断数组中是否有加载更多按钮
hasLoadBtn(arr){
return arr.some(item => {
return item.uid.toString().indexOf('loadBtn') > -1;
})
},
loadingBtnObj(parent_id, pagination = JSON.parse(JSON.stringify(this.pagination))){
let loadingBtn = {
uid: 'loadBtn-' + parent_id,
parent_id: parent_id,
title: '加载更多',
itemType: 'btn',
btnLoading: false,
selectable: false,
finished: false,
isLeaf: true,
pagination,
scopedSlots: {title: 'operate'}
}
return loadingBtn;
},
fansChange(is_fans){
this.is_fans = is_fans;
this.pagination.current = 1;
this.treeData = [];
this.spinning = true;
this.initGetUserRelationList(this.pagination);
},
// 初始化树数据请求
initGetUserRelationList(pageData){
getUserRelationList({
pageLimit: pageData.pageSize,
page: pageData.current,
referrer: 0,
keyword: "",
}).then(res => {
if(res.code == 200){
let currentList = res.data.list;
this.initTreeData(currentList)
this.addLoadMoreBtn(currentList, 0, cloneDeep(pageData))
this.treeData = [...currentList];
}
}).finally(() => {
this.spinning = false;
})
},
//
initTreeData(arr){
arr.forEach((item) => {
// 如果不存在插槽配置,添加
if(!item.scopedSlots){
this.$set(item, 'scopedSlots', {title: 'custom'})
}
if(item.is_fans == 1){
// 锁粉用户,设置为叶子节点
this.$set(item, 'isLeaf', true)
}
if(item.children && item.children.length > 0){
this.initTreeData(item.children)
}
})
},
// 根据uid在数组中找到某一项 arr不传则在treeData中查找
getItemOfKey(uid, arr = this.treeData){
let node;
let getItem = (arr) => {
for(let i = 0; i < arr.length; i++){
let item = arr[i];
if(item.uid == uid){
node = item;
break;
}else{
if(item.children && item.children.length > 0){
getItem(item.children);
}
}
}
}
getItem(arr)
return node;
},
// 列表中添加加载更多按钮
addLoadMoreBtn(list, parent_id, pagination){
// 不是最后一页 并且没有加载更多按钮 添加按钮
if(list.length >= this.pagination.pageSize && !this.hasLoadBtn(list)){
list.push(this.loadingBtnObj(parent_id, pagination))
}
},
// 列表中删除最后一项加载更多按钮
delLoadMoreBtn(list){
// 有按钮才执行删除
if(this.hasLoadBtn(list)){
list.pop();
}
},
// 点击加载更多按钮
loadMore(item){
// 按钮节点item
let data = item;
item.title = '加载中...';
item.btnLoading = true;
item.pagination.current += 1;
// 父级节点
let parentItem = null;
if(data.parent_id != 0){
parentItem = this.getItemOfKey(data.parent_id)
}
let pagination = cloneDeep(data.pagination);
getUserRelationList({
pageLimit: data.pagination.pageSize,
page: data.pagination.current,
referrer: data.parent_id,
keyword: "",
}).then(res => {
if(res.code == 200){
let currentList = res.data.list;
if(currentList.length > 0){
this.initTreeData(currentList);
}
// 请求成功后先删除按钮,后添加数据再次添加加载更多按钮;
if(parentItem){
this.delLoadMoreBtn(parentItem.children);
}else{
this.delLoadMoreBtn(this.treeData);
}
// 未加载完毕 按钮重新添加到末尾 加载完毕则不需要加载按钮,不再添加
if(parentItem){
this.addLoadMoreBtn(currentList, parentItem.uid, cloneDeep(pagination))
}else{
this.addLoadMoreBtn(currentList, 0, cloneDeep(pagination))
}
currentList.forEach(item => {
if(parentItem){
parentItem.children.push(item);
}else{
this.treeData.push(item);
}
})
this.treeData = [...this.treeData]
}
}).finally(() => {
this.spinning = false;
})
},
// 展开时的加载
onLoadData(treeNode){
let dataRef = treeNode.dataRef;
return new Promise((resolve, reject) => {
if (dataRef.children && dataRef.children.length > 0) {
resolve();
return;
}
getUserRelationList({
pageLimit: this.pagination.pageSize,
page: this.pagination.current,
referrer: dataRef.uid,
keyword: "",
}).then(res => {
if(res.code == 200){
let currentList = res.data.list;
if(currentList.length > 0){
this.initTreeData(currentList)
this.addLoadMoreBtn(currentList, dataRef.uid, cloneDeep(this.pagination))
this.$set(dataRef, 'children', currentList);
}
}
resolve();
}).catch(() => {
reject()
}).finally(() => {
this.spinning = false;
})
})
},
// 搜索组件方法
searchByKeyWord(){
if(!this.keyword.trim()){
layer.msg('请输入搜索内容。');
return;
}
this.spinning1 = true;
this.treeData1 = [];
getUserRelationList({
pageLimit: this.pagination1.pageSize,
page: this.pagination1.current,
referrer: 0,
keyword: this.keyword
}).then(res => {
if(res.code == 200){
this.searchResult = true;
this.activeKey = '2';
let currentList = res.data.list;
this.initTreeData(currentList);
let lastItem = this.getLastItem(currentList);
this.addLoadMoreBtn(currentList, 0, cloneDeep(this.pagination1))
this.defaultExpandedKeys = [this.spac(currentList)];
this.defaultSelectedKeys = [this.spac(currentList) + '-0'];
this.treeData1 = [...currentList];
}
}).finally(() => {
this.spinning1 = false;
})
},
spac(List){
var leaf = '0';
child(List);
function child(l){
if(l[0].children.length > 0){
leaf += '-0';
child(l[0].children);
}else{
return;
}
}
return leaf;
},
// 查找最后一项
getLastItem(arr){
let node = null;
let getItem = (arr) => {
for(let i = 0; i < arr.length; i++){
let item = arr[i];
if(item.children && item.children.length > 0){
getItem(item.children);
}else{
node = item;
}
}
}
getItem(arr)
return node;
},
// 展开时的加载
onLoadData1(treeNode){
let dataRef = treeNode.dataRef;
return new Promise((resolve, reject) => {
if (dataRef.children && dataRef.children.length > 0) {
resolve();
return;
}
getUserRelationList({
pageLimit: this.pagination1.pageSize,
page: this.pagination1.current,
referrer: dataRef.uid,
keyword: dataRef.uid?'':this.keyword,
}).then(res => {
if(res.code == 200){
let currentList = res.data.list;
if(currentList.length > 0){
this.initTreeData(currentList)
this.addLoadMoreBtn(currentList, dataRef.uid, cloneDeep(this.pagination1))
this.$set(dataRef, 'children', currentList);
}
}
resolve();
}).catch(() => {
reject()
}).finally(() => {
this.spinning = false;
})
})
},
// 点击加载更多按钮
loadMore1(item){
// 按钮节点item
let data = item;
data.pagination.current += 1;
// 父级节点
let parentItem = this.getItemOfKey(data.parent_id,this.treeData1);
let pagination = cloneDeep(data.pagination);
console.log(item)
console.log(parentItem)
console.log(pagination)
data.title = '加载中...';
data.btnLoading = true;
getUserRelationList({
pageLimit: data.pagination.pageSize,
page: data.pagination.current,
referrer: data.parent_id,
keyword: "",
}).then(res => {
if(res.code == 200){
let currentList = res.data.list;
if(currentList.length > 0){
this.initTreeData(currentList);
}
// 请求成功后先删除按钮,后添加数据再次添加加载更多按钮;
if(parentItem){
this.delLoadMoreBtn(parentItem.children);
}else{
this.delLoadMoreBtn(this.treeData1);
}
// 未加载完毕 按钮重新添加到末尾 加载完毕则不需要加载按钮,不再添加
if(parentItem){
this.addLoadMoreBtn(currentList, parentItem.uid, cloneDeep(pagination))
}else{
this.addLoadMoreBtn(currentList, 0, cloneDeep(pagination))
}
currentList.forEach(item => {
if(parentItem){
parentItem.children.push(item);
}else{
this.treeData1.push(item);
}
})
}
}).finally(() => {
this.spinning1 = false;
})
},
}
});
function type(obj) {
var toString = Object.prototype.toString;
var map = {
'[object Boolean]' : 'boolean',
'[object Number]' : 'number',
'[object String]' : 'string',
'[object Function]' : 'function',
'[object Array]' : 'array',
'[object Date]' : 'date',
'[object RegExp]' : 'regExp',
'[object Undefined]': 'undefined',
'[object Null]' : 'null',
'[object Object]' : 'object'
};
if(obj instanceof Element) {
return 'element';
}
return map[toString.call(obj)];
}
function cloneDeep(data) {
var t = type(data), o, i, ni;
if(t === 'array') {
o = [];
}else if( t === 'object') {
o = {};
}else {
return data;
}
if(t === 'array') {
for (i = 0, ni = data.length; i < ni; i++) {
o.push(cloneDeep(data[i]));
}
return o;
}else if( t === 'object') {
for( i in data) {
o[i] = cloneDeep(data[i]);
}
return o;
}
}
function getUserRelationList(pageData){
return new Promise((resolve, reject) => {
$.ajax({
url: "/admin/user/getTreeInfo",
type: "POST",
data: pageData,
success:function(res){
if(res.code == 200){
resolve(res);
}
}
});
})
}
});
</script>
数据格式:{
"code": 200,
"msg": "",
"data": {
"pages": {
"currentPage": "1",
"lastPage": 1,
"pageSize": "20",
"total": 14
},
"list": [
{
"uid": 2,
"pid": 1051,
"nickname": "英雄哥",
"realname": "石",
"telphone": "",
"createtime": 1652671276,
"isLeaf": false,
"created_at": "2022-05-16 11:21:16",
"status": 0,
"children": []
},
{
"uid": 995,
"pid": 769,
"nickname": "那个秋天2021",
"realname": null,
"telphone": "",
"createtime": 1661088860,
"isLeaf": false,
"created_at": "2022-08-21 21:34:20",
"status": 0,
"children": []
},
{
"uid": 1277,
"pid": 0,
"nickname": "A新环房产销售董琪",
"realname": null,
"telphone": null,
"createtime": 1662822961,
"isLeaf": true,
"created_at": "2022-09-10 23:16:01",
"status": 0,
"children": []
}
]
}
}
最后放一下代码文件下载地址:会员树下载