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

ProductCompare (产品对比)

产品对比展示组件,支持左右对比和拖拽切换【✅ 已发布】

加载中...
当前视口: 1920px × 600px场景: 浅色主题三产品对比
打开链接

功能特性

  • 2-3 产品对比支持 - 灵活支持2个或3个产品并排对比展示
  • 3种标题类型 - selling-point(卖点标题)、primary(一级标题)、secondary(二级标题)
  • 灵活布局比例 - 2产品支持2:3或1:1,3产品固定1:1:1等宽
  • 文本对齐配置 - 支持左对齐(left)和居中对齐(center)
  • 副标题图片展示 - 可在副标题下方显示对比图表或说明图
  • 媒体类型自适应 - 自动识别视频/图片并渲染对应元素
  • 响应式资源管理 - 支持桌面端和移动端独立媒体资源
  • 智能懒加载机制 - IntersectionObserver实现,提前1500px加载
  • 视频自动播放 - 支持自动播放、循环、静音、内联播放
  • 智能标签样式 - 最后一个产品使用品牌色,突出自家产品
  • 主题支持 - 内置light/dark主题切换
  • 移动端优化 - 移动端垂直布局,2产品时智能顺序交换

Props 参数

ProductCompareProps

Prop类型默认值必需说明
dataProductCompareData-产品对比完整配置数据
classNamestring''外层容器自定义类名

ProductCompareData

字段类型默认值必需说明
titlestring-主标题文案
titleTypeTitleType'primary'标题类型:‘selling-point’ | ‘primary’ | ‘secondary’
titleIconMedia-标题图标(仅当titleType=‘selling-point’时有效)
subtitlestring-副标题文案
subtitleImageMedia-副标题下方的图片(如对比表、图表)
textAlignTextAlign'left'文本对齐方式:‘left’ | ‘center’
productsProductItemData[][]产品列表(支持2-3个产品)
twoImageRatioTwoImageRatio'2:3'2张图片时的宽度比例:‘2:3’ | ‘1:1’
threeImageRatioThreeImageRatio'1:1:1'3张图片时的宽度比例(固定’1:1:1’)
themeTheme'light'主题模式:‘light’ | ‘dark’

ProductItemData

字段类型默认值必需说明
textstring-产品标签文本(如”其他品牌”、“Anker E25”)
mediaMedia-桌面端媒体资源(视频或图片)
mobMediaMedia-移动端媒体资源(未指定时使用media)
posterMedia-桌面端视频封面图
mobPosterMedia-移动端视频封面图

Media 类型

