在很多系统中都有选择联系人的需求,市面上也没什么好的参照,产品经理看企业微信的选人挺好用的,就说参照这个做一个吧。。。
算了,还是试着做吧,企业微信的选人的确做的挺好,不得不佩服。
先看看效果图吧,多层级无规律的嵌套都能搞定
一、设计解读
整个界面分为三部分:
- 最上面的返回上一层按钮
- 中间的显示部门、人员的列表
- 最下面显示和操作已选人员的 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;