友链页面实现说明
这篇文档记录 /friends 友链页面的功能设计、组件架构和数据维护方式。
功能概述
友链页面用于展示朋友和同行的个人站点,所有访客均可浏览。核心交互与功能点:
- 背景图片卡片:每个友链可以配置背景图,卡片采用深色叠加层 + 头像/站点名/URL 的视觉布局。无背景图的卡片降级为纯色卡片,补充展示站点描述。
- 分类筛选:友链可按
category字段分组。页面顶部渲染胶囊形标签按钮,点击切换分类过滤,支持键盘导航(Arrow 键、Home/End)。 - 滚动入场动画:卡片在
IntersectionObserver触发时以cubic-bezier(0.22, 1, 0.36, 1)缓动淡入上移,尊重prefers-reduced-motion: reduce。 - 申请引导区块:页面底部以虚线边框区块展示友链申请条件和联系方式,引导有意交换友链的访客主动联系。
- 国际化:页面所有文案(标题、筛选标签、申请区块)和友链数据字段(名称、描述、分类)均支持
zh-Hans/en双语。 - 移动端触摸交互:触屏设备采用两次点击模式——首次点击展开描述(添加
cardExpanded类),再 次点击跳转访问。点击卡片外区域收起描述。通过window.matchMedia('(hover: none) and (pointer: coarse)')检测触屏设备。
文件结构
src/
├── pages/
│ └── friends.mdx # 路由入口,挂载 FriendsSection 组件
├── components/
│ └── FriendsSection/
│ ├── index.js # 主组件,包含 Avatar、页面布局与状态逻辑
│ └── styles.module.css # CSS Modules 样式
└── data/
└── friends.js # 友链数据数组
数据格式
友链数据在 src/data/friends.js 中以数组形式维护,每个友链一个对象:
{
id: 'friend-slug', // 唯一标识(必填)
avatar: 'https://oss.nevergpdzy.com/avatars/friend.jpg', // 头像 URL(必填)
name: { 'zh-Hans': '站点名称', en: 'Site Name' }, // 站点名称(必填,支持 i18n)
description: { 'zh-Hans': '一句话介绍', en: 'One-line intro' }, // 描述(必填,支持 i18n)
url: 'https://example.com', // 站点 URL(必填)
background: 'https://example.com/screenshot.jpg', // 背景图片 URL(可选)
category: { 'zh-Hans': '技术', en: 'Tech' }, // 分类(可选,用于筛选)
note: { 'zh-Hans': '备注', en: 'Personal note' }, // 备注(可选,预留字段)
}
支持 i18n 的字段(name、description、category、note)可以传入普通字符串(所有语言共用)或包含 zh-Hans / en 键的对象。
新增友链
在 src/data/friends.js 的数组末尾追加一个对象即可。category 不填则归入"全部"中显示但不参与分类筛选;background 不填则卡片降级为纯色模式并展示描述文字。
组件架构
FriendsSection(主组件)
页面级组件,位于 src/components/FriendsSection/index.js。
状态管理:
activeCategory:当前选中的分类筛选值,null表示"全部"。categories:useMemo从友链数据中动态提取的 分类集合,随 locale 变化重新计算。filteredFriends:useMemo根据activeCategory过滤后的友链列表。expandedId:当前展开的友链卡片 ID(仅触屏设备使用),控制cardExpanded类的添加与移除。bgUrl:每日背景图片 URL,由 API 动态获取,通过 inline style 应用到.sectionBg元素。
副作用:
useEffect在组件挂载时请求https://goodimg.nevergpdzy.com/?format=url获取每日背景图片 URL,校验后写入bgUrlstate;使用AbortController在卸载时取消请求。useEffect在filteredFriends变化时重新绑定IntersectionObserver,为每个.card元素注册视口交叉监听,触发cardVisible类添加以实现入场动画。useEffect在expandedId变化时注册/注销document级click监听器,点击卡片外区域时清除展开状态。
键盘无障碍:
- 筛选标签栏使用
role="tablist"与role="tab",通过handleFilterKeyDown处理 ArrowRight/Left、ArrowUp/Down、Home/End 按键,实现焦点移动与激活。
辅助函数:
resolveLocale(value, locale):从 i18n 对象或纯字符串中解析当前语言的值。safeHostname(url):安全提取 URL 的 hostname,解析失败时返回原始 URL 作为兜底。