
项目概述
仿微信风格的通讯录页面,采用现代化设计,界面简洁美观,交互流畅自然。页面左侧为分组联系人列表,右侧为字母索引栏,支持点击和滚动高亮,带有动态圆形头像,整体体验高度还原主流社交 App 通讯录。
功能特点
- 字母分组与索引:联系人自动按首字母分组,右侧字母索引栏支持点击快速跳转分组,滑动列表时索引栏自动高亮当前分组。
- 动态高亮:滚动通讯录时,右侧字母索引栏会精准高亮当前分组,体验媲美微信、QQ 等主流 App。
- 圆形头像:每个联系人自动生成彩色圆形头像,首字母展示,提升辨识度与美观度。
- 响应式设计:适配桌面与移动端,支持触摸与鼠标操作。
- 美观 UI:采用 Tailwind CSS,渐变背景、卡片阴影、圆角等细节处理,整体风格现代、清新。
- 高性能渲染:分组渲染高效,滚动流畅不卡顿,适合大数据量通讯录场景。
技术亮点
- React Hooks:全程使用函数组件与 Hooks(useRef、useState、useEffect),代码简洁易维护。
- DOM 精确监听:通过 getBoundingClientRect 精确计算分组位置,实现高精度的索引高亮切换。
- Tailwind CSS:极简高效的样式方案,快速实现美观 UI,易于自定义扩展。
- 无第三方依赖:核心功能零依赖,易于集成到任意 React 项目。
- 可扩展性强:支持自定义头像、分组规则、索引栏样式等,满足多样化业务需求。
使用场景
- 企业/团队通讯录:适用于企业内部员工通讯录、组织架构展示等场景。
- 社交/IM 应用:可作为微信、QQ、钉钉等社交 App 的通讯录模块。
- 教育/校友录:班级、年级、校友录等分组联系人展示。
- 客户/会员管理:CRM、会员系统等需要分组管理大量联系人的场景。
- 移动端/小程序:适配移动端,体验友好,适合 H5、React Native、小程序等多端开发。
全部源码
import { useRef, useState, useEffect } from "react";
// 生成随机颜色(根据首字母)
function getColor(letter) {
const colors = [
"bg-blue-400",
"bg-green-400",
"bg-yellow-400",
"bg-pink-400",
"bg-purple-400",
"bg-red-400",
"bg-indigo-400",
"bg-teal-400",
"bg-orange-400",
"bg-cyan-400",
];
return colors[letter.charCodeAt(0) % colors.length];
}
// 示例数据,可替换为你的实际数据
const contacts = [
{ name: "Alice", initial: "A" },
{ name: "Aaron", initial: "A" },
{ name: "Bob", initial: "B" },
{ name: "Bella", initial: "B" },
{ name: "Cindy", initial: "C" },
{ name: "David", initial: "D" },
{ name: "Eve", initial: "E" },
{ name: "Frank", initial: "F" },
{ name: "Grace", initial: "G" },
{ name: "Helen", initial: "H" },
{ name: "Ivy", initial: "I" },
{ name: "Jack", initial: "J" },
{ name: "Kathy", initial: "K" },
{ name: "Leo", initial: "L" },
{ name: "Mona", initial: "M" },
{ name: "Nina", initial: "N" },
{ name: "Oscar", initial: "O" },
{ name: "Paul", initial: "P" },
{ name: "Queen", initial: "Q" },
{ name: "Rose", initial: "R" },
{ name: "Sam", initial: "S" },
{ name: "Tom", initial: "T" },
{ name: "Uma", initial: "U" },
{ name: "Vera", initial: "V" },
{ name: "Will", initial: "W" },
{ name: "Xander", initial: "X" },
{ name: "Yuki", initial: "Y" },
{ name: "Zack", initial: "Z" },
];
// 分组
const grouped = contacts.reduce((acc, cur) => {
acc[cur.initial] = acc[cur.initial] || [];
acc[cur.initial].push(cur);
return acc;
}, {});
const letters = Array.from({ length: 26 }, (_, i) =>
String.fromCharCode(65 + i)
);
export default function Contacts() {
const refs = useRef({});
const listRef = useRef();
const [activeLetter, setActiveLetter] = useState(letters[0]);
const scrollTo = (letter) => {
refs.current[letter]?.scrollIntoView({
behavior: "smooth",
block: "start",
});
};
useEffect(() => {
const container = listRef.current;
if (!container) return;
const handleScroll = () => {
const offsets = letters
.filter((letter) => grouped[letter])
.map((letter) => {
const el = refs.current[letter];
if (!el) return null;
const rect = el.getBoundingClientRect();
return { letter, top: rect.top, bottom: rect.bottom };
})
.filter(Boolean);
// 80 是标题高度,可根据实际调整
const threshold = 80;
// 找到第一个 bottom >= 80 的分组
const idx = offsets.findIndex((o) => o.bottom >= threshold);
if (idx === -1) {
setActiveLetter(offsets[offsets.length - 1].letter);
} else {
setActiveLetter(offsets[idx].letter);
}
};
container.addEventListener("scroll", handleScroll);
handleScroll();
return () => container.removeEventListener("scroll", handleScroll);
}, [grouped, letters]);
return (
<div className="flex relative h-screen bg-gradient-to-br from-blue-50 to-white">
{/* 通讯录列表 */}
<div ref={listRef} className="flex-1 overflow-y-auto px-4 py-4">
<h1 className="text-2xl font-bold mb-4 text-blue-700 tracking-wide">
通讯录
</h1>
{letters.map(
(letter) =>
grouped[letter] && (
<div key={letter} ref={(el) => (refs.current[letter] = el)}>
<div className="sticky top-0 z-10 text-blue-600 font-bold px-2 py-1 rounded mt-2 mb-1 shadow-sm backdrop-blur">
{letter}
</div>
<div className="space-y-2 mb-4">
{grouped[letter].map((c) => (
<div
key={c.name}
className="flex items-center gap-3 px-4 py-2 bg-white rounded-xl shadow hover:bg-blue-50 transition cursor-pointer group"
>
<div
className={`w-10 h-10 flex items-center justify-center rounded-full text-white text-lg font-bold shadow ${getColor(
c.initial
)}`}
>
{c.name[0]}
</div>
<span className="text-gray-800 text-base font-medium group-hover:text-blue-700 transition">
{c.name}
</span>
</div>
))}
</div>
</div>
)
)}
</div>
{/* 右侧字母索引栏 */}
<div
className="fixed right-2 top-1/2 -translate-y-1/2 z-20 flex flex-col items-center select-none rounded-xl shadow px-1 py-2 backdrop-blur"
style={{ userSelect: "none" }}
>
{letters.map((letter) => (
<div
key={letter}
className={`text-xs font-medium px-2 py-1 cursor-pointer rounded transition
${
grouped[letter]
? "text-gray-700"
: "text-gray-300 cursor-default"
}
${
activeLetter === letter && grouped[letter]
? "bg-blue-500 text-white font-bold scale-110 shadow"
: ""
}
hover:bg-blue-200 hover:text-blue-700`}
onClick={() => grouped[letter] && scrollTo(letter)}
>
{letter}
</div>
))}
</div>
</div>
);
}
原文链接:https://code.ifrontend.net/archives/661,转载请注明出处。
评论0