前端实现姓名 A-Z 的通讯录查找功能

146 阅读6分钟

前言 最近接到了一个需求,要求实现类似于手机通讯录功能这样界面,并且右侧有 A-Z 的姓名首字母快速检索功能。


一. 预览效果

1.gif

观前提示:本文需要你对 unocss 或者 tailwind css 有一定的了解。

二. 理清思路

  1. 整体 UI 结构图如下:
    image.png

  2. 页面结构整体是比较简单的,大致可以分为三个区域。

    • 顶部搜索栏
    • 中间主体内容
    • 右侧姓名字母表排序
  3. 实现这个功能其实就是将这三个功能点拼凑到一起,接下来我会分开讲解如何实现这三个部分。

三. 实现右侧 A-Z 的字母表

  1. 生成字母表的方式有两种,你可以等获取到联系人的数据以后,根据联系人的首字母数量来动态生成。还有一种就是先生成,为了讲解方便。这里我们采用比较简单的方式,采用硬编码的方式直接生成 26 个字母。

    const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");

  2. 然后直接使用 fixed 布局,将字母组合排列到屏幕右侧,这是你现在的代码。

  3. 这是你现在的样式。
    image.png

四. 实现内容区

  1. 数据部分,可以直接让 AI 帮我们生成一份假的联系人数据,你可以直接在 标题七 的源代码中直接复制这份数据。

  2. 可以观察到内容区实际上分为了两个部分,一个是姓名首字母,一个是姓名主体。 image.png

  3. 看到这里的布局,我们就应该条件反射的去想到,页面整体的数据应该是一个 Map 的结构,Map 的 key 对应首字母Mapvalue 应该对应一个人员表单数组。

  4. 接下来我们的问题,就是如何去构建这个 Map。在这里我们第一步要做的事情就是,将姓名的首个汉字转化为对应的字母。这里我们需要借助一个第三方库 pinyin 这个库提供一个函数,该函数接收一个 string 类型的参数,并且返回这个汉字的拼音格式。
    image.png 比如如上图,我输入了我的名字 "韩振方",则会返回 如下类型的一个数组。(这里只讲解该库在本需求中的用法,你可以按照你自己的需求查阅相关文档。)
    image.png

  5. 既然已经可以正确拿到姓名首字母了,那么我们就可以开始遍历我们的 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);
}

得到的结果如下
image.png
对应的界面效果:
2.gif

  1. 上面看似已经完成主要功能,但是我们这是理想情况下,获取到的联系人数据已经按照拼音首字母排序好以后才去填充的 Map,我们还需要考虑数据假如是乱序情况,比如下面的数据:
export const noSortedNameList = [
          { name: "兀突骨", stylename: "无" }, // U (南蛮乌戈国主)
          { name: "于禁", stylename: "文则" }, // Y (曹操五子良将)
          { name: "阿会喃", stylename: "无" }, // A (南蛮将领)
          { name: "赵云", stylename: "子龙" } // Z (蜀汉五虎将)
        ];

此时我们得到的 Map 就不再是理想的排序了,界面排序自然就不是我们期望的那样。
image.png

  1. 所以我们应该在获取到数据以后,率先进行一次排序流程,确保在构建 Map 的时候,顺序是已经排列好的。这里需要使用到 string.localeCompare 这个函数,在我们这个场景下,对 a,b,c,d 的排序方式,其实是取对应的 ASCII 码对应的数字排序。

image.png

  1. 所以我们现在改造一下我们的 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. 此时就可以保证获得的数据一定是排序以后的。
image.png

五. 实现字母导航和内容区的联动

  1. 这一步其实是最简单的一步,这一点可能出乎大家的预料。我们在渲染字母标题dom 元素上,绑定一个 id 为当前的字母。
    image.png
    所对应出的真实 dom 实际上就是下面这样: image.png

  2. 接下来我们给右侧字母导航栏增加一个点击事件即可,这里关键点其实就是调用 dom.scrollIntoView 这个函数,这个函数将指定元素滚动到可视区域。

 function hdlLetterNavigator(letter: string) {
    const dom = document.getElementById(letter);
      if (dom){
        dom.scrollIntoView({
              behavior: "smooth" // 这一行代码是关键作用
            });
        }
}

3. 实现对应的效果如下:
3.gif

六. 增加搜索功能

  1. 此时我们就不能直接渲染 nameMap 了,而是我们需要根据用户的输入值,来对 nameMap 的值进行筛选,这里我们可以用一个计算属性来表示。

  2. 整体思路也是比较简单的,就是在内部维护一个新的 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;
        });

得到的效果
4.gif

七. 源码

  1. 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>

  1. 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 (蜀汉五虎将)
];