MarqueeReview (评价Reviews-滚动展示)
评价内容滚动展示组件,支持无限循环和自动播放【✅ 已发布】
功能特性
MarqueeReview 是一个垂直滚动的评论轮播组件,专为展示客户评价和用户反馈而设计,提供流畅的无限循环滚动动画和灵活的响应式布局。
- ✅ 垂直无限滚动:评论卡片从下往上无缝循环滚动,创造流动的视觉效果
- ✅ 多列响应式布局:移动端 1 列、平板 2 列、桌面 3 列自适应布局
- ✅ 星级评分展示:支持 1-5 星评分系统,填充/未填充图标自动渲染
- ✅ 用户头像集成:Avatar 组件集成,支持图片和首字母回退显示
- ✅ 渐变边缘遮罩:顶部/底部自动淡出效果 (
mask-fade-vertical),提升视觉层次 - ✅ 智能自动补齐:少于 9 条评论时自动复制填充,确保布局完整
- ✅ 错位动画效果:列 1 和列 3 带 40px 垂直偏移,增强视觉动感
- ✅ 长文本截断:评论内容
line-clamp-5限制最多 5 行显示 - ✅ 主题切换支持:light/dark 主题无缝切换 (
aiui-dark类) - ✅ 响应式高度调整:480px → 384px (laptop) → 512px (desktop) → 640px (lg-desktop)
Props 参数
MarqueeReviewProps
| Prop | 类型 | 默认值 | 必需 | 说明 |
|---|---|---|---|---|
data | MarqueeReviewData | - | ✅ | 评论数据对象,包含标题、副标题和评论列表 |
className | string | - | - | 自定义 CSS 类名,追加到根容器以实现额外样式定制 |
MarqueeReviewData
| 字段 | 类型 | 默认值 | 必需 | 说明 |
|---|---|---|---|---|
title | string | - | ✅ | 主标题文本,建议 20 字以内 |
subtitle | string | - | ✅ | 副标题/描述文本,建议 50 字以内 |
items | ReviewItem[] | [] | ✅ | 评论列表数组,建议至少 6 条以获得最佳显示效果 |
theme | 'light' | 'dark' | 'light' | - | 主题模式,影响文本颜色和卡片背景色 |
ReviewItem
| 字段 | 类型 | 默认值 | 必需 | 说明 |
|---|---|---|---|---|
name | string | - | ✅ | 评论者姓名,建议 20 字以内 |
avatar | Media | - | ✅ | 评论者头像对象,包含 url 和 alt 属性 ({ url: string, alt: string, thumbnailURL?: string }) |
comment | string | - | ✅ | 评论内容文本,最多显示 5 行,超出部分自动截断 (line-clamp-5) |
rating | number | 5 | - | 评分星级,取值范围 1-5 的整数 (默认 5 星满分) |
类型定义
import type { Media, Theme } from '../../types/props.js'
/**
* 评论项数据结构
*/
export interface ReviewItem {
/** 评论者名称 */
name: string
/** 评论者头像 (Media 对象: { url, alt, thumbnailURL? }) */
avatar: Media
/** 评论内容 */
comment: string
/** 评分星级 (1-5星) */
rating?: number
}
/**
* 轮播评论数据结构
*/
export interface MarqueeReviewData {
/** 主标题 */
title: string
/** 副标题/描述 */
subtitle: string
/** 评论列表 */
items: ReviewItem[]
/** 主题模式: 'light' 或 'dark' */
theme?: Theme
}
/**
* MarqueeReview 组件 Props
*/
export interface MarqueeReviewProps {
/** 评论数据 */
data: MarqueeReviewData
/** 自定义类名 */
className?: string
}使用示例
示例 1: 基础评论轮播
最简单的使用方式,展示客户评价列表。
import { MarqueeReview } from '@anker-in/headless-ui/biz'
const reviews = [
{
name: 'Sarah Johnson',
avatar: {
url: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&q=80',
alt: 'Sarah Johnson avatar',
},
comment: 'Absolutely love this product! It has completely transformed my daily routine.',
rating: 5,
},
{
name: 'Michael Chen',
avatar: {
url: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&q=80',
alt: 'Michael Chen avatar',
},
comment: 'Excellent quality and great value for money. Highly recommend!',
rating: 5,
},
// ... 更多评论 (建议至少 6-9 条)
]
export default function CustomerReviews() {
return (
<MarqueeReview
data={{
title: 'Customer Reviews',
subtitle: 'See what our customers are saying',
items: reviews,
}}
/>
)
}示例 2: 少量评论自动补齐
当评论少于 9 条时,组件会自动复制填充以确保布局完整。
// 即使只有 3 条评论,组件也会自动复制填充到 9 条
const earlyReviews = [
{
name: 'Early Adopter 1',
avatar: {
url: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150',
alt: 'User 1',
},
comment: 'Love it! Great product for early adopters.',
rating: 5,
},
{
name: 'Early Adopter 2',
avatar: {
url: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=150',
alt: 'User 2',
},
comment: 'Impressive features and solid build quality.',
rating: 5,
},
{
name: 'Early Adopter 3',
avatar: {
url: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=150',
alt: 'User 3',
},
comment: 'Highly recommend to anyone looking for a reliable solution!',
rating: 4,
},
]
<MarqueeReview
data={{
title: 'Early Reviews',
subtitle: 'What our first customers are saying',
items: earlyReviews, // 组件内部会自动复制填充
}}
/>示例 3: 混合评分展示
展示不同星级的评价,增加真实性和可信度。
const mixedRatings = [
{
name: 'John Doe',
avatar: {
url: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=150',
alt: 'John Doe',
},
comment: 'Perfect product! Could not be happier with this purchase.',
rating: 5, // 5 颗星全部填充
},
{
name: 'Jane Smith',
avatar: {
url: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150',
alt: 'Jane Smith',
},
comment: 'Very good quality. Minor improvements could be made to the packaging.',
rating: 4, // 4 颗星填充,1 颗未填充
},
{
name: 'Bob Wilson',
avatar: {
url: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150',
alt: 'Bob Wilson',
},
comment: 'Good product for the price. Does exactly what it promises.',
rating: 3, // 3 颗星填充,2 颗未填充
},
]
<MarqueeReview
data={{
title: 'Customer Feedback',
subtitle: 'Real reviews from real customers',
items: mixedRatings,
}}
/>示例 4: 深色主题
适用于深色背景的页面,提供更好的视觉对比。
<MarqueeReview
data={{
title: 'User Testimonials',
subtitle: 'Trusted by thousands worldwide',
items: reviews,
theme: 'dark', // 启用深色主题
}}
className="bg-gray-900"
/>示例 5: 长评论内容
长文本评论会自动截断为 5 行,并添加省略号。
const detailedReviews = [
{
name: 'Alexandra Mitchell',
avatar: {
url: 'https://images.unsplash.com/photo-1487412720507-e7ab37603c6f?w=150',
alt: 'Alexandra Mitchell',
},
comment:
'I have been using this product for several months now and I must say it has completely exceeded all of my expectations. The quality is outstanding, the attention to detail is remarkable, and every aspect has been carefully thought through. The customer service team was also incredibly helpful when I had questions about the features. I would absolutely recommend this to anyone looking for a high-quality solution.', // 长文本会自动截断为 5 行
rating: 5,
},
]
<MarqueeReview
data={{
title: 'Detailed Reviews',
subtitle: 'In-depth feedback from our customers',
items: detailedReviews,
}}
/>示例 6: 自定义样式
添加自定义 CSS 类来调整组件外观。
<MarqueeReview
data={{
title: 'Featured Reviews',
subtitle: 'Hand-picked customer favorites',
items: reviews,
}}
className="border-t-4 border-orange-500 py-16 bg-gradient-to-b from-blue-50 to-white"
/>示例 7: 产品页评价墙
适用于电商产品详情页的社会证明 (Social Proof)。
const productReviews = [
{
name: 'Emily Thompson',
avatar: {
url: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150',
alt: 'Emily Thompson',
},
comment: 'This wireless charger is fantastic! Fast charging and sleek design.',
rating: 5,
},
// ... 更多产品评价
]
<MarqueeReview
data={{
title: 'What Our Customers Say',
subtitle: 'Over 10,000+ 5-star reviews',
items: productReviews,
theme: 'light',
}}
/>示例 8: 服务评价展示
适用于服务类业务的客户反馈展示。
const serviceReviews = [
{
name: 'David Chen',
avatar: {
url: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150',
alt: 'David Chen',
},
comment:
'Outstanding customer service! The team went above and beyond to help me with my project.',
rating: 5,
},
// ... 更多服务评价
]
<MarqueeReview
data={{
title: 'Client Testimonials',
subtitle: 'Why clients choose our services',
items: serviceReviews,
}}
/>示例 9: 品牌信任展示
用于首页或落地页建立品牌信任感。
<section className="py-20 bg-gray-50">
<div className="container mx-auto px-4">
<MarqueeReview
data={{
title: 'Loved by Users Worldwide',
subtitle: 'Join thousands of satisfied customers',
items: globalReviews,
}}
/>
</div>
</section>示例 10: 响应式容器集成
在不同容器尺寸下使用组件。
<div className="grid laptop:grid-cols-2 gap-8">
{/* 左侧产品信息 */}
<div className="product-details">
<h2>Product Information</h2>
{/* ... 产品详情 */}
</div>
{/* 右侧评价轮播 */}
<div className="reviews-section">
<MarqueeReview
data={{
title: 'Customer Reviews',
subtitle: 'Real feedback from real users',
items: reviews,
}}
/>
</div>
</div>滚动动画机制
列数据处理逻辑
MarqueeReview 使用智能的数据分组和复制机制来实现无缝循环滚动。
/**
* 步骤 1: 自动补齐逻辑
* 如果评论少于 9 条,通过复制现有评论填充到 9 条
* 时间复杂度: O(n), n 为 items.length
*/
const filledItems = items.length < 9
? [...items, ...items.slice(0, 9 - items.length)]
: items
// 示例:
// 输入: [A, B, C] (3 条)
// 输出: [A, B, C, A, B, C, A, B, C] (9 条)
/**
* 步骤 2: 分组逻辑
* 将 9 条评论分为 3 组,每组 3 条
* 使用 lodash 的 _chunk 函数
*/
import _chunk from 'lodash/chunk'
const chunkItems = _chunk(filledItems, 3) // [[A,B,C], [D,E,F], [G,H,I]]
.slice(0, 3) // 只取前 3 组
/**
* 步骤 3: 双倍复制
* 每组内容复制一遍,实现无缝循环
* 当滚动到一半时,会无缝切换到开头
*/
const scrollableColumns = chunkItems.map(group => [...group, ...group])
// 结果:
// [
// [A, B, C, A, B, C], // 列 0
// [D, E, F, D, E, F], // 列 1
// [G, H, I, G, H, I], // 列 2
// ]列响应式显示逻辑
{chunkItems.map((items, index) => (
<div
key={index}
className={cn('animate-marquee-scroll', {
'top-10': index === 0 || index === 2, // 列 0 和 2 带 40px 偏移
'hidden desktop:flex': index === 1, // 列 1 仅桌面 (≥1440px) 显示
'hidden tablet:flex': index === 2, // 列 2 平板及以上 (≥768px) 显示
})}
>
{items.map((item, innerIndex) => (
<Card key={innerIndex} item={item} />
))}
</div>
))}列显示规则表格
| 列索引 | Mobile (<768px) | Tablet (≥768px) | Desktop (≥1440px) | 垂直偏移量 | 说明 |
|---|---|---|---|---|---|
| 0 | ✅ 显示 | ✅ 显示 | ✅ 显示 | top-10 (40px) | 主列,始终显示 |
| 1 | ❌ 隐藏 | ❌ 隐藏 | ✅ 显示 | 无偏移 | 仅桌面显示,居中对齐 |
| 2 | ❌ 隐藏 | ✅ 显示 | ✅ 显示 | top-10 (40px) | 平板及以上显示 |
错位动画说明:
- 列 0 和列 2 添加
top-10(40px) 垂直偏移 - 列 1 无偏移,位于正常位置
- 这种错位布局创造了更有活力的视觉效果
响应式行为
容器高度
组件高度根据不同断点自适应调整,确保在各种设备上都有最佳显示效果。
| 断点 | 屏幕宽度 | 高度 | Tailwind 类 | 说明 |
|---|---|---|---|---|
| Mobile | < 768px | 480px | (默认) | 移动端较高,显示更多评论内容 |
| Laptop | ≥ 1025px | 384px | laptop:h-[384px] | 笔记本适中高度,节省垂直空间 |
| Desktop | ≥ 1440px | 512px | desktop:h-[512px] | 桌面增大高度,展示更多信息 |
| LG Desktop | ≥ 1920px | 640px | lg-desktop:h-[640px] | 超大屏幕最大高度,充分利用空间 |
卡片尺寸
评论卡片的尺寸和间距随断点变化,保持最佳可读性。
| 断点 | 卡片高度 | 内边距 | 头像尺寸 | 姓名字体 | 评论字体 |
|---|---|---|---|---|---|
| Mobile (< 768px) | 240px | 16px (p-4) | 40px | Text size 2 | Heading size 2 |
| Laptop+ (≥ 1025px) | 240px | 24px (p-6) | 48px | Text size 4 | Heading size 2 |
标题文本
主标题和副标题的排版随断点变化,优化不同设备的阅读体验。
{/* 主标题 (title) */}
<Heading
as="h1"
size={4}
html={title}
className="laptop:text-center w-full text-left"
/>
// Mobile: 左对齐
// Laptop+: 居中对齐
{/* 副标题 (subtitle) */}
<Text
size={2}
html={subtitle}
className="
tablet:mt-[16px] tablet:text-[14px]
laptop:text-center laptop:text-[14px]
desktop:mt-[8px] desktop:text-[16px]
lg-desktop:text-[18px]
mt-[4px] text-[14px]
"
/>响应式文本尺寸表:
| 断点 | 主标题对齐 | 副标题尺寸 | 副标题上边距 |
|---|---|---|---|
| Mobile (< 768px) | 左对齐 | 14px | 4px |
| Tablet (≥ 768px) | 左对齐 | 14px | 16px |
| Laptop (≥ 1025px) | 居中对齐 | 14px | 16px |
| Desktop (≥ 1440px) | 居中对齐 | 16px | 8px |
| LG Desktop (≥ 1920px) | 居中对齐 | 18px | 8px |
核心算法解析
1. 自动补齐算法
功能: 当评论少于 9 条时,自动复制填充以确保 3 列布局均匀分布。
/**
* 自动补齐算法
* @param items - 原始评论列表
* @returns 填充后的评论列表 (至少 9 条)
* @complexity 时间复杂度 O(n), 空间复杂度 O(1)
*/
function fillReviews(items: ReviewItem[]): ReviewItem[] {
if (items.length >= 9) {
return items // 已有 9 条或更多,无需填充
}
const needed = 9 - items.length // 需要填充的数量
const filled = [...items, ...items.slice(0, needed)]
return filled
}
// 示例演示:
// 输入 3 条: [A, B, C]
// 需要填充: 9 - 3 = 6 条
// slice(0, 6): [A, B, C, A, B, C]
// 结果: [A, B, C, A, B, C, A, B, C]
// 输入 5 条: [A, B, C, D, E]
// 需要填充: 9 - 5 = 4 条
// slice(0, 4): [A, B, C, D]
// 结果: [A, B, C, D, E, A, B, C, D]为什么是 9 条?
- 3 列布局,每列 3 条评论
- 双倍复制后每列 6 条 (3 + 3)
- 总共需要 9 条评论 (3 列 × 3 条/列)
2. 列分组算法
功能: 将评论列表分为 3 组,每组分配给一列显示。
import _chunk from 'lodash/chunk'
/**
* 列分组算法
* @param items - 填充后的评论列表 (9 条)
* @returns 3 组评论,每组 3 条
* @complexity 时间复杂度 O(n), 空间复杂度 O(n)
*/
function groupByColumns(items: ReviewItem[]): ReviewItem[][] {
return _chunk(items, 3).slice(0, 3)
}
// lodash chunk 实现原理:
function chunk<T>(array: T[], size: number): T[][] {
const result: T[][] = []
for (let i = 0; i < array.length; i += size) {
result.push(array.slice(i, i + size))
}
return result
}
// 示例:
// 输入: [A, B, C, D, E, F, G, H, I]
// chunk(items, 3): [[A,B,C], [D,E,F], [G,H,I]]
// slice(0, 3): [[A,B,C], [D,E,F], [G,H,I]] (取前 3 组)3. 双倍复制算法
功能: 每组评论复制一遍,实现无缝循环滚动。
/**
* 双倍复制算法
* @param columnGroups - 3 组评论
* @returns 每组评论双倍复制后的结果
* @complexity 时间复杂度 O(n), 空间复杂度 O(n)
*/
function duplicateForScroll(
columnGroups: ReviewItem[][]
): ReviewItem[][] {
return columnGroups.map(group => [...group, ...group])
}
// 示例:
// 输入: [[A,B,C], [D,E,F], [G,H,I]]
// 输出: [
// [A, B, C, A, B, C], // 列 0
// [D, E, F, D, E, F], // 列 1
// [G, H, I, G, H, I], // 列 2
// ]
// 滚动原理:
// 1. CSS 动画将列从 translateY(0) 移动到 translateY(-50%)
// 2. 当滚动到 -50% 时,第二组 [A,B,C] 正好出现在视口中
// 3. 动画无缝循环,用户感觉是无限滚动4. 响应式列显示算法
功能: 根据屏幕尺寸动态显示不同数量的列。
/**
* 响应式列显示逻辑
* @param columnIndex - 列索引 (0, 1, 2)
* @returns CSS 类名字符串
*/
function getColumnClasses(columnIndex: number): string {
const classes = [
'animate-marquee-scroll', // 滚动动画
'relative',
'flex',
'h-fit',
'flex-1',
'flex-col',
'gap-4',
]
// 列 0: 始终显示,带偏移
if (columnIndex === 0) {
classes.push('top-10') // 40px 垂直偏移
}
// 列 1: 仅桌面 (≥1440px) 显示,无偏移
if (columnIndex === 1) {
classes.push('hidden', 'desktop:flex')
}
// 列 2: 平板及以上 (≥768px) 显示,带偏移
if (columnIndex === 2) {
classes.push('hidden', 'tablet:flex', 'top-10')
}
return classes.join(' ')
}
// 列显示矩阵:
// ┌────────┬────────┬────────┬─────────┐
// │ 列 │ Mobile │ Tablet │ Desktop │
// ├────────┼────────┼────────┼─────────┤
// │ 列 0 │ ✅ │ ✅ │ ✅ │
// │ 列 1 │ ❌ │ ❌ │ ✅ │
// │ 列 2 │ ❌ │ ✅ │ ✅ │
// └────────┴────────┴────────┴─────────┘5. 星级评分渲染算法
功能: 根据 rating 数值渲染对应数量的填充/未填充星星图标。
/**
* 星级评分渲染算法
* @param rating - 评分值 (1-5)
* @returns 5 个星星图标 (JSX 元素)
*/
function renderStars(rating: number = 5): JSX.Element[] {
const stars: JSX.Element[] = []
const clampedRating = Math.max(1, Math.min(5, rating)) // 限制在 1-5 范围
for (let i = 1; i <= 5; i++) {
const isFilled = i <= clampedRating
stars.push(
isFilled ? (
<StarIcon key={i} /> // 填充星 (#F77234 橙色)
) : (
<UnStarIcon key={i} /> // 未填充星 (#75787F 灰色)
)
)
}
return stars
}
// 示例:
// rating = 5: ★★★★★ (5 个填充星)
// rating = 4: ★★★★☆ (4 个填充星 + 1 个未填充星)
// rating = 3: ★★★☆☆ (3 个填充星 + 2 个未填充星)
// rating = 1: ★☆☆☆☆ (1 个填充星 + 4 个未填充星)6. 渐变遮罩算法
功能: 在容器顶部和底部添加渐变淡出效果,提升视觉层次感。
/* Tailwind CSS 自定义工具类: mask-fade-vertical */
.mask-fade-vertical {
mask-image: linear-gradient(
to bottom,
transparent 0%, /* 顶部完全透明 */
black 10%, /* 10% 处完全不透明 */
black 90%, /* 90% 处完全不透明 */
transparent 100% /* 底部完全透明 */
);
-webkit-mask-image: linear-gradient(
to bottom,
transparent 0%,
black 10%,
black 90%,
transparent 100%
);
}
/**
* 遮罩计算原理:
* - 0-10%: 从透明渐变到不透明 (顶部淡入)
* - 10-90%: 完全不透明 (中间区域正常显示)
* - 90-100%: 从不透明渐变到透明 (底部淡出)
*
* 视觉效果:
* ┌──────────────────┐
* │ ░░░░░░░░░░░░░░░░ │ ← 顶部淡入 (0-10%)
* │ ████████████████ │
* │ ████████████████ │ ← 中间正常 (10-90%)
* │ ████████████████ │
* │ ░░░░░░░░░░░░░░░░ │ ← 底部淡出 (90-100%)
* └──────────────────┘
*/7. 无限滚动动画算法
功能: 实现流畅的垂直无限滚动效果。
/* Keyframes 定义 */
@keyframes marquee-scroll {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-50%); /* 移动到一半位置 */
}
}
/* 动画应用 */
.animate-marquee-scroll {
animation: marquee-scroll 30s linear infinite;
will-change: transform; /* GPU 加速提示 */
}
/**
* 无限滚动原理:
*
* 1. 初始状态 (0%):
* ┌──────────┐ ← 视口顶部
* │ [A B C] │ ← 第一组评论 (可见)
* │ [A B C] │ ← 第二组评论 (可见)
* └──────────┘ ← 视口底部
*
* 2. 中间状态 (50%):
* ┌──────────┐ ← 视口顶部
* │ [A B C] │ ← 第二组评论 (可见)
* └──────────┘ ← 视口底部
* 原第一组 [A B C] 已移出视口顶部
*
* 3. 结束状态 (100% = 0%):
* 由于 transform: translateY(-50%),
* 第二组 [A B C] 的位置与初始第一组完全相同!
* 视觉上实现了无缝循环。
*
* 关键点:
* - 双倍复制保证有足够内容滚动
* - translateY(-50%) 正好移动一组内容的高度
* - linear infinite 确保匀速循环
* - 30s 持续时间可通过 CSS 自定义调整
*/
/**
* 性能优化:
* 1. 使用 transform 而非 top/bottom (GPU 加速)
* 2. will-change: transform 提前告知浏览器
* 3. linear timing 避免额外的缓动计算
*/设计规范
使用建议
1. 评论数量
推荐数量:
- 最佳: 6-9 条评论
- 3 列布局,每列 2-3 条评论
- 双倍复制后每列 4-6 条,滚动流畅不重复感明显
- 最少: 3 条评论
- 组件会自动复制填充到 9 条
- 注意: 过少的评论会导致明显的重复
- 最多: 无限制
- 超过 18 条后建议精选高质量评论
- 过多评论会增加加载时间和内存占用
2. 评论内容
长度建议:
- 推荐: 50-150 字符 (约 1-3 行)
- 简洁有力,突出核心评价
- 用户阅读负担小
- 最短: 20 字符以上
- 避免过短的评论如 “Good!”、”👍” 等
- 最长: 自动截断为 5 行
- 使用
line-clamp-5CSS 类 - 超出部分显示省略号 (…)
- 使用
格式要求:
- ✅ 使用纯文本,避免 HTML 标签
- ✅ 自然的语言表达,真实的用户反馈
- ❌ 避免过度使用表情符号
- ❌ 避免全大写文本 (ALL CAPS)
3. 头像图片
图片规格:
- 尺寸: 建议 150x150px 或更大
- 组件会自动缩放到 40px (移动端) 或 48px (桌面端)
- 高分辨率图片在 Retina 屏幕上显示更清晰
- 格式: JPEG, PNG, WebP
- 优先使用 WebP 格式 (文件更小,质量更好)
- 避免使用 GIF 动图 (影响性能)
- 优化: 压缩到 20KB 以下
- 使用图片压缩工具 (TinyPNG, ImageOptim 等)
回退方案:
- 若图片加载失败,Avatar 组件会显示用户名前 2 个字符
- 示例: “张伟” → “张伟”, “John Doe” → “JO”
4. 评分星级
星级设置:
- 默认: 5 星 (未设置
rating时) - 范围: 1-5 星,仅支持整数
- 视觉:
- 填充星: 橙色
#F77234(Anker 品牌色) - 未填充星: 灰色
#75787F
- 填充星: 橙色
真实性建议:
- ✅ 展示混合评分 (4-5 星为主,适当展示 3-4 星)
- ✅ 避免全部 5 星 (会降低可信度)
- ❌ 避免展示 1-2 星差评 (负面影响)
最佳实践
✅ 推荐做法
-
优先展示高质量评论
- 5 星评价和详细的长文本反馈
- 突出产品核心卖点和用户痛点
-
混合评分增加真实性
- 80% 五星好评 + 20% 四星好评
- 避免清一色满分评价
-
多样化用户名
- 中英文名字混合
- 避免所有名字格式相同 (如全是 “User 1”, “User 2”)
-
真实头像优先
- 使用真实用户照片 (需获得授权)
- 若无法获取,使用高质量的头像占位图
-
定期更新评论内容
- 展示最新的客户反馈
- 保持内容新鲜度
-
适配品牌主题
- Light 主题适用于白色/浅色背景
- Dark 主题适用于深色背景
❌ 避免做法
-
虚假评价
- 所有评论应基于真实用户反馈
- 避免编造虚假好评
-
过短评论
- 如 “Good!”、“Nice!”、”👍” 等单词评价
- 缺乏信息量,降低可信度
-
重复内容
- 确保评论列表中无重复文本
- 即使自动填充,也应提供足够的原始评论
-
过度设计
- 避免过多自定义样式干扰用户注意力
- 保持简洁清晰的视觉风格
无障碍性
ARIA 属性
MarqueeReview 组件通过 Avatar 子组件自动提供基础的 ARIA 支持:
{/* Avatar 组件自动包含 ARIA 支持 */}
<Avatar size="small">
<AvatarImage
src={item.avatar?.url}
alt={item.avatar?.alt || ''} // 必需的 alt 文本
/>
<AvatarFallback>
{(item.name ?? '').slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>建议增强 ARIA 支持
当前组件可进一步增强无障碍性,建议添加以下属性:
{/* 为根容器添加语义化角色 */}
<section
role="region"
aria-label={title}
aria-describedby="review-subtitle"
className={cn('marquee-review-container', className)}
>
<Heading as="h1" size={4} html={title} />
<Text
id="review-subtitle"
size={2}
html={subtitle}
/>
{/* 为滚动容器添加 aria-live */}
<div
className="mask-fade-vertical ..."
aria-live="polite" // 通知屏幕阅读器内容变化
aria-atomic="false" // 仅读取变化部分
>
{/* 评论列 */}
</div>
</section>ARIA 属性说明:
role="region": 标识这是一个独立的页面区域aria-label: 为区域提供描述性标签aria-describedby: 关联副标题作为详细描述aria-live="polite": 通知屏幕阅读器内容动态变化 (不打断当前阅读)aria-atomic="false": 仅读取变化的部分,而非整个区域
键盘导航
当前状态: ⚠️ 纯展示组件,不支持键盘交互
- 评论卡片不可聚焦 (无
tabindex) - 无法通过 Tab 键导航
- 无法通过方向键滚动
如需添加交互:
如果需要让评论卡片可点击 (如查看详情、跳转链接),请使用语义化标签:
const Card = ({ item, onClick }: CardProps & { onClick?: () => void }) => {
return (
<button
onClick={onClick}
className="rounded-box bg-container-primary ... cursor-pointer hover:shadow-lg transition-shadow focus:outline-none focus:ring-2 focus:ring-primary"
aria-label={`Review by ${item.name}, ${item.rating} stars`}
>
{/* 卡片内容 */}
</button>
)
}键盘操作建议:
Tab/Shift+Tab: 在可聚焦卡片间导航Enter/Space: 激活选中的卡片Esc: 关闭详情视图 (如有)
屏幕阅读器
已支持:
-
✅ 头像 alt 文本: 每个头像必须提供
alt属性描述avatar: { url: "...", alt: "Sarah Johnson avatar" // 清晰描述头像内容 } -
✅ 语义化组件: 使用
<Heading>和<Text>组件- 自动生成正确的 HTML 语义标签
- 屏幕阅读器能正确识别标题和内容层级
待改进:
- ⚠️ 星级评分: 当前为纯图标,屏幕阅读器无法获取评分信息
建议改进:
{/* 为星级评分添加可访问性标签 */}
<div
className="flex items-center gap-2"
role="img"
aria-label={`Rating: ${item.rating} out of 5 stars`}
>
{renderStars(item.rating)}
</div>屏幕阅读器读取示例:
- “Region: Customer Reviews”
- “Heading: Customer Reviews”
- “Text: See what our customers are saying”
- “Image: Sarah Johnson avatar”
- “Heading: Sarah Johnson”
- “Rating: 5 out of 5 stars”
- “Heading: Absolutely love this product…”
性能优化
1. React 组件优化
使用 React.memo() 防止不必要的重渲染。
import React, { memo } from 'react'
/**
* 卡片组件优化
* 使用 memo 包裹,仅在 props 变化时重渲染
*/
const Card = memo(({ item }: CardProps) => {
return (
<div className="rounded-box bg-container-primary ...">
<div className="flex items-center gap-2">
<Avatar size="small">
<AvatarImage
src={item.avatar?.url}
alt={item.avatar?.alt || ''}
/>
<AvatarFallback>
{(item.name ?? '').slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<Text size={4} html={item.name} />
<div className="flex items-center gap-2">
{renderStars(item.rating)}
</div>
</div>
</div>
<Heading
as="h6"
size={2}
html={item.comment}
className="line-clamp-5 min-h-[120px] text-wrap"
/>
</div>
)
})
Card.displayName = 'ReviewCard'
/**
* 性能提升:
* - 父组件重渲染时,Card 组件不会重渲染 (除非 item prop 变化)
* - 减少 Virtual DOM diff 计算
* - 特别适用于列表渲染场景
*/2. 图片懒加载
为头像图片添加原生懒加载属性,减少初始加载时间。
<AvatarImage
src={item.avatar?.url}
alt={item.avatar?.alt || ''}
loading="lazy" // 原生懒加载
decoding="async" // 异步解码,不阻塞渲染
/>
/**
* 懒加载原理:
* 1. 浏览器仅加载视口内及附近的图片
* 2. 用户滚动时才加载即将进入视口的图片
* 3. 减少初始页面加载时间和带宽消耗
*
* 浏览器支持:
* - Chrome 76+
* - Firefox 75+
* - Safari 15.4+
* - Edge 79+
*/3. 动画性能
使用 GPU 加速的 CSS 动画,避免性能瓶颈。
/* ✅ 推荐: 使用 transform (GPU 加速) */
@keyframes marquee-scroll {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-50%);
}
}
.animate-marquee-scroll {
animation: marquee-scroll 30s linear infinite;
will-change: transform; /* 提前告知浏览器优化 */
}
/* ❌ 避免: 使用 top/bottom (CPU 计算,会触发 reflow) */
@keyframes marquee-scroll-bad {
0% {
top: 0;
}
100% {
top: -50%;
}
}
/**
* 性能对比:
*
* transform (GPU 加速):
* - 在 GPU 的 Composite Layer 上执行
* - 不触发 reflow 和 repaint
* - 60fps 流畅动画
*
* top/bottom (CPU 计算):
* - 触发 layout 计算 (reflow)
* - 触发 paint 绘制 (repaint)
* - 可能掉帧到 30fps 或更低
*/4. 数据处理优化
使用 useMemo 缓存数据处理结果,避免重复计算。
import { useMemo } from 'react'
function MarqueeReview({ data }: MarqueeReviewProps) {
/**
* 缓存分组结果
* 仅在 data.items 变化时重新计算
*/
const chunkItems = useMemo(() => {
const { items } = data
// 步骤 1: 自动补齐
const filledItems = items.length < 9
? [...items, ...items.slice(0, 9 - items.length)]
: items
// 步骤 2: 分组
const chunked = _chunk(filledItems, 3).slice(0, 3)
// 步骤 3: 双倍复制
return chunked.map(group => [...group, ...group])
}, [data.items])
return (
<div>
{chunkItems.map((items, index) => (
<div key={index}>
{items.map((item, innerIndex) => (
<Card key={innerIndex} item={item} />
))}
</div>
))}
</div>
)
}
/**
* 性能提升:
* - 避免每次渲染都执行数组操作 (slice, chunk, map)
* - 当 data.items 未变化时,直接使用缓存结果
* - 减少内存分配和 GC 压力
*
* 时间复杂度:
* - 首次计算: O(n), n 为 items.length
* - 后续渲染: O(1) (使用缓存)
*/5. 长列表优化
当评论数量超过 50 条时,建议优化数据传输。
/**
* 服务端筛选高质量评论
* 避免在客户端传递过大的数组
*/
// ❌ 避免: 传递所有 500 条评论
const allReviews = await fetchReviews() // 500 条
<MarqueeReview data={{ items: allReviews }} />
// ✅ 推荐: 服务端筛选高质量评论
const topReviews = await fetchReviews({
filter: 'top_rated',
limit: 18, // 仅获取 18 条高评分评论
})
<MarqueeReview data={{ items: topReviews }} />
/**
* 优化效果:
* - 减少网络传输数据量 (500 条 → 18 条)
* - 减少客户端内存占用
* - 加快组件初始化速度
* - 展示精选内容,提升用户体验
*/常见问题 (FAQ)
1. 评论少于 9 条时会怎样?
问题: 我只有 3 条评论,组件能正常工作吗?
答案: 可以! 组件内部会自动复制评论填充到 9 条,确保 3 列布局均匀分布。
// 内部自动补齐逻辑
const filledItems = items.length < 9
? [...items, ...items.slice(0, 9 - items.length)]
: items
// 示例:
// 输入: [A, B, C] (3 条)
// 输出: [A, B, C, A, B, C, A, B, C] (9 条)注意: 评论数量过少会导致明显的重复感,建议至少提供 6 条不同的评论。
2. 如何自定义滚动速度?
问题: 滚动动画太快/太慢,如何调整?
答案: 需要修改 CSS 动画的 duration 属性。
/* 在你的全局样式文件或 tailwind.config.js 中 */
@keyframes marquee-scroll {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-50%);
}
}
.animate-marquee-scroll {
animation: marquee-scroll 30s linear infinite;
/* ↑ 调整这个值控制速度 */
}
/* 速度对照表:
* - 20s: 快速滚动
* - 30s: 正常速度 (默认)
* - 40s: 慢速滚动
* - 60s: 超慢速度
*/3. 为什么列 1 和列 3 有垂直偏移?
问题: 为什么有些列位置不一样?
答案: 这是设计上的错位动画效果,增强视觉层次感。
className={cn('animate-marquee-scroll', {
'top-10': index === 0 || index === 2, // 列 0 和 2 有 40px 偏移
})}
/**
* 视觉效果:
* ┌─────────────────┐
* │ 列0 (偏移40px) │ ← 起始位置较低
* │ 列1 (无偏移) │ ← 正常位置
* │ 列2 (偏移40px) │ ← 起始位置较低
* └─────────────────┘
*/如需禁用: 移除 'top-10': index === 0 || index === 2 条件即可。
4. 如何修改星级图标颜色?
问题: 橙色星星图标不符合我们的品牌色,如何修改?
答案: 修改 StarIcon 和 unStarIcon 组件中的 fill 属性。
// 位置: packages/ui/src/biz-components/MarqueeReview/index.tsx
const StarIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M8 0L10.3 5.5L16 6.2L12 10.1L13 16L8 13.3L3 16L4 10.1L0 6.2L5.7 5.5L8 0Z"
fill="#F77234" // ← 修改为你的品牌色
/>
</svg>
)
const unStarIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M8 0L10.3 5.5L16 6.2L12 10.1L13 16L8 13.3L3 16L4 10.1L0 6.2L5.7 5.5L8 0Z"
fill="#75787F" // ← 修改未填充星的灰色
/>
</svg>
)5. 评论内容过长怎么处理?
问题: 用户评论太长,会破坏布局吗?
答案: 不会。组件使用 line-clamp-5 自动截断超过 5 行的内容。
<Heading
as="h6"
size={2}
html={item.comment}
className="line-clamp-5 min-h-[120px] text-wrap"
/>
/**
* line-clamp-5 效果:
* - 最多显示 5 行文本
* - 超出部分自动截断并添加省略号 (...)
* - 保持布局整洁统一
*/如需修改行数:
line-clamp-3: 截断为 3 行line-clamp-6: 截断为 6 行line-clamp-none: 不截断 (不推荐)
6. 如何在移动端显示 2 列或 3 列?
问题: 移动端只显示 1 列太单调,能显示 2 列吗?
答案: 可以,修改列的响应式隐藏类。
// 当前默认: 移动端仅显示列 0
'hidden tablet:flex': index === 2, // 列 2 从平板 (≥768px) 开始显示
// 修改为: 移动端显示 2 列
'': index === 2, // 移除 hidden,列 2 始终显示
/**
* 修改后效果:
* - Mobile: 显示列 0 和列 2 (2 列)
* - Tablet: 显示列 0 和列 2 (2 列)
* - Desktop: 显示列 0, 1, 2 (3 列)
*/7. 评论卡片能否点击跳转?
问题: 能让评论卡片可点击,跳转到详情页吗?
答案: 当前卡片为纯展示组件,若需添加点击功能,需修改 Card 组件。
const Card = ({ item, onClick }: CardProps & { onClick?: () => void }) => {
return (
<button
onClick={onClick}
className="rounded-box bg-container-primary ... cursor-pointer hover:shadow-lg transition-shadow"
aria-label={`View review by ${item.name}`}
>
{/* 原有内容 */}
</button>
)
}
// 使用示例:
<Card
item={item}
onClick={() => {
router.push(`/reviews/${item.id}`)
}}
/>8. 如何禁用无限滚动动画?
问题: 我想要静态显示评论,不需要滚动动画。
答案: 移除 animate-marquee-scroll 类。
<div
key={index}
className={cn(
'relative flex h-fit flex-1 flex-col gap-4',
// 移除: 'animate-marquee-scroll'
{
'top-10': index === 0 || index === 2,
'hidden desktop:flex': index === 1,
'hidden tablet:flex': index === 2,
}
)}
>9. 如何添加”查看更多评论”按钮?
问题: 评论很多,想添加一个按钮让用户查看全部评论。
答案: 在组件下方添加自定义按钮。
<div>
<MarqueeReview data={data} />
<div className="mt-8 text-center">
<Button
onClick={() => router.push('/reviews')}
variant="outline"
>
View All {totalReviews} Reviews →
</Button>
</div>
</div>10. 如何使用真实用户头像?
问题: 我想展示真实用户的头像照片,如何实现?
答案: 确保已获得用户授权,然后直接传入头像 URL。
// 从数据库获取真实用户数据
const realUserReviews = await db.reviews.findMany({
where: { rating: { gte: 4 } },
include: { user: true },
take: 9,
})
const reviewsData = realUserReviews.map(review => ({
name: review.user.name,
avatar: {
url: review.user.avatarUrl, // 真实头像 URL
alt: `${review.user.name} avatar`,
},
comment: review.content,
rating: review.rating,
}))
<MarqueeReview
data={{
title: 'Real Customer Reviews',
subtitle: 'Verified purchases',
items: reviewsData,
}}
/>
/**
* 注意事项:
* 1. 确保已获得用户授权展示头像和评论
* 2. 遵守隐私政策和法律法规 (GDPR, CCPA 等)
* 3. 提供用户删除评论和头像的选项
* 4. 优化图片大小,建议压缩到 20KB 以下
*/相关资源
- Storybook: 查看 MarqueeReview 的所有示例和交互演示
- 源代码:
packages/ui/src/biz-components/MarqueeReview/index.tsx - Story 文件:
packages/ui/src/stories/MarqueeReview.stories.tsx - 单元测试:
packages/ui/tests/MarqueeReview.test.tsx - 依赖组件:
- 相关组件:
Marquee- 水平滚动轮播 (P0 通用组件)Evaluate- 静态评价卡片 (P0 通用组件)
- 设计资源:
- 技术文档:
最后更新: 2026-01-18 组件版本: v1.0.0 维护者: DTC IT Team