给博客侧边栏添加一个可折叠的好友模块
最近给自己的博客侧边栏加了一个"好友"模块,默认折叠,点开就能看到好友列表,每个好友都能直接跳转到对方的网站。这篇文章把整个制作过程记录下来,方便有同样需求的朋友参考。
准备工作
在动手之前,先确认一下你手头的东西:
- 一个基于 Nuxt 3 的博客项目(本文以 Clarity 主题为例)
- 装好了 Node.js 和 pnpm
- 一个顺手的代码编辑器(VS Code 就行)
- 对 Vue 3 组件有最基本的了解(知道
script setup、template、style是什么)
不需要你是 Vue 高手,只要能看懂基本的模板语法就够用了。
整体思路
先想清楚这个模块要做什么:
- 在侧边栏导航下方显示一个"好友"标题行
- 默认是折叠的,只显示标题和一个展开箭头
- 点击标题行可以展开/折叠好友列表
- 每个好友显示头像、名称和简短描述
- 点击好友条目在新标签页打开对方的网站
- 鼠标悬停时有视觉反馈
拆成三个部分来做:
- 组件文件:负责展示和交互逻辑
- 数据配置:好友列表的数据源
- 集成接入:把组件放进侧边栏
第一步:创建好友组件
在 app/components/blog/ 目录下新建 BlogFriends.vue 文件。Nuxt 会自动根据目录结构注册组件,所以放在 blog/ 目录下就能以 <BlogFriends> 的方式使用。
编写模板结构
先搭好 HTML 骨架:
<template>
<div class="blog-friends">
<!-- 标题行,点击切换展开/折叠 -->
<button class="friends-header" @click="isExpanded = !isExpanded">
<Icon name="tabler:users" />
<span class="nav-text">好友</span>
<Icon class="toggle-icon" :class="{ expand: isExpanded }" name="tabler:chevron-down" />
</button>
<!-- 好友列表,用 Transition 做动画 -->
<Transition name="collapse">
<div v-show="isExpanded" class="friends-content">
<a
v-for="friend in list"
:key="friend.name"
:href="friend.url"
target="_blank"
rel="noopener"
class="friend-item"
>
<NuxtImg v-if="friend.avatar" :src="friend.avatar" :alt="friend.name" class="friend-avatar" loading="lazy" />
<Icon v-else name="tabler:user" class="friend-avatar-icon" />
<div class="friend-info">
<span class="friend-name">{{ friend.name }}</span>
<span v-if="friend.desc" class="friend-desc">{{ friend.desc }}</span>
</div>
</a>
</div>
</Transition>
</div>
</template>
几个要点说明:
- 标题行用
<button>:保证键盘可访问,屏幕阅读器也能正确识别 v-show而非v-if:配合<Transition>做展开/折叠动画时,v-show更合适,因为组件始终在 DOM 中,动画才能平滑过渡target="_blank":好友链接在新标签页打开,不打断当前浏览NuxtImg+loading="lazy":头像懒加载,不拖慢首屏速度- 没有头像时显示默认图标:用
v-if/v-else做兜底
编写脚本逻辑
脚本部分很简单,只需要定义数据和状态:
<script setup lang="ts">
interface FriendItem {
name: string
url: string
avatar?: string
desc?: string
}
const props = defineProps<{
list?: FriendItem[]
}>()
const isExpanded = ref(false)
</script>
FriendItem接口:定义好友数据的类型,avatar和desc是可选的list属性:从外部传入好友列表数据isExpanded:控制展开/折叠状态,默认false(折叠)
编写样式
样式要和侧边栏现有的导航项保持一致,关键是复用已有的 CSS 变量:
<style lang="scss" scoped>
.blog-friends {
display: flex;
flex-direction: column;
}
.friends-header {
display: flex;
align-items: center;
gap: 0.5em;
padding: 0.5em 1em;
border-radius: 0.5em;
font: inherit;
color: inherit;
cursor: pointer;
transition: all 0.2s;
&:hover {
background-color: var(--c-bg-soft);
color: var(--c-text);
}
> .iconify {
font-size: 1.5em;
}
> .nav-text {
flex-grow: 1;
text-align: start;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.toggle-icon {
font-size: 1em;
transition: transform 0.2s;
&.expand {
transform: rotate(180deg);
}
}
</style>
这里有几个设计决策值得提一下:
font: inherit和color: inherit:让按钮的字体和颜色继承父元素,和侧边栏其他导航项保持一致var(--c-bg-soft):使用项目定义的 CSS 变量,自动适配浅色/深色主题- 箭头旋转动画:展开时箭头旋转 180 度,给用户明确的视觉反馈
好友条目的样式:
.friend-item {
display: flex;
align-items: center;
gap: 0.5em;
padding: 0.4em 0.8em;
border-radius: 0.5em;
text-decoration: none;
color: var(--c-text-2);
transition: all 0.2s;
&:hover {
background-color: var(--c-bg-soft);
color: var(--c-text);
}
}
.friend-avatar {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
.friend-info {
display: flex;
flex-direction: column;
overflow: hidden;
line-height: 1.3;
}
.friend-name {
font-size: 0.9em;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.friend-desc {
font-size: 0.75em;
opacity: 0.5;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
- 头像用
border-radius: 50%做圆形:和友链页面的风格统一 - 文字溢出省略号:名称和描述都可能很长,用
text-overflow: ellipsis处理 - 描述用更小字号 + 半透明:层次分明,不抢名称的视觉权重
折叠动画
展开/折叠的过渡动画:
.collapse-enter-active,
.collapse-leave-active {
transition: all 0.25s ease;
}
.collapse-enter-from,
.collapse-leave-to {
opacity: 0;
max-height: 0;
}
.collapse-enter-to,
.collapse-leave-from {
opacity: 1;
max-height: 500px;
}
这里同时用了 opacity 和 max-height 两个属性做过渡。max-height 从 0 到 500px 模拟高度变化,opacity 做淡入淡出,两者配合效果比较自然。
第二步:添加好友数据配置
数据放在 app.config.ts 中,这样运行时通过 useAppConfig() 就能读取,修改也不需要重新构建。
在 app.config.ts 中添加 friends 字段:
friends: [
{ name: 'GuuGuai', url: 'https://blog.guuguai.site/', avatar: 'https://cdn.libravatar.org/avatar/646331bff8f19a0e05679c3cc0fc54d6?s=160', desc: '古怪杂记本' },
{ name: '小李同学', url: 'https://blog.junjieli.top/', avatar: 'https://www.junjieli.top/logo_64x64.png', desc: '一支努力变强的小彩笔' },
// 添加更多好友...
] satisfies { name: string, url: string, avatar?: string, desc?: string }[],
用 satisfies 而不是 as 来做类型约束,这样既能保证数据格式正确,又不会丢失类型推断。
每个好友条目只需要填:
| 字段 | 是否必填 | 说明 |
|---|---|---|
name | 必填 | 好友名称 |
url | 必填 | 好友网站链接 |
avatar | 可选 | 头像图片地址,不填则显示默认图标 |
desc | 可选 | 简短描述,不填则不显示描述行 |
第三步:集成到侧边栏
打开 app/components/blog/BlogSidebar.vue,在导航列表的 </template> 之后、</nav> 之前加入一行:
<BlogFriends :list="appConfig.friends" />
就这么简单。因为 Nuxt 会自动注册 blog/ 目录下的组件,所以不需要手动 import。
完整的侧边栏结构变成了:
<nav class="sidebar-nav scrollcheck-y"> <!-- 搜索按钮 --> <div class="search-btn ...">...</div> <!-- 原有导航项 --> <template v-for="...">...</template> <!-- 新增:好友模块 --> <BlogFriends :list="appConfig.friends" /> </nav>
注意事项
头像图片的选择
头像 URL 尽量用稳定的图床。推荐几个方案:
- Gravatar / Cravatar:根据邮箱哈希生成头像,比较通用
- GitHub 头像:
https://avatars.githubusercontent.com/用户名,GitHub 用户的首选 - QQ 头像:项目中已有
getOicqAvatar()工具函数可以直接用
避免使用对方网站根目录下的 favicon 作为头像,那种图片通常分辨率太低,显示效果不好。
数据量控制
侧边栏空间有限,好友条目建议控制在 5-10 个。如果好友很多,可以只放最常互动的几个,其余的引导到友链页面查看。
主题适配
样式里全部使用了项目定义的 CSS 变量(如 var(--c-bg-soft)、var(--c-text-2)),所以浅色和深色主题都能自动适配,不需要额外处理。
常见问题
组件没有显示出来
检查以下几点:
- 文件是否放在了
app/components/blog/目录下,文件名是否为BlogFriends.vue app.config.ts中的friends数组是否有数据- 侧边栏模板中是否正确添加了
<BlogFriends :list="appConfig.friends" />
头像加载失败显示空白
NuxtImg 在图片加载失败时不会显示占位符。可以给好友条目加一个 CSS 背景色作为兜底:
.friend-avatar {
background-color: var(--c-bg-soft);
}
这样即使图片加载失败,也会显示一个带背景色的圆形区域,不至于完全空白。
折叠动画不流畅
max-height 过渡的一个已知问题是:如果实际内容高度远小于设定的 max-height 值,收起动画会有延迟感。如果好友数量较少,可以把 max-height 从 500px 调小到 300px 左右,动画会更紧凑。
移动端显示异常
侧边栏在移动端是以抽屉形式弹出的,组件使用了 scoped 样式,不会和外部样式冲突。如果发现布局问题,检查一下 .friends-content 的 padding-inline-start 是否在窄屏下过大,可以加一个媒体查询调整:
@media (max-width: $breakpoint-mobile) {
.friends-content {
padding-inline-start: 0.5em;
}
}
总结
整个模块的搭建过程其实就是三步:写组件 → 配数据 → 接进去。核心工作量在组件的模板和样式上,逻辑部分非常轻量。关键是复用项目已有的 CSS 变量和图标体系,这样不用写很多代码就能和现有风格保持一致。
评论区
评论加载中...