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

Ksp (KSP)

突出展示产品核心卖点的组件,支持图标、标题和描述的网格布局【✅ 已发布】

加载中...
当前视口: 1920px × 600px场景: 默认
打开链接

功能特性

Ksp (Key Selling Points) 是一个功能强大的关键卖点展示组件,采用复杂的响应式瓦片布局系统:

  • 动态瓦片布局 - 支持 4-9 个卡片,每种数量有独特的栅格布局配置
  • 10 列栅格系统 - laptop 及以上使用 10 列网格,精确控制每个卡片的位置和大小
  • 双媒体类型 - 每个卡片可配置图片或视频,自动识别渲染
  • 三端独立媒体 - 桌面端和移动端可使用不同的图片/视频资源
  • 文字叠加 - 标题和描述叠加在媒体内容上,增强视觉冲击力
  • 视频自动播放 - 视频静音、循环、行内播放,无需用户操作
  • 动态宽高比 - 每个卡片根据位置和屏幕尺寸使用不同的 aspect ratio
  • 响应式断点 - 5 个断点(mobile/tablet/laptop/desktop/lg-desktop)完整适配
  • BEM 类名体系 - 完整的 BEM 命名,支持深度样式定制
  • 主题切换 - 支持 light/dark 主题
  • TypeScript 严格模式 - 完整类型定义和类型安全

Props 参数

主要参数

参数类型默认值必需说明
dataKspData-组件数据配置
classNamestring-自定义类名

KspData

字段类型默认值必需说明
titlestring-主标题,可选
itemsKspCardItem[]-卡片列表,推荐 7 个,支持 4-9 个
theme'light' | 'dark''light'主题颜色

KspCardItem

字段类型必需说明
titlestring卡片标题,叠加在媒体内容上
descstring卡片描述,叠加在标题下方
imageMedia桌面端图片(视频模式下作为 poster)
mobImageMedia移动端图片(fallback 到 image)
videoMedia桌面端视频 URL,存在时优先显示视频
mobVideoMedia移动端视频 URL(fallback 到 video)

重要说明:

  • 如果同时提供 videoimage,则 image 作为视频的 poster(封面图)
  • 如果只提供 image,则显示静态图片
  • mobImagemobVideo 不提供时,自动 fallback 到 imagevideo

类型定义

完整类型定义

import { Ksp } from '@anker-in/headless-ui/biz' // 媒体资源类型 interface Media { url: string // 媒体资源 URL alt: string // 无障碍替代文本 thumbnailURL?: string // 缩略图 URL(可选) mimeType?: string // MIME 类型(可选) } // 卡片项接口 interface KspCardItem { title?: string // 卡片标题 desc?: string // 卡片描述 image?: Media // 桌面端图片 mobImage?: Media // 移动端图片 video?: Media // 桌面端视频 mobVideo?: Media // 移动端视频 } // 数据配置接口 interface KspData { title?: string // 主标题(可选) items: KspCardItem[] // 卡片列表(必需,4-9个) theme?: 'light' | 'dark' // 主题(可选,默认 light) } // 组件 Props interface KspProps { data: KspData // 数据配置(必需) className?: string // 自定义类名(可选) }

使用示例

1. 标准 7 卡片图片布局

7 卡片是最常用且视觉效果最丰富的配置:

import { Ksp } from '@anker-in/headless-ui/biz' export default function StandardSevenCardsExample() { return ( <Ksp data={{ title: 'Why Choose Our Product', theme: 'light', items: [ { title: 'Improved Cleaning Efficiency', desc: 'HydroJet™ Floor Washing System', image: { url: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200&h=800', alt: 'Improved Cleaning Efficiency', }, mobImage: { url: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=600&h=800', alt: 'Improved Cleaning Efficiency Mobile', }, }, { title: '100% Cleaning Coverage', desc: 'Edge-Cleaning CornerRover™ Arm', image: { url: 'https://images.unsplash.com/photo-1563213126-a4273aed2016?w=1200&h=800', alt: '100% Cleaning Coverage', }, mobImage: { url: 'https://images.unsplash.com/photo-1563213126-a4273aed2016?w=600&h=800', alt: '100% Cleaning Coverage Mobile', }, }, { title: 'All You Need, All-in-One', desc: 'Complete Cleaning Solution', image: { url: 'https://images.unsplash.com/photo-1556911220-bff31c812dba?w=1200&h=800', alt: 'All You Need', }, mobImage: { url: 'https://images.unsplash.com/photo-1556911220-bff31c812dba?w=600&h=800', alt: 'All You Need Mobile', }, }, { title: 'Less Maintenance', desc: 'DuoSpiral™ Detangle Brushes', image: { url: 'https://images.unsplash.com/photo-1550745165-9bc0b252726f?w=1200&h=800', alt: 'Less Maintenance', }, mobImage: { url: 'https://images.unsplash.com/photo-1550745165-9bc0b252726f?w=600&h=800', alt: 'Less Maintenance Mobile', }, }, { title: 'Cleaning Power Upgrade', desc: '20,000 Pa Turbo Suction', image: { url: 'https://images.unsplash.com/photo-1558002038-1055907df827?w=1200&h=800', alt: 'Cleaning Power', }, mobImage: { url: 'https://images.unsplash.com/photo-1558002038-1055907df827?w=600&h=800', alt: 'Cleaning Power Mobile', }, }, { title: 'Intelligent Cleaning', desc: 'Smart App Control & Scheduling', image: { url: 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?w=1200&h=800', alt: 'Intelligent Cleaning', }, mobImage: { url: 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?w=600&h=800', alt: 'Intelligent Cleaning Mobile', }, }, { title: 'AI.See™ Obstacle Avoidance', desc: 'Advanced Object Recognition', image: { url: 'https://images.unsplash.com/photo-1535378917042-10a22c95931a?w=1200&h=800', alt: 'AI Obstacle Avoidance', }, mobImage: { url: 'https://images.unsplash.com/photo-1535378917042-10a22c95931a?w=600&h=800', alt: 'AI Obstacle Avoidance Mobile', }, }, ], }} /> ) }

