仿企業微信實現多選及多層級無規則巢狀
在很多系統中都有選擇聯絡人的需求,市面上也沒什麼好的參照,產品經理看企業微信的選人挺好用的,就說參照這個做一個吧。。。
算了,還是試著做吧,企業微信的選人的確做的挺好,不得不佩服。
先看看效果圖吧,多層級無規律的巢狀都能搞定
一、設計解讀
整個介面分為三部分:
- 最上面的返回上一層按鈕
- 中間的顯示部門、人員的列表
- 最下面顯示和操作已選人員的 footer。
為什麼加一個返回上一層按鈕呢?
我也覺得比較醜,但小程式無法直接控制左上角返回鍵(自定義 Title 貌似可以,沒試過),點左上角的返回箭頭的話就退出選人控制元件到上個頁面了。
我們的需求是點選一個資料夾,通過重新整理當前列表進入下一級目錄,感覺像是又進了一個頁面,但其實並沒有,只是列表的資料變化了。由此實現不定層級、無規律的部門和人員巢狀的支援。
比如先點選了首屏資料的第二個 item
,它的 index
是 1
,就將 1
存入 indexList
;返回上一層時將最後一個元素刪除。
當勾選了某個人或部門時,會在底部的框中顯示所有已選人員或部門的名字,當文字超過螢幕寬度時可以向右無限滑動,底部 footer
始終保持一行。
最終選擇的人以底部 footer
裡顯示的為準,點選確定時根據業務需要將已選人員資料傳送給需要的介面。
二、功能邏輯分析
先看看資料格式
{
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()
的時候就會報錯:
超過最大長度了。。。所以只能分步載入資料。
當然如果你的資料量小,幾十人或幾百人,也可以選擇一次性載入。
這個控制元件邏輯上還是比較複雜的,要考慮的細節太多……下面梳理一下主要的邏輯點
主要邏輯點
1. 需要一個數組儲存所有被點選的部門在當前列表的索引 index
,這裡用 indexList
表示
點選某個部門進入下一層目錄時,將被點選部門的 index
索引 push
進 indexList
中。點選返回上一層按鈕時,刪除 indexList
中最後一個元素。
2. 要動態的更新當前列表 currentList
每進入新的一層,或返回上一層,都需要重新整理 currentList
來實現頁面的更新。知道下一層資料很容易,直接取被點選 item
的 children
賦值給 currentList
即可。
但如何還原上一層的資料呢?
第一點記錄的 indexList
就發揮作用了,原始資料樹為 originalList
,迴圈遍歷 indexList
,根據索引依次取出每層的 currentList
直到 indexList
的最後一個元素,就得到了返回上一層需要顯示的資料。
3. 每一次勾選或取消選中都要更新原始的資料樹 originalList
頁面是根據每個 item
的 checked
屬性判斷是否選中的,所以每次改變勾選狀態都要設定被改變的 item
的 checked
屬性,然後更新 originalList
。這樣即使返回上一層了,再進到當前層級選中狀態還會被保留,否則重新整理 currentList
後已選狀態將丟失。
4. 列表中選擇狀態的改變與底部 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 的程式碼
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