interface Media { url: string // 媒体资源URL alt?: string // 替代文本(无障碍必需) mimeType?: string // MIME类型,用于判断视频/图片 width?: number // 媒体宽度(可选) height?: number // 媒体高度(可选) }

类型定义

import type { Media, Theme } from '../../types/props.js' export interface ProductItemData { /** 产品标签文本 */ text?: string /** 桌面端媒体(视频或图片) */ media?: Media /** 移动端媒体(视频或图片) */ mobMedia?: Media /** 桌面端封面图(仅用于视频) */ poster?: Media /** 移动端封面图(仅用于视频) */ mobPoster?: Media } /** 标题类型 */ export type TitleType = 'selling-point' | 'primary' | 'secondary' /** 文本对齐方式 */ export type TextAlign = 'left' | 'center' /** 图片宽度比例(2张图片时) */ export type TwoImageRatio = '2:3' | '1:1' /** 图片宽度比例(3张图片时) */ export type ThreeImageRatio = '1:1:1' export interface ProductCompareData { title?: string titleType?: TitleType titleIcon?: Media subtitle?: string subtitleImage?: Media textAlign?: TextAlign products?: ProductItemData[] twoImageRatio?: TwoImageRatio threeImageRatio?: ThreeImageRatio theme?: Theme } export interface ProductCompareProps { data: ProductCompareData className?: string }

使用示例

示例 1: 基础2产品对比(默认2:3比例)

import { ProductCompare } from '@anker-in/headless-ui/biz' export default function BasicCompare() { return ( <ProductCompare data={{ title: 'Cleaning Performance Comparison', titleType: 'primary', subtitle: 'Real-world cleaning effectiveness test results', textAlign: 'left', twoImageRatio: '2:3', // 左侧40%,右侧60% theme: 'light', products: [ { text: 'Standard Mop', media: { url: 'https://images.unsplash.com/photo-1581578731548-c64695cc6952?w=800&h=600&q=80', alt: 'Standard mop cleaning', mimeType: 'image/jpeg', width: 800, height: 600, }, }, { text: 'Anker E25 Robot', media: { url: 'https://images.unsplash.com/photo-1556911220-e15b29be8c8f?w=800&h=600&q=80', alt: 'Anker E25 robot cleaning', mimeType: 'image/jpeg', width: 800, height: 600, }, }, ], }} /> ) }

示例 2: 卖点标题 + 图标 + 居中对齐

import { ProductCompare } from '@anker-in/headless-ui/biz' export default function SellingPointCompare() { return ( <ProductCompare data={{ titleType: 'selling-point', title: 'Smart Cleaning System', titleIcon: { url: 'https://cdn-icons-png.flaticon.com/512/2917/2917995.png', alt: 'Smart icon', mimeType: 'image/png', }, subtitle: 'Comprehensive comparison of cleaning technologies', subtitleImage: { url: 'https://images.unsplash.com/photo-1581291518633-83b4ebd1d83e?w=800&h=200&fit=crop', alt: 'AI cleaning system comparison chart', mimeType: 'image/jpeg', width: 800, height: 200, }, textAlign: 'center', products: [ { text: 'Traditional Cleaning', media: { url: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=800&h=600&q=80', alt: 'Traditional cleaning method', mimeType: 'image/jpeg', }, }, { text: 'Anker Smart Cleaning', media: { url: 'https://images.unsplash.com/photo-1593642632559-0c6d3fc62b89?w=800&h=600&q=80', alt: 'Anker smart cleaning', mimeType: 'image/jpeg', }, }, ], }} /> ) }

示例 3: 3产品对比(1:1:1等宽)

import { ProductCompare } from '@anker-in/headless-ui/biz' export default function ThreeProductCompare() { return ( <ProductCompare data={{ title: 'Three Flagship Models Comparison', titleType: 'primary', subtitle: 'Performance, battery life, and smart features', textAlign: 'left', products: [ { text: 'Brand A', media: { url: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=800&h=600&q=80', alt: 'Brand A product', mimeType: 'image/jpeg', }, mobMedia: { url: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=600&h=800&fit=crop', alt: 'Brand A mobile', mimeType: 'image/jpeg', }, }, { text: 'Brand B', media: { url: 'https://images.unsplash.com/photo-1572635196237-14b3f281503f?w=800&h=600&q=80', alt: 'Brand B product', mimeType: 'image/jpeg', }, mobMedia: { url: 'https://images.unsplash.com/photo-1572635196237-14b3f281503f?w=600&h=800&fit=crop', alt: 'Brand B mobile', mimeType: 'image/jpeg', }, }, { text: 'Anker Flagship', media: { url: 'https://images.unsplash.com/photo-1593642632559-0c6d3fc62b89?w=800&h=600&q=80', alt: 'Anker flagship', mimeType: 'image/jpeg', }, mobMedia: { url: 'https://images.unsplash.com/photo-1593642632559-0c6d3fc62b89?w=600&h=800&fit=crop', alt: 'Anker flagship mobile', mimeType: 'image/jpeg', }, }, ], }} /> ) }

示例 4: 2产品1:1等宽比例

import { ProductCompare } from '@anker-in/headless-ui/biz' export default function EqualWidthCompare() { return ( <ProductCompare data={{ title: 'Before & After', titleType: 'secondary', subtitle: 'Cleaning results comparison', twoImageRatio: '1:1', // 等宽显示 products: [ { text: 'Before Cleaning', media: { url: 'https://images.unsplash.com/photo-1581578731548-c64695cc6952?w=800&h=600&q=80', alt: 'Before cleaning', mimeType: 'image/jpeg', }, }, { text: 'After Cleaning', media: { url: 'https://images.unsplash.com/photo-1556911220-e15b29be8c8f?w=800&h=600&q=80', alt: 'After cleaning', mimeType: 'image/jpeg', }, }, ], }} /> ) }

示例 5: 响应式视频资源(桌面/移动端不同)

import { ProductCompare } from '@anker-in/headless-ui/biz' export default function ResponsiveVideoCompare() { return ( <ProductCompare data={{ title: 'Cross-Device Optimization', subtitle: 'Optimal experience on every device', products: [ { text: 'Standard Version', // 桌面端横版视频 media: { url: 'https://cdn.example.com/standard-desktop.mp4', alt: 'Standard version desktop demo', mimeType: 'video/mp4', }, poster: { url: 'https://images.unsplash.com/photo-1581578731548-c64695cc6952?w=800&h=600&q=80', alt: 'Standard desktop poster', mimeType: 'image/jpeg', }, // 移动端竖版视频 mobMedia: { url: 'https://cdn.example.com/standard-mobile.mp4', alt: 'Standard version mobile demo', mimeType: 'video/mp4', }, mobPoster: { url: 'https://images.unsplash.com/photo-1581578731548-c64695cc6952?w=600&h=800&fit=crop', alt: 'Standard mobile poster', mimeType: 'image/jpeg', }, }, { text: 'Flagship Version', media: { url: 'https://cdn.example.com/flagship-desktop.mp4', alt: 'Flagship version desktop demo', mimeType: 'video/mp4', }, poster: { url: 'https://images.unsplash.com/photo-1556911220-e15b29be8c8f?w=800&h=600&q=80', alt: 'Flagship desktop poster', mimeType: 'image/jpeg', }, mobMedia: { url: 'https://cdn.example.com/flagship-mobile.mp4', alt: 'Flagship version mobile demo', mimeType: 'video/mp4', }, mobPoster: { url: 'https://images.unsplash.com/photo-1556911220-e15b29be8c8f?w=600&h=800&fit=crop', alt: 'Flagship mobile poster', mimeType: 'image/jpeg', }, }, ], }} /> ) }

示例 6: Dark主题 + 无标签

import { ProductCompare } from '@anker-in/headless-ui/biz' export default function DarkThemeNoLabel() { return ( <ProductCompare data={{ title: 'Noise Level Comparison', subtitle: 'Real noise level testing', theme: 'dark', products: [ { // 不设置text,不显示标签 media: { url: 'https://cdn.example.com/competitor-noise.mp4', alt: 'Competitor noise test', mimeType: 'video/mp4', }, poster: { url: 'https://images.unsplash.com/photo-1595246140625-573b715d11dc?w=800&h=600&q=80', alt: 'Competitor poster', mimeType: 'image/jpeg', }, }, { media: { url: 'https://cdn.example.com/anker-noise.mp4', alt: 'Anker noise test', mimeType: 'video/mp4', }, poster: { url: 'https://images.unsplash.com/photo-1609091839311-d98929fc8f1e?w=800&h=600&q=80', alt: 'Anker poster', mimeType: 'image/jpeg', }, }, ], }} /> ) }

示例 7: 自定义容器样式

import { ProductCompare } from '@anker-in/headless-ui/biz' export default function CustomStyleCompare() { return ( <div className="bg-gray-50 py-16"> <ProductCompare className="max-w-7xl mx-auto shadow-2xl rounded-xl" data={{ title: 'Premium Comparison', subtitle: 'See the difference', products: [ { text: 'Competitor', media: { url: 'https://images.unsplash.com/photo-1625772452888-7e9b8f8f583e?w=800&h=600&q=80', alt: 'Competitor product', mimeType: 'image/jpeg', }, }, { text: 'Anker Premium', media: { url: 'https://images.unsplash.com/photo-1485827404703-89b55fcc595e?w=800&h=600&q=80', alt: 'Anker premium', mimeType: 'image/jpeg', }, }, ], }} /> </div> ) }

示例 8: 副标题图片展示

import { ProductCompare } from '@anker-in/headless-ui/biz' export default function WithSubtitleImage() { return ( <ProductCompare data={{ title: 'Performance Metrics', subtitle: 'Comprehensive testing results', subtitleImage: { url: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=1200&h=300&fit=crop', alt: 'Performance comparison chart', mimeType: 'image/jpeg', width: 1200, height: 300, }, products: [ { text: 'Standard Model', media: { url: 'https://images.unsplash.com/photo-1625772452888-7e9b8f8f583e?w=800&h=600&q=80', alt: 'Standard model', mimeType: 'image/jpeg', }, }, { text: 'Pro Model', media: { url: 'https://images.unsplash.com/photo-1485827404703-89b55fcc595e?w=800&h=600&q=80', alt: 'Pro model', mimeType: 'image/jpeg', }, }, ], }} /> ) }

示例 9: 纯图片对比(无视频)

import { ProductCompare } from '@anker-in/headless-ui/biz' export default function ImageOnlyCompare() { return ( <ProductCompare data={{ title: 'Design Comparison', titleType: 'secondary', products: [ { text: 'Old Design', media: { url: 'https://images.unsplash.com/photo-1581578731548-c64695cc6952?w=800&h=600&q=80', alt: 'Old design', mimeType: 'image/jpeg', }, }, { text: 'New Design', media: { url: 'https://images.unsplash.com/photo-1556911220-e15b29be8c8f?w=800&h=600&q=80', alt: 'New design', mimeType: 'image/jpeg', }, }, ], }} /> ) }

示例 10: 完整配置(所有可选参数)

import { ProductCompare } from '@anker-in/headless-ui/biz' export default function FullConfigCompare() { return ( <ProductCompare className="my-custom-class" // 可选 data={{ title: 'Complete Feature Set', // 可选 titleType: 'selling-point', // 可选,默认'primary' titleIcon: { // 可选,仅selling-point时有效 url: 'https://cdn-icons-png.flaticon.com/512/2917/2917995.png', alt: 'Feature icon', mimeType: 'image/png', }, subtitle: 'Everything you need in one place', // 可选 subtitleImage: { // 可选 url: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=1200&h=300&fit=crop', alt: 'Comparison chart', mimeType: 'image/jpeg', }, textAlign: 'center', // 可选,默认'left' twoImageRatio: '2:3', // 可选,默认'2:3' theme: 'dark', // 可选,默认'light' products: [ // 必需 { text: 'Competitor', // 可选 media: { // 可选 url: 'https://cdn.example.com/video1.mp4', alt: 'Competitor demo', mimeType: 'video/mp4', width: 800, height: 600, }, mobMedia: { // 可选 url: 'https://cdn.example.com/video1-mobile.mp4', alt: 'Competitor mobile demo', mimeType: 'video/mp4', }, poster: { // 可选 url: 'https://images.unsplash.com/photo-1581578731548-c64695cc6952?w=800&h=600&q=80', alt: 'Video poster', mimeType: 'image/jpeg', }, mobPoster: { // 可选 url: 'https://images.unsplash.com/photo-1581578731548-c64695cc6952?w=600&h=800&fit=crop', alt: 'Mobile poster', mimeType: 'image/jpeg', }, }, { text: 'Anker Premium', media: { url: 'https://cdn.example.com/video2.mp4', alt: 'Anker demo', mimeType: 'video/mp4', }, poster: { url: 'https://images.unsplash.com/photo-1556911220-e15b29be8c8f?w=800&h=600&q=80', alt: 'Anker poster', mimeType: 'image/jpeg', }, }, ], }} /> ) }

核心算法解析

1. 布局计算算法

功能说明: 根据产品数量和比例配置,动态计算每个产品的宽度占比。

核心代码:

function getProductLayoutClasses( index: number, totalProducts: number, twoImageRatio?: TwoImageRatio ): string { // 2产品布局 if (totalProducts === 2) { if (twoImageRatio === '1:1') { return 'laptop:flex-[1]' // 等宽1:1 } // 默认2:3比例 return index === 0 ? 'laptop:flex-[2]' : 'laptop:flex-[3]' } // 3产品布局(1:1:1等宽) if (totalProducts === 3) { return 'laptop:flex-[1]' } return 'laptop:flex-[1]' }

算法原理:

Flexbox比例分配: - flex-[1]: 占据1份宽度 - flex-[2]: 占据2份宽度 - flex-[3]: 占据3份宽度 2产品默认2:3比例: 总宽度 = 2 + 3 = 5份 产品1宽度 = (2/5) * 100% = 40% 产品2宽度 = (3/5) * 100% = 60% 2产品1:1比例: 总宽度 = 1 + 1 = 2份 产品1宽度 = (1/2) * 100% = 50% 产品2宽度 = (1/2) * 100% = 50% 3产品1:1:1比例: 总宽度 = 1 + 1 + 1 = 3份 每个产品宽度 = (1/3) * 100% ≈ 33.33%

应用示例:

<div className={cn('product-item', getProductLayoutClasses(0, 2, '2:3'))}> {/* laptop:flex-[2] → 40%宽度 */} </div> <div className={cn('product-item', getProductLayoutClasses(1, 2, '2:3'))}> {/* laptop:flex-[3] → 60%宽度 */} </div>

2. 懒加载算法

功能说明: 使用IntersectionObserver API实现智能懒加载,提前1500px加载媒体资源。

核心代码:

function LazyMedia({ children, offset = 800, }: { children: React.ReactNode offset?: number }) { const [loaded, setLoaded] = useState(false) const ref = useRef<HTMLDivElement>(null) useEffect(() => { // 1. 创建IntersectionObserver const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setLoaded(true) // 进入视口时标记为已加载 } }, { rootMargin: `${offset}px`, // 提前offset px触发 } ) // 2. 观察元素 if (ref.current) { observer.observe(ref.current) } // 3. 清理观察器 return () => { observer.disconnect() } }, [offset]) return ( <div ref={ref} className="size-full"> {loaded && children} {/* 仅在加载后渲染 */} </div> ) }

算法流程:

1. 组件挂载时创建IntersectionObserver 2. 设置rootMargin为1500px (元素距离视口1500px时触发) 3. 观察目标元素 4. 元素进入视口? ├─ 是 → setLoaded(true) │ └─ 渲染children(视频/图片) └─ 否 → 继续等待 5. 组件卸载时断开观察器

性能优化:

  • 提前加载: 1500px提前量确保用户滚动到时媒体已加载
  • 节省资源: 屏幕外的媒体不加载,减少初始加载时间
  • 内存管理: 组件卸载时自动断开观察器,避免内存泄漏

使用示例:

<LazyMedia offset={1500}> <video src="video.mp4" autoPlay loop muted /> </LazyMedia>

3. 媒体类型检测算法

功能说明: 根据mimeType自动识别视频或图片,渲染对应HTML元素。

核心代码:

function MediaElement({ media, poster, className, }: { media?: Media poster?: Media className?: string }) { if (!media?.url) return null // 1. 检测是否为视频 const isVideo = media.mimeType?.startsWith('video/') // 2. 渲染视频元素 if (isVideo) { return ( <video src={media.url} playsInline autoPlay muted loop poster={poster?.url || ''} preload="metadata" disablePictureInPicture disableRemotePlayback className={className} width={media.width} height={media.height} /> ) } // 3. 渲染图片元素 return ( <img src={media.url} alt={media.alt || ''} className={className} width={media.width} height={media.height} /> ) }

检测逻辑:

输入: media.mimeType 是否以'video/'开头? ├─ 是 → 视频类型 │ ↓ │ 渲染<video> │ - autoPlay: 自动播放 │ - muted: 静音(移动端自动播放必需) │ - loop: 循环播放 │ - playsInline: 内联播放(iOS必需) │ - poster: 视频封面图 │ - preload="metadata": 只预加载元数据 └─ 否 → 图片类型 渲染<img> - alt: 替代文本(无障碍) - width/height: 避免布局抖动

mimeType 示例:

文件类型mimeType结果
MP4视频'video/mp4'渲染&lt;video&gt;
WebM视频'video/webm'渲染&lt;video&gt;
JPEG图片'image/jpeg'渲染&lt;img&gt;
PNG图片'image/png'渲染&lt;img&gt;
WebP图片'image/webp'渲染&lt;img&gt;

4. 标签样式算法

功能说明: 最后一个产品使用品牌色背景,其他产品使用半透明黑色,突出自家产品。

核心代码:

const isLastProduct = index === products.length - 1 // 标签样式:最后一个产品用bg-brand-0,其他用rgba(0,0,0,0.2) const labelBgClass = isLastProduct ? 'bg-brand-0' : 'bg-[rgba(0,0,0,0.2)]'

视觉效果:

2产品对比: ┌─────────────┬─────────────┐ │ 竞品产品 │ Anker产品 │ │ ┌─────────┐ │ ┌─────────┐ │ │ │ 其他品牌│ │ │Anker E25│ │ │ └─────────┘ │ └─────────┘ │ │ 半透明黑 │ 品牌色 │ ← 标签背景 │ bg-[rgba │ bg-brand-0 │ │ (0,0,0, │ │ │ 0.2)] │ │ └─────────────┴─────────────┘ 3产品对比: ┌────────┬────────┬────────┐ │ 品牌A │ 品牌B │ Anker │ │半透明黑│半透明黑│ 品牌色 │ └────────┴────────┴────────┘

设计理念:

  1. 突出自家产品: 最后一个产品通常是自家产品,使用品牌色更醒目
  2. 弱化竞品: 竞品使用低调的半透明黑色,不喧宾夺主
  3. 视觉层次: 创造清晰的视觉层次,引导用户关注重点

5. 移动端顺序交换算法(2产品时)

功能说明: 移动端垂直布局时,自家产品显示在上方,提升视觉优先级。

核心代码:

<div className={cn('product-item', { 'order-2 tablet:order-1': index === 0 && products.length === 2, 'order-1 tablet:order-2': index === 1 && products.length === 2, })} > {/* 产品内容 */} </div>

顺序变化:

桌面端(水平布局): ┌──────────┬──────────┐ │ 竞品 │ Anker │ │ (index=0)│ (index=1)│ └──────────┴──────────┘ order-1 order-2 移动端(垂直布局): ┌──────────┐ │ Anker │ ← order-1(优先显示) │ (index=1)│ ├──────────┤ │ 竞品 │ ← order-2 │ (index=0)│ └──────────┘

原理:

  • Flexbox Order属性: 控制元素的显示顺序
  • 移动端:
    • index=0(竞品): order-2 → 显示在下方
    • index=1(Anker): order-1 → 显示在上方
  • 桌面端:
    • index=0: tablet:order-1 → 恢复原顺序
    • index=1: tablet:order-2 → 恢复原顺序

为什么交换顺序?

  1. 移动端屏幕小: 用户可能只看到第一个产品
  2. 优先展示自家产品: 确保用户先看到重点内容
  3. 桌面端恢复: 桌面端水平布局,左右对比更直观

6. 响应式资源选择算法

功能说明: 根据设备断点自动选择桌面端或移动端媒体资源。

核心代码:

// 桌面端媒体(laptop及以上) {product.media && ( <div className="laptop:block hidden"> <LazyMedia offset={1500}> <MediaElement media={product.media} poster={product.poster} /> </LazyMedia> </div> )} // 移动端媒体(laptop以下) {(product.mobMedia || product.media) && ( <div className="laptop:hidden block"> <LazyMedia offset={1500}> <MediaElement media={product.mobMedia || product.media} poster={product.mobPoster || product.poster} /> </LazyMedia> </div> )}

决策树:

加载媒体资源 当前断点? ├─ laptop及以上(≥1024px) │ ↓ │ 显示product.media(桌面端资源) │ 隐藏移动端 └─ laptop以下(&lt;1024px) product.mobMedia存在? ├─ 是 → 使用mobMedia(移动端专用资源) └─ 否 → 回退到media(桌面端资源)

优势:

  1. 流量优化: 移动端加载更小的资源
  2. 适配方向: 桌面端横版,移动端竖版
  3. 性能提升: 减少不必要的资源加载
  4. 灵活回退: mobMedia未指定时自动使用media

7. 标题类型渲染算法

功能说明: 根据titleType渲染不同样式的标题。

核心代码:

function renderTitle(titleType?: TitleType, title?: string, titleIcon?: Media) { if (!title) return null switch (titleType) { case 'selling-point': // 卖点标题:带图标,品牌色 return ( <div className="flex items-center justify-center gap-2"> {titleIcon && ( <img src={titleIcon.url} alt={titleIcon.alt || ''} className="size-[24px] laptop:size-[32px]" /> )} <Heading size={4} className="text-[#00BEFA]" > {title} </Heading> </div> ) case 'secondary': // 二级标题:size=3,较小字号 return <Heading size={3}>{title}</Heading> case 'primary': default: // 一级标题:size=4,常规样式 return <Heading size={4}>{title}</Heading> } }

标题类型对比:

titleTypesize颜色图标使用场景
selling-point4#00BEFA(品牌色)强调卖点,营销页面
primary4默认常规对比标题
secondary3默认次要对比,子页面

响应式行为

断点定义

断点最小宽度设备类型布局变化
默认0px手机垂直布局(flex-col),产品间距16px,2产品时顺序交换
tablet768px平板水平布局(flex-row),2产品顺序恢复,字号14px
laptop1024px笔记本应用flex比例,间距32px,标签位置28px,资源切换
desktop1440px桌面标签位置32px,字号16px
lg-desktop1920px大屏字号18px,最大化视觉效果

响应式特性表

特性MobileTabletLaptop+说明
布局方向垂直(flex-col)水平(flex-row)水平(flex-row)移动端堆叠,桌面端并排
产品顺序(2产品)交换(自家在上)原顺序原顺序移动端优先展示自家产品
flex比例不应用不应用应用(2:3或1:1)仅桌面端应用宽度比例
产品间距16px16px32px桌面端增大间距
标签位置16px16px28px/32px桌面端标签外移
副标题字号14px14px14px/16px/18px随断点递增
媒体资源mobMediamobMediamedia桌面端切换资源

断点行为示例

// 容器布局 <div className=" flex flex-col // Mobile: 垂直布局 tablet:flex-row tablet:flex-nowrap // Tablet+: 水平布局 mt-[24px] laptop:mt-[32px] // 上边距响应式 gap-[16px] // 产品间距 "> // 产品项 <div className={cn(' w-full // Mobile: 全 laptop:flex-[2] // Laptop: flex比例 order-2 tablet:order-1 // 移动端顺序交换 ')}> // 标签定位 <div className=" absolute left-[16px] top-[16px] // Mobile/Tablet laptop:left-[28px] laptop:top-[28px] // Laptop desktop:left-[32px] desktop:top-[32px] // Desktop "> // 副标题字号 <Text className=" text-[14px] // Mobile/Tablet/Laptop desktop:text-[16px] // Desktop lg-desktop:text-[18px] // LG Desktop "> // 桌面/移动端媒体切换 <div className="laptop:block hidden"> {/* 桌面端显示 */} <MediaElement media={product.media} /> </div> <div className="laptop:hidden block"> {/* 移动端显示 */} <MediaElement media={product.mobMedia || product.media} /> </div>

设计规范

使用建议

1. 产品数量选择

产品数量推荐度适用场景注意事项
2个⭐⭐⭐⭐⭐ 强烈推荐单品对比,竞品对比对比最直观,视觉焦点清晰
3个⭐⭐⭐⭐ 推荐多品牌横向对比等宽展示,注意内容平衡
1个⭐ 不推荐-无法体现”对比”功能
4+个⭐ 不推荐-屏幕宽度有限,影响体验

2. 布局比例选择

场景推荐比例原因
突出自家产品twoImageRatio='2:3'右侧产品占60%,更醒目
平等对比twoImageRatio='1:1'等宽展示,公平对比
3产品对比固定1:1:1等宽展示,视觉平衡

3. 标签文本建议

长度示例效果
✅ 2-8字符”其他品牌”、“Anker E25”清晰易读,不影响排版
⚠️ 9-12字符”竞品扫地机器人”可接受,注意换行
❌ 13+字符”其他品牌智能扫地机器人”过长,影响排版和视觉

4. 视频优化建议

参数推荐值说明
时长5-15秒短视频,快速传达对比效果
格式MP4(H.264)兼容性最佳
分辨率1280x720平衡清晰度和文件大小
码率2-5 Mbps5-10秒视频约1-3MB
poster必须提供避免加载时黑屏
mimeType必须设置’video/mp4’用于类型检测

5. 标题类型选择指南

titleType使用场景视觉特点
selling-point营销页面,强调卖点品牌色标题 + 图标,吸引眼球
primary常规对比页面size=4,常规样式,通用性强
secondary子页面,次要对比size=3,较小字号,层次清晰

注意事项

1. 必填项验证

// ❌ 错误:products为空或未定义 <ProductCompare data={{ title: 'Compare' }} /> // ✅ 正确:至少包含2个产品 <ProductCompare data={{ title: 'Compare', products: [ { text: 'A', media: {...} }, { text: 'B', media: {...} } ] }} />

2. mimeType必填

// ❌ 错误:未设置mimeType,无法判断类型 media: { url: 'video.mp4', alt: 'Demo' } // ✅ 正确:明确指定mimeType media: { url: 'video.mp4', alt: 'Demo', mimeType: 'video/mp4' // 必需! }

3. 视频自动播放限制

  • 桌面端: 通常支持autoPlay + muted自动播放
  • 移动端: 部分浏览器可能阻止,即使设置了autoPlaymuted
  • 解决方案: 提供poster海报图,或添加播放按钮

4. 懒加载offset固定

  • 当前值: 1500px(硬编码在LazyMedia组件中)
  • 修改: 需要修改源码,暂不支持通过props配置
  • 建议: 保持默认值,适用于大多数场景

5. 响应式资源建议

// ✅ 推荐:桌面端和移动端使用不同资源 { text: 'Product', media: { url: 'desktop-video.mp4', ... }, // 桌面端横版视频 mobMedia: { url: 'mobile-video.mp4', ... }, // 移动端竖版视频 poster: { url: 'desktop-poster.jpg', ... }, // 桌面端海报 mobPoster: { url: 'mobile-poster.jpg', ... } // 移动端海报 } // ⚠️ 可接受:仅提供桌面端资源(移动端自动使用) { text: 'Product', media: { url: 'universal-image.jpg', ... } // 桌面端和移动端共用 }

无障碍性

键盘导航

ProductCompare为纯展示组件,无交互元素,不需要键盘导航。

ARIA属性建议

// 建议在使用时添加 <ProductCompare data={{ title: '性能对比', products: [...] }} aria-label="产品性能对比展示" role="region" />

添加ARIA标签到图片/视频:

// 图片alt属性(必需) media: { url: 'image.jpg', alt: '清洁效果演示', // 屏幕阅读器会读取 mimeType: 'image/jpeg' } // 视频描述(建议添加) <video src="video.mp4" aria-label="清洁效果视频演示" aria-describedby="video-desc" > <track kind="captions" src="captions.vtt" /> </video> <p id="video-desc" className="sr-only"> 视频展示Anker E25在硬木地板上的清洁效果 </p>

屏幕阅读器支持

1. 图片替代文本

  • ✅ 所有图片必须提供alt属性
  • ✅ alt文本应描述图片内容,不要包含”图片”、“照片”等词
  • ✅ 装饰性图片使用空alt: alt=""
// ✅ 正确 alt: 'Anker E25 robot vacuum cleaning hardwood floor' // ❌ 错误 alt: 'Image of vacuum' alt: '照片'

2. 视频字幕

<video src="demo.mp4"> <track kind="captions" src="demo-zh.vtt" srcLang="zh-CN" label="简体中文" default /> <track kind="captions" src="demo-en.vtt" srcLang="en" label="English" /> 您的浏览器不支持视频播放。 </video>

3. 语义化标签

  • ✅ 标题使用&lt;Heading&gt;组件(渲染为&lt;h2&gt;/&lt;h3&gt;)
  • ✅ 产品标签使用&lt;Heading as="h6"&gt;,保持语义层级

颜色对比度

标签文本对比度:

背景色文字颜色对比度WCAG等级
rgba(0,0,0,0.2)白色4.8:1✅ AA
bg-brand-0(品牌色)白色根据品牌色而定需测试

建议: 使用WebAIM Contrast Checker 测试品牌色对比度。

性能优化

React优化

1. 懒加载媒体

// ✅ 使用LazyMedia组件 <LazyMedia offset={1500}> <MediaElement media={product.media} /> </LazyMedia> // 原理:IntersectionObserver,进入视口才渲染 // 优势:减少初始加载,提升首屏性能

2. 条件渲染

// ✅ 仅在loaded后渲染 {loaded && children} // ✅ 桌面/移动端分别渲染 <div className="laptop:block hidden"> {/* 桌面端内容 */} </div> <div className="laptop:hidden block"> {/* 移动端内容 */} </div>

3. 视频预加载策略

<video preload="metadata" // ✅ 只预加载元数据(时长、尺寸) // preload="auto" // ❌ 预加载整个视频,浪费流量 // preload="none" // ⚠️ 不预加载,可能导致播放延迟 />

视频优化建议

1. 视频压缩

视频时长推荐码率预估文件大小
5秒2-3 Mbps1-2 MB
10秒3-5 Mbps2-3 MB
15秒5-8 Mbps3-6 MB

使用FFmpeg压缩:

# H.264编码,码率3Mbps ffmpeg -i input.mp4 -c:v libx264 -b:v 3M -c:a aac -b:a 128k output.mp4 # 优化移动端(竖版,分辨率720x1280) ffmpeg -i input.mp4 -vf scale=720:1280 -c:v libx264 -b:v 2M output.mp4

2. 海报图优化

poster: { url: 'poster.webp', // ✅ WebP格式,体积更小 alt: 'Video poster', mimeType: 'image/webp' }

生成海报图:

# 提取视频第1帧作为海报 ffmpeg -i video.mp4 -vframes 1 -q:v 2 poster.jpg

3. 视频属性优化

<video preload="metadata" // 只预加载元数据 disablePictureInPicture // 禁用画中画 disableRemotePlayback // 禁用远程播放 playsInline // iOS内联播放 muted // 静音(自动播放必需) loop // 循环播放 />

图片优化建议

1. 格式选择

图片类型推荐格式原因
照片WebP > JPEGWebP体积小30%,质量相当
图标/图表WebP > PNG支持透明度,体积更小
简单图形SVG矢量格式,无限缩放

2. 尺寸建议

设备推荐宽度说明
桌面端media1200px足够清晰,文件不大
移动端mobMedia600px移动端屏幕较小
海报poster800px视频封面图

3. 压缩工具

# ImageMagick压缩JPEG(质量85%) convert input.jpg -quality 85 output.jpg # 转换为WebP(质量80%) cwebp -q 80 input.jpg -o output.webp

4. 响应式图片

<picture> <source srcSet="image.webp" type="image/webp" /> <source srcSet="image.jpg" type="image/jpeg" /> <img src="image.jpg" alt="Product" loading="lazy" decoding="async" /> </picture>

性能监控

使用Lighthouse测试:

# 1. 构建项目 pnpm run build # 2. 运行生产服务器 pnpm run preview # 3. 打开Chrome DevTools # 4. 运行Lighthouse审计

性能目标:

指标目标值说明
Performance> 90整体性能得分
First Contentful Paint< 1.8s首次内容绘制
Largest Contentful Paint< 2.5s最大内容绘制
Cumulative Layout Shift< 0.1累积布局偏移

常见问题 FAQ

1. 为什么移动端视频不自动播放?

原因: iOS Safari和部分Android浏览器限制非用户触发的视频播放。

解决方案:

// 1. 确保视频设置了muted和playsInline(组件已内置) <video autoPlay muted playsInline /> // 2. 如果仍不播放,添加播放按钮 const [isPlaying, setIsPlaying] = useState(false) const videoRef = useRef<HTMLVideoElement>(null) <div onClick={() => { videoRef.current?.play() setIsPlaying(true) }}> {!isPlaying && <PlayIcon />} <video ref={videoRef} muted playsInline /> </div>

2. 如何自定义2产品布局比例?

当前支持: '2:3'(默认) 和 '1:1'

// 默认2:3比例(左40%,右60%) <ProductCompare data={{ twoImageRatio: '2:3', ... }} /> // 等宽1:1比例 <ProductCompare data={{ twoImageRatio: '1:1', ... }} />

扩展其他比例(需修改源码):

// 在getProductLayoutClasses函数中添加 if (twoImageRatio === '1:2') { return index === 0 ? 'laptop:flex-[1]' : 'laptop:flex-[2]' }

3. 如何修改懒加载提前距离?

当前值: 1500px(硬编码)

修改方法(需修改源码):

// 文件:index.tsx:254 <LazyMedia offset={1500}> // 修改这里 // 修改为800px <LazyMedia offset={800}> // 修改为2500px <LazyMedia offset={2500}>

未来建议: 通过props配置

// 理想API(未实现) <ProductCompare data={{ lazyLoadOffset: 2000, // 自定义offset products: [...] }} />

4. 如何自定义标签背景色?

当前逻辑: 最后一个产品bg-brand-0,其他rgba(0,0,0,0.2)

修改方法(源码index.tsx:229):

// 当前 const labelBgClass = isLastProduct ? 'bg-brand-0' : 'bg-[rgba(0,0,0,0.2)]' // 全部使用品牌色 const labelBgClass = 'bg-brand-0' // 使用渐变背景 const labelBgClass = isLastProduct ? 'bg-gradient-to-r from-[#3ad1ff] to-[#008cd6]' : 'bg-[rgba(0,0,0,0.3)]' // 根据index区分颜色 const labelBgClass = index === 0 ? 'bg-red-500' : index === 1 ? 'bg-blue-500' : 'bg-brand-0'

5. 如何禁用移动端顺序交换?

当前行为: 2产品时,移动端自家产品显示在上方

禁用方法(源码index.tsx:235-236):

// 当前 <div className={cn('product-item', { 'order-2 tablet:order-1': index === 0 && products.length === 2, 'order-1 tablet:order-2': index === 1 && products.length === 2, })} > // 修改为:移除条件判断 <div className="product-item">

6. 3产品时如何自定义布局比例?

当前: 3产品固定1:1:1等宽

修改为1:2:1(中间产品更宽):

// getProductLayoutClasses函数 if (totalProducts === 3) { if (index === 1) return 'laptop:flex-[2]' // 中间产品2份宽度 return 'laptop:flex-[1]' // 左右产品1份宽度 } // 结果:左33.33% | 中50% | 右16.67%

修改为2:3:2:

if (totalProducts === 3) { if (index === 0) return 'laptop:flex-[2]' if (index === 1) return 'laptop:flex-[3]' return 'laptop:flex-[2]' }

7. 如何添加点击放大功能?

添加模态框:

import { useState } from 'react' import { Dialog } from '@anker-in/headless-ui' const [modalOpen, setModalOpen] = useState(false) const [currentMedia, setCurrentMedia] = useState<Media | null>(null) // 产品项添加点击事件 <div onClick={() => { setCurrentMedia(product.media) setModalOpen(true) }} className="cursor-pointer transition-transform hover:scale-105" > <MediaElement media={product.media} /> </div> // 模态框 {modalOpen && ( <Dialog open={modalOpen} onOpenChange={setModalOpen}> <div className="max-w-4xl mx-auto"> <MediaElement media={currentMedia} /> </div> </Dialog> )}

8. 如何处理视频加载失败?

添加错误处理:

const [videoError, setVideoError] = useState(false) <video src={media.url} onError={() => setVideoError(true)} autoPlay muted loop /> {videoError && ( <div className="flex items-center justify-center h-full bg-gray-100"> <p className="text-gray-500">视频加载失败</p> {poster && <img src={poster.url} alt="Fallback poster" />} </div> )}

9. 如何添加加载动画?

修改LazyMedia组件:

function LazyMedia({ children, offset = 800 }) { const [loaded, setLoaded] = useState(false) const ref = useRef<HTMLDivElement>(null) // IntersectionObserver逻辑... return ( <div ref={ref} className="size-full"> {!loaded && ( <div className="flex items-center justify-center h-full bg-gray-100"> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-brand-0" /> </div> )} {loaded && children} </div> ) }

10. 如何支持4+产品对比?

当前限制: 仅支持2-3个产品

扩展支持(需修改源码):

// 1. 修改类型定义 export type FourImageRatio = '1:1:1:1' // 2. 修改布局函数 function getProductLayoutClasses( index: number, totalProducts: number, twoImageRatio?: TwoImageRatio, fourImageRatio?: FourImageRatio ): string { if (totalProducts === 4) { return 'laptop:flex-[1]' // 4个产品等宽 } // 原有逻辑... } // 3. 注意:4个产品在小屏幕上可能过于拥挤

不推荐: 4+产品会导致单个产品显示区域过小,影响用户体验。建议使用其他布局方式(如轮播、网格等)。

相关资源

Storybook示例

查看组件的交互式文档和所有变体:

pnpm run storybook

访问 http://localhost:6006/?path=/story/blocks-创意图文模块-productcompare--default

源代码

  • 组件源码: packages/ui/src/biz-components/ProductCompare/index.tsx
  • 类型定义: packages/ui/src/biz-components/ProductCompare/types.ts
  • 单元测试: packages/ui/tests/ProductCompare.test.tsx
  • Story文件: packages/ui/src/stories/productCompare.stories.tsx

依赖组件

@anker-in/headless-ui 导入:

  • Heading: 标题组件,支持多级标题(h1-h6)和自定义字号
  • Text: 文本组件,用于副标题和描述性文本
  • withLayout: 布局HOC,提供统一的容器样式和间距

内部组件

  • LazyMedia: 懒加载容器组件,使用IntersectionObserver API
  • MediaElement: 媒体元素组件,自动识别视频/图片并渲染

相关组件

  • ImageWithText: 单图文组合展示
  • ImageTextFeature: 图文特性展示组件
  • MediaPlayerBase: 单视频播放器
  • MediaSceneSwitcher: 多场景自动切换组件

设计资源

  • Figma设计稿: 查看设计 
  • 设计系统文档: 请查阅项目Design System文档
  • Tailwind配置: packages/ui/tailwind.config.js

技术文章

性能工具

Last updated on