加载中...
当前视口: 1920px × 600px场景: 四张卡片布局
打开链接功能特性
FeatureCards 组件是一个智能网格布局的特性卡片组件,具有以下核心能力:
- ✅ 智能网格布局 - 根据卡片数量(2-5张)自动计算最佳布局比例
- ✅ 响应式切换 - 移动端使用 Swiper 滑动,桌面端使用 Flex 网格
- ✅ 图片文字叠加 - 背景图片与文字内容叠加展示,支持主题切换
- ✅ 自动换行 - 4/5张卡片时自动在特定位置换行
- ✅ 曝光追踪 - 内置 useExposure hook 支持曝光埋点
- ✅ 链接跳转 - 支持卡片点击跳转
- ✅ 主题支持 - 支持浅色/深色主题,影响文字颜色
- ✅ 文本截断 - 副标题自动限制2行显示
Props 参数
FeatureCardsData
| 属性 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
title | string | 否 | - | 标题 |
subtitle | string | 否 | - | 副标题 |
items | FeatureCardItem[] | 是 | - | 卡片列表 |
theme | 'light' | 'dark' | 否 | 'light' | 主题,影响文字颜色 |
FeatureCardItem
| 属性 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
title | string | 是 | - | 卡片标题 |
subtitle | string | 否 | - | 卡片副标题 |
image | Media | 是 | - | 背景图片 |
theme | 'light' | 'dark' | 否 | - | 卡片主题,优先级高于全局主题 |
link | string | 否 | - | 点击跳转链接 |
Media
| 属性 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
url | string | 是 | - | 图片URL |
alt | string | 是 | - | 图片替代文本 |
使用示例
基础用法 - 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' },
},
],
}
// 只渲染卡片列表,不显示顶部标题区域响应式行为
断点切换
组件在不同屏幕尺寸下有不同的展示逻辑:
| 屏幕尺寸 | 布局方式 | 行为说明 |
|---|---|---|
< 1025px | Swiper 滑动 | 使用 Swiper.js 实现横向滑动,支持触摸和鼠标拖拽 |
≥ 1025px | Flex 网格 | 使用 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: 33张卡片: 1:1:1 等分
/* 全部 */ flex: 14张卡片: 两行,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张 */自动换行逻辑
组件在特定位置插入换行符(<div className="laptop:basis-full laptop:h-0" />):
{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]'
}卡片结构
每张卡片包含:
- 背景图片 - 使用
bg-cover bg-center填充整个卡片 - 渐变遮罩 - 从透明到半透明黑色,增强文字可读性
- 文字内容 - 定位在底部,包含标题和副标题
<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',
},
})尺寸规范
| 元素 | 移动端 | 桌面端 |
|---|---|---|
| 卡片间距 | 16px | 24px |
| 卡片高度 | 300px | 400px |
| 标题字号 | 20px | 24px |
| 副标题字号 | 14px | 16px |
| 内容内边距 | 20px | 24px |
| 圆角半径 | 8px | 8px |
无障碍性
键盘导航
- 使用
<a>标签包裹卡片内容,支持键盘 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张卡片优化,超出此范围可能导致布局不理想。如需显示更多卡片,建议:
- 分页显示
- 使用滚动容器
- 自定义布局组件
如何实现卡片点击事件?
卡片支持 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>如何自定义换行逻辑?
换行逻辑内置于组件,无法直接自定义。如需不同的布局规则,建议:
- Fork 组件源码修改
- 使用 CSS Grid 自定义布局
- 联系维护者讨论新的布局需求
副标题截断位置能否调整?
副标题使用 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 - 手风琴卡片,适合分步展示
外部文档
设计资源
- Figma 设计稿: IPC To-C Design System - FeatureCards
Last updated on