📅 我们继续 50 个小项目挑战!—— TestimonialBoxSwitcher
组件
仓库地址:https://github.com/SunACong/50-vue-projects
项目预览地址:https://50-vue-projects.vercel.app/
使用 Vue 3 的组合式 API (<script setup>
) 和 Tailwind CSS 构建一个优雅、自动轮播的客户评价(Testimonial)展示组件。这个组件非常适合用在个人作品集、公司官网或产品页面,能够动态展示客户的好评,提升可信度和吸引力。
我们将实现一个带有平滑进度条指示器的自动轮播功能,当用户悬停时暂停,并在图片加载失败时优雅降级。让我们开始吧!🚀
🎯 应用目标
- 创建一个自动轮播的客户评价展示区域
- 展示每位客户的头像、姓名、职位和评价文本
- 添加一个从左到右流动的进度条,指示当前轮播状态
- 实现自动切换(10秒/条),并在用户悬停时暂停
- 处理头像图片加载失败的情况,显示默认占位图
- 使用 Vue 3 的响应式系统和生命周期钩子管理定时器
🔧 技术实现点
技术点 | 描述 |
---|---|
Vue 3 <script setup> |
使用 ref , computed , watch , onMounted , onUnmounted |
computed 计算属性 |
currentTestimonial 动态返回当前显示的评价 |
onMounted / onUnmounted |
组件挂载时启动定时器,卸载时清除定时器,防止内存泄漏 |
setInterval |
创建两个定时器:一个用于切换评价,一个用于更新进度条 |
watch |
监听 currentIndex 变化,重置进度条(虽然逻辑上由 updateTestimonial 重置,但可增强健壮性) |
@error 事件 |
图片加载失败时触发 handleImageError ,替换为默认头像 |
Tailwind CSS transition-all duration-10000 ease-linear |
为进度条 div 添加超长持续时间(10秒)的线性过渡,实现平滑流动效果 |
:style="{ width: progress + '%' }" |
动态绑定进度条宽度 |
📚 核心数据与状态
1. 评价数据 (testimonials
)
这是一个包含所有客户评价信息的数组,使用 ref
包裹使其响应式。每条评价包含:
name
: 客户姓名position
: 客户职位photo
: 头像图片 URLtext
: 评价文本
2. 响应式状态管理
状态变量 | 类型 | 初始值 | 作用 |
---|---|---|---|
currentIndex |
ref(Number) |
0 |
当前显示评价的索引 |
progress |
ref(Number) |
0 |
进度条的百分比 (0-100) |
intervalId |
let |
null |
存储评价切换定时器的 ID |
progressIntervalId |
let |
null |
存储进度条更新定时器的 ID |
3. 常量
常量 | 值 | 作用 |
---|---|---|
DEFAULT_AVATAR |
'https://via.placeholder.com/150?text=User' |
当头像图片加载失败时使用的默认占位图 URL |
🖌️ 组件实现
🎨 模板结构 <template>
<template>
<div
class="font-montserrat m-0 flex min-h-screen items-center justify-center overflow-hidden bg-gray-100 p-4">
<div
class="relative mx-auto w-full max-w-2xl rounded-lg bg-blue-600 p-8 text-white shadow-md">
<!-- 进度条 -->
<div
class="absolute top-0 left-0 h-1 w-full origin-left bg-white transition-all duration-10000 ease-linear"
:style="{ width: progress + '%' }"></div>
<!-- 引用图标 -->
<div class="absolute text-3xl text-white/30">
<i class="fas fa-quote-right absolute top-6 left-150"></i>
<i class="fas fa-quote-left absolute top-6 right-1"></i>
</div>
<!-- 推荐文本 -->
<p class="testimonial mb-6 pt-4 text-justify leading-relaxed">
{{ currentTestimonial.text }}
</p>
<!-- 用户信息 -->
<div class="user flex items-center justify-center">
<img
:src="currentTestimonial.photo"
:alt="currentTestimonial.name"
class="user-image mr-4 h-16 w-16 rounded-full object-cover"
@error="handleImageError" />
<div class="user-details text-left">
<h4 class="username text-lg font-bold">{{ currentTestimonial.name }}</h4>
<p class="role text-white/80">{{ currentTestimonial.position }}</p>
</div>
</div>
</div>
</div>
</template>
模板结构解析:
外层容器:
flex min-h-screen items-center justify-center
:使用 Flexbox 将评价卡片在视口中完美居中。bg-gray-100
:浅灰色背景,营造干净的氛围。font-montserrat
:使用 Montserrat 字体(需引入)。
评价卡片 (
div
):relative
:为内部的绝对定位元素(进度条、引用图标)提供定位上下文。bg-blue-600
:深蓝色背景,专业且醒目。text-white
:白色文字,确保在深色背景上清晰可读。rounded-lg
/shadow-md
:圆角和阴影,提升视觉层次。p-8
:内部留白。
核心元素:
- 进度条 (
div
):absolute top-0 left-0
:定位在卡片顶部。h-1
:高度为 1。w-full
:宽度为父容器的 100%。origin-left
:设置变换原点为左端,确保width
从左向右增长。transition-all duration-10000 ease-linear
:关键! 这个超长的duration-10000
(10秒) 过渡,配合 JavaScript 每 10ms 更新一次progress
,实现了非常平滑的线性流动效果,完美匹配 10秒的轮播周期。:style="{ width: progress + '%' }"
:动态控制宽度。
- 引用图标 (
i
):- 使用 Font Awesome 图标 (
fas fa-quote-right
,fas fa-quote-left
)。 absolute
定位,放置在卡片左右上角。text-white/30
:白色,30% 透明度,作为装饰性背景。
- 使用 Font Awesome 图标 (
- 评价文本 (
p
):text-justify
:文本两端对齐,看起来更整齐。leading-relaxed
:增加行高,提升可读性。pt-4
:顶部内边距,避免与进度条重叠。
- 用户信息 (
div
):flex items-center justify-center
:水平居中用户头像和信息。img
使用object-cover
确保图片填充容器且不被拉伸。@error="handleImageError"
:监听图片加载错误。
- 进度条 (
💻 脚本逻辑 <script setup>
<script setup>
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
// 默认头像 - 当图片加载失败时使用
const DEFAULT_AVATAR = 'https://via.placeholder.com/150?text=User'
// 推荐语数据
const testimonials = ref([
// ... 评价数据
])
// 当前推荐语索引
const currentIndex = ref(0)
// 进度条百分比
const progress = ref(0)
// 定时器ID
let intervalId = null
let progressIntervalId = null
// 当前推荐语
const currentTestimonial = computed(() => testimonials.value[currentIndex.value])
// 处理图片加载错误
const handleImageError = (e) => {
e.target.src = DEFAULT_AVATAR
}
// 更新推荐语
const updateTestimonial = () => {
currentIndex.value = (currentIndex.value + 1) % testimonials.value.length
progress.value = 0 // 重置进度条
}
// 启动定时器
const startTimer = () => {
// 10秒切换一次推荐语
intervalId = setInterval(updateTestimonial, 10000)
// 更新进度条
progressIntervalId = setInterval(() => {
progress.value += 0.1 // 每10ms增加0.1%,10秒 = 10000ms,10000/10 = 1000次,100/1000 = 0.1
if (progress.value >= 100) progress.value = 100
}, 10)
}
// 组件挂载时启动定时器
onMounted(startTimer)
// 组件卸载时清除定时器
onUnmounted(() => {
clearInterval(intervalId)
clearInterval(progressIntervalId)
})
// 当currentIndex变化时重置进度条动画
// (虽然 updateTestimonial 已重置,但此 watch 可确保万无一失)
watch(currentIndex, () => {
progress.value = 0
})
</script>
脚本逻辑详解:
startTimer
方法:- 创建两个独立的
setInterval
:intervalId
:每 10000 毫秒(10秒)调用一次updateTestimonial
,切换到下一条评价。progressIntervalId
:每 10 毫秒将progress
增加0.1
。10秒内总共增加10000ms / 10ms * 0.1% = 100%
,完美匹配轮播周期。
- 使用两个定时器是为了获得比单个
10000ms
定时器更平滑的进度条动画(1000次更新 vs 1次更新)。
- 创建两个独立的
updateTestimonial
方法:- 使用模运算
(currentIndex.value + 1) % testimonials.value.length
实现索引的循环递增。 - 关键一步:在切换评价后立即将
progress.value
重置为0
,为下一次进度条动画做准备。
- 使用模运算
生命周期钩子:
onMounted(startTimer)
:组件挂载后立即启动轮播。onUnmounted
:至关重要!在组件卸载(如页面跳转)时清除所有定时器,防止内存泄漏和不必要的后台运行。
watch
监听器:- 虽然
updateTestimonial
已经重置了progress
,但添加watch
可以确保在任何情况下currentIndex
变化时进度条都会被重置,增加了代码的健壮性。
- 虽然
handleImageError
方法:- 当
<img>
的src
加载失败时触发。 e.target.src = DEFAULT_AVATAR
:将出错的img
元素的src
替换为预设的默认头像,保证 UI 不会因图片 404 而出现破图。
- 当
🎨 Tailwind CSS 样式重点
类名 | 作用 |
---|---|
font-montserrat |
使用 Montserrat 字体 |
m-0 / p-4 / p-8 / pt-4 / mb-6 / mr-4 |
外边距和内边距 |
flex / items-center / justify-center / justify-between |
Flexbox 布局 |
min-h-screen |
最小高度为视口高度 |
overflow-hidden |
隐藏溢出内容 |
bg-gray-100 / bg-blue-600 |
背景颜色 |
w-full / max-w-2xl / mx-auto |
宽度和居中 |
overflow-hidden |
隐藏溢出 |
rounded-lg |
圆角 |
text-white / text-white/30 / text-white/80 / text-lg / text-3xl |
文字颜色、透明度和大小 |
font-bold / font-medium |
字体粗细 |
shadow-md |
阴影 |
transition-all duration-10000 ease-linear |
核心! 为进度条提供10秒的平滑线性过渡 |
h-1 / h-16 / w-16 / w-full |
高度和宽度 |
origin-left |
设置变换原点为左端 |
absolute / relative / top-0 / left-0 / top-6 / left-150 / right-1 |
定位 |
text-justify / leading-relaxed |
文本对齐和行高 |
rounded-full |
圆形(头像) |
object-cover |
图片填充容器且不拉伸 |
cursor-pointer |
(可选)如果添加了手动切换按钮 |
⚠️ 注意事项与潜在问题
- Font Awesome 图标:模板中使用了
<i class="fas fa-quote-right">
,这需要在项目中正确引入 Font Awesome CSS 文件,否则图标不会显示。可以考虑使用 SVG 图标或 Tailwind 的@apply
定义图标类来替代。 left-150
类:Tailwind 默认没有left-150
这个类(除非自定义了 theme.spacing)。可能需要使用left-[600px]
(如果需要 600px)或一个存在的类如left-10
。- 性能:
progressIntervalId
每 10ms 触发一次,虽然对于现代浏览器来说通常不是问题,但在低端设备上可能有轻微影响。可以考虑增加间隔(如 50ms 或 100ms)并相应调整progress
的增加值。 - 暂停功能:当前代码没有实现“鼠标悬停暂停”的功能。可以通过在卡片上添加
@mouseenter
和@mouseleave
事件监听器,在悬停时clearInterval
并在离开时重新setInterval
来实现。
📁 常量定义 + 组件路由
constants/index.js
添加组件预览常量:
{
id: 47,
title: 'TestimonialBoxSwitcher',
image: 'https://50projects50days.com/img/projects-img/47-testimonial-box-switcher.png',
link: 'TestimonialBoxSwitcher',
},
router/index.js
中添加路由选项:
{
path: '/TestimonialBoxSwitcher',
name: 'TestimonialBoxSwitcher',
component: () => import('@/projects/TestimonialBoxSwitcher.vue'),
},
🏁 总结
我们成功构建了一个功能完善、视觉效果出色的自动轮播客户评价组件。通过巧妙地结合 Vue 3 的响应式系统、生命周期钩子和 Tailwind CSS 的实用类(特别是 transition-all duration-10000
的妙用),我们实现了流畅的用户体验。
你还可以扩展以下功能:
- ✅ 添加手动控制:增加“上一条”/“下一条”按钮,让用户可以手动切换。
- ✅ 指示点:在卡片底部添加小圆点,指示当前是第几条评价,并可点击跳转。
- ✅ 悬停暂停:如上所述,实现鼠标悬停时暂停轮播和进度条。
- ✅ 动画效果:为评价文本的切换添加淡入淡出或滑动动画。
- ✅ 响应式调整:在小屏幕上调整字体大小、间距和头像尺寸。
- ✅ 更多数据:从 API 动态加载评价数据。
- ✅ 评分星级:在用户信息旁添加评分星级(如 5颗星)。
👉 下一篇,我们将完成RandomImageGenerator
组件,一个随机图片生成器组件。🚀
感谢阅读,欢迎点赞、收藏和分享 😊