Skip to Content
@anker-in/headless-ui 2.0 is released 🎉

WhyChoose (品牌亮点模块)

优势特性展示组件,用于横向滚动展示产品优势、服务特色,图标+标题+描述的卡片形式。【✅ 已发布】

加载中...
当前视口: 1920px × 600px场景: 四项网格展示
打开链接

功能特性

  • 卡片轮播 - 基于 SwiperBox 的横向滚动展示
  • 图文卡片 - 图标 + 标题 + 描述的固定布局
  • 响应式滑动 - 移动端 1 张,平板 2.3 张,桌面 4 张
  • 文本截断 - 标题和描述超长自动截断(line-clamp-2)
  • 固定样式 - 统一的灰色背景和圆角卡片
  • 富文本支持 - 标题和描述支持 HTML 格式
  • 性能优化 - 使用 forwardRef 和 useImperativeHandle
  • 无障碍友好 - 所有图片必须提供 alt 属性
  • 懒加载 - 通过 Picture 组件实现图片懒加载

Props 参数

WhyChooseProps

Prop类型默认值必需说明
dataWhyChooseData-卡片数据配置对象
classNamestring''-自定义类名
refReact.Ref<HTMLDivElement>--根容器 ref

WhyChooseData 配置

参数类型默认值必需说明
productDataWhyChooseItem[]-卡片列表

WhyChooseItem 配置

参数类型必需说明
imgMedia卡片图标
titlestring卡片标题(支持 HTML)
descstring卡片描述(支持 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" />

响应式行为

轮播配置

屏幕尺寸slidesPerViewspaceBetween说明
移动端 (< 374px)112px每次显示 1 张完整卡片
移动端 (≥ 374px)1.212px显示 1.2 张,露出下一张边缘
平板 (≥ 768px)2.316px显示 2.3 张
笔记本 (≥ 1025px)416px显示 4 张完整卡片
桌面 (≥ 1440px)416px显示 4 张
大屏 (≥ 1920px)416px显示 4 张

注意: 笔记本及以上尺寸固定显示 4 张卡片,不再滑动。

卡片内边距

屏幕尺寸内边距
移动端 (< 1025px)16px
笔记本及以上 (≥ 1025px)24px

图标尺寸

  • 固定宽度: 36px
  • 高度: 自适应(保持图片原始宽高比)

文本尺寸

元素字号字重行高
标题20pxbold1.2
描述14pxbold1.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 标签?

支持常见内联标签:

  • &lt;b&gt;, &lt;strong&gt;: 加粗
  • &lt;i&gt;, &lt;em&gt;: 斜体
  • &lt;span&gt;: 自定义样式
  • &lt;br&gt;: 换行

不推荐使用块级标签(如 &lt;div&gt;, &lt;p&gt;),会破坏布局。

如何添加点击事件?

当前组件不支持点击事件。如需添加,可以在父组件中包裹链接:

<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

相关资源

Last updated on