前言 最近接到了一个需求,要求实现类似于手机通讯录功能这样界面,并且右侧有 A-Z 的姓名首字母快速检索功能。
一. 预览效果
观前提示:本文需要你对 unocss 或者 tailwind css 有一定的了解。
二. 理清思路
-
整体 UI 结构图如下:
-
页面结构整体是比较简单的,大致可以分为三个区域。
- 顶部搜索栏
- 中间主体内容
- 右侧姓名字母表排序
-
实现这个功能其实就是将这三个功能点拼凑到一起,接下来我会分开讲解如何实现这三个部分。
三. 实现右侧 A-Z 的字母表
-
生成字母表的方式有两种,你可以等获取到联系人的数据以后,根据联系人的首字母数量来动态生成。还有一种就是先生成,为了讲解方便。这里我们采用比较简单的方式,采用硬编码的方式直接生成 26 个字母。
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); -
然后直接使用
fixed布局,将字母组合排列到屏幕右侧,这是你现在的代码。 -
这是你现在的样式。
四. 实现内容区
-
数据部分,可以直接让 AI 帮我们生成一份假的联系人数据,你可以直接在 标题七 的源代码中直接复制这份数据。
-
可以观察到内容区实际上分为了两个部分,一个是姓名首字母,一个是姓名主体。
-
看到这里的布局,我们就应该条件反射的去想到,页面整体的数据应该是一个 Map 的结构,Map 的 key 对应首字母,Map 的 value 应该对应一个人员表单数组。
-
接下来我们的问题,就是如何去构建这个 Map。在这里我们第一步要做的事情就是,将姓名的首个汉字转化为对应的字母。这里我们需要借助一个第三方库 pinyin 这个库提供一个函数,该函数接收一个
string类型的参数,并且返回这个汉字的拼音格式。比如如上图,我输入了我的名字 "韩振方",则会返回 如下类型的一个数组。(这里只讲解该库在本需求中的用法,你可以按照你自己的需求查阅相关文档。)
-
既然已经可以正确拿到姓名首字母了,那么我们就可以开始遍历我们的
namelist去填充我们的 Map 了。这里我们写一个工具函数getNameMap来实现这个步骤。
const nameMap = reactive<Map<string, Array<typeof nameList>[number]>>(
new Map()
);
function getNameMap() {
nameList.forEach((name) => {
const firstLetter = getFirstLetter(name.name);
if (nameMap.get(firstLetter)) {
nameMap.get(firstLetter).push(name);
return;
}
nameMap.set(firstLetter, [name]);
});
console.log("nameMap", nameMap);
}
得到的结果如下
对应的界面效果:
- 上面看似已经完成主要功能,但是我们这是理想情况下,获取到的联系人数据已经按照拼音首字母排序好以后才去填充的 Map,我们还需要考虑数据假如是乱序情况,比如下面的数据:
export const noSortedNameList = [
{ name: "兀突骨", stylename: "无" }, // U (南蛮乌戈国主)
{ name: "于禁", stylename: "文则" }, // Y (曹操五子良将)
{ name: "阿会喃", stylename: "无" }, // A (南蛮将领)
{ name: "赵云", stylename: "子龙" } // Z (蜀汉五虎将)
];
此时我们得到的 Map 就不再是理想的排序了,界面排序自然就不是我们期望的那样。
- 所以我们应该在获取到数据以后,率先进行一次排序流程,确保在构建 Map 的时候,顺序是已经排列好的。这里需要使用到
string.localeCompare这个函数,在我们这个场景下,对a,b,c,d的排序方式,其实是取对应的ASCII码对应的数字排序。
- 所以我们现在改造一下我们的
getNameMap函数,该函数接收一个数组,在填充nameMap的时候,提前进行一次对于原数组的排序。
function getNameMap(list: Array<typeof nameList>[number]) {
list.sort((a, b) => {
const aFirstLetter = getFirstLetter(a.name);
const bFirstLetter = getFirstLetter(b.name);
return aFirstLetter.localeCompare(bFirstLetter);
});
list.forEach((name) => {
const firstLetter = getFirstLetter(name.name);
if (nameMap.get(firstLetter)) {
nameMap.get(firstLetter).push(name);
return;
}
nameMap.set(firstLetter, [name]);
});
console.log("nameMap", nameMap);
}
9. 此时就可以保证获得的数据一定是排序以后的。
五. 实现字母导航和内容区的联动
-
这一步其实是最简单的一步,这一点可能出乎大家的预料。我们在渲染字母标题的 dom 元素上,绑定一个
id为当前的字母。
所对应出的真实 dom 实际上就是下面这样: -
接下来我们给右侧字母导航栏增加一个点击事件即可,这里关键点其实就是调用
dom.scrollIntoView这个函数,这个函数将指定元素滚动到可视区域。
function hdlLetterNavigator(letter: string) {
const dom = document.getElementById(letter);
if (dom){
dom.scrollIntoView({
behavior: "smooth" // 这一行代码是关键作用
});
}
}
3. 实现对应的效果如下:
六. 增加搜索功能
-
此时我们就不能直接渲染
nameMap了,而是我们需要根据用户的输入值,来对nameMap的值进行筛选,这里我们可以用一个计算属性来表示。 -
整体思路也是比较简单的,就是在内部维护一个新的 Map,根据用户输入的值,来对
nameMap进行筛选。
const keyword=ref("")
const filteredList = computed(() => {
const _keyword = keyword.value.trim();
if (!_keyword) return nameMap;
const result = new Map();
nameMap.forEach((nameInfo, letter) => {
const filtered = nameInfo.filter(({ name, stylename }) => {
return (
name.includes(_keyword) || (stylename && stylename.includes(_keyword))
);
});
if (filtered.length) {
result.set(letter, filtered);
}
});
console.log("result", result);
return result;
});
得到的效果
七. 源码
- addressBook 的源码
<script lang="ts" setup>
import { ref, reactive, computed, onMounted } from "vue";
import pinyin from "pinyin";
import { nameList, noSortedNameList } from "./name.js";
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
const keyword = ref<string>("");
const nameMap = reactive<Map<string, Array<typeof nameList>[number]>>(
new Map()
);
const filteredList = computed(() => {
const _keyword = keyword.value.trim();
if (!_keyword) return nameMap;
const result = new Map();
nameMap.forEach((nameInfo, letter) => {
const filtered = nameInfo.filter(({ name, stylename }) => {
return (
name.includes(_keyword) || (stylename && stylename.includes(_keyword))
);
});
if (filtered.length) {
result.set(letter, filtered);
}
});
console.log("result", result);
return result;
});
function getFirstLetter(name: string) {
const letter = pinyin(name, {
style: pinyin.STYLE_FIRST_LETTER
});
return letter[0][0];
}
function getNameMap(list: Array<typeof nameList>[number]) {
list.sort((a, b) => {
const aFirstLetter = getFirstLetter(a.name);
const bFirstLetter = getFirstLetter(b.name);
return aFirstLetter.localeCompare(bFirstLetter);
});
list.forEach((name) => {
const firstLetter = getFirstLetter(name.name);
if (nameMap.get(firstLetter)) {
nameMap.get(firstLetter).push(name);
return;
}
nameMap.set(firstLetter, [name]);
});
console.log("nameMap", nameMap);
}
function hdlLetterNavigator(letter: string) {
const dom = document.getElementById(letter);
if (dom)
dom.scrollIntoView({
behavior: "smooth"
});
}
onMounted(() => {
getNameMap(nameList);
});
</script>
<template>
<div class="w-100vw h-100vh bg-#efedf3">
<div class="fixed right-10px flex flex-col top-[25%]">
<span
@click="hdlLetterNavigator(item)"
v-for="item in alphabet"
class="text-black text-14px active:text-blue">
{{ item }}
</span>
</div>
<div class="w-full h-full text-black flex flex-col overflow-auto">
<div class="w-full h-100px">
<input v-model="keyword" class="w-full h-100px" />
</div>
<div
v-for="[letter, person] of filteredList"
class="text-30px pt-50px px-10px"
:id="letter.toUpperCase()">
<span>{{ letter.toUpperCase() }}</span>
<div
v-for="{ name, stylename } in person"
class="flex flex-col gap-5px px-20px border-1px border-red border-b-solid">
<span class="text-16px">{{ name }}</span>
<span class="text-12px text-red">{{ stylename }}</span>
</div>
</div>
</div>
</div>
</template>
- namelist 的源码
export const nameList = [
{ name: "阿会喃", stylename: "无" }, // A (南蛮将领)
{ name: "鲍信", stylename: "允诚" }, // B (东汉末官吏)
{ name: "曹操", stylename: "孟德" }, // C (魏武帝)
{ name: "典韦", stylename: "无" }, // D (曹操部将)
{ name: "二乔", stylename: "无" }, // E (乔公二女,泛指大乔小乔)
{ name: "费祎", stylename: "文伟" }, // F (蜀汉四相之一)
{ name: "关羽", stylename: "云长" }, // G (蜀汉五虎将)
{ name: "黄盖", stylename: "公覆" }, // H (东吴老将)
{ name: "蒋琬", stylename: "公琰" }, // J (蜀汉丞相)
{ name: "孔融", stylename: "文举" }, // K (建安七子之一)
{ name: "吕布", stylename: "奉先" }, // L (三国第一猛将)
{ name: "马超", stylename: "孟起" }, // M (蜀汉五虎将)
{ name: "牛金", stylename: "无" }, // N (魏国将领)
{ name: "欧阳建", stylename: "坚石" }, // O (西晋文人,三国末期人物)
{ name: "庞统", stylename: "士元" }, // P (蜀汉谋士)
{ name: "桥瑁", stylename: "元伟" }, // Q (东汉末太守)
{ name: "司马懿", stylename: "仲达" }, // S (晋宣帝)
{ name: "太史慈", stylename: "子义" }, // T (东吴名将)
{ name: "兀突骨", stylename: "无" }, // U (南蛮乌戈国主)
{ name: "文丑", stylename: "无" }, // V (袁绍部将,V视为W通假)
{ name: "王平", stylename: "子均" }, // W (蜀汉将领)
{ name: "夏侯渊", stylename: "妙才" }, // X (曹魏八虎骑)
{ name: "于禁", stylename: "文则" }, // Y (曹操五子良将)
{ name: "赵云", stylename: "子龙" } // Z (蜀汉五虎将)
];
export const noSortedNameList = [
{ name: "兀突骨", stylename: "无" }, // U (南蛮乌戈国主)
{ name: "于禁", stylename: "文则" }, // Y (曹操五子良将)
{ name: "阿会喃", stylename: "无" }, // A (南蛮将领)
{ name: "赵云", stylename: "子龙" } // Z (蜀汉五虎将)
];