Skip to Content
@anker-in/headless-ui 2.0 is released 🎉
业务组件📐 基础图文模块图文叠加4-特性卡片

FeatureCards (图文叠加4-特性卡片)

智能网格布局的特性卡片,支持2-5张卡片的自适应布局比例、响应式切换和图片文字叠加效果。【✅ 已发布】

加载中...
当前视口: 1920px × 600px场景: 四张卡片布局
打开链接

功能特性

FeatureCards 组件是一个智能网格布局的特性卡片组件,具有以下核心能力:

  • 智能网格布局 - 根据卡片数量(2-5张)自动计算最佳布局比例
  • 响应式切换 - 移动端使用 Swiper 滑动,桌面端使用 Flex 网格
  • 图片文字叠加 - 背景图片与文字内容叠加展示,支持主题切换
  • 自动换行 - 4/5张卡片时自动在特定位置换行
  • 曝光追踪 - 内置 useExposure hook 支持曝光埋点
  • 链接跳转 - 支持卡片点击跳转
  • 主题支持 - 支持浅色/深色主题,影响文字颜色
  • 文本截断 - 副标题自动限制2行显示

Props 参数

FeatureCardsData

属性类型必填默认值说明
titlestring-标题
subtitlestring-副标题
itemsFeatureCardItem[]-卡片列表
theme'light' | 'dark''light'主题,影响文字颜色

FeatureCardItem

属性类型必填默认值说明
titlestring-卡片标题
subtitlestring-卡片副标题
imageMedia-背景图片
theme'light' | 'dark'-卡片主题,优先级高于全局主题
linkstring-点击跳转链接

Media

属性类型必填默认值说明
urlstring-图片URL
altstring-图片替代文本

使用示例

基础用法 - 3张卡片

import { FeatureCards } from '@anker-in/headless-ui/biz' import scenarios from './scenarios.json' const data = { title: 'Advanced Cleaning Technology', subtitle: 'Powered by AI and innovative hardware', items: [ { title: 'Smart Obstacle Detection', subtitle: 'Automatically detects and avoids obstacles in real-time.', image: { url: '/images/feature1.jpg', alt: 'Smart Detection', }, theme: 'dark', }, { title: 'Precision Navigation', subtitle: 'Advanced SLAM technology for efficient cleaning paths.', image: { url: '/images/feature2.jpg', alt: 'Navigation', }, theme: 'dark', }, { title: 'Multi-Surface Cleaning', subtitle: 'Adapts cleaning power for different floor types.', image: { url: '/images/feature3.jpg', alt: 'Multi-Surface', }, theme: 'dark', }, ], } export default function App() { return <FeatureCards data={data} /> }

2张卡片 - 2:3 布局比例

const data = { title: 'Core Features', items: [ { title: 'Feature A', subtitle: 'Description A', image: { url: '/a.jpg', alt: 'Feature A' }, theme: 'light', }, { title: 'Feature B', subtitle: 'Description B', image: { url: '/b.jpg', alt: 'Feature B' }, theme: 'dark', }, ], } // 桌面端布局: 第一张 flex-[2], 第二张 flex-[3]

4张卡片 - 两行布局(2:3, 3:2)

const data = { items: [ { title: 'Feature 1', subtitle: 'First row left', image: { url: '/1.jpg', alt: 'Feature 1' }, }, { title: 'Feature 2', subtitle: 'First row right', image: { url: '/2.jpg', alt: 'Feature 2' }, }, { title: 'Feature 3', subtitle: 'Second row left', image: { url: '/3.jpg', alt: 'Feature 3' }, }, { title: 'Feature 4', subtitle: 'Second row right', image: { url: '/4.jpg', alt: 'Feature 4' }, }, ], } // 桌面端布局: // 第一行: flex-[2] : flex-[3] // 第二行: flex-[3] : flex-[2]

5张卡片 - 两行布局(1:1:1, 2:3)