适用场景: 产品详情页的关键卖点展示,7 卡片是最常用的配置

布局特点(laptop+ 尺寸):

  • 行 1: 大卡片(59.53%) + 小卡片(39.30%)
  • 行 2: 小卡片(29.19%) + 中卡片(39.30%) + 小卡片(29.19%)
  • 行 3: 小卡片(39.30%) + 大卡片(59.53%)

2. 混合图片和视频

前几个卡片使用视频吸引注意力,其余使用图片:

<Ksp data={{ title: 'Experience the Difference', items: [ // 卡片 1: 视频 { title: 'Powerful Performance', desc: 'See it in action', video: { url: 'https://cdn.shopify.com/videos/performance.mp4', alt: 'Performance Video', }, mobVideo: { url: 'https://cdn.shopify.com/videos/performance-mobile.mp4', alt: 'Performance Mobile Video', }, image: { url: 'https://cdn.shopify.com/posters/performance-desktop.jpg', alt: 'Performance Poster', }, mobImage: { url: 'https://cdn.shopify.com/posters/performance-mobile.jpg', alt: 'Performance Poster Mobile', }, }, // 卡片 2: 视频 { title: 'Smart Navigation', desc: 'Advanced mapping technology', video: { url: 'https://cdn.shopify.com/videos/navigation.mp4', alt: 'Navigation Video', }, mobVideo: { url: 'https://cdn.shopify.com/videos/navigation-mobile.mp4', alt: 'Navigation Mobile Video', }, image: { url: 'https://cdn.shopify.com/posters/navigation-desktop.jpg', alt: 'Navigation Poster', }, mobImage: { url: 'https://cdn.shopify.com/posters/navigation-mobile.jpg', alt: 'Navigation Poster Mobile', }, }, // 卡片 3-7: 图片(使用示例1的配置) ], }} />

适用场景: 需要通过视频动态展示产品功能

关键特性:

  • 视频自动播放、循环、静音
  • image 字段作为 video 的 poster(封面图)
  • 桌面端和移动端可使用不同的视频资源(节省移动端流量)

3. 深色主题

使用深色主题适配深色背景页面:

<Ksp data={{ title: '为什么选择我们的产品', theme: 'dark', // 启用深色主题 items: [ { title: '清洁效率大幅提升', desc: 'HydroJet™ 地板清洁系统', image: { url: 'https://cdn.example.com/efficiency.jpg', alt: '清洁效率', }, mobImage: { url: 'https://cdn.example.com/efficiency-mobile.jpg', alt: '清洁效率移动端', }, }, // ... 其他 6 个 items ], }} />

适用场景: 深色背景页面,需要组件融入整体设计

4. 4 卡片简洁布局

强调 4 个核心功能的简洁布局:

<Ksp data={{ title: 'Core Features', items: [ { title: 'Feature 1', desc: 'Description 1', image: { url: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200&h=800', alt: 'Feature 1', }, }, { title: 'Feature 2', desc: 'Description 2', image: { url: 'https://images.unsplash.com/photo-1563213126-a4273aed2016?w=1200&h=800', alt: 'Feature 2', }, }, { title: 'Feature 3', desc: 'Description 3', image: { url: 'https://images.unsplash.com/photo-1556911220-bff31c812dba?w=1200&h=800', alt: 'Feature 3', }, }, { title: 'Feature 4', desc: 'Description 4', image: { url: 'https://images.unsplash.com/photo-1550745165-9bc0b252726f?w=1200&h=800', alt: 'Feature 4', }, }, ], }} />

布局特点(laptop+ 尺寸):

  • 行 1: 大卡片(59.53%) + 小卡片(39.30%)
  • 行 2: 小卡片(39.30%) + 大卡片(59.53%)

适用场景: 强调 4 个核心功能,简洁布局

5. 5 卡片不对称布局

5 个差异化卖点,不对称布局创造视觉层次:

<Ksp data={{ title: 'Five Reasons to Choose', items: [ // 5 个 items 配置 ], }} />

布局特点(laptop+ 尺寸):

  • 行 1: 大卡片(59.53%) + 小卡片(39.30%)
  • 行 2: 小卡片(29.19%) + 中卡片(39.30%) + 小卡片(29.19%)

适用场景: 5 个差异化卖点,不对称布局创造视觉层次

6. 无主标题布局

不显示主标题,卡片直接从顶部开始:

<Ksp data={{ items: [ // 7 个 items 配置 ], // 不提供 title 字段 }} />

效果: 主标题区域不渲染,卡片布局直接从顶部开始

适用场景:

  • 页面其他位置已有标题
  • 卡片本身已足够说明功能,无需额外标题

7. 仅提供桌面端媒体

移动端自动 fallback 到桌面端图片:

<Ksp data={{ title: 'Product Features', items: [ { title: 'Feature 1', desc: 'Description 1', image: { url: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200&h=800', alt: 'Feature 1', }, // 不提供 mobImage,自动使用 image }, // ... 其他 items 同样不提供 mobImage ], }} />

适用场景: 图片尺寸和比例适合所有设备,无需单独提供移动端资源

8. 8 卡片密集布局

展示 8 个完整功能点的密集布局:

<Ksp data={{ title: 'Complete Feature Set', items: [ // 8 个 items 配置 ], }} />

布局特点(laptop+ 尺寸): 3 行对称复杂布局

适用场景: 完整功能展示,信息量较大

9. 自定义样式

使用 BEM 类名体系深度定制样式:

<Ksp data={{ title: 'Premium Features', items: [ // 7 个 items ], }} className="custom-ksp" />

对应 CSS:

/* 主标题 - 渐变色文字 */ .custom-ksp .ksp-title { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } /* 卡片 hover 效果 */ .custom-ksp .ksp-card { transition: transform 0.3s ease; } .custom-ksp .ksp-card:hover { transform: scale(1.02); } /* 卡片标题 - 金色 */ .custom-ksp .ksp-card-title { color: #fbbf24; } /* 卡片描述 - 浅灰色 */ .custom-ksp .ksp-card-desc { color: #d1d5db; }

适用场景: 需要品牌定制化样式的高端产品页

10. 9 卡片最大信息量

展示 9 个功能点的最密集布局:

<Ksp data={{ title: 'All Features', items: [ // 9 个 items 配置 ], }} />

布局特点(laptop+ 尺寸): 3 行最密集布局

适用场景: 信息量最大化,展示所有功能点

注意: 超过 9 个卡片布局配置不存在,组件可能显示异常

响应式行为

Ksp 组件在不同屏幕尺寸下的表现:

断点定义

断点屏幕宽度栅格列数间距特性
Mobile< 768px2 列12px (gap-3)简化布局,半宽或全宽卡片
Tablet≥ 768px3 列16px (gap-4)过渡布局,部分卡片合并
Laptop≥ 1025px10 列16px (gap-4)完整布局,精确栅格控制
Desktop≥ 1440px10 列16px (gap-4)增大卡片尺寸,优化间距
LG Desktop≥ 1920px10 列16px (gap-4)最大尺寸,最佳视觉效果

7 卡片布局配置表

卡片索引MobileTabletLaptop+说明
0 (第1张)col-span-2-col-span-6大卡片,占行首 59.53%
1 (第2张)col-span-1-col-span-4小卡片,占行尾 39.30%
2 (第3张)col-span-1-col-span-3小卡片,占第2行首 29.19%
3 (第4张)col-span-2col-span-1col-span-4中卡片,占第2行中 39.30%
4 (第5张)col-span-1-col-span-3小卡片,占第2行尾 29.19%
5 (第6张)col-span-1-col-span-4小卡片,占第3行首 39.30%
6 (第7张)col-span-2-col-span-6大卡片,占第3行尾 59.53%

文字大小响应式

// 主标题 (Heading size) Tablet+: size={3} // 较大字号 Mobile: size={2} // 较小字号 // 卡片标题 (Heading size) Tablet+: size={3} Mobile: size={2} // 卡片描述 (Text size) Mobile: 14px (text-[14px]) Tablet: 14px (tablet:text-[14px]) Laptop: 14px (laptop:text-[14px]) Desktop: 16px (desktop:text-[16px]) LG Desktop: 18px (lg-desktop:text-[18px])

内容间距响应式

// 卡片内边距 Mobile: p-4 (16px) Desktop: desktop:p-6 (24px) LG Desktop: lg-desktop:p-8 (32px) // 标题 margin-top desc 和 title 之间: mt-[4px] (固定 4px)

视频显示响应式

// 桌面端视频 laptop:inline-block hidden // Laptop+ 显示 // 移动端视频 laptop:hidden inline-block // Mobile 和 Tablet 显示 translate-y-[-80px] // 向上偏移 80px(设计需求)

不同卡片数量的布局差异

卡片数量布局特点行数适用场景
42 行,对称布局2核心功能简洁展示
52 行,不对称布局25 个差异化卖点
62 行,对称复杂布局2平衡的功能展示
73 行,不对称布局(推荐)3最常用,视觉层次丰富
83 行,对称复杂布局3完整功能展示
93 行,最密集布局3信息量最大化

核心算法解析

1. 布局配置算法

根据卡片数量和索引动态计算栅格布局类:

/** * 布局配置对象 * key: 卡片总数量 * value: 每个索引对应的布局类名字符串 */ const LAYOUT_CONFIG: Record<number, Record<number, string>> = { 7: { 0: 'col-span-2 laptop:col-span-6 aspect-w-[358] aspect-h-[240] tablet:aspect-w-[465] tablet:aspect-h-[240] laptop:aspect-w-[440] laptop:aspect-h-[240] desktop:aspect-w-[648] desktop:aspect-h-[256] lg-desktop:aspect-w-[824] lg-desktop:aspect-h-[320]', 1: 'col-span-1 laptop:col-span-4 aspect-w-[173] aspect-h-[240] tablet:aspect-w-[355] tablet:aspect-h-[240] laptop:aspect-w-[288] laptop:aspect-h-[240] desktop:aspect-w-[424] desktop:aspect-h-[256] lg-desktop:aspect-w-[540] lg-desktop:aspect-h-[320]', 2: 'col-span-1 laptop:col-span-3 aspect-w-[173] aspect-h-[240] tablet:aspect-w-[355] tablet:aspect-h-[240] laptop:aspect-w-[209] laptop:aspect-h-[240] desktop:aspect-w-[308] desktop:aspect-h-[256] lg-desktop:aspect-w-[391] lg-desktop:aspect-h-[320]', 3: 'col-span-2 tablet:col-span-1 laptop:col-span-4 aspect-w-[358] aspect-h-[240] tablet:aspect-w-[232] tablet:aspect-h-[240] laptop:aspect-w-[288] laptop:aspect-h-[240] desktop:aspect-w-[424] desktop:aspect-h-[256] lg-desktop:aspect-w-[540] lg-desktop:aspect-h-[320]', 4: 'col-span-1 laptop:col-span-3 aspect-w-[173] aspect-h-[240] tablet:aspect-w-[355] tablet:aspect-h-[240] laptop:aspect-w-[209] laptop:aspect-h-[240] desktop:aspect-w-[308] desktop:aspect-h-[256] lg-desktop:aspect-w-[391] lg-desktop:aspect-h-[320]', 5: 'col-span-1 laptop:col-span-4 aspect-w-[173] aspect-h-[240] tablet:aspect-w-[355] tablet:aspect-h-[240] laptop:aspect-w-[288] laptop:aspect-h-[240] desktop:aspect-w-[424] desktop:aspect-h-[256] lg-desktop:aspect-w-[540] lg-desktop:aspect-h-[320]', 6: 'col-span-2 laptop:col-span-6 aspect-w-[358] aspect-h-[240] tablet:aspect-w-[465] tablet:aspect-h-[240] laptop:aspect-w-[440] laptop:aspect-h-[240] desktop:aspect-w-[648] desktop:aspect-h-[256] lg-desktop:aspect-w-[824] lg-desktop:aspect-h-[320]', }, // ... 配置 4-9 的其他布局 } /** * 获取卡片布局类名 * @param index 卡片索引(0-based) * @param totalCount 卡片总数量 * @returns 布局类名字符串 */ function getCardLayoutClass(index: number, totalCount: number): string { const layoutConfig = LAYOUT_CONFIG[totalCount] if (!layoutConfig) { console.warn(`No layout config for ${totalCount} cards`) return '' } return layoutConfig[index] || '' } // 使用示例 {items.map((item, index) => ( <div key={index} className={cn( 'ksp-card', getCardLayoutClass(index, items.length) )} > {/* 卡片内容 */} </div> ))} // 时间复杂度: O(1) - 哈希表查找 // 空间复杂度: O(1) - 常量对象存储

2. 动态宽高比系统

根据卡片位置和断点动态计算 aspect ratio:

/** * 宽高比计算逻辑 * 以卡片 0 (7卡片布局) 为例 */ const aspectRatioConfig = { mobile: { w: 358, h: 240 }, // 358/240 ≈ 1.49:1 tablet: { w: 465, h: 240 }, // 465/240 ≈ 1.94:1 laptop: { w: 440, h: 240 }, // 440/240 ≈ 1.83:1 desktop: { w: 648, h: 256 }, // 648/256 ≈ 2.53:1 lgDesktop: { w: 824, h: 320 }, // 824/320 ≈ 2.58:1 } /** * 计算宽高比类名 * @param cardIndex 卡片索引 * @param totalCards 卡片总数 * @param breakpoint 断点 * @returns aspect ratio 类名 */ function getAspectRatioClass( cardIndex: number, totalCards: number, breakpoint: string ): string { const config = aspectRatioConfig[breakpoint] return `aspect-w-[${config.w}] aspect-h-[${config.h}]` } /** * 为什么需要动态宽高比? * * 1. 栅格列数不同: * - Mobile: 2列 (每列约50%) * - Tablet: 3列 (每列约33%) * - Laptop+: 10列 (col-span-3=30%, col-span-4=40%, col-span-6=60%) * * 2. 保持视觉协调: * - 大卡片(col-span-6)需要更宽的宽高比(2.5:1) * - 中卡片(col-span-4)使用中等宽高比(1.9:1) * - 小卡片(col-span-3)使用较窄宽高比(1.5:1) * * 3. 屏幕尺寸适配: * - Desktop+ 增大 height 值,避免卡片过扁 * - Mobile 减小 width 值,适配小屏幕 */ // 时间复杂度: O(1) // 空间复杂度: O(1)

3. 媒体资源 Fallback 逻辑

智能 fallback 确保所有设备都有媒体显示:

/** * 获取桌面端图片 URL * @param item 卡片项 * @returns 桌面端图片 URL */ function getDesktopImageUrl(item: KspCardItem): string { return item.image?.url || item.mobImage?.url || '' } /** * 获取移动端图片 URL * @param item 卡片项 * @returns 移动端图片 URL */ function getMobileImageUrl(item: KspCardItem): string { return item.mobImage?.url || item.image?.url || '' } /** * 获取桌面端视频 URL * @param item 卡片项 * @returns 桌面端视频 URL */ function getDesktopVideoUrl(item: KspCardItem): string { return item.video?.url || item.mobVideo?.url || '' } /** * 获取移动端视频 URL * @param item 卡片项 * @returns 移动端视频 URL */ function getMobileVideoUrl(item: KspCardItem): string { return item.mobVideo?.url || item.video?.url || '' } /** * 判断是否为视频卡片 * @param item 卡片项 * @returns 是否为视频卡片 */ function isVideoCard(item: KspCardItem): boolean { return !!(item.video?.url || item.mobVideo?.url) } /** * Fallback 优先级: * * 桌面端: * 1. image (优先) * 2. mobImage (fallback) * * 移动端: * 1. mobImage (优先) * 2. image (fallback) * * 视频同理 */ // 使用示例 <Picture source={getDesktopImageUrl(item)} alt={item.image?.alt || item.mobImage?.alt || ''} className="laptop:inline-block hidden" // 仅桌面端显示 /> <Picture source={getMobileImageUrl(item)} alt={item.mobImage?.alt || item.image?.alt || ''} className="laptop:hidden inline-block" // 仅移动端显示 /> // 时间复杂度: O(1) // 空间复杂度: O(1)

4. BEM 类名生成算法

完整的 BEM 命名体系,支持深度定制:

/** * BEM 类名常量 */ const BEM_CLASSES = { root: 'ksp-container', darkTheme: 'aiui-dark', title: 'ksp-title', layout: 'ksp-layout', card: 'ksp-card', cardImage: 'ksp-card-image', cardVideo: 'ksp-card-video', cardVideoDesktop: 'ksp-card-video-desktop', cardVideoMobile: 'ksp-card-video-mobile', cardContent: 'ksp-card-content', cardTitle: 'ksp-card-title', cardDesc: 'ksp-card-desc', } /** * 生成根容器类名 * @param theme 主题 * @param customClassName 自定义类名 * @returns 完整类名字符串 */ function getRootClassName( theme: 'light' | 'dark', customClassName?: string ): string { return cn( BEM_CLASSES.root, theme === 'dark' && BEM_CLASSES.darkTheme, customClassName ) } /** * 生成卡片类名 * @param index 卡片索引 * @param totalCount 卡片总数 * @returns 卡片类名字符串 */ function getCardClassName(index: number, totalCount: number): string { return cn( BEM_CLASSES.card, 'relative', 'overflow-hidden', 'rounded-lg', getCardLayoutClass(index, totalCount) ) } /** * BEM 命名规范: * * Block: ksp * Element: title, layout, card, card-image, card-video, card-content, card-title, card-desc * Modifier: dark (通过 aiui-dark 类实现) * * 示例: * - ksp-container (Block) * - ksp-title (Block__Element) * - ksp-card (Block__Element) * - ksp-card-title (Block__Element__SubElement) * - ksp-container.aiui-dark (Block--Modifier) */ // 时间复杂度: O(n), n为类名数量(通常很小) // 空间复杂度: O(1)

5. 响应式栅格计算

10 列栅格系统的精确计算:

/** * 栅格系统配置 */ const GRID_CONFIG = { mobile: { columns: 2, gap: 12, // gap-3 }, tablet: { columns: 3, gap: 16, // gap-4 }, laptop: { columns: 10, gap: 16, // gap-4 }, desktop: { columns: 10, gap: 16, // gap-4 }, lgDesktop: { columns: 10, gap: 16, // gap-4 }, } /** * 计算列宽度百分比 * @param colSpan 列跨度 * @param totalColumns 总列数 * @param gap 间距(px) * @returns 宽度百分比 */ function calculateColumnWidth( colSpan: number, totalColumns: number, gap: number ): number { // 总间距 = (列数 - 1) × 间距 const totalGap = (totalColumns - 1) * gap // 每列宽度 = (100% - 总间距) / 列数 const columnWidth = (100 - totalGap) / totalColumns // 跨列宽度 = 列宽度 × 跨度 + 间距 × (跨度 - 1) const width = columnWidth * colSpan + gap * (colSpan - 1) return width } // 示例计算(Laptop, 10列) // col-span-6 (大卡片): calculateColumnWidth(6, 10, 16) // = (100 - 9*16) / 10 * 6 + 16 * 5 // = (100 - 144) / 10 * 6 + 80 // = -4.4 * 6 + 80 // ≈ 59.53% // col-span-4 (中卡片): calculateColumnWidth(4, 10, 16) // ≈ 39.30% // col-span-3 (小卡片): calculateColumnWidth(3, 10, 16) // ≈ 29.19% // 时间复杂度: O(1) // 空间复杂度: O(1)

6. 文字叠加渲染算法

文字内容叠加在媒体上的渲染逻辑:

/** * 渲染卡片内容(文字叠加) */ function renderCardContent(item: KspCardItem) { return ( <div className={cn( BEM_CLASSES.cardContent, 'absolute', 'bottom-0', 'left-0', 'right-0', 'p-4', 'desktop:p-6', 'lg-desktop:p-8', 'bg-gradient-to-t', 'from-black/60', // 半透明黑色渐变 'to-transparent' )} > {item.title && ( <Heading as="h3" size={3} className={cn( BEM_CLASSES.cardTitle, 'text-white', // 白色文字确保可读性 'mb-[4px]' )} > {item.title} </Heading> )} {item.desc && ( <Text as="p" size={2} className={cn( BEM_CLASSES.cardDesc, 'text-white/90', // 稍透明的白色 'text-[14px]', 'desktop:text-[16px]', 'lg-desktop:text-[18px]' )} > {item.desc} </Text> )} </div> ) } /** * 文字叠加关键技术: * * 1. 绝对定位: absolute bottom-0 left-0 right-0 * - 覆盖在图片/视频上方 * - 固定在卡片底部 * * 2. 渐变背景: bg-gradient-to-t from-black/60 to-transparent * - 从底部向上渐变 * - 确保文字区域有足够对比度 * * 3. 白色文字: text-white * - 在深色背景上清晰可读 * - 符合 WCAG 对比度标准(至少 4.5:1) * * 4. 响应式内边距: p-4 desktop:p-6 lg-desktop:p-8 * - 小屏幕 16px,桌面端 24px,超大屏 32px * - 确保文字不贴边 */ // 时间复杂度: O(1) // 空间复杂度: O(1)

7. 视频处理算法

视频自动播放和响应式显示:

/** * 渲染视频元素 */ function renderVideo( videoUrl: string, posterUrl: string, alt: string, isMobile: boolean ) { return ( <video playsInline // 移动端行内播放,不全屏 autoPlay // 自动播放 muted // 静音(确保自动播放不被浏览器阻止) loop // 循环播放 src={videoUrl} poster={posterUrl} // 封面图 aria-label={alt} className={cn( isMobile ? BEM_CLASSES.cardVideoMobile : BEM_CLASSES.cardVideoDesktop, 'absolute', 'inset-0', 'w-full', 'h-full', 'object-cover', isMobile ? 'laptop:hidden inline-block' : 'laptop:inline-block hidden', isMobile && 'translate-y-[-80px]' // 移动端向上偏移 )} /> ) } /** * 视频处理关键点: * * 1. 自动播放要求: * - muted: 必须静音才能自动播放(浏览器策略) * - playsInline: 移动端不全屏播放 * * 2. 封面图 (poster): * - 视频加载前显示 * - 提升首屏体验 * - 使用 image 字段作为 poster * * 3. 响应式显示: * - 桌面端: laptop:inline-block hidden * - 移动端: laptop:hidden inline-block * - 避免两个视频同时加载 * * 4. 移动端偏移: * - translate-y-[-80px] * - 将重要内容移至卡片中心 * - 避免被文字叠加遮挡 */ // 时间复杂度: O(1) // 空间复杂度: O(1)

设计规范

卡片数量规范

  • 推荐数量: 7 个卡片(最常用,布局最丰富)
  • 支持范围: 4-9 个卡片
  • 最小数量: 4 个(低于 4 个布局配置不存在)
  • 最大数量: 9 个(超过 9 个布局会过于拥挤)

为什么推荐 7 个?

7 个卡片布局特点:

  • 视觉层次丰富: 大中小三种尺寸交替,创造动态视觉效果
  • 不对称美学: 不对称布局更具现代感和吸引力
  • 信息密度适中: 不会过于拥挤或过于空旷
  • 适合产品页: 7 个卖点刚好涵盖产品核心特性,不会信息过载

标题和描述规范

  • 主标题长度: 建议 10-30 个字符,过长影响阅读
  • 卡片标题长度: 建议 5-20 个字符,需要在图片上清晰可读
  • 卡片描述长度: 建议 10-40 个字符,简洁有力

图片规格

  • 桌面端图片尺寸: 推荐 1200x800px 或更大
  • 移动端图片尺寸: 推荐 600x800px 或更大
  • 图片格式: 推荐 WebP 或 AVIF(fallback 到 JPG/PNG)
  • 图片质量: 80-90% 质量,平衡视觉效果和加载速度
  • 宽高比: 根据卡片位置自动适配,无需固定比例

视频规格

  • 视频格式: 推荐 MP4 (H.264 编码)
  • 桌面端视频大小: 建议 5MB 以内
  • 移动端视频大小: 建议 2MB 以内(使用压缩版本)
  • 视频时长: 建议 5-15 秒,循环播放
  • 视频分辨率:
    • 桌面端: 1920x1080 或 1280x720
    • 移动端: 720x480 或更低
  • 封面图 (poster): 必须提供,使用 image/mobImage 字段

文字叠加规范

  • 文字背景: 卡片内容区域自动添加半透明渐变背景,确保文字可读性
  • 文字颜色: 使用白色文字,根据主题自动调整
  • 文字对齐: 左对齐,确保在所有卡片尺寸下保持一致
  • 内边距: 移动端 16px,桌面端 24px,超大屏 32px

颜色规范

/* Light 主题 */ :root { --ksp-text-primary: #111827; /* 主标题 */ --ksp-text-overlay: #ffffff; /* 叠加文字 */ --ksp-overlay-bg: rgba(0, 0, 0, 0.6); /* 叠加背景 */ } /* Dark 主题 */ :root[data-theme='dark'] { --ksp-text-primary: #f9fafb; --ksp-text-overlay: #ffffff; --ksp-overlay-bg: rgba(0, 0, 0, 0.7); }

无障碍性

Ksp 组件遵循 WCAG 2.1 AA 标准:

语义化 HTML

<section data-ui-component-id="ksp"> {/* 使用 <section> 作为根元素 */} <Heading as="h1" size={4}> {/* 主标题使用 h1 */} {title} </Heading> <div className="ksp-layout"> {items.map((item, index) => ( <div className="ksp-card"> {/* 卡片容器 */} <Picture /> {/* 图片使用 Picture 组件 */} <video /> {/* 视频使用原生 video 标签 */} <div className="ksp-card-content"> <Heading as="h3" size={3}> {/* 卡片标题使用 h3 */} {item.title} </Heading> <Text as="p" size={2}> {/* 描述使用 p 标签 */} {item.desc} </Text> </div> </div> ))} </div> </section>

ARIA 属性

  • data-ui-component-id: "ksp" - 组件标识,便于测试和追踪
  • video 元素:
    • playsInline - 移动端行内播放,不全屏
    • muted - 静音播放,避免打扰用户
    • poster - 视频封面图,提升加载体验
    • loop - 循环播放
    • autoPlay - 自动播放(静音状态下)
    • aria-label - 视频描述

图片无障碍

  • alt 属性: 所有 Media 对象必须提供有意义的 alt 文本
  • 视频 alt: 描述视频内容,如 “产品清洁演示视频”
  • 图片 alt: 描述图片实际内容,避免通用词如 “image”、“photo”

示例:

// ❌ 错误: 通用 alt 文本 image: { url: '...', alt: 'Image', } // ✅ 正确: 描述性 alt 文本 image: { url: '...', alt: 'HydroJet Floor Washing System in action', }

颜色对比度

所有文本与背景的对比度符合WCAG AA标准(最低4.5:1):

// 文字叠加对比度检查 const contrastRatios = { '白色文字/渐变黑色背景': '12:1', // ✅ #ffffff / rgba(0,0,0,0.6) '主标题/浅色背景': '7:1', // ✅ #111827 / #ffffff '卡片描述/叠加背景': '11:1', // ✅ rgba(255,255,255,0.9) / rgba(0,0,0,0.6) }

键盘导航

  • 组件为静态展示,无需键盘交互
  • 视频自动播放,无需用户操作
  • 如需添加交互(如卡片点击跳转),需使用 &lt;a&gt; 标签或 &lt;button&gt; 标签

性能优化

1. 布局配置常量化

布局配置对象常量化,避免重复计算:

// ✅ 正确: 常量化布局配置 const LAYOUT_CONFIG: Record<number, Record<number, string>> = { 7: { 0: 'col-span-2 laptop:col-span-6 ...', 1: 'col-span-1 laptop:col-span-4 ...', // ... }, // ... 其他配置 } // ❌ 错误: 每次渲染都计算 function getCardLayoutClass(index: number, totalCount: number): string { // 复杂的计算逻辑... }

2. 响应式图片策略

使用 Picture 组件自动处理响应式图片:

// 桌面端显示 image <Picture source={image?.url || mobImage?.url} alt={image?.alt || mobImage?.alt || ''} className="laptop:inline-block hidden" /> // 移动端显示 mobImage <Picture source={mobImage?.url || image?.url} alt={mobImage?.alt || image?.alt || ''} className="laptop:hidden inline-block" />

优化要点:

  • 使用 CSS 控制显示/隐藏,避免两张图片同时加载
  • object-cover 确保图片填满容器
  • object-bottom 定位到底部,避免裁剪重要内容

3. 视频优化

// 桌面端视频 <video playsInline autoPlay muted loop src={video?.url} poster={image?.url} // 使用 image 作为封面图 className="laptop:inline-block hidden" // 仅在 laptop+ 显示 /> // 移动端视频 <video playsInline autoPlay muted loop src={mobVideo?.url} poster={mobImage?.url} // 使用 mobImage 作为封面图 className="laptop:hidden inline-block" // 仅在 mobile/tablet 显示 />

优化要点:

  • 使用 CSS 控制桌面端和移动端视频显示,避免两个视频同时加载
  • poster 属性提供封面图,优化首屏体验
  • playsInline 避免移动端全屏播放
  • muted 确保自动播放不被浏览器阻止

4. CSS 优化

/* 使用 Tailwind aspect ratio 类 */ aspect-w-[358] aspect-h-[240] /* 动态宽高比 */ /* 使用 Tailwind 断点类 */ tablet:aspect-w-[465] laptop:aspect-w-[440] /* 避免使用复杂的 CSS 动画 */ /* 卡片布局纯静态,无动画开销 */

优化效果:

  • Tailwind 类编译为原子 CSS,体积小
  • aspect ratio 使用 CSS aspect-ratio 属性(现代浏览器原生支持)
  • 无 JS 计算,纯 CSS 布局,性能最优

5. React 优化

// 1. 使用 index 作为 key(卡片顺序固定,不会动态增删) {items.map((item, index) => ( <Card key={index} {...item} /> ))} // 2. 卡片组件独立抽离 const Card = ({ title, desc, image, mobImage, video, mobVideo }) => { // 卡片渲染逻辑 } // 3. 布局计算函数 const getCardLayoutClass = (index: number, totalCount: number): string => { return LAYOUT_CONFIG[totalCount]?.[index] || '' }

常见问题 FAQ

1. 为什么推荐 7 个卡片?

7 个卡片是经过设计优化的最佳配置:

7 卡片布局特点:

  • 行 1: 大卡片 + 小卡片 (2 个)
  • 行 2: 小卡片 + 中卡片 + 小卡片 (3 个)
  • 行 3: 小卡片 + 大卡片 (2 个)

优势:

  • 视觉层次丰富: 大中小三种尺寸交替,创造动态视觉效果
  • 不对称美学: 不对称布局更具现代感和吸引力
  • 信息密度适中: 不会过于拥挤或过于空旷
  • 适合产品页: 7 个卖点刚好涵盖产品核心特性,不会信息过载

其他数量:

  • 4-6 个: 适合简化场景,信息量较少
  • 8-9 个: 适合信息密集场景,但布局会较拥挤

2. 卡片数量必须是 7 个吗?

不是,组件支持 4-9 个卡片:

// 支持的卡片数量和对应布局 4: 2 行布局 5: 2 行布局 6: 2 行布局 7: 3 行布局(推荐) 8: 3 行布局 9: 3 行布局

注意:

  • 少于 4 个: 布局配置不存在,组件可能显示异常
  • 超过 9 个: 布局配置不存在,超出的卡片可能显示异常
  • 建议根据实际需求选择 4-9 个之间的数量

3. 图片和视频可以混合使用吗?

可以,每个卡片可以独立配置:

// 示例: 前 2 个卡片使用视频,其余使用图片 items: [ { title: 'Video Card 1', video: { url: 'video1.mp4', alt: 'Video 1' }, mobVideo: { url: 'video1-mobile.mp4', alt: 'Video 1 Mobile' }, image: { url: 'poster1.jpg', alt: 'Poster 1' }, // 作为 poster mobImage: { url: 'poster1-mobile.jpg', alt: 'Poster 1 Mobile' }, }, { title: 'Video Card 2', video: { url: 'video2.mp4', alt: 'Video 2' }, mobVideo: { url: 'video2-mobile.mp4', alt: 'Video 2 Mobile' }, image: { url: 'poster2.jpg', alt: 'Poster 2' }, mobImage: { url: 'poster2-mobile.jpg', alt: 'Poster 2 Mobile' }, }, { title: 'Image Card 3', image: { url: 'image3.jpg', alt: 'Image 3' }, mobImage: { url: 'image3-mobile.jpg', alt: 'Image 3 Mobile' }, }, // ... 其他图片卡片 ]

最佳实践:

  • 视频卡片放在前面,吸引用户注意力
  • 不要全部使用视频,避免页面加载过慢
  • 建议 1-3 个视频卡片,其余使用图片

4. mobImage/mobVideo 不提供会怎样?

组件使用 fallback 机制:

// 图片 fallback 桌面端: image 移动端: mobImage || image // 未提供 mobImage 时使用 image // 视频 fallback 桌面端: video 移动端: mobVideo || video // 未提供 mobVideo 时使用 video

建议:

  • 至少提供 image,确保桌面端和移动端都有图片显示
  • 如果图片尺寸和比例合适,可以不提供 mobImage
  • 视频建议提供 mobVideo,使用压缩版本节省移动端流量

5. 如何自定义卡片样式?

组件使用完整的 BEM 类名体系:

/* 根元素 */ .ksp-container { } .ksp-container.aiui-dark { } /* 深色主题 */ /* 主标题 */ .ksp-title { } /* 布局容器 */ .ksp-layout { } /* 卡片 */ .ksp-card { } .ksp-card-image { } .ksp-card-video { } .ksp-card-video-desktop { } .ksp-card-video-mobile { } .ksp-card-content { } .ksp-card-title { } .ksp-card-desc { }

示例:

/* 卡片 hover 效果 */ .ksp-card { transition: transform 0.3s ease; } .ksp-card:hover { transform: scale(1.02); } /* 主标题渐变色 */ .ksp-title { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } /* 卡片标题金色 */ .ksp-card-title { color: #fbbf24; }

6. 为什么移动端视频有 translate-y-[-80px]?

这是设计需求,为了视觉效果:

className="translate-y-[-80px] object-contain object-center"

原因:

  • 移动端卡片高度固定(aspect ratio 控制)
  • 视频内容可能在顶部或中部
  • 向上偏移 80px 可以将视频重要内容移至卡片中心,避免被文字叠加遮挡

如需调整:

  • 修改 translate-y
  • 或移除该类,使用默认定位

7. 可以不显示主标题吗?

可以,title 字段是可选的:

<Ksp data={{ items: [ // 7 个 items 配置 ], // 不提供 title 字段 }} />

效果: 主标题区域不渲染,卡片布局直接从顶部开始

适用场景:

  • 页面其他位置已有标题
  • 卡片本身已足够说明功能,无需额外标题

8. 布局配置可以自定义吗?

布局配置在组件内部硬编码:

const LAYOUT_CONFIG: Record<number, Record<number, string>> = { 7: { 0: 'col-span-2 laptop:col-span-6 aspect-w-[358]...', 1: 'col-span-1 laptop:col-span-4 aspect-w-[173]...', // ... } }

如需自定义:

  1. 方案 1: 通过 className prop 传入自定义类覆盖
  2. 方案 2: 复制组件代码,修改 LAYOUT_CONFIG
  3. 方案 3: 使用 CSS 覆盖对应的 BEM 类

不建议修改原因:

  • 布局经过精心设计,确保各尺寸协调
  • 修改可能导致某些断点显示异常
  • 宽高比计算复杂,需要同时修改多个断点

9. 如何确保视频自动播放?

视频自动播放需要满足浏览器策略:

<video playsInline // ✅ 必需: 移动端行内播放 autoPlay // ✅ 必需: 自动播放 muted // ✅ 必需: 静音(浏览器只允许静音视频自动播放) loop // ✅ 推荐: 循环播放 src={videoUrl} poster={posterUrl} // ✅ 推荐: 封面图 />

关键点:

  • muted 是必需的,浏览器只允许静音视频自动播放
  • playsInline 避免移动端全屏播放
  • poster 提供封面图,优化首屏体验

10. 如何优化移动端流量?

提供移动端专用的压缩资源:

items: [ { title: 'Feature', desc: 'Description', // 桌面端: 高清图片/视频 image: { url: 'https://cdn.example.com/desktop-1920x1080.jpg', // 1920x1080, ~500KB alt: 'Feature', }, video: { url: 'https://cdn.example.com/desktop-video.mp4', // 1080p, ~5MB alt: 'Feature Video', }, // 移动端: 压缩图片/视频 mobImage: { url: 'https://cdn.example.com/mobile-720x480.jpg', // 720x480, ~150KB alt: 'Feature Mobile', }, mobVideo: { url: 'https://cdn.example.com/mobile-video.mp4', // 480p, ~1.5MB alt: 'Feature Mobile Video', }, }, ]

优化策略:

  • 移动端图片: 宽度 600-800px,质量 75-80%
  • 移动端视频: 分辨率 720x480 或更低,码率 1-2Mbps
  • 使用 WebP/AVIF 图片格式(fallback 到 JPG)
  • 使用 H.264 视频编码,兼容性最好

相关资源

  • Storybook 文档: /storybook/ksp - 查看所有变体和交互示例
  • 组件源码: /packages/ui/src/biz-components/Ksp - 查看完整实现代码
  • 类型定义: /packages/ui/src/biz-components/Ksp/types.ts - 完整TypeScript类型
  • Figma设计稿: Ksp Design 
  • 单元测试: /packages/ui/tests/Ksp.test.tsx - 测试用例和覆盖率
  • 布局配置文档: /docs/ksp-layout-system.md - 详细的布局系统说明
  • 响应式断点指南: /docs/responsive-breakpoints.md - 断点使用最佳实践
  • 视频优化指南: /docs/video-optimization.md - 视频压缩和格式建议
  • 无障碍性测试报告: /docs/ksp-accessibility-audit.md - WCAG合规性报告
Last updated on