前言
近期闲来无事,想着闲着也是闲着,不如给自己搞点事情做!敢想敢做,于是选择了给微信小程序做个仿iPhone通讯录效果的自定义组件。
先来整理一下,瞧瞧需要实现的核心功能。
![](http://img50.lidatong.com.cn//uploads/allimg/c20201214/5e3782b3f31f9d774dbb7125ffad7a68.gif)
- 按照第一个字的首字母排序;
- 实现输入搜索功能;
- 侧边栏字母导航;
从易到难,先来看看页面的结构布局。
![](http://img50.lidatong.com.cn//uploads/allimg/c20201214/857dc84b9d049949193d5f80b4c1280d.gif)
基本上分为3块:
- 顶部的搜索区域;
- 内容的展示区域;
- 侧边字母导航栏区域;
// index.wxml
<view class="main">
<view class="header">
</view>
<scroll-view class="scroll">
</scroll-view>
<view class="sub_nav">
</view>
</view>
复制代码
【顶部的搜索区域】
![](http://img50.lidatong.com.cn//uploads/allimg/c20201214/ead8ea318ac592b07418812739a8e22b.gif)
一目了然就直接贴代码了。
<view class="header">
// 这里或许有人要问,为啥不用小程序的label组件呢。?_?
// 原因就是...我就不用,你还能咬我?!^(oo)^
// 哈哈哈哈~开个玩笑,其实是小程序的label组件还没支持input!
<view class="label">
<icon></icon>
<input type="text" placeholder="搜索" />
</view>
</view>
复制代码
【内容的展示区域】
![](http://img50.lidatong.com.cn//uploads/allimg/c20201214/153258652f48f388573a94508babbb33.gif)
再说一目了然会不会被打呢?
根据图片就可以看出来,存在2个区域。
- 红框包围的外框,负责圈定展示的范围;
- 绿框包围的范围,包含有字母标题和对应的子项。
代码如下:
<scroll-view class="scroll">
<view class="dl">
<view class="dt">这里是字母标题。</view>
<view class="dd">
<span>这里当然是展示的内容啦。</span>
</view>
</view>
</scroll-view>
复制代码
【侧边字母导航栏区域】
为了节省一下文章的篇幅,这里就不贴图了,很简单,就是并排下来就好了。
<view class="sub_nav">
<view class="option">这里是输出字母。</view>
</view>
复制代码
接下来是wxss的样式了。
考虑到wxss的样式较多,我就直接贴代码链接吧,有兴趣的童鞋可以瞧瞧。
完成之后,是时候贴个效果图了。(不许吐槽丑,宝宝会不开心的!)
![](http://img50.lidatong.com.cn//uploads/allimg/c20201214/8cd842532a450cae0ea94a2d4c6fd364.gif)
结构样式弄完了,也贴一下自定组件的基础文件
// index.json
{
"component": true
}
复制代码
Component({
properties: {},
data: {},
lifetimes: {},
methods: {}
});
复制代码
现在开始实现功能了!!!
按照第一个字的首字母排序
说实话,实现这块功能呢,我是没啥头绪的,所以这个时候就要求助伟大的“度娘/Google”了。
经过楼主“遍寻网络”,查找到如下页面的源码参考:
![](http://img50.lidatong.com.cn//uploads/allimg/c20201214/772dd1a1d368f617c36d8c96e02da8bc.gif)
因楼主问题,遗忘了该网址,如有知道的童鞋,贴个链接告诉下楼主,楼主立马麻溜的加上。
源码的原理大概描述下:
收录20902个汉字和375个多音字的Unicode编码,然后用JS切割首字母并转换成Unicode进行对比,最后返回对应首字母的拼音。
import firstStore from './firstChineseLetter';
function getFirstLetter (val) {
const firstVal = val.charAt(0);
if (/.*[u4e00-u9fa5]+.*/.test(firstVal)) {
const code = firstVal.charCodeAt(0);
return code in firstStore.oMultiDiff ? firstStore.oMultiDiff[code] : firstStore.firstLetterMap.charAt(code - 19968);
} else {
return /^[a-zA-Z]+$/.test(firstVal) ? firstVal.toUpperCase() : '#';
}
}
getFirstLetter('东城区');
复制代码
firstChineseLetter.js地址
获取首字母的方法有了之后,就该对数据进行处理了。
首先定义一下组件所需要的参数。
Component({
properties: {
data: { type: Array,value: [],},
attr: { type: String,value: 'label' },
},...
})
复制代码
然后,针对组件外传递进来的数据,做一次转换。
const Static = {
list: []
}
Component({
...
methods: {
init () {
const { data,attr } = this.properties;
let changeData = [],
inChangeData = {};
data.map(v => {
let firstLetter = this.getFirstLetter(v[attr]);
firstLetter.split('').map(str => {
if (str in inChangeData) {
changeData[inChangeData[str]].list.push(v);
} else {
changeData.push({ firstLetter: str,list: [v] });
inChangeData[str] = changeData.length - 1;
}
});
});
changeData.sort((pre,next) => pre.firstLetter < next.firstLetter ? -1 : 1);
if (changeData[0].firstLetter === '#') {
const firstArr = changeData.splice(0,1);
changeData = [...changeData,...firstArr];
}
this.setData({ list: changeData });
Static.list = changeData;
},}
...
});
复制代码
初始化函数有了之后呢,当然是调用它啦。
Component({
lifetimes: {
attached () {
this.init();
}
},observers: {
'data,attr,icon' (data,attr) {
this.init();
}
},})
复制代码
接下来是搜索功能啦~
先给页面搜索框加个监听事件(input)
<view class="main">
...
<view class="header">
<view class="label">
<icon></icon>
<input type="text" placeholder="搜索" value="{{ search }}" bindinput="searchData" />
</view>
</view>
...
</view>
复制代码
接着是JS的事件
const Static = {
list: []
}
Component({
...
methods: {
searchData (e) {
const { value } = e.detail;
const { list } = Static;
const { attr } = this.properties;
let result = [],tem = {};
if (value.length === 0) { this.setData({ list: Static.list }); return; }
list.map(v => {
const searchList = v.list.filter(v => v[attr].indexOf(value) !== -1);
if (searchList.length > 0) {
if (v.firstLetter in tem) {
const _list = result[tem[v.firstLetter]].lish;
result[tem[v.firstLetter]].lish = [..._list,...searchList];
} else {
result.push({ firstLetter: v.firstLetter,list: [...searchList] });
tem[v.firstLetter] = result.length - 1;
}
}
});
this.setData({ list: result,search: value });
}
},...
});
复制代码
侧边栏字母导航
(突然觉得,写文好累啊!!!)
写这块的时候呢,楼主发现了iPhone通讯录侧边导航栏有个问题,手指在字母导航栏上滑动的时候,有时候很难确认自己滑到了哪个区域?!
然鹅这个问题呢,楼主发现了微信的通讯录,针对这块添加了手指滑动的时候,添加了个结构来帮助用户确认目前所处的区域。
楼主本着学习的精神,借(chao)鉴(xi)了这个效果,来个效果图。
![](http://img50.lidatong.com.cn//uploads/allimg/c20201214/e9420d7fcc57c61ff6d4d2ce0d7d8161.gif)
贴一下新的wxml结构
<view class="sub_nav" id="subNav" catchtouchstart="subTouchStart" catchtouchmove="subTouchMove" catchtouchend="subTouchEnd">
<view class="option" wx:for="{{ list }}" data-firstLetter="{{ item.firstLetter }}" wx:key="firstLetter">
{{ item.firstLetter }}
<view
class="max {{ item.firstLetter === scrollIntoView && subNavHint ? 'show' : '' }}"
data-desc="{{ item.firstLetter }}"
></view>
</view>
</view>
复制代码
const Static = {
list: [],timer: null
}
Component({
...
data: {
scrollIntoView: '',
subNavHint: false,
},methods: {
subTouchStart () {
this.setData({ subNavHint: true,scrollIntoView: '' });
},subTouchEnd () {
this.setData({ subNavHint: false });
},subTouchMove (e) {
const query = this.createSelectorQuery();
query.select('#subNav').boundingClientRect();
query.selectViewport().scrollOffset();
query.exec(res => {
const { clientY } = e.touches[0];
const DomTop = res[0].top;
const { list } = this.data;
let index = Math.round((clientY - DomTop) / 20);
index = index >= list.length ? list.length - 1 : index;
index = index < 0 ? 0 : index;
if (list[index].firstLetter !== this.data.scrollIntoView) {
this.setData({ scrollIntoView: list[index].firstLetter });
wx.vibrateShort();
}
});
}
},}
...
});
复制代码
结语
文章写到这呢,基本上核心的功能都已经实现啦~(终于写完了...)
通过自己封装组件,楼主还是有挺大收获的!
当然,这个组件还有很多可以继续完善的地方,有兴趣的童鞋呢,可以提出你的优化建议,楼主有时(xing)间(qu)的话,会继续完善下去。
最后,还是推一下这个组件啦,希望它能帮到有需要的童鞋。
github地址
手写不易,欢迎提issues,欢迎star,内附有使用方法哦。