const data = { items: [ { title: 'Feature 1', image: { url: '/1.jpg', alt: '1' } }, { title: 'Feature 2', image: { url: '/2.jpg', alt: '2' } }, { title: 'Feature 3', image: { url: '/3.jpg', alt: '3' } }, { title: 'Feature 4', image: { url: '/4.jpg', alt: '4' } }, { title: 'Feature 5', image: { url: '/5.jpg', alt: '5' } }, ], } // 桌面端布局: // 第一行: flex-[1] : flex-[1] : flex-[1] // 第二行: flex-[2] : flex-[3]

带链接跳转

const data = { items: [ { title: 'Learn More', subtitle: 'Click to view details', image: { url: '/card.jpg', alt: 'Card' }, link: '/products/detail', }, ], } // 点击卡片将跳转至 /products/detail

浅色主题

const data = { title: 'Features', theme: 'light', // 全局浅色主题 items: [ { title: 'Feature 1', image: { url: '/1.jpg', alt: '1' }, // 使用全局浅色主题,文字为深色 }, { title: 'Feature 2', image: { url: '/2.jpg', alt: '2' }, theme: 'dark', // 卡片级别覆盖为深色主题 }, ], }

无标题副标题

const data = { items: [ { title: 'Feature 1', subtitle: 'Description', image: { url: '/1.jpg', alt: '1' }, }, { title: 'Feature 2', image: { url: '/2.jpg', alt: '2' }, }, ], } // 只渲染卡片列表,不显示顶部标题区域

响应式行为

断点切换

组件在不同屏幕尺寸下有不同的展示逻辑:

屏幕尺寸布局方式行为说明
< 1025pxSwiper 滑动使用 Swiper.js 实现横向滑动,支持触摸和鼠标拖拽
≥ 1025pxFlex 网格使用 Flexbox 布局,根据卡片数量应用智能比例

移动端(Swiper 模式)

// < 1025px 时使用 Swiper <Swiper slidesPerView={1.2} spaceBetween={16} breakpoints={{ 768: { slidesPerView: 2.2 }, }} > {items.map(item => ( <SwiperSlide key={item.title}> <FeatureCard {...item} /> </SwiperSlide> ))} </Swiper>

特点:

  • 手机端: 1.2张卡片可见,露出后续内容
  • 平板端: 2.2张卡片可见
  • 支持触摸滑动和鼠标拖拽

桌面端(Flex 网格模式)

智能网格布局规则:

2张卡片: 2:3 比例

/* 第一张 */ flex: 2 /* 第二张 */ flex: 3

3张卡片: 1:1:1 等分

/* 全部 */ flex: 1

4张卡片: 两行,2:3 + 3:2

/* 第一行 */ flex: 2 /* 第1张 */ flex: 3 /* 第2张 */ /* 换行 */ /* 第二行 */ flex: 3 /* 第3张 */ flex: 2 /* 第4张 */

5张卡片: 两行,1:1:1 + 2:3

/* 第一行 */ flex: 1 /* 第1张 */ flex: 1 /* 第2张 */ flex: 1 /* 第3张 */ /* 换行 */ /* 第二行 */ flex: 2 /* 第4张 */ flex: 3 /* 第5张 */

自动换行逻辑

组件在特定位置插入换行符(&lt;div className="laptop:basis-full laptop:h-0" /&gt;):

{items.map((item, index) => ( <React.Fragment key={item.title}> {renderCard(item, index)} {shouldBreakRow(index, items.length) && ( <div className="laptop:basis-full laptop:h-0" /> )} </React.Fragment> ))}

换行规则:

  • 4张卡片: 在第2张后换行 (index === 1)
  • 5张卡片: 在第3张后换行 (index === 2)
  • 其他: 不换行

设计规范

布局算法

组件使用 getCardLayoutClasses 函数计算每张卡片的 flex 比例:

function getCardLayoutClasses(index: number, totalCards: number): string { // 2张: 2:3 if (totalCards === 2) { return index === 0 ? 'laptop:flex-[2]' : 'laptop:flex-[3]' } // 3张: 1:1:1 if (totalCards === 3) { return 'laptop:flex-[1]' } // 4张: 第一行 2:3,第二行 3:2 if (totalCards === 4) { const ratios = [2, 3, 3, 2] return `laptop:flex-[${ratios[index]}]` } // 5张: 第一行 1:1:1,第二行 2:3 if (totalCards === 5) { if (index < 3) return 'laptop:flex-[1]' return index === 3 ? 'laptop:flex-[2]' : 'laptop:flex-[3]' } // 默认等分 return 'laptop:flex-[1]' }

卡片结构

每张卡片包含:

  1. 背景图片 - 使用 bg-cover bg-center 填充整个卡片
  2. 渐变遮罩 - 从透明到半透明黑色,增强文字可读性
  3. 文字内容 - 定位在底部,包含标题和副标题
<div className="relative h-full overflow-hidden rounded-lg"> {/* 背景图片 */} <img src={image.url} alt={image.alt} className="absolute inset-0 h-full w-full object-cover" /> {/* 渐变遮罩 */} <div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" /> {/* 文字内容 */} <div className="absolute bottom-0 left-0 right-0 p-6"> <h3 className={titleClasses}>{title}</h3> {subtitle && ( <p className={cn(subtitleClasses, 'line-clamp-2')}> {subtitle} </p> )} </div> </div>

主题变体

使用 CVA 定义主题变体:

const titleVariants = cva('text-2xl font-bold', { variants: { theme: { light: 'text-gray-900', dark: 'text-white', }, }, defaultVariants: { theme: 'light', }, }) const subtitleVariants = cva('mt-2 text-base', { variants: { theme: { light: 'text-gray-600', dark: 'text-gray-200', }, }, defaultVariants: { theme: 'light', }, })

尺寸规范

元素移动端桌面端
卡片间距16px24px
卡片高度300px400px
标题字号20px24px
副标题字号14px16px
内容内边距20px24px
圆角半径8px8px

无障碍性

键盘导航

  • 使用 &lt;a&gt; 标签包裹卡片内容,支持键盘 Tab 导航
  • 按 Enter 或 Space 触发链接跳转
  • Swiper 模式支持键盘方向键切换

屏幕阅读器

<a href={link} className="block h-full" aria-label={`${title}${subtitle ? `: ${subtitle}` : ''}`} > <img src={image.url} alt={image.alt} role="img" /> <div aria-hidden="true"> {/* 视觉装饰性内容 */} </div> </a>

优化要点:

  • 所有图片必须提供 alt 属性
  • 卡片添加 aria-label 描述完整信息
  • 装饰性渐变遮罩使用 aria-hidden="true"

颜色对比度

  • 浅色主题文字: text-gray-900 (对比度 > 7:1)
  • 深色主题文字: text-white (对比度 > 7:1)
  • 渐变遮罩: from-black/60 增强底部文字可读性

性能优化

图片懒加载

<img src={image.url} alt={image.alt} loading="lazy" decoding="async" />

曝光追踪优化

组件内置 useExposure hook,使用 IntersectionObserver 实现高性能曝光检测:

useExposure({ ref: cardRef, onExposure: () => { // 仅触发一次 trackEvent('card_exposure', { title: item.title, position: index, }) }, threshold: 0.5, // 50% 可见时触发 once: true, // 仅触发一次 })

Swiper 性能配置

<Swiper lazy={true} preloadImages={false} watchSlidesProgress={true} watchSlidesVisibility={true} > {/* 仅加载可见卡片 */} </Swiper>

组件记忆化

const FeatureCard = React.memo(({ item, index }: Props) => { // 避免父组件更新时重渲染所有卡片 return <div>...</div> })

避免布局抖动

使用固定高度防止图片加载时布局偏移:

.feature-card { height: 300px; /* 或 h-[300px] */ } @media (min-width: 1025px) { .feature-card { height: 400px; } }

常见问题

如何自定义卡片高度?

默认高度是固定的(移动端 300px,桌面端 400px)。如需自定义,可以通过 CSS 变量或 Tailwind 配置覆盖:

<div className="[&_.feature-card]:h-[500px]"> <FeatureCards data={data} /> </div>

为什么只支持2-5张卡片?

布局算法针对2-5张卡片优化,超出此范围可能导致布局不理想。如需显示更多卡片,建议:

  1. 分页显示
  2. 使用滚动容器
  3. 自定义布局组件

如何实现卡片点击事件?

卡片支持 link 属性进行页面跳转。如需自定义点击事件,可以包裹组件:

<div onClick={handleCardClick}> <FeatureCards data={data} /> </div>

或使用事件委托:

<div onClick={(e) => { const card = e.target.closest('[data-card-id]') if (card) { const id = card.dataset.cardId handleCardClick(id) } }} > <FeatureCards data={data} /> </div>

如何自定义换行逻辑?

换行逻辑内置于组件,无法直接自定义。如需不同的布局规则,建议:

  1. Fork 组件源码修改
  2. 使用 CSS Grid 自定义布局
  3. 联系维护者讨论新的布局需求

副标题截断位置能否调整?

副标题使用 line-clamp-2 限制2行。如需调整,可以通过 CSS 覆盖:

.feature-cards [data-subtitle] { -webkit-line-clamp: 3; /* 改为3行 */ }

如何禁用 Swiper 的拖拽?

// 组件不直接暴露 Swiper 配置 // 需要通过 CSS 禁用交互 <div className="[&_.swiper]:pointer-events-none"> <FeatureCards data={data} /> </div>

技术实现

核心依赖

  • Swiper.js 11.x - 移动端滑动交互
  • React 18+ - 组件框架
  • Tailwind CSS 3+ - 样式系统
  • class-variance-authority - 主题变体管理

关键技术点

1. 条件渲染(Swiper vs Flex)

const FeatureCards = ({ data }: Props) => { return ( <div> {/* 移动端 Swiper */} <div className="laptop:hidden"> <Swiper {...swiperConfig}> {data.items.map(item => ( <SwiperSlide key={item.title}> <FeatureCard item={item} /> </SwiperSlide> ))} </Swiper> </div> {/* 桌面端 Flex 网格 */} <div className="hidden laptop:flex laptop:flex-wrap laptop:gap-6"> {data.items.map((item, index) => ( <React.Fragment key={item.title}> <div className={getCardLayoutClasses(index, data.items.length)}> <FeatureCard item={item} /> </div> {shouldBreakRow(index, data.items.length) && ( <div className="laptop:basis-full laptop:h-0" /> )} </React.Fragment> ))} </div> </div> ) }

2. 智能布局算法实现

function getCardLayoutClasses(index: number, total: number): string { const layoutMap: Record<number, number[]> = { 2: [2, 3], 3: [1, 1, 1], 4: [2, 3, 3, 2], 5: [1, 1, 1, 2, 3], } const ratios = layoutMap[total] if (!ratios) return 'laptop:flex-[1]' return `laptop:flex-[${ratios[index]}]` } function shouldBreakRow(index: number, total: number): boolean { const breakPoints: Record<number, number> = { 4: 1, // 在第2张后换行 5: 2, // 在第3张后换行 } return breakPoints[total] === index }

3. Swiper 动态断点配置

const generateSwiperBreakpoints = (itemCount: number) => { return { 0: { slidesPerView: 1.2, spaceBetween: 16, }, 768: { slidesPerView: Math.min(itemCount, 2.2), spaceBetween: 24, }, } }

4. 高阶组件包裹

export const FeatureCards = withLayout( FeatureCardsComponent, 'feature-cards' )

withLayout HOC 提供:

  • 容器宽度管理
  • 响应式断点检测
  • 公共样式注入

5. 曝光追踪集成

const FeatureCard = ({ item, index }: Props) => { const cardRef = useRef<HTMLDivElement>(null) useExposure({ ref: cardRef, onExposure: () => { window.dataLayer?.push({ event: 'card_exposure', card_title: item.title, card_position: index, }) }, threshold: 0.5, once: true, }) return <div ref={cardRef}>...</div> }

相关资源

组件源码

  • 组件实现: /packages/ui/src/biz-components/FeatureCards/
  • 类型定义: /packages/ui/src/biz-components/FeatureCards/types.ts
  • Stories: /packages/ui/src/stories/featureCards.stories.tsx

相关组件

  • ImageTextFeature - 图文分栏组件,适合详细功能介绍
  • GraphicOverlay - 图文叠加组件,支持更复杂的布局
  • AccordionCards - 手风琴卡片,适合分步展示

外部文档

设计资源

Last updated on