在不少系統中都有選擇聯繫人的需求,市面上也沒什麼好的參照,產品經理看企業微信的選人挺好用的,就說參照這個作一個吧。。。css
算了,仍是試着作吧,企業微信的選人的確作的挺好,不得不佩服。git
先看看效果圖吧,多層級無規律的嵌套都能搞定github
整個界面分爲三部分:json
爲何加一個返回上一層按鈕呢?小程序
我也以爲比較醜,但小程序沒法直接控制左上角返回鍵(自定義 Title 貌似能夠,沒試過),點左上角的返回箭頭的話就退出選人控件到上個頁面了。數組
咱們的需求是點擊一個文件夾,經過刷新當前列表進入下一級目錄,感受像是又進了一個頁面,但其實並無,只是列表的數據變化了。由此實現不定層級、無規律的部門和人員嵌套的支持。服務器
好比先點擊了首屏數據的第二個 item
,它的 index
是 1
,就將 1
存入 indexList
;返回上一層時將最後一個元素刪除。微信
當勾選了某我的或部門時,會在底部的框中顯示全部已選人員或部門的名字,當文字超過屏幕寬度時能夠向右無限滑動,底部 footer
始終保持一行。網絡
最終選擇的人以底部 footer
裏顯示的爲準,點擊肯定時根據業務須要將已選人員數據發送給須要的界面。app
先看看數據格式
{
id: TEACHER_ID,
name: '教師',
parentId: '',
checked: false,
isPeople: false,
children: [
{
id: TEACHER_DEPARTMENT_ID,
name: '部門',
parentId: 'teacher',
checked: false,
isPeople: false,
children: []
},
{
id: TEACHER_SUBJECT_ID,
name: '學科',
parentId: 'teacher',
checked: false,
isPeople: false,
children: []
},
{
id: TEACHER_GRADECLASS_ID,
name: '年級班級',
parentId: 'teacher',
checked: false,
isPeople: false,
children: []
},
]
}
複製代碼
全部的數據組成一個數據樹,子節點嵌套在父節點下。
id
, name
不說了,parentId
指明它的父節點,children
包含它的全部子節點,checked
用來判斷勾選狀態,isPeople
判斷是部門仍是人員,由於二者的圖標不同。
注意:
本控件採用了數據分步加載的模式,除了最上層固定的幾個分類,其餘的每層數據都是點擊具體的部門後纔去請求服務器加載本部門下的數據的,而後再拼接到原始數據樹上。這樣能夠提升加載速度,提高用戶體驗。
我也試了一次性把全部數據都拉下來,一是太慢,得三五秒,二是數據量太大的話(我這裏應該是超過1000,閾值多少沒測過),setData()
的時候就會報錯:
超過最大長度了。。。因此只能分步加載數據。
固然若是你的數據量小,幾十人或幾百人,也能夠選擇一次性加載。
這個控件邏輯上仍是比較複雜的,要考慮的細節太多……下面梳理一下主要的邏輯點
index
,這裏用 indexList
表示點擊某個部門進入下一層目錄時,將被點擊部門的 index
索引 push
進 indexList
中。點擊返回上一層按鈕時,刪除 indexList
中最後一個元素。
currentList
每進入新的一層,或返回上一層,都須要刷新 currentList
來實現頁面的更新。知道下一層數據很容易,直接取被點擊 item
的 children
賦值給 currentList
便可。
但如何還原上一層的數據呢?
第一點記錄的 indexList
就發揮做用了,原始數據樹爲 originalList
,循環遍歷 indexList
,根據索引依次取出每層的 currentList
直到 indexList
的最後一個元素,就獲得了返回上一層須要顯示的數據。
originalList
頁面是根據每一個 item
的 checked
屬性判斷是否選中的,因此每次改變勾選狀態都要設置被改變的 item
的 checked
屬性,而後更新 originalList
。這樣即便返回上一層了,再進到當前層級選中狀態還會被保留,不然刷新 currentList
後已選狀態將丟失。
footer
的雙聯動咱們指望的效果是,選中currentList
列表的某一項,底部 footer
會自動添加被選人的名字。取消選中,底部 footer
也會自動刪除。
也能夠經過 footer
來刪除已選人,點擊 footer
中人名,會將此人從已選列表中刪除,currentList
列表中也會自動取消勾選狀態。
嗯,這個功能比較耗性能,每一次都須要大量的計算。考慮到性能和速度因素,本次只作了從 footer
刪除只更新 currentList
的勾選狀態。
什麼意思呢?假若有兩層,A 和 B,B 是 A 的下一層數據,即 A 是 B 的父節點。在 A 中選中了一個部門 校長室
,點擊下一層到 B,在 B 中又選了兩我的 張三
和 李四
,這時底部 footer
裏顯示的應該是三個: 校長室
、 張三
、 李四
。此時點擊 footer
的 張三
, footer
會把 張三
刪除,中間列表中 張三
會被置爲未選中狀態,這沒問題。但點擊 footer
的 校長室
, 在 footer
中是把 校長室
刪除了,但再返回到上一層時,中間列表中的 校長室
依然是勾選狀態,由於此時沒有更新原始數據樹 originalList
。若是以爲這是個 bug
, 能夠加個更新 originalList
的操做。這樣就要遍歷 originalList
的每一個元素判斷與本次刪除的 id 是否相等,而後改變 checked
值,若是數據量很大,會很是慢。我作了妥協……
關鍵的邏輯就這四塊了,固然還有不少小細節,直接看代碼吧,註釋寫的也比較詳細。
目錄結構:
footer
文件夾下是抽離出的 footer
組件,userSelect
是選人控件的主要邏輯。把這幾個文件複製過去就能夠用了。
把 userSelect.js
裏網絡請求的代碼替換爲你的請求代碼,注意數據的字段名是否一致。
userSelect.js
import API from '../../../utils/API.js'
import ArrayUtils from '../../../utils/ArrayUtils.js'
import EventBus from '../../../components/NotificationCenter/WxNotificationCenter.js'
let TEACHER_ID = 'teacher';
let TEACHER_DEPARTMENT_ID = 't_department';
let TEACHER_SUBJECT_ID = 't_subject';
let TEACHER_GRADECLASS_ID = 't_gradeclass';
let STUDENT_ID = 'student';
let PARENT_ID = 'parent'
let TEACHER = {
id: TEACHER_ID,
name: '教師',
parentId: '',
checked: false,
isPeople: false,
children: [
{
id: TEACHER_DEPARTMENT_ID,
name: '部門',
parentId: 'teacher',
checked: false,
isPeople: false,
children: []
},
{
id: TEACHER_SUBJECT_ID,
name: '學科',
parentId: 'teacher',
checked: false,
isPeople: false,
children: []
},
{
id: TEACHER_GRADECLASS_ID,
name: '年級班級',
parentId: 'teacher',
checked: false,
isPeople: false,
children: []
},
]
}
let STUDENT = {
id: STUDENT_ID,
name: '學生',
parentId: '',
checked: false,
isPeople: false,
children: []
}
let PARENT = {
id: PARENT_ID,
name: '家長',
parentId: '',
checked: false,
isPeople: false,
children: []
}
let ORIGINAL_DATA = [
TEACHER, STUDENT, PARENT
]
Page({
data: {
currentList: [], //當前展現的列表
selectList: [], //已選擇的元素列表
originalList: [], //最原始的數據列表
indexList: [], //存儲目錄層級的數組,用於準確的返回上一層
selectList: [], //已選中的人員列表
},
onLoad: function (options) {
wx.setNavigationBarTitle({
title: '選人控件'
})
this.init();
},
init(){
//用戶的單位id
this.unitId = getApp().globalData.userInfo.unitId;
//用戶類型
this.userType = 0;
//上次選中的列表,用於判斷是否是取消選中了
this.lastTimeSelect = []
this.setData({
currentList: ORIGINAL_DATA, //當前展現的列表
originalList: ORIGINAL_DATA, //最原始的數據列表
})
},
clickItem(res){
console.log(res)
let index = res.currentTarget.id;
let item = this.data.currentList[index]
console.log("item", item)
if (!item.isPeople) {
//點擊教師,下一層數據是寫死的,不用請求接口
if (item.id === TEACHER_ID) {
this.userType = 2;
this.setData({
currentList: item.children
})
} else if (item.id === TEACHER_SUBJECT_ID) {
if (item.children.length === 0){
this._getTeacherSubjectData()
}else{
//children的長度不爲0時,更新 currentList
this.setData({
currentList: item.children
})
}
} else if (item.id === TEACHER_DEPARTMENT_ID) {
if (item.children.length === 0) {
this._getTeacherDepartmentData()
} else {
//children的長度不爲0時,更新 currentList
this.setData({
currentList: item.children
})
}
} else if (item.id === TEACHER_GRADECLASS_ID) {
if (item.children.length === 0) {
this._getTeacherGradeClassData()
} else {
//children的長度不爲0時,更新 currentList
this.setData({
currentList: item.children
})
}
} else if (item.id === STUDENT_ID) {
this.userType = 1;
if (item.children.length === 0) {
this._getStudentGradeClassData()
} else {
//children的長度不爲0時,更新 currentList
this.setData({
currentList: item.children
})
}
} else if (item.id === PARENT_ID) {
this.userType = 3;
if (item.children.length === 0) {
this._getParentGradeClassData()
} else {
//children的長度不爲0時,更新 currentList
this.setData({
currentList: item.children
})
}
} else{
//children的長度爲0時,請求服務器
if(item.children.length === 0){
this._getUserByGroup(item)
}else{
//children的長度不爲0時,更新 currentList
this.setData({
currentList: item.children
})
}
}
//將當前的索引存入索引目錄中。索引多一個表示目錄多一級
let indexes = this.data.indexList
indexes.push(index)
//是目錄不是具體的用戶
this.setData({
indexList: indexes
})
//清空上次選中的元素列表,並設置上一層的選中狀態給lastTimeSelect
this.setLastTimeSelectList();
}
},
//返回按鈕
goBack() {
let indexList = this.data.indexList
if (indexList.length > 0) {
//返回時刪掉最後一個索引
indexList.pop()
if (indexList.length == 0) {
//indexList長度爲0說明回到了最頂層
this.setData({
currentList: this.data.originalList,
indexList: indexList
})
} else {
//循環將當前索引的對應數組賦值給currentList
let list = this.data.originalList
for (let i = 0; i < indexList.length; i++) {
let index = indexList[i]
list = list[index].children
}
this.setData({
currentList: list,
indexList: indexList
})
}
//清空上次選中的元素列表,並設置上一層的選中狀態給lastTimeSelect
this.setLastTimeSelectList();
}
},
//清空上次選中的元素列表,並設置上一層的選中狀態給lastTimeSelect
setLastTimeSelectList(){
this.lastTimeSelect = []
this.data.currentList.forEach(item => {
if (item.checked) {
this.lastTimeSelect.push(item)
}
})
},
//獲取教師部門數據
_getTeacherDepartmentData() {
this._commonRequestMethod(2, 'department')
},
//請求教師的學科數據
_getTeacherSubjectData(){
this._commonRequestMethod(2, 'subject')
},
//請求教師的年級班級
_getTeacherGradeClassData() {
this._commonRequestMethod(2, 'gradeclass')
},
//請求學生的年級班級
_getStudentGradeClassData() {
this._commonRequestMethod(1, 'gradeclass')
},
//請求家長的年級班級
_getParentGradeClassData() {
this._commonRequestMethod(3, 'gradeclass')
},
//根據部門查詢人
_getUserByGroup(item){
let params = {
userType: this.userType,
unitId: this.unitId,
groupType: item.type,
groupId: item.id
}
console.log('params', params)
getApp().get(API.selectUserByGroup(), params, result => {
console.log('result', result)
let list = this.transformData(result.data.data, item.id)
this.setData({
currentList: list
})
this.addList2DataTree()
//清空上次選中的元素列表,並設置上一層的選中狀態給lastTimeSelect。寫在這裏防止異步請求時執行順序問題
this.setLastTimeSelectList();
})
},
//通用的請求部門方法
_commonRequestMethod(userType, groupType){
wx.showLoading({
title: '',
})
let params = {
userType: userType,
unitId: this.unitId,
groupType: groupType
}
console.log('params', params)
getApp().get(API.selectUsersByUserGroupsTree(), params, result => {
console.log('result', result)
wx.hideLoading()
let data = result.data.data
this.setData({
currentList: data
})
this.addList2DataTree();
//清空上次選中的元素列表,並設置上一層的選中狀態給lastTimeSelect。寫在這裏防止異步請求時執行順序問題
this.setLastTimeSelectList();
})
},
//將請求的數據轉化爲須要的格式
transformData(list, parentId){
//先將數據轉化爲固定的格式
let newList = []
for(let i=0; i<list.length; i++){
let item = list[i]
newList.push({
id: item.id,
name: item.realName,
parentId: parentId,
checked: false,
isPeople: true,
userType: item.userType,
gender: item.gender,
children: []
})
}
return newList;
},
//將當前列表掛載在原數據樹上, 目前支持5層目錄,如需更多接着往下寫就好
addList2DataTree(){
let currentList = this.data.currentList;
let originalList = this.data.originalList;
let indexes = this.data.indexList
switch (indexes.length){
case 1:
originalList[indexes[0]].children = currentList
break;
case 2:
originalList[indexes[0]].children[indexes[1]].children = currentList
break;
case 3:
originalList[indexes[0]].children[indexes[1]].children[indexes[2]].children = currentList
break;
case 4:
originalList[indexes[0]].children[indexes[1]].children[indexes[2]].children[indexes[3]].children = currentList
break;
case 5:
originalList[indexes[0]].children[indexes[1]].children[indexes[2]].children[indexes[3]].children[indexes[4]].children = currentList
break;
}
this.setData({
originalList: originalList
})
console.log("originalList", originalList)
},
//選框變化回調
checkChange(res){
console.log(res)
let values = res.detail.value
let selectItems = []
//將值取出拼接成 id,name 格式
values.forEach(value => {
let arrs = value.split(",")
selectItems.push({id: arrs[0], name: arrs[1]})
})
console.log("selectItems", selectItems)
console.log("lastTimeSelect", this.lastTimeSelect)
//將本次選擇的與上次選擇的比對,本次比上次多說明新增了,本次比上次少說明刪除了,找出被刪除的那條數據,在footer中也刪除
if (selectItems.length > this.lastTimeSelect.length){
//將 selectList 與 selectItems 拼接並去重
let newList = this.data.selectList.concat(selectItems)
newList = ArrayUtils.checkRepeat(newList)
this.setData({
selectList: newList
})
}else{
//找出取消勾選的item,從selectList中刪除
//比對出取消勾選的是哪一個元素
let diffItem = {}
this.lastTimeSelect.forEach(item => {
let flag = false;
selectItems.forEach(item2 => {
if(item.id === item2.id){
flag = true
}
})
if(!flag){
diffItem = item
console.log("diff=", item)
}
})
//找出被刪除的元素在 selectList 中的位置
let list = this.data.selectList
let delIndex = 0;
for(let i=0; i<list.length; i++){
if (list[i].id === diffItem.id){
delIndex = i;
break;
}
}
//從list中刪除這個元素
list.splice(delIndex, 1)
this.setData({
selectList: list
})
}
console.log("selectList", this.data.selectList)
//更新 currentList 選中狀態並從新掛載在數據樹上,以保存選擇狀態
this.updateCurrentList(this.data.currentList, this.data.selectList)
},
//footer點擊刪除回調
footerDelete(res){
console.log(res)
this.setData({
selectList: res.detail.selectList
})
console.log('selectList', this.data.selectList)
this.updateCurrentList(this.data.currentList, res.detail.selectList)
},
//點擊 footer 的肯定按鈕提交數據
submitData(res){
let selectList = this.data.selectList
//經過 WxNotificationCenter 發送選擇的結果通知
EventBus.postNotificationName("SelectPeopleDone", selectList)
//將選擇結果存入 app.js 的 globalData
getApp().globalData.selectPeopleList = selectList
//返回
wx.navigateBack({
delta: 1
})
console.log("selectdone", selectList)
},
//更新 currentList 並將更新後的列表掛載在數據樹上
updateCurrentList(currentList, selectList){
let newList = []
currentList.forEach(item => {
let flag = false;
selectList.forEach(item2 => {
if (item.id === item2.id) {
flag = true
}
})
if (flag) {
item.checked = true
} else {
item.checked = false
}
newList.push(item)
})
this.setData({
currentList: newList
})
this.addList2DataTree()
this.setLastTimeSelectList()
}
})
複製代碼
userSelect.wxml
<view class='container'>
<view class='btn-wrapper'>
<button bindtap='goBack'>返回上一層</button>
</view>
<view class='people-wrapper'>
<scroll-view scroll-y class='scrollview'>
<checkbox-group bindchange="checkChange">
<view class='item' wx:for='{{currentList}}' wx:key='{{item.id}}'>
<checkbox checked='{{item.checked}}' value='{{item.id + "," + item.name}}'>
</checkbox>
<view id='{{index}}' class='item-content' bindtap='clickItem'>
<image class='img' wx:if='{{!item.isPeople}}' src='../../../assets/file.png'></image>
<image class='avatar' wx:if='{{item.isPeople}}' src='../../../assets/avatar.png'></image>
<text class='itemtext'>{{item.name}}</text>
</view>
</view>
</checkbox-group>
<view class='no-data' wx:if='{{currentList.length===0}}'>暫無數據</view>
</scroll-view>
</view>
<view class='footer'>
<footer list='{{selectList}}' binddelete='footerDelete' bindsubmit="submitData"/>
</view>
</view>
複製代碼
userSelect.wxss
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 20rpx;
overflow-x: hidden;
box-sizing: border-box;
background-color: #fff;
}
.btn-wrapper {
width: 100%;
padding: 0 20rpx;
box-sizing: border-box;
}
.btn {
font-size: 24rpx;
width: 100%;
}
.people-wrapper {
width: 100%;
margin-top: 10rpx;
margin-bottom: 100rpx;
}
.scrollview {
width: 100%;
display: flex;
flex-direction: column;
}
.item {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
padding: 30rpx 0;
margin: 0 20rpx;
border-bottom: 1rpx solid rgba(7, 17, 27, 0.1);
}
.item-content {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
margin-left: 20rpx;
}
.itemtext {
font-size: 36rpx;
color: #333;
margin-left: 20rpx;
text-align: center;
}
.img {
width: 50rpx;
height: 40rpx;
}
.avatar {
width: 50rpx;
height: 50rpx;
}
.footer {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
}
.no-data{
width: 100%;
font-size: 32rpx;
text-align: center;
padding: 40rpx 0;
}
複製代碼
userSelect.json
{
"usingComponents": {
"footer": "footer/footer"
}
}
複製代碼
footer.js
Component({
/** * 組件的屬性列表 */
properties: {
list: {
type: Array
}
},
/** * 組件的初始數據 */
data: {
},
/** * 組件的方法列表 */
methods: {
delete(res){
console.log(res)
let index = res.currentTarget.id
let list = this.data.list
list.splice(index,1)
this.setData({list: list})
this.triggerEvent("delete", {selectList: list})
},
/** * 點擊肯定按鈕 */
confirm(){
this.triggerEvent("submit", "")
}
}
})
複製代碼
footer.wxml
<view class='container'>
<view class='scroll-wrapper'>
<scroll-view scroll-x style='scroll'>
<text id='{{index}}' class='text' wx:for='{{list}}' wx:key='{{index}}' bindtap='delete'>{{item.name}}</text>
</scroll-view>
</view>
<text class='btn' bindtap='confirm'>肯定</text>
</view>
複製代碼
footer.wxss
.container {
width: 100%;
height: 100rpx;
display: flex;
flex-direction: row;
padding: 20rpx;
box-sizing: border-box;
background-color: #fff;
align-items: center;
overflow-x: hidden;
white-space: nowrap;
border-top: 2rpx solid rgba(7, 17, 27, 0.1)
}
.scroll-wrapper {
flex: 1;
overflow-x: hidden;
white-space: nowrap;
}
.scroll {
width: 100%;
}
.text {
font-size: 32rpx;
color: #333;
padding: 40rpx 20rpx;
margin-right: 10rpx;
background-color: #f5f5f5;
}
.btn {
padding: 10rpx 20rpx;
background-color: rgb(26, 173, 25);
border-radius: 10rpx;
font-size: 32rpx;
color: #fff;
}
複製代碼
footer.json
{
"component": true,
"usingComponents": {}
}
複製代碼
再補一個用到的 ArrayUtils
的代碼
export default{
/** * 給數組去重 */
checkRepeat(list) {
let noRepList = [list[0]]
for (let i = 0; i < list.length; i++) {
let repeat = false
for (let j = 0; j < noRepList.length; j++) {
if (noRepList[j].id === list[i].id) {
repeat = true
break
}
}
if (!repeat) {
noRepList.push(list[i])
}
}
return noRepList
},
//刪除list中id爲 delId 的元素
deleteItemById(list, delId){
for (let i = 0; i < list.length; i++) {
if (list[i].id == delId) {
list.splice(i, 1)
return list;
}
}
return list;
}
}
複製代碼
因爲時間緊張,尚未把這個控件單獨從項目中抽出來寫個 Demo,有時間了會給 github 地址的。
代碼還有不少能夠優化的地方,好比有幾個方法太長了,不符合單一職責原則等等,不想改了,之後再優化吧。。
水平有限,各位大俠請輕噴~
有問題或發現 Bug
請在評論區留言,畢竟剛寫完就分享出來了,還沒通過嚴格的測試。不過應該沒什麼大的問題。。。有些細節可能沒注意到。