加载中...
当前视口: 1920px × 600px场景: 四项网格展示
打开链接功能特性
- ✅ 卡片轮播 - 基于 SwiperBox 的横向滚动展示
- ✅ 图文卡片 - 图标 + 标题 + 描述的固定布局
- ✅ 响应式滑动 - 移动端 1 张,平板 2.3 张,桌面 4 张
- ✅ 文本截断 - 标题和描述超长自动截断(line-clamp-2)
- ✅ 固定样式 - 统一的灰色背景和圆角卡片
- ✅ 富文本支持 - 标题和描述支持 HTML 格式
- ✅ 性能优化 - 使用 forwardRef 和 useImperativeHandle
- ✅ 无障碍友好 - 所有图片必须提供 alt 属性
- ✅ 懒加载 - 通过 Picture 组件实现图片懒加载
Props 参数
WhyChooseProps
| Prop | 类型 | 默认值 | 必需 | 说明 |
|---|---|---|---|---|
data | WhyChooseData | - | ✅ | 卡片数据配置对象 |
className | string | '' | - | 自定义类名 |
ref | React.Ref<HTMLDivElement> | - | - | 根容器 ref |
WhyChooseData 配置
| 参数 | 类型 | 默认值 | 必需 | 说明 |
|---|---|---|---|---|
productData | WhyChooseItem[] | - | ✅ | 卡片列表 |
WhyChooseItem 配置
| 参数 | 类型 | 必需 | 说明 |
|---|---|---|---|
img | Media | ✅ | 卡片图标 |
title | string | ✅ | 卡片标题(支持 HTML) |
desc | string | ✅ | 卡片描述(支持 HTML) |
Media 类型
interface Media {
url: string // 图片 URL(必需)
alt: string // 替代文本(必需)
thumbnailURL?: string // 缩略图 URL
mimeType?: string // MIME 类型(如 'image/png')
}使用示例
最简示例
import { WhyChoose } from '@anker-in/headless-ui/biz'
<WhyChoose
data={{
productData: [
{
img: {
url: 'https://images.unsplash.com/photo-1614292247047-a966dd17c2e3',
alt: 'Warranty icon',
},
title: '24-Month Warranty',
desc: 'Comprehensive protection for your peace of mind'
},
{
img: {
url: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64',
alt: 'Shipping icon',
},
title: 'Free Shipping',
desc: 'Fast and free delivery on all orders'
}
]
}}
/>基础优势展示
<WhyChoose
data={{
productData: [
{
img: {
url: 'https://images.unsplash.com/photo-1614292247047-a966dd17c2e3',
alt: 'Warranty'
},
title: '24-Month Warranty',
desc: 'Comprehensive Protection'
},
{
img: {
url: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64',
alt: 'Support'
},
title: '24/7 Customer Support',
desc: 'Always Here to Help'
},
{
img: {
url: 'https://images.unsplash.com/photo-1609091839311-d5365f9ff1c5',
alt: 'Shipping'
},
title: 'Free Shipping',
desc: 'On All Orders'
},
{
img: {
url: 'https://images.unsplash.com/photo-1565688534245-05d6b5be184a',
alt: 'Returns'
},
title: '30-Day Returns',
desc: 'Hassle-Free Refunds'
}
]
}}
/>带 HTML 格式的富文本
<WhyChoose
data={{
productData: [
{
img: {
url: 'https://images.unsplash.com/photo-1600783245777-2e60b4a4d2c5',
alt: 'Fast'
},
title: '<b>Ultra-Fast</b> Charging',
desc: 'Get <b>50%</b> charge in just <b>30 minutes</b>'
},
{
img: {
url: 'https://images.unsplash.com/photo-1600783245777-2e60b4a4d2c5',
alt: 'Safe'
},
title: '<b>Multi-Layer</b> Protection',
desc: 'Temperature control, overcurrent protection, and more'
}
]
}}
/>产品特性展示
<WhyChoose
data={{
productData: [
{
img: {
url: 'https://images.unsplash.com/photo-1609091839311-d5365f9ff1c5',
alt: 'Battery'
},
title: 'Long Battery Life',
desc: 'Up to 48 hours of continuous use'
},
{
img: {
url: 'https://images.unsplash.com/photo-1565688534245-05d6b5be184a',
alt: 'Compact'
},
title: 'Ultra-Compact Design',
desc: 'Fits easily in your pocket or bag'
},
{
img: {
url: 'https://images.unsplash.com/photo-1600783245777-2e60b4a4d2c5',
alt: 'Durable'
},
title: 'Military-Grade Durability',
desc: 'Built to withstand harsh conditions'
},
{
img: {
url: 'https://images.unsplash.com/photo-1614292247047-a966dd17c2e3',
alt: 'Eco'
},
title: 'Eco-Friendly Materials',
desc: '100% recyclable packaging'
}
]
}}
/>自定义样式
<WhyChoose
data={{
productData: [...]
}}
className="my-custom-wrapper"
/>响应式行为
轮播配置
| 屏幕尺寸 | slidesPerView | spaceBetween | 说明 |
|---|---|---|---|
| 移动端 (< 374px) | 1 | 12px | 每次显示 1 张完整卡片 |
| 移动端 (≥ 374px) | 1.2 | 12px | 显示 1.2 张,露出下一张边缘 |
| 平板 (≥ 768px) | 2.3 | 16px | 显示 2.3 张 |
| 笔记本 (≥ 1025px) | 4 | 16px | 显示 4 张完整卡片 |
| 桌面 (≥ 1440px) | 4 | 16px | 显示 4 张 |
| 大屏 (≥ 1920px) | 4 | 16px | 显示 4 张 |
注意: 笔记本及以上尺寸固定显示 4 张卡片,不再滑动。
卡片内边距
| 屏幕尺寸 | 内边距 |
|---|---|
| 移动端 (< 1025px) | 16px |
| 笔记本及以上 (≥ 1025px) | 24px |
图标尺寸
- 固定宽度: 36px
- 高度: 自适应(保持图片原始宽高比)
文本尺寸
| 元素 | 字号 | 字重 | 行高 |
|---|---|---|---|
| 标题 | 20px | bold | 1.2 |
| 描述 | 14px | bold | 1.2 |
设计规范
文本截断
标题截断:
line-clamp-2 // 最多显示 2 行,超出显示省略号示例:
This is a Very Long Title That Will Be
Truncated After Two Lines...描述截断:
line-clamp-2 // 最多显示 2 行建议:
- 标题控制在 30 个字符内(中文 15 个字)
- 描述控制在 60 个字符内(中文 30 个字)
卡片样式规范
固定样式:
// 背景色: 浅灰色
bg-[#EAEAEC]
// 圆角: 16px
rounded-[16px]
// 内边距: 响应式
p-[16px] laptop:p-[24px]
// 宽度: 自适应
w-full shrink-0注意: 当前组件使用固定的灰色背景,如需自定义颜色,需要通过 CSS 覆盖 .whyChooseBlock 类。
图标样式
- 固定宽度: 36px
- 上边距(相对标题): 16px
- 推荐使用 SVG 格式图标
- 图标应简洁、清晰、辨识度高
内容数量建议
- 最少: 2-3 个特性
- 推荐: 4-6 个特性
- 最多: 8 个特性(笔记本端最多显示 4 个,超出需要滑动)
无障碍性
图片
- ✅ 所有图片必须提供
alt属性 - ✅ alt 文本应清晰描述图标含义
- ✅ 支持图片懒加载
文本
- ✅ 标题和描述支持富文本(HTML)
- ✅ 文本颜色对比度符合标准
- ✅ line-clamp 确保文本不会影响布局
交互
- ✅ 卡片支持键盘导航(通过 SwiperBox 实现)
- ⚠️ 建议添加: 为根容器添加
role="region"和aria-label="product features" - ⚠️ 建议添加: 为卡片添加
tabindex="0"支持键盘聚焦
性能优化
- ✅ React.forwardRef() 和 useImperativeHandle - 优化 ref 传递
- ✅ SwiperBox 懒加载 - 通过 SwiperBox 组件实现懒加载
- ✅ 图片优化 - 通过 Picture 组件自动优化
- ✅ 条件渲染 - 减少 DOM 节点
- ✅ 文本截断 - 避免过长内容影响性能
常见问题
为什么笔记本及以上尺寸固定显示 4 张卡片?
这是设计决策,确保桌面端用户可以一次看到所有优势特性。如果卡片数量超过 4 个,建议将内容分成多组或调整轮播配置。
如何自定义卡片背景色?
当前背景色硬编码为 #EAEAEC。如需自定义,可以通过 CSS 覆盖:
.whyChooseBlock {
background-color: #f5f5f5 !important;
}或直接修改组件源码中的 bg-[#EAEAEC] 类。
如何调整图标尺寸?
图标宽度固定为 36px。如需调整,可以修改组件源码中的 w-[36px] 类:
<Picture source={data.img.url} className="w-[48px]" /> // 改为 48px为什么移动端显示 1.2 张卡片而不是 1 张?
slidesPerView: 1.2 的设计目的是露出下一张卡片的边缘,提示用户可以滑动查看更多内容,提升交互体验。
如何禁用轮播分页指示器?
修改 SwiperBox 的 configuration 配置:
configuration: {
shape: 'card',
isTab: false // 禁用分页指示器
}注意: 需要修改组件源码。
支持垂直滚动吗?
当前组件仅支持横向滚动。如需垂直布局,建议使用网格布局:
<div className="grid grid-cols-1 tablet:grid-cols-2 laptop:grid-cols-4 gap-4">
{productData.map((item, index) => (
<WhyChooseItem key={index} data={item} />
))}
</div>如何处理图片加载失败?
使用 Picture 组件的 fallback 功能:
img: {
url: 'https://example.com/icon.svg',
alt: 'Feature icon',
thumbnailURL: 'https://example.com/fallback.svg' // 失败时显示
}标题和描述支持哪些 HTML 标签?
支持常见内联标签:
<b>,<strong>: 加粗<i>,<em>: 斜体<span>: 自定义样式<br>: 换行
不推荐使用块级标签(如 <div>, <p>),会破坏布局。
如何添加点击事件?
当前组件不支持点击事件。如需添加,可以在父组件中包裹链接:
<Link href="/feature-details">
<WhyChoose data={...} />
</Link>或修改组件源码添加 onClick 回调。
技术实现
基于 SwiperBox
组件基于 SwiperBox 组件进行轮播,默认配置:
{
configuration: {
shape: 'card', // 卡片形状
isTab: true // 启用分页指示器
},
breakpoints: {
0: { slidesPerView: 1, spaceBetween: 12 },
374: { slidesPerView: 1.2, spaceBetween: 12 },
768: { slidesPerView: 2.3, spaceBetween: 16 },
1025: { slidesPerView: 4, spaceBetween: 16 },
1440: { slidesPerView: 4, spaceBetween: 16 },
1920: { slidesPerView: 4, spaceBetween: 16 },
}
}特殊配置:
!overflow-visible: 允许卡片阴影溢出[&_.swiper-wrapper]:flex: 强制 flex 布局
核心代码
import React, { forwardRef, useImperativeHandle, useRef } from 'react'
import { SwiperBox } from '../SwiperBox'
import { Picture } from '../Picture'
import { Text } from '../Text'
import scenarios from './scenarios.json'
interface WhyChooseItem {
img: Media
title: string
desc: string
}
interface WhyChooseProps {
data: {
productData: WhyChooseItem[]
}
className?: string
}
const WhyChoose = forwardRef<HTMLDivElement, WhyChooseProps>(
({ data, className }, ref) => {
const containerRef = useRef<HTMLDivElement>(null)
useImperativeHandle(ref, () => containerRef.current!)
return (
<div ref={containerRef} className={className}>
<SwiperBox
configuration={{ shape: 'card', isTab: true }}
breakpoints={{
0: { slidesPerView: 1, spaceBetween: 12 },
374: { slidesPerView: 1.2, spaceBetween: 12 },
768: { slidesPerView: 2.3, spaceBetween: 16 },
1025: { slidesPerView: 4, spaceBetween: 16 },
1440: { slidesPerView: 4, spaceBetween: 16 },
1920: { slidesPerView: 4, spaceBetween: 16 },
}}
>
{data.productData.map((item, index) => (
<div
key={index}
className="whyChooseBlock bg-[#EAEAEC] rounded-[16px] p-[16px] laptop:p-[24px] w-full shrink-0"
>
<Picture source={item.img.url} className="w-[36px]" />
<Text
data={item.title}
className="text-[20px] font-bold leading-[1.2] mt-[16px] line-clamp-2"
/>
<Text
data={item.desc}
className="text-[14px] font-bold leading-[1.2] mt-[8px] line-clamp-2"
/>
</div>
))}
</SwiperBox>
</div>
)
}
)
WhyChoose.displayName = 'WhyChoose'
export default WhyChoose相关资源
- Storybook 文档 - 查看更多交互示例
- 完整 README 文档 - 详细的 API 文档和使用指南
- Figma 设计稿 - 查看设计规范(注:figma不完整但可用)
- Swiper 文档 - 了解 Swiper 库
- 源代码 - GitHub 源代码
Last updated on