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

Tab媒体组件 TabsWithMedia

组件分类: 新品页组件 | 适用场景: 产品功能展示、特性介绍、视频教程 | Figma: 查看设计稿 

Tab媒体组件(TabsWithMedia) 是一个视频同步的 Tab 切换组件,支持视频时间轴自动切换Tab点击Tab跳转视频时间Swiper内容切换Framer Motion动画效果

核心功能: 视频时间轴同步、Tab自动切换、点击跳转视频、Swiper滑动切换、Framer Motion动画、响应式布局、多媒体支持

使用场景: 产品功能特性展示、新品介绍页、教学视频配合文字说明、多功能点展示

TabsWithMedia (Tab媒体组件)

带媒体内容的选项卡组件,支持图片和视频展示【✅ 已发布】

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

功能特性

TabsWithMedia 是一个高度交互的视频同步 Tab 组件,提供了丰富的功能来展示产品的多个特性:

核心特性

  • 视频时间自动切换 - 根据视频播放时间自动高亮对应的 Tab
  • 点击 Tab 跳转视频 - 点击 Tab 时视频跳转到对应时间点
  • 轮播 Tab 显示 - 只显示 5 个 Tab,自动轮播切换
  • Swiper 内容切换 - 使用 Swiper EffectFade 淡入淡出切换描述
  • Framer Motion 动画 - Tab 项使用 motion.div 实现平滑进入/退出动画
  • 响应式视频 - 支持桌面端和移动端不同视频源
  • 自动播放循环 - 视频自动播放、静音、循环播放
  • 主题切换 - 支持 light/dark 两种主题
  • 完整 BEM 命名 - 提供完整的 CSS 类名便于自定义样式
  • 曝光埋点 - 内置 useExposure 钩子支持数据埋点

Props参数

TabsWithMediaProps

Prop类型默认值必需说明
dataTabsWithMediaData-组件数据配置对象
classNamestring--自定义 CSS 类名

TabsWithMediaData

字段类型默认值必需说明
titlestring-主标题
videoMedia-桌面端视频对象
itemsTabItem[]-Tab 项列表(建议 3-6 个)
posterMedia--默认视频封面图
mobvideoMedia--移动端视频对象
timeIdxTimeIndex[]defaultTimeIdx-时间点配置(视频自动切换 Tab)
theme'light' | 'dark''light'-主题色

TabItem

字段类型默认值必需说明
tabstring-Tab 标签文本
descstring-描述文本
iconMedia-Tab 图标对象
posterMedia--视频封面图(可选)
videoMedia--视频对象(可选)

TimeIndex

字段类型默认值必需说明
timenumber-该片段时长(秒)
pointnumber-累计时间节点(秒)
highlightIdxnumber-高亮的 Tab 索引

Media 类型

interface Media { url: string // 资源 URL alt: string // 替代文本(必需) thumbnailURL?: string // 缩略图 URL mimeType?: string // MIME 类型 }

类型定义

import type { Media, Theme } from '../../types/props' export interface TabItem { tab: string desc: string icon: Media poster?: Media tabIcon?: Media video?: Media } export interface TimeIndex { time: number // 片段时长 point: number // 时间节点 highlightIdx: number // 高亮索引 } export interface TabsWithMediaProps { data: { title: string poster?: Media video: Media mobvideo?: Media items: TabItem[] timeIdx?: TimeIndex[] theme?: Theme } className?: string } // 默认时间点配置(6 个 Tab) const defaultTimeIdx: TimeIndex[] = [ { time: 2.4, point: 2.4, highlightIdx: 0 }, { time: 2.5, point: 4.9, highlightIdx: 1 }, { time: 2.5, point: 7.4, highlightIdx: 2 }, { time: 2.8, point: 10.2, highlightIdx: 3 }, { time: 2.6, point: 12.8, highlightIdx: 4 }, { time: 3.2, point: 16, highlightIdx: 5 }, ]

使用示例

示例 1: 基础 3 项展示

展示最简单的 3 个功能特性,使用自定义时间索引,每个 Tab 分配 3 秒时间:

import { TabsWithMedia } from '@anker-in/headless-ui/biz' export default function BasicDemo() { return ( <TabsWithMedia data={{ title: 'Three Key Features', video: { url: 'https://cdn.example.com/video.mp4', alt: 'Product Features Video', }, poster: { url: 'https://cdn.example.com/poster.jpg', alt: 'Video Poster', }, items: [ { tab: 'Smart AI', desc: 'Intelligent navigation with obstacle avoidance.', icon: { url: 'https://cdn.example.com/icon-ai.png', alt: 'AI Icon', }, }, { tab: 'Long Battery', desc: 'Up to 180 minutes of continuous cleaning.', icon: { url: 'https://cdn.example.com/icon-battery.png', alt: 'Battery Icon', }, }, { tab: 'Quiet Mode', desc: 'Ultra-quiet operation at 55dB.', icon: { url: 'https://cdn.example.com/icon-quiet.png', alt: 'Quiet Icon', }, }, ], timeIdx: [ { time: 3.0, point: 3.0, highlightIdx: 0 }, { time: 3.0, point: 6.0, highlightIdx: 1 }, { time: 3.0, point: 9.0, highlightIdx: 2 }, ], }} /> ) }

示例 2: 完整 6 个特性展示(默认配置)

使用默认 timeIdx 配置展示 6 个 All-in-One 功能特性:

<TabsWithMedia data={{ title: 'All-in-One Robot Vacuum', video: { url: 'https://cdn.shopify.com/videos/all-in-one.mp4', alt: 'All-in-One Features', }, mobvideo: { url: 'https://cdn.shopify.com/videos/all-in-one-mobile.mp4', alt: 'All-in-One Mobile', }, poster: { url: 'https://cdn.shopify.com/images/poster.png', alt: 'Poster Image', }, items: [ { tab: 'Self-Emptying', desc: '3L dust bag supports up to 75 days of maintenance-free cleaning.', icon: { url: '/icons/emptying.png', alt: 'Self-Emptying' }, }, { tab: 'Self-Refilling', desc: 'Features a 2.5L clean water tank, sufficient for 4 mopping sessions.', icon: { url: '/icons/refilling.png', alt: 'Self-Refilling' }, }, { tab: 'Self-Washing', desc: 'Automatically washes the mop after each session.', icon: { url: '/icons/washing.png', alt: 'Self-Washing' }, }, { tab: 'Hot Air Drying', desc: 'Intelligent 35-50°C temperature control for efficient drying.', icon: { url: '/icons/drying.png', alt: 'Hot Air Drying' }, }, { tab: 'Wastewater Collection', desc: '1.8L tank collects dirty water.', icon: { url: '/icons/wastewater.png', alt: 'Wastewater' }, }, { tab: 'Auto Detergent', desc: 'Automatically dispenses detergent, lasting up to 6 months.', icon: { url: '/icons/detergent.png', alt: 'Detergent' }, }, ], // 使用默认 timeIdx 配置 }} />

示例 3: Dark 主题

使用深色主题展示组件,适合深色背景页面:

<TabsWithMedia data={{ title: 'Premium Features', video: { url: 'https://example.com/video.mp4', alt: 'Video' }, items: [ { tab: 'Feature 1', desc: 'Description 1', icon: { url: '/icon1.png', alt: 'Icon 1' }, }, { tab: 'Feature 2', desc: 'Description 2', icon: { url: '/icon2.png', alt: 'Icon 2' }, }, { tab: 'Feature 3', desc: 'Description 3', icon: { url: '/icon3.png', alt: 'Icon 3' }, }, ], theme: 'dark', // 深色主题 }} className="bg-gray-900" // 配合深色背景 />

示例 4: 自定义时间切换逻辑

根据内容重要性自定义时间分配,重要功能分配更长时间:

<TabsWithMedia data={{ title: 'Custom Timing', video: { url: 'https://example.com/custom-video.mp4', alt: 'Video' }, items: [ { tab: 'Intro', desc: 'Introduction', icon: { url: '...', alt: 'Intro' } }, { tab: 'Demo', desc: 'Product Demo', icon: { url: '...', alt: 'Demo' } }, { tab: 'Specs', desc: 'Specifications', icon: { url: '...', alt: 'Specs' } }, { tab: 'Summary', desc: 'Summary', icon: { url: '...', alt: 'Summary' } }, ], timeIdx: [ { time: 5.0, point: 5.0, highlightIdx: 0 }, // Intro: 0-5s { time: 8.0, point: 13.0, highlightIdx: 1 }, // Demo: 5-13s (最重要) { time: 6.0, point: 19.0, highlightIdx: 2 }, // Specs: 13-19s { time: 4.0, point: 23.0, highlightIdx: 3 }, // Summary: 19-23s ], }} />

TimeIndex 计算方法:

// 方案 1: 平均分配(假设视频 20 秒, 4 个 Tab) const timeIdx = [ { time: 5.0, point: 5.0, highlightIdx: 0 }, // 0-5s { time: 5.0, point: 10.0, highlightIdx: 1 }, // 5-10s { time: 5.0, point: 15.0, highlightIdx: 2 }, // 10-15s { time: 5.0, point: 20.0, highlightIdx: 3 }, // 15-20s ] // 方案 2: 根据内容重要性分配 const timeIdx = [ { time: 3.0, point: 3.0, highlightIdx: 0 }, // Intro: 3s { time: 7.0, point: 10.0, highlightIdx: 1 }, // Main: 7s (重点) { time: 5.0, point: 15.0, highlightIdx: 2 }, // Feature: 5s { time: 5.0, point: 20.0, highlightIdx: 3 }, // Summary: 5s ]

示例 5: 仅移动端视频

如果只在移动端显示不同视频,桌面端不传 mobvideo:

<TabsWithMedia data={{ title: 'Mobile Optimized', video: { url: 'https://cdn.example.com/desktop-video.mp4', alt: 'Desktop Video', }, mobvideo: { url: 'https://cdn.example.com/mobile-video.mp4', alt: 'Mobile Video', // 移动端专用视频 }, items: [...], }} />

组件会根据屏幕宽度(768px)自动切换视频源。

示例 6: 禁用自动切换

不传 timeIdx,组件将只响应用户手动点击 Tab:

<TabsWithMedia data={{ title: 'Manual Control Only', video: { url: 'https://example.com/video.mp4', alt: 'Video' }, items: [ { tab: 'Tab 1', desc: 'Description 1', icon: {...} }, { tab: 'Tab 2', desc: 'Description 2', icon: {...} }, { tab: 'Tab 3', desc: 'Description 3', icon: {...} }, ], // 不传 timeIdx,视频不会自动切换 Tab }} />

注意: 这种情况下,点击 Tab 仍然可以手动跳转视频时间点,但视频播放不会触发 Tab 切换。

示例 7: 自定义样式

使用自定义 className 和 CSS 变量自定义组件样式:

<TabsWithMedia data={{ title: 'Custom Styled Tabs', video: { url: 'https://example.com/video.mp4', alt: 'Video' }, items: [...], }} className="custom-tabs" />

CSS 自定义:

/* 自定义标题颜色 */ .custom-tabs .tabs-with-media__title { color: #ff6b6b; font-weight: 700; } /* 自定义 Tab 图标背景 */ .custom-tabs .tabs-with-media__tab-icon { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } /* 自定义活跃 Tab 样式 */ .custom-tabs .tabs-with-media__tab-content { box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); border-radius: 12px; } /* 自定义描述文本 */ .custom-tabs .tabs-with-media__description-text { font-size: 18px; line-height: 1.8; color: #333; } /* 自定义视频容器 */ .custom-tabs .tabs-with-media__video-wrapper { border-radius: 24px; overflow: hidden; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); }

示例 8: 4 个 Tab 均衡分配

4 个 Tab,每个分配相同时间(4 秒):

<TabsWithMedia data={{ title: 'Four Features', video: { url: 'https://example.com/video-16s.mp4', alt: 'Video' }, items: [ { tab: 'Power', desc: 'Powerful suction', icon: {...} }, { tab: 'Smart', desc: 'Intelligent navigation', icon: {...} }, { tab: 'Quiet', desc: 'Low noise operation', icon: {...} }, { tab: 'Long', desc: 'Extended battery life', icon: {...} }, ], timeIdx: [ { time: 4.0, point: 4.0, highlightIdx: 0 }, { time: 4.0, point: 8.0, highlightIdx: 1 }, { time: 4.0, point: 12.0, highlightIdx: 2 }, { time: 4.0, point: 16.0, highlightIdx: 3 }, ], }} />

示例 9: 使用 CSS 变量自定义渐变

通过 CSS 变量自定义 Tab 图标渐变背景:

:root { --tab-gradient-start: #3AD1FF; --tab-gradient-end: #008CD6; } .tabs-with-media__tab-icon { background: linear-gradient( 90deg, var(--tab-gradient-start) 0%, var(--tab-gradient-end) 100% ); }

示例 10: 完整功能演示

结合所有功能的完整示例:

<TabsWithMedia data={{ title: 'Complete Feature Showcase', video: { url: 'https://cdn.example.com/complete-demo.mp4', alt: 'Complete Demo Video', }, mobvideo: { url: 'https://cdn.example.com/complete-demo-mobile.mp4', alt: 'Complete Demo Mobile Video', }, poster: { url: 'https://cdn.example.com/poster.jpg', alt: 'Video Poster', }, items: [ { tab: 'Feature A', desc: 'Detailed description for Feature A with benefits and use cases.', icon: { url: 'https://cdn.example.com/icon-a.png', alt: 'Feature A Icon', }, }, { tab: 'Feature B', desc: 'Detailed description for Feature B with benefits and use cases.', icon: { url: 'https://cdn.example.com/icon-b.png', alt: 'Feature B Icon', }, }, { tab: 'Feature C', desc: 'Detailed description for Feature C with benefits and use cases.', icon: { url: 'https://cdn.example.com/icon-c.png', alt: 'Feature C Icon', }, }, { tab: 'Feature D', desc: 'Detailed description for Feature D with benefits and use cases.', icon: { url: 'https://cdn.example.com/icon-d.png', alt: 'Feature D Icon', }, }, { tab: 'Feature E', desc: 'Detailed description for Feature E with benefits and use cases.', icon: { url: 'https://cdn.example.com/icon-e.png', alt: 'Feature E Icon', }, }, ], timeIdx: [ { time: 3.0, point: 3.0, highlightIdx: 0 }, { time: 3.5, point: 6.5, highlightIdx: 1 }, { time: 3.0, point: 9.5, highlightIdx: 2 }, { time: 3.5, point: 13.0, highlightIdx: 3 }, { time: 3.0, point: 16.0, highlightIdx: 4 }, ], theme: 'light', }} className="my-custom-tabs" />

核心算法解析

算法 1: Tab 轮播算法

组件实现了只显示 5 个 Tab 的轮播效果,通过数组操作实现无限循环:

// 核心配置 const row = 5 // 显示 5 个 Tab const center = 2 // 中心位置 index = 2 const listDouble = [...list, ...list] // 双倍列表实现无限循环 // 初始显示 0-4 索引的 Tab const [cureentList, setCureentList] = useState(listDouble.slice(0, row)) // 向右滚动(点击右侧 Tab) if (index > center) { const gap = index - center // 计算滚动距离 const copy = [...cureentList] copy.splice(0, gap) // 删除左侧 gap 个元素 const lastkey = cureentList[cureentList.length - 1].key + 1 const add = listDouble.splice(lastkey, gap) // 从右侧添加 gap 个元素 copy.push(...add) setCureentList([...copy]) } // 向左滚动(点击左侧 Tab) if (index < center) { const gap = center - index const copy = [...cureentList] copy.splice(-gap) // 删除右侧 gap 个元素 const firstkey = cureentList[0].key const add = listDouble.splice(listLength + firstkey - gap, gap) copy.unshift(...add) // 从左侧添加 gap 个元素 setCureentList([...copy]) }

算法说明:

  1. 双倍列表: 将原列表复制一份拼接,实现循环滚动
  2. 固定中心: 始终保持 index=2 位置为活跃 Tab
  3. 动态替换: 根据点击位置计算需要替换的元素数量
  4. 无缝循环: 通过 key 值跟踪确保无限循环

示例场景:

原始列表: [A, B, C, D, E, F] 双倍列表: [A, B, C, D, E, F, A, B, C, D, E, F] 初始显示(0-4): [A, B, C*, D, E] (* 表示活跃 Tab) 点击 D(index=3): [B, C, D*, E, F] 点击 E(index=4): [C, D, E*, F, A] 点击 A(index=0): [D, C, A*, B, C]

算法 2: 视频时间同步算法

根据视频播放时间自动切换 Tab:

const handleTimeUpdate = () => { const currentTime = video.currentTime // 遍历 timeIdx 找到当前时间对应的 Tab for (let i = 0; i < timeIdx.length; i++) { const prev = i === 0 ? 0 : timeIdx[i - 1].point if (currentTime >= prev && currentTime < timeIdx[i].point) { // isAuto=true 表示由视频触发,不再跳转视频时间 handleNavClick(timeIdx[i].highlightIdx, timeIdx[i].highlightIdx, true) break } } // 视频播放完自动重播 if (currentTime >= timeIdx[timeIdx.length - 1].point) { video.currentTime = 0 video.play() } } video.addEventListener('timeupdate', handleTimeUpdate)

算法说明:

  1. 监听 timeupdate: 视频播放时持续监听时间更新
  2. 区间匹配: 找到当前时间落在哪个 timeIdx 区间
  3. 自动切换: 调用 handleNavClick 切换 Tab(isAuto=true 避免递归)
  4. 循环播放: 播放完毕后重置到 0 秒并重新播放

时间区间计算:

timeIdx = [ { time: 2.4, point: 2.4, highlightIdx: 0 }, // 区间: [0, 2.4) { time: 2.5, point: 4.9, highlightIdx: 1 }, // 区间: [2.4, 4.9) { time: 2.5, point: 7.4, highlightIdx: 2 }, // 区间: [4.9, 7.4) ] currentTime = 3.0 秒 → 落在区间 [2.4, 4.9) → 高亮 highlightIdx = 1 (第 2 个 Tab)

算法 3: 点击 Tab 跳转视频算法

点击 Tab 时计算累计时间并跳转视频:

const handleNavClick = (key: number, index: number, isAuto?: boolean) => { // 更新 Tab 轮播显示 setIdx(key) swiper?.slideTo(key) // 切换 Swiper 描述 // 如果是手动点击(非视频自动触发) if (!isAuto) { let time = 0 // 计算累计时间 timeIdx.forEach(item => { if (item.highlightIdx < key) { time += item.time } }) // 跳转到对应时间点 if (videoRef.current) { videoRef.current.currentTime = time } } }

算法说明:

  1. isAuto 判断: 区分手动点击和视频自动触发
  2. 累计时间: 累加目标 Tab 之前所有片段的时长
  3. 视频跳转: 设置 video.currentTime 跳转到计算的时间点
  4. Swiper 同步: 同步切换 Swiper 显示对应描述

跳转时间计算示例:

timeIdx = [ { time: 2.4, point: 2.4, highlightIdx: 0 }, { time: 2.5, point: 4.9, highlightIdx: 1 }, { time: 2.5, point: 7.4, highlightIdx: 2 }, ] // 点击第 3 个 Tab (highlightIdx=2) time = 0 item[0]: highlightIdx=0 < 2 → time += 2.4 = 2.4 item[1]: highlightIdx=1 < 2 → time += 2.5 = 4.9 item[2]: highlightIdx=2 不小于 2 → 跳过 → 视频跳转到 4.9

BEM 类结构

组件使用完整的 BEM 命名规范,便于自定义样式:

根元素

  • .tabs-with-media - 组件根容器

标题区域

  • .tabs-with-media__title - 主标题

Tab 区域

  • .tabs-with-media__tabs-wrapper - Tab 外层包装器(处理滚动)
  • .tabs-with-media__tabs-inner - Tab 内层容器
  • .tabs-with-media__tabs-container - Tab 容器(包含所有 Tab 项)
  • .tabs-with-media__tab-item - 单个 Tab 项(motion.div)
  • .tabs-with-media__tab-inner - Tab 项内层容器
  • .tabs-with-media__tab-content - Tab 项内容容器
  • .tabs-with-media__tab-icon - Tab 图标容器
  • .tabs-with-media__tab-icon-image - Tab 图标图片
  • .tabs-with-media__tab-text-wrapper - Tab 文本包装器
  • .tabs-with-media__tab-text - Tab 文本(动态宽度)

描述区域

  • .tabs-with-media__description-container - 描述区域外层容器
  • .tabs-with-media__description-swiper - 描述 Swiper 实例
  • .tabs-with-media__description-slide - 描述滑块
  • .tabs-with-media__description-content - 描述内容容器
  • .tabs-with-media__description-text - 描述文本

视频区域

  • .tabs-with-media__video-wrapper - 视频外层容器
  • .tabs-with-media__video - 视频元素

响应式行为

断点说明

组件在不同断点下的行为变化:

断点范围Tab 图标尺寸Tab 文本视频高度Gap
Mobile0-767px28px动态显示360px16px
Tablet768-1024px28px动态显示360px8px
Laptop1025-1439px28px动态显示360px8px
Desktop1440-1919px28px动态显示448px8px
LG Desktop≥1920px48px动态显示560px8px

响应式视频切换

const isMob = useMediaQuery({ query: '(max-width: 768px)' }) <video src={isMob ? mobvideo?.url : video?.url} // 移动端使用 mobvideo poster={poster?.url} playsInline autoPlay muted loop />

说明:

  • 桌面端(≥768px): 使用 video.url
  • 移动端(<768px): 优先使用 mobvideo.url,如果未提供则回退到 video.url
  • playsInline: iOS Safari 内联播放
  • muted: Chrome 要求静音才能自动播放
  • loop: 自动循环播放

Tab 文本动态宽度

Tab 文本使用 CSS calc-size(auto, size) 实现动态宽度展开效果:

<Heading className={cn( 'tabs-with-media__tab-text size-0 opacity-0', { 'h-auto w-[calc-size(auto,size)] px-[6px] opacity-100': idx === item?.key, // 仅活跃 Tab 显示文本,其他 Tab 隐藏 } )} />

CSS 动画效果:

.tabs-with-media__tab-text { width: 0; height: 0; opacity: 0; overflow: hidden; transition: all 0.3s ease-linear; } .tabs-with-media__tab-text.active { width: calc-size(auto, size); /* 自适应宽度 */ height: auto; padding: 0 6px; opacity: 1; }

响应式布局示意图

┌─────────────────────────────────────────────────────────────┐ │ Mobile (0-767px) │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Title (Heading Size 4) │ │ │ └───────────────────────────────────────────────────────┘ │ │ ┌──────┬──────┬──────┬──────┬──────┐ Gap: 16px │ │ │ Tab1 │ Tab2 │ Tab3*│ Tab4 │ Tab5 │ Icon: 28px │ │ └──────┴──────┴──────┴──────┴──────┘ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Description (Swiper) │ │ │ └───────────────────────────────────────────────────────┘ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Video (360px height) │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ │ Desktop (≥1440px) │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Title (Heading Size 4) │ │ │ └───────────────────────────────────────────────────────┘ │ │ ┌──────┬──────┬──────┬──────┬──────┐ Gap: 8px │ │ │ Tab1 │ Tab2 │ Tab3*│ Tab4 │ Tab5 │ Icon: 28px │ │ └──────┴──────┴──────┴──────┴──────┘ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Description (Swiper) │ │ │ └───────────────────────────────────────────────────────┘ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Video (448px height) │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ │ LG Desktop (≥1920px) │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Title (Heading Size 4) │ │ │ └───────────────────────────────────────────────────────┘ │ │ ┌──────┬──────┬──────┬──────┬──────┐ Gap: 8px │ │ │ Tab1 │ Tab2 │ Tab3*│ Tab4 │ Tab5 │ Icon: 48px (larger)│ │ └──────┴──────┴──────┴──────┴──────┘ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Description (Swiper) │ │ │ └───────────────────────────────────────────────────────┘ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Video (560px height) │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘

设计规范

使用建议

  1. Tab 数量: 建议 3-6 个 Tab,过多会影响用户体验
  2. 视频时长: 建议 15-25 秒,每个 Tab 分配 2-5 秒
  3. 图标设计: 图标建议 48x48px PNG 透明背景,简洁明了
  4. 文案长度: Tab 文本建议 8-15 个字符,描述文本建议 60-100 个字符
  5. 视频格式: 推荐 MP4 H.264 编码,码率 2-5 Mbps

最佳实践

// ✅ 推荐: 6 个 Tab,均衡分配时间 const timeIdx = [ { time: 2.4, point: 2.4, highlightIdx: 0 }, { time: 2.5, point: 4.9, highlightIdx: 1 }, { time: 2.5, point: 7.4, highlightIdx: 2 }, { time: 2.8, point: 10.2, highlightIdx: 3 }, { time: 2.6, point: 12.8, highlightIdx: 4 }, { time: 3.2, point: 16, highlightIdx: 5 }, ] // ❌ 不推荐: 时间分配不均 const timeIdx = [ { time: 1.0, point: 1.0, highlightIdx: 0 }, // 太短 { time: 8.0, point: 9.0, highlightIdx: 1 }, // 太长 { time: 1.5, point: 10.5, highlightIdx: 2 }, // 太短 ]

视频制作建议

  1. 时长控制: 每个 Tab 对应的视频片段建议 2-4 秒
  2. 转场设计: 视频片段之间使用淡入淡出转场
  3. 节奏把控: 视频节奏与 Tab 切换保持同步
  4. 画面构图: 确保每个片段的主体清晰可见
  5. 字幕辅助: 建议在视频中添加字幕辅助理解

颜色规范

Light 主题 (默认):

  • 标题: text-gray-900
  • Tab 文本: text-gray-700
  • 描述文本: text-gray-600
  • Tab 图标背景(活跃): linear-gradient(90deg, #3AD1FF 0%, #008CD6 100%)
  • Tab 图标背景(非活跃): 透明

Dark 主题:

  • 标题: text-gray-100
  • Tab 文本: text-gray-200
  • 描述文本: text-gray-300
  • Tab 图标背景(活跃): linear-gradient(90deg, #3AD1FF 0%, #008CD6 100%)
  • Tab 图标背景(非活跃): 透明

无障碍性

ARIA 标签

组件实现了完整的 ARIA 标签以支持屏幕阅读器:

<section ref={boxRef} data-ui-component-id="TabsWithMedia" role="region" aria-label={title} > <Heading as="h3" size={4} html={title} id="tabs-title" /> <div role="tablist" aria-labelledby="tabs-title" > {items.map((item, index) => ( <div role="tab" aria-selected={idx === index} aria-controls={`panel-${index}`} tabIndex={idx === index ? 0 : -1} onClick={() => handleNavClick(index, index)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { handleNavClick(index, index) } }} > {item.tab} </div> ))} </div> <div role="tabpanel" id={`panel-${idx}`} aria-labelledby={`tab-${idx}`} > <video ref={videoRef} aria-label={`${title} demonstration video`} > <track kind="captions" src="/captions.vtt" srcLang="en" label="English" /> </video> </div> </section>

键盘导航

操作
Tab在 Tab 项之间导航
Enter / Space激活当前 Tab 并跳转视频
Arrow Left切换到上一个 Tab(建议实现)
Arrow Right切换到下一个 Tab(建议实现)
Home跳转到第一个 Tab(建议实现)
End跳转到最后一个 Tab(建议实现)

屏幕阅读器支持

  • Tab 角色: 每个 Tab 项使用 role="tab"aria-selected 属性
  • Tab 面板: 描述区域使用 role="tabpanel" 和对应的 aria-labelledby
  • 视频标签: 视频元素使用 aria-label 说明内容
  • 字幕支持: 建议添加 &lt;track&gt; 元素提供字幕

焦点管理

// 自动管理焦点,活跃 Tab 可聚焦,其他不可聚焦 <div tabIndex={idx === index ? 0 : -1} role="tab" aria-selected={idx === index} > {item.tab} </div>

性能优化

React 优化

// 使用 useMemo 缓存列表计算 const [list, listDouble, row, center, listLength] = useMemo(() => { const row = 5 const center = 2 const listLength = items.length const list = items?.map((item, index) => ({ ...item, key: index, index: index, })) || [] const listSecond = items?.map((item, index) => ({ ...item, key: index + listLength, index: index, })) || [] return [list, list.concat(listSecond), row, center, listLength] }, [items, isMob]) // 使用 React.memo 包裹子组件 const TabItem = React.memo(({ item, isActive, onClick }) => ( <motion.div onClick={onClick}> {/* Tab 内容 */} </motion.div> ))

视频优化

// 1. 视频预加载 <video preload="metadata" // 只预加载元数据,不预加载视频内容 playsInline // 移动端内联播放 muted // 静音自动播放(Chrome 要求) /> // 2. 响应式视频源 const isMob = useMediaQuery({ query: '(max-width: 768px)' }) const videoSrc = isMob ? mobvideo?.url : video?.url // 3. 视频格式优化 // - 使用 MP4 + H.264 编码 // - 码率: 2-5 Mbps // - 分辨率: 桌面 1920x1080, 移动 720p // - 压缩: 使用 FFmpeg 优化

Framer Motion 优化

// 使用 AnimatePresence 的 mode="popLayout" <AnimatePresence mode="popLayout"> {cureentList?.map((item) => ( <motion.div layout // 自动布局动画 initial={{ opacity: 1 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.3, // 短动画时长 ease: 'easeOut', }} > {/* Tab 内容 */} </motion.div> ))} </AnimatePresence>

Swiper 优化

// 使用 EffectFade 代替 Slide <Swiper effect="fade" fadeEffect={{ crossFade: true }} // 交叉淡入淡出 modules={[Pagination, EffectFade]} // 只引入需要的模块 slidesPerView={1} />

性能监控

// 使用 useExposure 钩子监控曝光 import { useExposure } from '@/hooks/useExposure' const TabsWithMedia = ({ data }) => { const boxRef = useRef(null) useExposure({ ref: boxRef, onExposure: () => { // 曝光埋点逻辑 console.log('TabsWithMedia exposed') }, }) return <section ref={boxRef}>...</section> }

常见问题 FAQ

1. 如何自定义 Tab 数量和时间分配?

: 通过 timeIdx 配置自定义时间点:

// 假设 4 个 Tab,视频总时长 15 秒 <TabsWithMedia data={{ items: [ { tab: 'Tab 1', desc: '...', icon: {...} }, { tab: 'Tab 2', desc: '...', icon: {...} }, { tab: 'Tab 3', desc: '...', icon: {...} }, { tab: 'Tab 4', desc: '...', icon: {...} }, ], timeIdx: [ { time: 3.0, point: 3.0, highlightIdx: 0 }, // Tab 1: 0-3s { time: 4.0, point: 7.0, highlightIdx: 1 }, // Tab 2: 3-7s { time: 5.0, point: 12.0, highlightIdx: 2 }, // Tab 3: 7-12s { time: 3.0, point: 15.0, highlightIdx: 3 }, // Tab 4: 12-15s ], }} />

计算公式:

  • time: 当前片段时长
  • point: 累计到当前的总时长 = 前面所有 time 之和 + 当前 time
  • highlightIdx: Tab 索引(从 0 开始)

2. 为什么只显示 5 个 Tab,如何修改?

: 组件设计为同时显示 5 个 Tab 以优化视觉效果。如需修改,可以调整源码中的 row 常量:

// 在 TabsWithMedia.tsx 中 const [list, listDouble, row, center, listLength] = useMemo(() => { const row = 7 // 修改为 7,同时显示 7 个 Tab const center = 3 // 中心位置改为 3 (即第 4 个 Tab) // ... }, [items, isMob])

注意: 修改后需要同步调整 center 值,建议 center = Math.floor(row / 2)

3. 视频播放到最后会怎样?

: 组件会自动检测视频播放到最后一个时间点后,重置到 0 秒并重新播放:

// 自动循环播放逻辑 if (currentTime >= timeIdx[timeIdx.length - 1].point) { video.currentTime = 0 video.play() }

4. 如何禁用视频自动切换 Tab?

: 移除 timeIdx 配置,组件将只响应用户点击:

<TabsWithMedia data={{ title: 'Manual Control Only', video: { url: '...', alt: '...' }, items: [...], // 不传 timeIdx,视频不会自动切换 Tab }} />

注意: 这种情况下,点击 Tab 仍然可以手动跳转视频时间点,但视频播放不会触发 Tab 切换。

5. 移动端和桌面端可以使用不同视频吗?

: 可以,通过 videomobvideo 配置:

<TabsWithMedia data={{ video: { url: 'https://cdn.example.com/desktop-video.mp4', // 桌面端视频 alt: 'Desktop Video', }, mobvideo: { url: 'https://cdn.example.com/mobile-video.mp4', // 移动端视频 alt: 'Mobile Video', }, items: [...], }} />

组件会根据屏幕宽度(768px)自动切换视频源。

6. Tab 文本如何实现动态宽度效果?

: 使用 CSS calc-size(auto, size) 和条件类名:

.tabs-with-media__tab-text { /* 初始状态: 宽度为 0,透明 */ width: 0; height: 0; opacity: 0; overflow: hidden; transition: all 0.3s ease-linear; } /* 活跃状态: 宽度自动,不透明 */ .tabs-with-media__tab-text.active { width: calc-size(auto, size); height: auto; padding: 0 6px; opacity: 1; }

7. 如何自定义 Tab 图标的渐变背景?

: 修改 inline style 中的 background 属性:

<div style={ idx === item?.key ? { // 自定义渐变背景 background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', } : undefined } > <Picture source={item?.icon?.url} alt={item?.icon?.alt} /> </div>

或者通过 CSS 变量:

:root { --tab-gradient: linear-gradient(90deg, #3AD1FF 0%, #008CD6 100%); } .tabs-with-media__tab-icon { background: var(--tab-gradient); }

8. Swiper 淡入淡出效果如何配置?

: 组件使用 effect="fade"EffectFade 模块:

import { Swiper, SwiperSlide } from 'swiper/react' import { Pagination, EffectFade } from 'swiper/modules' <Swiper slidesPerView={1} effect="fade" fadeEffect={{ crossFade: true }} // 交叉淡入淡出 modules={[Pagination, EffectFade]} onSlideChange={(swiper) => { setIdx(swiper.realIndex) }} > {items.map((item, index) => ( <SwiperSlide key={index}> <Text html={item.desc} /> </SwiperSlide> ))} </Swiper>

9. 如何优化视频加载性能?

: 采用以下优化策略:

  1. 预加载优化: 使用 preload="metadata" 只加载元数据
  2. 响应式视频: 移动端使用较小分辨率视频
  3. 视频格式: 使用 MP4 + H.264 编码,码率 2-5 Mbps
  4. 视频压缩: 使用 FFmpeg 压缩视频文件
  5. CDN 加速: 将视频托管在 CDN 上
# 使用 FFmpeg 压缩视频 ffmpeg -i input.mp4 -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k output.mp4

10. Tab 轮播动画如何自定义?

: 通过修改 Framer Motion 的 transition 配置:

<motion.div layout initial={{ opacity: 1 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.5, // 修改动画时长 ease: 'easeInOut', // 修改缓动函数 delay: 0.1, // 添加延迟 }} > {/* Tab 内容 */} </motion.div>

相关资源

设计资源

代码资源

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

依赖组件

  • Heading - 标题组件 (@anker-in/headless-ui)
  • Text - 文本组件 (@anker-in/headless-ui)
  • Picture - 图片组件 (@anker-in/headless-ui)

外部依赖

  • framer-motion - 动画库 (v10.0+)
  • swiper - 轮播库 (v11.0+)
  • react-responsive - 响应式判断 (v9.0+)

相关文档


Changelog

v1.0.0 (2026-01-16)

首次发布

  • ✅ 视频时间自动切换 Tab
  • ✅ 点击 Tab 跳转视频
  • ✅ 轮播 Tab 显示(5 个)
  • ✅ Swiper EffectFade 切换
  • ✅ Framer Motion 动画
  • ✅ 响应式视频(桌面/移动端)
  • ✅ 主题切换(light/dark)
  • ✅ 完整 BEM 命名
  • ✅ TypeScript 严格模式
  • ✅ 曝光埋点支持

已知问题:

  • 视频自动播放在某些移动浏览器可能受限(需用户交互)
  • Figma 设计稿不完整但可用

待优化项:

  • 支持自定义 Tab 显示数量(目前固定 5 个)
  • 支持键盘方向键切换 Tab
  • 支持 Home/End 键跳转首尾 Tab
Last updated on