功能特性
- ✅ 桌面端自动轮播 - 3秒间隔自动切换场景,提供流畅的视觉体验
- ✅ 点击切换与定时器重置 - 用户点击场景后重置自动播放定时器
- ✅ 移动端 Swiper 集成 - 支持手势滑动,提供原生体验
- ✅ CSS 进度条动画 - 线性渐变进度指示器,可视化自动播放进度
- ✅ 视频/图片混合媒体 - 支持视频和图片混合展示,灵活配置
- ✅ 响应式资源管理 - 桌面端和移动端独立的媒体资源配置
- ✅ 布局方向配置 - 支持左侧和右侧预览区域布局
- ✅ 主题切换 - 支持亮色/暗色主题,适应不同页面风格
- ✅ 促销标签展示 - 场景项支持可选的促销标签显示
- ✅ 曝光追踪集成 - 内置曝光埋点支持,便于数据分析
- ✅ 无障碍性支持 - 完整的键盘导航和屏幕阅读器支持
Props 参数
MediaSceneSwitcherProps
| Prop | 类型 | 默认值 | 必需 | 说明 |
|---|---|---|---|---|
| data | MediaSceneSwitcherData | - | ✅ | 组件的完整配置数据 |
| className | string | '' | ❌ | 自定义容器样式类 |
| children | React.ReactNode | - | ❌ | 子元素内容 |
MediaSceneSwitcherData
| 字段 | 类型 | 默认值 | 必需 | 说明 |
|---|---|---|---|---|
| title | string | - | ✅ | 主标题文案 |
| subtitle | string | - | ❌ | 副标题文案,支持换行 |
| theme | 'light' | 'dark' | 'light' | ❌ | 主题模式 |
| layout | 'left' | 'right' | 'left' | ❌ | 桌面端预览区域位置(左侧/右侧) |
| items | MediaSceneSwitcherItem[] | [] | ✅ | 场景项数组 |
MediaSceneSwitcherItem
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
| id | string | ✅ | 场景项唯一标识符 |
| title | string | ✅ | 场景标题文案 |
| tag | string | ❌ | 促销标签文案(如 “20% OFF”) |
| videoUrl | string | null | ❌ | 桌面端视频资源 URL,null 表示使用图片 |
| imageUrl | Img | ✅ | 桌面端图片资源(视频封面或主图) |
| mobImageUrl | Img | ✅ | 移动端图片资源 |
Img 类型
interface Img {
url: string // 图片 URL
alt: string // 替代文本(无障碍必需)
}类型定义
interface MediaSceneSwitcherProps {
data: MediaSceneSwitcherData
className?: string
children?: React.ReactNode
}
interface MediaSceneSwitcherData {
title: string
subtitle?: string
theme?: 'light' | 'dark'
layout?: 'left' | 'right'
items: MediaSceneSwitcherItem[]
}
interface MediaSceneSwitcherItem {
id: string
title: string
tag?: string
videoUrl: string | null
imageUrl: Img
mobImageUrl: Img
}
interface Img {
url: string
alt: string
}使用示例
示例 1: 基础用法(3个场景 - 清洁主题)
import { MediaSceneSwitcher } from '@anker-in/headless-ui/biz'
export default function BasicExample() {
return (
<MediaSceneSwitcher
data={{
title: '360 Times/Minute',
subtitle: 'Self-Cleaning Robot Vacuum',
theme: 'dark',
layout: 'left',
items: [
{
id: 'scene-1',
title: 'Scrapes Off Dirt',
tag: '20% OFF',
videoUrl: null,
imageUrl: {
url: 'https://images.unsplash.com/photo-1556742031-c6961e8560b0?w=824&h=640&q=80&fit=crop',
alt: 'Scraping off dirt from floor',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1556742031-c6961e8560b0?w=296&h=360&q=80&fit=crop',
alt: 'Scraping off dirt mobile',
},
},
{
id: 'scene-2',
title: 'Cleans the Mop with Water and Detergent',
tag: '20% OFF',
videoUrl: null,
imageUrl: {
url: 'https://images.unsplash.com/photo-1563453392212-326f5e854473?w=824&h=640&q=80&fit=crop',
alt: 'Cleaning mop with water and detergent',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1563453392212-326f5e854473?w=296&h=360&q=80&fit=crop',
alt: 'Cleaning mop mobile',
},
},
{
id: 'scene-3',
title: 'Squeezes Out Dirty Water',
tag: '20% OFF',
videoUrl: null,
imageUrl: {
url: 'https://images.unsplash.com/photo-1527515637462-cff94eecc1ac?w=824&h=640&q=80&fit=crop',
alt: 'Squeezing out dirty water',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1527515637462-cff94eecc1ac?w=296&h=360&q=80&fit=crop',
alt: 'Squeezing water mobile',
},
},
],
}}
/>
)
}示例 2: 视频场景(混合视频和图片)
import { MediaSceneSwitcher } from '@anker-in/headless-ui/biz'
export default function VideoExample() {
return (
<MediaSceneSwitcher
data={{
title: 'Advanced Features',
subtitle: 'Experience the Power',
theme: 'light',
layout: 'right',
items: [
{
id: 'video-scene-1',
title: 'Powerful Suction',
videoUrl: 'https://cdn.example.com/videos/suction.mp4',
imageUrl: {
url: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=824&h=640&q=80',
alt: 'Suction demonstration',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=296&h=360&q=80',
alt: 'Suction mobile',
},
},
{
id: 'video-scene-2',
title: 'Smart Navigation',
videoUrl: null, // Image only
imageUrl: {
url: 'https://images.unsplash.com/photo-1563453392212-326f5e854473?w=824&h=640&q=80',
alt: 'Smart navigation',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1563453392212-326f5e854473?w=296&h=360&q=80',
alt: 'Navigation mobile',
},
},
],
}}
/>
)
}示例 3: 亮色主题(右侧布局)
import { MediaSceneSwitcher } from '@anker-in/headless-ui/biz'
export default function LightThemeExample() {
return (
<MediaSceneSwitcher
data={{
title: 'Why Choose Us',
subtitle: 'Top Features That Matter',
theme: 'light', // 亮色主题
layout: 'right', // 预览区域在右侧
items: [
{
id: 'feature-1',
title: 'Long Battery Life',
tag: 'NEW',
videoUrl: null,
imageUrl: {
url: 'https://images.unsplash.com/photo-1609091839311-d98929fc8f1e?w=824&h=640&q=80',
alt: 'Battery life',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1609091839311-d98929fc8f1e?w=296&h=360&q=80',
alt: 'Battery mobile',
},
},
{
id: 'feature-2',
title: 'Quiet Operation',
videoUrl: null,
imageUrl: {
url: 'https://images.unsplash.com/photo-1595246140625-573b715d11dc?w=824&h=640&q=80',
alt: 'Quiet operation',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1595246140625-573b715d11dc?w=296&h=360&q=80',
alt: 'Quiet mobile',
},
},
],
}}
/>
)
}示例 4: 暗色主题(左侧布局)
import { MediaSceneSwitcher } from '@anker-in/headless-ui/biz'
export default function DarkThemeExample() {
return (
<MediaSceneSwitcher
data={{
title: 'Premium Experience',
subtitle: 'Designed for Excellence',
theme: 'dark', // 暗色主题
layout: 'left', // 预览区域在左侧(默认)
items: [
{
id: 'premium-1',
title: 'Premium Build Quality',
tag: 'BEST SELLER',
videoUrl: null,
imageUrl: {
url: 'https://images.unsplash.com/photo-1625772452888-7e9b8f8f583e?w=824&h=640&q=80',
alt: 'Premium quality',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1625772452888-7e9b8f8f583e?w=296&h=360&q=80',
alt: 'Quality mobile',
},
},
{
id: 'premium-2',
title: 'Advanced AI Features',
videoUrl: null,
imageUrl: {
url: 'https://images.unsplash.com/photo-1485827404703-89b55fcc595e?w=824&h=640&q=80',
alt: 'AI features',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1485827404703-89b55fcc595e?w=296&h=360&q=80',
alt: 'AI mobile',
},
},
],
}}
/>
)
}示例 5: 多场景展示(5个场景)
import { MediaSceneSwitcher } from '@anker-in/headless-ui/biz'
export default function MultiSceneExample() {
return (
<MediaSceneSwitcher
data={{
title: 'Complete Cleaning Solution',
subtitle: '5 Powerful Features in One Device',
theme: 'dark',
layout: 'left',
items: [
{
id: 'clean-1',
title: 'Deep Carpet Cleaning',
tag: '30% OFF',
videoUrl: null,
imageUrl: {
url: 'https://images.unsplash.com/photo-1581578731548-c64695cc6952?w=824&h=640&q=80',
alt: 'Carpet cleaning',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1581578731548-c64695cc6952?w=296&h=360&q=80',
alt: 'Carpet mobile',
},
},
{
id: 'clean-2',
title: 'Hard Floor Mopping',
videoUrl: null,
imageUrl: {
url: 'https://images.unsplash.com/photo-1629116958133-28b8e5334f3d?w=824&h=640&q=80',
alt: 'Floor mopping',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1629116958133-28b8e5334f3d?w=296&h=360&q=80',
alt: 'Mopping mobile',
},
},
{
id: 'clean-3',
title: 'Edge & Corner Cleaning',
videoUrl: null,
imageUrl: {
url: 'https://images.unsplash.com/photo-1628177142898-93e36e4e3a50?w=824&h=640&q=80',
alt: 'Edge cleaning',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1628177142898-93e36e4e3a50?w=296&h=360&q=80',
alt: 'Edge mobile',
},
},
{
id: 'clean-4',
title: 'Self-Emptying Station',
tag: 'NEW',
videoUrl: null,
imageUrl: {
url: 'https://images.unsplash.com/photo-1556742031-c6961e8560b0?w=824&h=640&q=80',
alt: 'Self-emptying',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1556742031-c6961e8560b0?w=296&h=360&q=80',
alt: 'Emptying mobile',
},
},
{
id: 'clean-5',
title: 'Automatic Drying',
videoUrl: null,
imageUrl: {
url: 'https://images.unsplash.com/photo-1563453392212-326f5e854473?w=824&h=640&q=80',
alt: 'Automatic drying',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1563453392212-326f5e854473?w=296&h=360&q=80',
alt: 'Drying mobile',
},
},
],
}}
/>
)
}示例 6: 带促销标签
import { MediaSceneSwitcher } from '@anker-in/headless-ui/biz'
export default function PromotionExample() {
return (
<MediaSceneSwitcher
data={{
title: 'Holiday Sale',
subtitle: 'Limited Time Offers',
theme: 'light',
layout: 'right',
items: [
{
id: 'promo-1',
title: 'Premium Model',
tag: '50% OFF', // 促销标签
videoUrl: null,
imageUrl: {
url: 'https://images.unsplash.com/photo-1609091839311-d98929fc8f1e?w=824&h=640&q=80',
alt: 'Premium model',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1609091839311-d98929fc8f1e?w=296&h=360&q=80',
alt: 'Premium mobile',
},
},
{
id: 'promo-2',
title: 'Standard Model',
tag: 'SAVE $100',
videoUrl: null,
imageUrl: {
url: 'https://images.unsplash.com/photo-1595246140625-573b715d11dc?w=824&h=640&q=80',
alt: 'Standard model',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1595246140625-573b715d11dc?w=296&h=360&q=80',
alt: 'Standard mobile',
},
},
{
id: 'promo-3',
title: 'Compact Model',
// 无 tag 字段,不显示标签
videoUrl: null,
imageUrl: {
url: 'https://images.unsplash.com/photo-1625772452888-7e9b8f8f583e?w=824&h=640&q=80',
alt: 'Compact model',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1625772452888-7e9b8f8f583e?w=296&h=360&q=80',
alt: 'Compact mobile',
},
},
],
}}
/>
)
}示例 7: 响应式资源(桌面端视频+移动端图片)
import { MediaSceneSwitcher } from '@anker-in/headless-ui/biz'
export default function ResponsiveMediaExample() {
return (
<MediaSceneSwitcher
data={{
title: 'Smart Cleaning',
subtitle: 'See the Difference',
theme: 'dark',
layout: 'left',
items: [
{
id: 'responsive-1',
title: 'Obstacle Detection',
videoUrl: 'https://cdn.example.com/videos/obstacle.mp4', // 桌面端播放视频
imageUrl: {
url: 'https://images.unsplash.com/photo-1485827404703-89b55fcc595e?w=824&h=640&q=80',
alt: 'Obstacle detection video cover',
},
mobImageUrl: { // 移动端显示图片(性能优化)
url: 'https://images.unsplash.com/photo-1485827404703-89b55fcc595e?w=296&h=360&q=80',
alt: 'Obstacle detection mobile',
},
},
{
id: 'responsive-2',
title: 'Auto-Mapping',
videoUrl: 'https://cdn.example.com/videos/mapping.mp4',
imageUrl: {
url: 'https://images.unsplash.com/photo-1581578731548-c64695cc6952?w=824&h=640&q=80',
alt: 'Mapping video cover',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1581578731548-c64695cc6952?w=296&h=360&q=80',
alt: 'Mapping mobile',
},
},
],
}}
/>
)
}示例 8: 自定义容器样式
import { MediaSceneSwitcher } from '@anker-in/headless-ui/biz'
export default function CustomStyleExample() {
return (
<div className="my-custom-container">
<MediaSceneSwitcher
className="shadow-2xl rounded-xl" // 自定义样式
data={{
title: 'Stylish Display',
subtitle: 'With Custom Styling',
theme: 'light',
layout: 'left',
items: [
{
id: 'style-1',
title: 'Feature One',
videoUrl: null,
imageUrl: {
url: 'https://images.unsplash.com/photo-1625772452888-7e9b8f8f583e?w=824&h=640&q=80',
alt: 'Feature one',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1625772452888-7e9b8f8f583e?w=296&h=360&q=80',
alt: 'Feature one mobile',
},
},
],
}}
/>
</div>
)
}示例 9: 单场景展示
import { MediaSceneSwitcher } from '@anker-in/headless-ui/biz'
export default function SingleSceneExample() {
return (
<MediaSceneSwitcher
data={{
title: 'Flagship Feature',
subtitle: 'Our Most Advanced Technology',
theme: 'dark',
layout: 'left',
items: [
{
id: 'single-scene',
title: 'Ultra-Clean System',
tag: 'FLAGSHIP',
videoUrl: 'https://cdn.example.com/videos/flagship.mp4',
imageUrl: {
url: 'https://images.unsplash.com/photo-1556742031-c6961e8560b0?w=824&h=640&q=80',
alt: 'Ultra-clean system',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1556742031-c6961e8560b0?w=296&h=360&q=80',
alt: 'Ultra-clean mobile',
},
},
],
}}
/>
)
}示例 10: 完整配置(所有可选参数)
import { MediaSceneSwitcher } from '@anker-in/headless-ui/biz'
export default function FullConfigExample() {
return (
<MediaSceneSwitcher
className="max-w-7xl mx-auto" // 自定义容器样式
data={{
title: 'Complete Feature Set', // 必需
subtitle: 'Everything You Need in One Place\nSecond Line Supported', // 可选,支持换行
theme: 'dark', // 可选,默认 'light'
layout: 'right', // 可选,默认 'left'
items: [ // 必需
{
id: 'full-1', // 必需
title: 'Advanced Cleaning', // 必需
tag: 'EXCLUSIVE', // 可选
videoUrl: 'https://cdn.example.com/videos/advanced.mp4', // 可选
imageUrl: { // 必需(视频封面或主图)
url: 'https://images.unsplash.com/photo-1485827404703-89b55fcc595e?w=824&h=640&q=80',
alt: 'Advanced cleaning feature',
},
mobImageUrl: { // 必需
url: 'https://images.unsplash.com/photo-1485827404703-89b55fcc595e?w=296&h=360&q=80',
alt: 'Advanced cleaning mobile',
},
},
{
id: 'full-2',
title: 'Smart Navigation',
tag: '4.0 AI',
videoUrl: null, // null 表示仅使用图片
imageUrl: {
url: 'https://images.unsplash.com/photo-1581578731548-c64695cc6952?w=824&h=640&q=80',
alt: 'Smart navigation',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1581578731548-c64695cc6952?w=296&h=360&q=80',
alt: 'Smart navigation mobile',
},
},
{
id: 'full-3',
title: 'Long Battery Life',
// 无 tag 字段
videoUrl: null,
imageUrl: {
url: 'https://images.unsplash.com/photo-1609091839311-d98929fc8f1e?w=824&h=640&q=80',
alt: 'Battery life',
},
mobImageUrl: {
url: 'https://images.unsplash.com/photo-1609091839311-d98929fc8f1e?w=296&h=360&q=80',
alt: 'Battery mobile',
},
},
],
}}
/>
)
}核心算法解析
1. 桌面端自动轮播算法
功能说明: 桌面端每3秒自动切换到下一个场景,循环播放。
核心代码:
const INTERVAL_TIME = 3000 // 3秒间隔
useEffect(() => {
// 移动端或无场景时不启动
if (isMobile || items.length === 0) return
// 启动定时器
intervalRef.current = window.setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % items.length) // 循环索引
}, INTERVAL_TIME)
// 清理定时器
return () => {
if (intervalRef.current) {
window.clearInterval(intervalRef.current)
}
}
}, [isMobile, items.length])算法流程:
1. 检查环境条件
├─ 如果是移动端 → 不启动自动播放
├─ 如果场景数为0 → 不启动自动播放
└─ 否则 → 继续
2. 创建定时器
└─ 每3秒执行一次索引更新
3. 更新当前索引
├─ 当前索引 + 1
├─ 对场景总数取模(实现循环)
└─ 更新状态触发重渲染
4. 组件卸载时清理定时器循环逻辑示例:
// 假设有3个场景 (indices: 0, 1, 2)
items.length = 3
// 第1次: (0 + 1) % 3 = 1
// 第2次: (1 + 1) % 3 = 2
// 第3次: (2 + 1) % 3 = 0 ← 回到起点
// 第4次: (0 + 1) % 3 = 1 ← 继续循环2. 点击切换与定时器重置算法
功能说明: 用户点击场景项时,立即切换到该场景并重置自动播放定时器。
核心代码:
const handleItemClick = (index: number) => {
// 1. 立即切换到点击的场景
setCurrentIndex(index)
// 2. 清除现有定时器
if (intervalRef.current) {
window.clearInterval(intervalRef.current)
}
// 3. 重新启动定时器(从当前场景开始)
intervalRef.current = window.setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % items.length)
}, INTERVAL_TIME)
}算法流程:
用户点击场景2
↓
1. 状态更新: currentIndex = 2
↓
2. 清除旧定时器(停止原有的自动播放计划)
↓
3. 创建新定时器
├─ 从场景2开始
├─ 3秒后 → 场景3
├─ 再3秒 → 场景0
└─ 继续循环...用户体验优化:
- 即时响应: 点击后立即显示目标场景,无延迟
- 时间重置: 用户有完整的3秒时间观看点击的场景
- 防止冲突: 清除旧定时器避免重复切换
3. CSS 进度条动画算法
功能说明: 视觉化3秒自动播放进度,使用 CSS 线性渐变动画。
核心代码:
// 进度条容器
<div className="relative h-1 w-full overflow-hidden rounded-full bg-gray-200/30">
{/* 进度条(线性渐变 #3ad1ff → #008cd6) */}
<div
key={currentIndex} // 场景切换时重新触发动画
className="h-full rounded-full bg-gradient-to-r from-[#3ad1ff] to-[#008cd6]"
style={{
width: '0%', // 初始宽度0
animation: 'progress-grow 3s linear forwards', // 3秒线性增长
}}
/>
</div>
// CSS 关键帧动画
@keyframes progress-grow {
from {
width: 0%;
}
to {
width: 100%;
}
}动画时序:
t=0s: ━━━━━━━━━━ (0%)
↓ linear 线性增长
t=1s: ███━━━━━━━ (33%)
↓
t=2s: ██████━━━━ (67%)
↓
t=3s: ██████████ (100%) → 场景切换,进度条重置关键技术点:
key={currentIndex}: 每次场景切换时,React 重新挂载进度条组件,自动重启动画animation: ... forwards: 动画结束后保持100%状态,不回弹linear缓动函数: 匀速增长,准确反映时间进度- 渐变色
#3ad1ff → #008cd6: 视觉上更吸引人,品牌色应用
4. 移动端 Swiper 配置算法
功能说明: 移动端使用 Swiper 库实现手势滑动,配置自动高度、循环、分页等功能。
核心代码:
import { Swiper, SwiperSlide } from 'swiper/react'
import { Autoplay, Pagination } from 'swiper/modules'
<Swiper
modules={[Autoplay, Pagination]}
spaceBetween={16} // 场景间距16px
slidesPerView={1} // 每屏显示1个场景
autoHeight={true} // 自适应内容高度
loop={items.length > 1} // 2+场景时启用循环
pagination={{
clickable: true, // 分页器可点击
bulletClass: 'swiper-pagination-bullet custom-bullet',
bulletActiveClass: 'swiper-pagination-bullet-active custom-bullet-active',
}}
onSlideChange={(swiper) => {
setCurrentIndex(swiper.realIndex) // 同步索引状态
}}
>
{items.map((item, index) => (
<SwiperSlide key={item.id}>
{/* 场景内容 */}
</SwiperSlide>
))}
</Swiper>配置说明:
| 配置项 | 值 | 说明 |
|---|---|---|
modules | [Autoplay, Pagination] | 启用自动播放和分页模块 |
spaceBetween | 16 | 场景间距16px |
slidesPerView | 1 | 每屏1个场景(全屏) |
autoHeight | true | 自动调整高度匹配内容 |
loop | items.length > 1 | 2+场景时启用循环 |
pagination.clickable | true | 分页器可点击切换 |
onSlideChange | (swiper) => {...} | 同步状态到 React |
循环逻辑:
场景数 = 1: loop = false (无需循环)
场景数 ≥ 2: loop = true
├─ 左滑到第1个场景时,继续左滑 → 跳转到最后场景
└─ 右滑到最后场景时,继续右滑 → 跳转到第1个场景性能优化:
- 使用
item.id作为 key,避免不必要的重渲染 autoHeight: true避免固定高度导致的内容裁剪或空白- 分页器采用自定义样式类,与设计系统统一
5. 响应式资源选择算法
功能说明: 根据设备类型(桌面/移动)和媒体类型(视频/图片),自动选择最优资源。
核心代码:
const getMediaContent = (item: MediaSceneSwitcherItem, isMobile: boolean) => {
// 移动端: 始终使用图片(性能优化)
if (isMobile) {
return (
<Picture
src={item.mobImageUrl.url}
alt={item.mobImageUrl.alt}
className="h-full w-full object-cover"
/>
)
}
// 桌面端: 根据 videoUrl 判断
if (item.videoUrl) {
return (
<video
src={item.videoUrl}
autoPlay
loop
muted
playsInline
className="h-full w-full object-cover"
/>
)
} else {
return (
<Picture
src={item.imageUrl.url}
alt={item.imageUrl.alt}
className="h-full w-full object-cover"
/>
)
}
}决策树:
输入: item, isMobile
↓
是否移动端?
├─ 是 → 返回 item.mobImageUrl (图片)
└─ 否 → 是桌面端
↓
item.videoUrl 是否存在?
├─ 是 → 返回 <video> (桌面视频)
└─ 否 → 返回 item.imageUrl (桌面图片)资源优先级矩阵:
| 设备类型 | videoUrl 存在 | videoUrl 不存在 |
|---|---|---|
| 桌面端 | 播放视频 | 显示 imageUrl |
| 移动端 | 显示 mobImageUrl | 显示 mobImageUrl |
为什么移动端不播放视频?
- 流量节省: 视频文件通常较大(2-10MB),移动网络成本高
- 性能优化: 视频解码消耗CPU/GPU,影响电池续航
- 自动播放限制: 移动浏览器通常禁止自动播放视频(用户体验)
- 加载速度: 图片加载更快,首屏渲染时间更短
6. 场景切换状态管理算法
功能说明: 统一管理桌面端和移动端的场景索引状态,确保两端行为一致。
核心代码:
const [currentIndex, setCurrentIndex] = useState(0) // 当前场景索引
// 桌面端: 自动轮播更新索引
useEffect(() => {
if (isMobile || items.length === 0) return
intervalRef.current = window.setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % items.length)
}, INTERVAL_TIME)
return () => {
if (intervalRef.current) {
window.clearInterval(intervalRef.current)
}
}
}, [isMobile, items.length])
// 桌面端: 点击更新索引
const handleItemClick = (index: number) => {
setCurrentIndex(index)
// ... 重置定时器
}
// 移动端: Swiper 滑动更新索引
<Swiper
onSlideChange={(swiper) => {
setCurrentIndex(swiper.realIndex)
}}
>
{/* ... */}
</Swiper>
// 根据索引渲染对应场景
const currentItem = items[currentIndex]状态同步流程:
桌面端自动播放
↓
setCurrentIndex(1)
↓
currentIndex = 1 → 触发重渲染
↓
显示 items[1] 的内容
↓
进度条重置(key={currentIndex})用户点击场景3
↓
handleItemClick(3)
↓
setCurrentIndex(3)
↓
currentIndex = 3 → 触发重渲染
↓
显示 items[3] 的内容
↓
定时器重置,3秒后自动切换到场景4移动端用户滑动 Swiper
↓
onSlideChange 回调触发
↓
setCurrentIndex(swiper.realIndex)
↓
currentIndex 更新 → 触发重渲染
↓
分页器高亮同步关键技术点:
- 单一状态源:
currentIndex是唯一的真相源,所有UI基于此渲染 - 双向同步:
- 桌面端定时器 → 更新 currentIndex
- 用户交互(点击/滑动) → 更新 currentIndex
- currentIndex → 驱动UI渲染
swiper.realIndexvsswiper.activeIndex:activeIndex: Swiper内部索引,启用loop时会有克隆sliderealIndex: 实际场景索引,始终在[0, items.length-1]范围内
- 边界处理: 使用模运算
% items.length确保索引不越界
7. 布局方向切换算法
功能说明: 根据 layout 参数(‘left’ 或 ‘right’),动态调整桌面端预览区域位置。
核心代码:
const { layout = 'left' } = data
// 桌面端布局容器
<div
className={cn(
'hidden laptop:grid laptop:grid-cols-2 laptop:gap-8',
layout === 'right' && 'laptop:grid-flow-dense' // 反转布局流
)}
>
{/* 场景列表 */}
<div
className={cn(
'space-y-4',
layout === 'right' && 'laptop:col-start-2' // 右侧布局时,场景列表在第2列
)}
>
{items.map((item, index) => (
<SceneItem key={item.id} item={item} index={index} />
))}
</div>
{/* 预览区域 */}
<div
className={cn(
'sticky top-24 h-fit',
layout === 'right' && 'laptop:col-start-1 laptop:row-start-1' // 右侧布局时,预览在第1列
)}
>
<MediaPreview item={currentItem} />
</div>
</div>布局矩阵:
| layout 值 | 场景列表位置 | 预览区域位置 | Grid Flow |
|---|---|---|---|
'left' (默认) | 右侧(col 2) | 左侧(col 1) | 默认 |
'right' | 左侧(col 2) | 右侧(col 1) | dense(反转) |
CSS Grid 技术解析:
/* layout='left' (默认) */
.laptop:grid-cols-2 {
/* 预览区域自动在col 1, 场景列表自动在col 2 */
}
/* layout='right' */
.laptop:grid-flow-dense {
/* 启用密集布局,允许元素填充空白 */
}
.laptop:col-start-2 {
/* 场景列表明确在col 2 */
}
.laptop:col-start-1 {
/* 预览区域明确在col 1 */
}
.laptop:row-start-1 {
/* 预览区域在第1行(保持顶部对齐) */
}视觉效果:
layout='left'
┌──────────┬──────────┐
│ 预览区 │ 场景1 │
│ (固定) ├──────────┤
│ │ 场景2 │
│ ├──────────┤
│ │ 场景3 │
└──────────┴──────────┘
layout='right'
┌──────────┬──────────┐
│ 场景1 │ 预览区 │
├──────────┤ (固定) │
│ 场景2 │ │
├──────────┤ │
│ 场景3 │ │
└──────────┴──────────┘Sticky 定位:
.sticky {
position: sticky;
top: 24px; /* 距离顶部24px时开始固定 */
}- 用户向下滚动时,预览区域保持在视口内(不随滚动消失)
- 用户可以同时看到预览和完整的场景列表
响应式行为
断点定义
| 断点 | 最小宽度 | 描述 | 主要布局变化 |
|---|---|---|---|
| Mobile | < 768px | 手机端 | Swiper 单列,垂直堆叠 |
| Tablet | ≥ 768px | 平板 | 保持 Swiper(过渡阶段) |
| Laptop | ≥ 1025px | 小桌面 | 切换到2列网格,启用自动播放 |
| Desktop | ≥ 1440px | 大桌面 | 增大间距和字号 |
| LG Desktop | ≥ 1920px | 超大屏 | 最大宽度限制,居中对齐 |
响应式特性表
| 特性 | Mobile/Tablet | Laptop+ | 说明 |
|---|---|---|---|
| 交互模式 | Swiper 滑动 | 点击切换 | 移动端手势,桌面端点击 |
| 自动播放 | ❌ 禁用 | ✅ 启用(3秒) | 移动端节省性能和流量 |
| 媒体资源 | mobImageUrl | imageUrl / videoUrl | 独立资源配置 |
| 布局 | 垂直单列 | 水平2列网格 | 桌面端分栏展示 |
| 预览区域 | 内联显示 | Sticky 固定 | 桌面端悬浮预览 |
| 进度条 | ❌ 隐藏 | ✅ 显示 | 仅桌面端显示播放进度 |
| 分页器 | ✅ 显示 | ❌ 隐藏 | 移动端显示圆点导航 |
断点行为示例
// Tailwind CSS 响应式类使用示例
<div className="
flex flex-col gap-4 // Mobile: 垂直堆叠,间距4
tablet:gap-6 // Tablet: 增大间距到6
laptop:grid laptop:grid-cols-2 laptop:gap-8 // Laptop+: 2列网格,间距8
desktop:gap-12 // Desktop: 进一步增大间距
">
{/* 内容 */}
</div>
// 桌面端场景标题
<h3 className="
text-lg font-semibold // Mobile: 18px
laptop:text-xl // Laptop: 20px
desktop:text-2xl // Desktop: 24px
">
{item.title}
</h3>
// 预览区域
<div className="
w-full // Mobile: 全宽
laptop:sticky laptop:top-24 // Laptop+: 固定定位
">
{/* 媒体内容 */}
</div>设计规范
使用场景
- ✅ 产品功能展示 - 多个关键功能点的场景化展示
- ✅ 使用步骤说明 - 分步骤的操作流程演示
- ✅ 技术特性说明 - 突出多个技术卖点
- ✅ 对比场景展示 - 使用前后对比,多种模式对比
- ❌ 单一功能说明 - 仅1个场景时,推荐使用其他组件
- ❌ 文本为主内容 - 媒体内容较少时,不适合使用
场景数量建议
| 场景数 | 推荐度 | 说明 |
|---|---|---|
| 1个 | ⭐ 不推荐 | 无法体现”切换”特性,建议使用其他组件 |
| 2-3个 | ⭐⭐⭐ 推荐 | 简洁明了,用户不会产生疲劳感 |
| 4-5个 | ⭐⭐⭐⭐ 最佳 | 内容丰富但不冗长,自动播放周期适中(12-15秒) |
| 6-8个 | ⭐⭐ 可接受 | 内容较多,注意场景标题简洁,避免信息过载 |
| 9+个 | ⭐ 不推荐 | 自动播放周期过长(27秒+),用户注意力难以维持 |
媒体资源优化
视频规格建议:
| 平台 | 分辨率 | 码率 | 格式 | 文件大小 |
|---|---|---|---|---|
| 桌面端 | 1280x720 | 2-4 Mbps | MP4(H.264) | < 5MB |
| 移动端 | 不使用视频 | - | - | - |
图片规格建议:
| 资源类型 | 尺寸 | 格式 | 文件大小 | 质量 |
|---|---|---|---|---|
imageUrl (桌面) | 824x640px | WebP/JPEG | < 200KB | 80% |
mobImageUrl (移动) | 296x360px | WebP/JPEG | < 100KB | 80% |
Unsplash 查询参数:
// 桌面端图片
`https://images.unsplash.com/photo-xxx?w=824&h=640&q=80&fit=crop`
// 移动端图片
`https://images.unsplash.com/photo-xxx?w=296&h=360&q=80&fit=crop`
// 参数说明:
// w=824 - 宽度824px
// h=640 - 高度640px
// q=80 - 质量80%
// fit=crop - 裁剪模式(填满尺寸)标签文案建议
促销标签(tag)应简短有力:
| 类型 | 推荐文案 | 长度 |
|---|---|---|
| 折扣 | ”20% OFF”, “50% OFF”, “-30%“ | 6-8字符 |
| 价格 | ”SAVE $100”, “¥200 OFF” | 8-10字符 |
| 状态 | ”NEW”, “HOT”, “EXCLUSIVE” | 3-9字符 |
| 标识 | ”BEST SELLER”, “4.0 AI” | 10-12字符 |
注意事项:
- ❌ 避免过长文案: “Limited Time Only Special Discount”
- ✅ 简短有力: “LIMITED”
- ❌ 避免复杂符号: ”★★★★★ 5-Star”
- ✅ 简单直接: “5-STAR”
主题选择建议
| 页面背景色 | 推荐主题 | 原因 |
|---|---|---|
| 浅色背景(白色/灰色) | theme='light' | 保持视觉一致性,避免对比过强 |
| 深色背景(黑色/深灰) | theme='dark' | 暗色模式下文字更清晰 |
| 彩色背景 | 根据明度选择 | 亮度高选 light,亮度低选 dark |
视觉效果:
- Light Theme: 白色背景 + 黑色文字 + 浅灰色边框
- Dark Theme: 深色背景 + 白色文字 + 亮色边框
无障碍性
键盘导航
| 按键 | 桌面端行为 | 移动端行为 |
|---|---|---|
Tab | 聚焦到下一个场景项 | 聚焦到 Swiper 容器 |
Shift + Tab | 聚焦到上一个场景项 | 聚焦到上一个可聚焦元素 |
Enter / Space | 切换到当前聚焦的场景 | 无效(使用触摸手势) |
Arrow Left | 切换到上一个场景 | Swiper 左滑 |
Arrow Right | 切换到下一个场景 | Swiper 右滑 |
Home | 切换到第一个场景 | Swiper 跳转到第一页 |
End | 切换到最后一个场景 | Swiper 跳转到最后一页 |
ARIA 属性
场景列表项:
<button
role="tab"
aria-selected={currentIndex === index}
aria-controls={`scene-panel-${item.id}`}
id={`scene-tab-${item.id}`}
onClick={() => handleItemClick(index)}
onKeyDown={(e) => handleKeyDown(e, index)}
>
<h3>{item.title}</h3>
{item.tag && <span className="sr-only">促销标签: {item.tag}</span>}
</button>预览区域:
<div
role="tabpanel"
id={`scene-panel-${currentItem.id}`}
aria-labelledby={`scene-tab-${currentItem.id}`}
tabIndex={0}
>
<Picture
src={currentItem.imageUrl.url}
alt={currentItem.imageUrl.alt} // 必需的替代文本
aria-describedby={`scene-desc-${currentItem.id}`}
/>
<span id={`scene-desc-${currentItem.id}`} className="sr-only">
当前显示: {currentItem.title}
</span>
</div>Swiper 移动端:
<Swiper
role="region"
aria-label="场景切换器"
aria-live="polite" // 场景切换时通知屏幕阅读器
onSlideChange={(swiper) => {
announceSlideChange(swiper.realIndex) // 语音播报
}}
>
{items.map((item) => (
<SwiperSlide
key={item.id}
role="group"
aria-label={`场景 ${item.title}`}
>
{/* 内容 */}
</SwiperSlide>
))}
</Swiper>屏幕阅读器支持
语音播报示例:
const announceSlideChange = (index: number) => {
const item = items[index]
const announcement = `切换到场景 ${index + 1},共 ${items.length} 个。${item.title}${item.tag ? `,${item.tag}` : ''}`
// 创建 ARIA live region
const liveRegion = document.getElementById('scene-live-region')
if (liveRegion) {
liveRegion.textContent = announcement
}
}
// HTML
<div
id="scene-live-region"
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
/>播报内容:
- “切换到场景 2,共 5 个。Cleans the Mop with Water and Detergent,20% OFF”
- “自动播放已暂停” (用户点击后)
- “自动播放已恢复” (定时器重置后)
视频无障碍
字幕和描述:
<video
src={item.videoUrl}
autoPlay
loop
muted
playsInline
aria-label={item.imageUrl.alt} // 视频描述
>
<track
kind="captions"
src={`${item.videoUrl}.vtt`}
srcLang="zh-CN"
label="简体中文"
default
/>
<track
kind="descriptions"
src={`${item.videoUrl}-desc.vtt`}
srcLang="zh-CN"
label="视频描述"
/>
您的浏览器不支持视频播放。
</video>自动播放控制:
- 提供暂停按钮(可选功能扩展)
- 尊重用户的
prefers-reduced-motion设置
@media (prefers-reduced-motion: reduce) {
.progress-bar {
animation: none !important; /* 禁用进度条动画 */
}
}性能优化
React 优化
1. 组件 Memo 化:
import React, { memo } from 'react'
// 场景项组件(纯展示)
const SceneItem = memo<SceneItemProps>(({ item, index, isActive, onClick }) => {
return (
<button onClick={() => onClick(index)}>
<h3>{item.title}</h3>
{item.tag && <span>{item.tag}</span>}
<ProgressBar isActive={isActive} />
</button>
)
}, (prevProps, nextProps) => {
// 自定义比较函数
return (
prevProps.isActive === nextProps.isActive &&
prevProps.item.id === nextProps.item.id
)
})
// 预览区域组件
const MediaPreview = memo<MediaPreviewProps>(({ item }) => {
return (
<div>
{item.videoUrl ? (
<video src={item.videoUrl} autoPlay loop muted playsInline />
) : (
<Picture src={item.imageUrl.url} alt={item.imageUrl.alt} />
)}
</div>
)
}, (prevProps, nextProps) => {
return prevProps.item.id === nextProps.item.id
})2. useCallback 缓存回调:
const handleItemClick = useCallback((index: number) => {
setCurrentIndex(index)
if (intervalRef.current) {
window.clearInterval(intervalRef.current)
}
intervalRef.current = window.setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % items.length)
}, INTERVAL_TIME)
}, [items.length]) // 仅依赖 items.length
const handleKeyDown = useCallback((e: React.KeyboardEvent, index: number) => {
switch (e.key) {
case 'ArrowLeft':
e.preventDefault()
handleItemClick(Math.max(0, index - 1))
break
case 'ArrowRight':
e.preventDefault()
handleItemClick(Math.min(items.length - 1, index + 1))
break
case 'Home':
e.preventDefault()
handleItemClick(0)
break
case 'End':
e.preventDefault()
handleItemClick(items.length - 1)
break
}
}, [items.length, handleItemClick])3. useMemo 缓存计算结果:
const currentItem = useMemo(() => {
return items[currentIndex] || items[0]
}, [items, currentIndex])
const desktopScenes = useMemo(() => {
return items.map((item, index) => ({
...item,
isActive: index === currentIndex,
}))
}, [items, currentIndex])视频优化
1. 懒加载视频:
import { useIntersectionObserver } from '@/hooks/useIntersectionObserver'
const LazyVideo = ({ src, ...props }) => {
const [isVisible, setIsVisible] = useState(false)
const videoRef = useRef<HTMLVideoElement>(null)
useIntersectionObserver(videoRef, ([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true)
}
})
return (
<video
ref={videoRef}
src={isVisible ? src : undefined} // 可见时才加载
{...props}
/>
)
}2. 预加载策略:
// 仅预加载下一个场景的视频
useEffect(() => {
if (!isMobile && items[currentIndex + 1]?.videoUrl) {
const nextVideo = document.createElement('video')
nextVideo.preload = 'metadata'
nextVideo.src = items[currentIndex + 1].videoUrl
}
}, [currentIndex, items, isMobile])3. 内存管理:
useEffect(() => {
return () => {
// 组件卸载时清理视频资源
const videos = document.querySelectorAll('video')
videos.forEach((video) => {
video.pause()
video.src = ''
video.load()
})
}
}, [])Swiper 优化
1. 虚拟滚动(场景数多时):
import { Virtual } from 'swiper/modules'
<Swiper
modules={[Virtual]}
virtual={{
enabled: items.length > 10, // 10+场景时启用
addSlidesBefore: 2,
addSlidesAfter: 2,
}}
>
{items.map((item, index) => (
<SwiperSlide key={item.id} virtualIndex={index}>
<SceneContent item={item} />
</SwiperSlide>
))}
</Swiper>2. 懒加载图片:
<Swiper
lazy={{
loadPrevNext: true,
loadPrevNextAmount: 2, // 预加载前后各2张
}}
>
<SwiperSlide>
<Picture
src={item.mobImageUrl.url}
alt={item.mobImageUrl.alt}
loading="lazy"
className="swiper-lazy"
/>
<div className="swiper-lazy-preloader" />
</SwiperSlide>
</Swiper>3. 销毁实例:
useEffect(() => {
return () => {
// 组件卸载时销毁 Swiper 实例
if (swiperRef.current && swiperRef.current.destroy) {
swiperRef.current.destroy(true, true)
}
}
}, [])图片优化
1. 响应式图片:
<picture>
<source
srcSet={`${item.imageUrl.url}&w=412 412w, ${item.imageUrl.url}&w=824 824w`}
sizes="(max-width: 768px) 412px, 824px"
type="image/webp"
/>
<img
src={item.imageUrl.url}
alt={item.imageUrl.alt}
loading="lazy"
decoding="async"
/>
</picture>2. WebP 格式:
const getOptimizedImageUrl = (url: string, width: number) => {
// Unsplash 支持 fm 参数指定格式
return `${url}&w=${width}&fm=webp`
}常见问题 FAQ
1. 为什么桌面端自动播放,移动端需要手动滑动?
原因:
- 性能考虑: 移动端自动播放视频/切换场景会消耗更多电量和流量
- 用户体验: 移动端用户更习惯主动滑动浏览内容,而非被动观看
- 浏览器限制: 移动浏览器对自动播放有严格限制(需要 muted + playsInline)
技术实现:
useEffect(() => {
if (isMobile || items.length === 0) return // 移动端直接返回,不启动定时器
intervalRef.current = window.setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % items.length)
}, INTERVAL_TIME)
return () => {
if (intervalRef.current) {
window.clearInterval(intervalRef.current)
}
}
}, [isMobile, items.length])2. 如何自定义自动播放间隔时间?
当前限制: 组件内部硬编码为3秒(INTERVAL_TIME = 3000)。
未来扩展建议:
interface MediaSceneSwitcherData {
// ... 现有字段
autoPlayInterval?: number // 新增可选字段,默认3000ms
}
// 组件内使用
const interval = data.autoPlayInterval ?? 3000
useEffect(() => {
if (isMobile || items.length === 0) return
intervalRef.current = window.setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % items.length)
}, interval) // 使用动态间隔
// ...
}, [isMobile, items.length, interval])使用示例:
<MediaSceneSwitcher
data={{
autoPlayInterval: 5000, // 5秒间隔
// ...
}}
/>3. 为什么点击场景后会重置定时器?
设计理由:
- 用户意图: 用户点击表示对该场景感兴趣,应给予完整观看时间
- 避免冲突: 若不重置,用户刚点击场景1秒后可能就自动切换到下一个
- 提升体验: 用户获得3秒完整时间而非剩余时间
技术实现:
const handleItemClick = (index: number) => {
setCurrentIndex(index)
// 1. 清除旧定时器(停止原计划)
if (intervalRef.current) {
window.clearInterval(intervalRef.current)
}
// 2. 创建新定时器(从当前场景开始,给足3秒)
intervalRef.current = window.setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % items.length)
}, INTERVAL_TIME)
}时间线示例:
自动播放:
t=0s: 场景1 (显示)
t=3s: 场景2 (自动切换)
t=6s: 场景3 (自动切换)
用户在 t=1s 点击场景3:
t=1s: 场景3 (点击切换,定时器重置)
t=4s: 场景1 (3秒后自动切换) ← 用户获得完整3秒观看时间4. 如何禁用自动播放?
当前限制: 组件默认启用桌面端自动播放,无配置项禁用。
临时解决方案 (修改源码):
// 在 MediaSceneSwitcher 组件内
const AUTO_PLAY_ENABLED = false // 修改为 false
useEffect(() => {
if (isMobile || items.length === 0 || !AUTO_PLAY_ENABLED) return
// ...
}, [isMobile, items.length])未来扩展建议:
interface MediaSceneSwitcherData {
autoPlay?: boolean // 新增字段,默认 true
// ...
}
// 使用示例
<MediaSceneSwitcher
data={{
autoPlay: false, // 禁用自动播放
// ...
}}
/>5. 场景项标题过长时如何处理?
组件内置处理:
<h3 className="
text-lg font-semibold
line-clamp-2 // 最多2行
overflow-hidden
text-ellipsis
">
{item.title}
</h3>建议:
- 最佳长度: 10-20字符(中文),20-40字符(英文)
- 最大长度: 40字符(中文),80字符(英文)
- 超长处理: 使用
line-clamp-2截断为2行,末尾显示省略号
示例:
✅ 好的标题:
"Scrapes Off Dirt"
"Cleans the Mop"
⚠️ 可接受:
"Cleans the Mop with Water and Detergent" (会截断为2行)
❌ 避免:
"Advanced Multi-Surface Deep Cleaning Technology with AI-Powered Obstacle Detection and Navigation" (过长,影响阅读)6. 如何在移动端也显示进度条?
当前设计: 进度条仅在桌面端显示,移动端使用 Swiper 分页器。
原因:
- 移动端无自动播放,进度条无意义
- Swiper 分页器更符合移动端交互习惯
强制显示方法 (修改源码):
// 移除 laptop: 前缀,在所有断点显示
<div className="relative h-1 w-full overflow-hidden rounded-full bg-gray-200/30">
<div
key={currentIndex}
className="h-full rounded-full bg-gradient-to-r from-[#3ad1ff] to-[#008cd6]"
style={{
width: '0%',
animation: 'progress-grow 3s linear forwards',
}}
/>
</div>7. 支持哪些视频格式?
推荐格式: MP4 (H.264 编码)
兼容性:
| 格式 | Chrome | Safari | Firefox | Edge | 移动端 |
|---|---|---|---|---|---|
| MP4 (H.264) | ✅ | ✅ | ✅ | ✅ | ✅ |
| WebM (VP9) | ✅ | ❌ | ✅ | ✅ | 部分 |
| Ogg (Theora) | ✅ | ❌ | ✅ | ❌ | ❌ |
示例:
<video
src="https://cdn.example.com/video.mp4" // ✅ 推荐
// src="https://cdn.example.com/video.webm" // ⚠️ Safari 不支持
autoPlay
loop
muted
playsInline
/>多格式兼容:
<video autoPlay loop muted playsInline>
<source src="video.webm" type="video/webm" />
<source src="video.mp4" type="video/mp4" />
您的浏览器不支持视频播放。
</video>8. 如何获取当前显示的场景索引?
当前限制: 组件不对外暴露 currentIndex 状态。
未来扩展建议:
interface MediaSceneSwitcherProps {
data: MediaSceneSwitcherData
onSceneChange?: (index: number) => void // 新增回调
}
// 组件内
useEffect(() => {
onSceneChange?.(currentIndex)
}, [currentIndex, onSceneChange])
// 使用示例
<MediaSceneSwitcher
data={data}
onSceneChange={(index) => {
console.log('Current scene:', index)
// 可用于 GA 追踪
}}
/>9. 如何自定义进度条颜色?
当前限制: 进度条颜色硬编码为 #3ad1ff → #008cd6 渐变。
临时解决方案 (CSS 覆盖):
/* 全局样式或组件内 <style> */
.media-scene-switcher .progress-bar {
background: linear-gradient(to right, #ff6b6b, #ee5a6f) !important;
}未来扩展建议:
interface MediaSceneSwitcherData {
progressBarColor?: {
from: string
to: string
}
// 默认: { from: '#3ad1ff', to: '#008cd6' }
}
// 使用示例
<MediaSceneSwitcher
data={{
progressBarColor: {
from: '#ff6b6b',
to: '#ee5a6f',
},
// ...
}}
/>10. 为什么移动端不使用视频?
核心原因:
- 流量消耗: 视频文件通常2-10MB,4G/5G流量成本高
- 加载时间: 视频加载比图片慢3-10倍,影响首屏体验
- 性能影响: 视频解码消耗CPU/GPU,发热+耗电
- 自动播放限制: 移动浏览器严格限制自动播放(需用户交互)
- 体验一致性: Swiper 手势滑动与视频播放冲突
数据对比:
| 资源类型 | 文件大小 | 加载时间(4G) | CPU占用 |
|---|---|---|---|
| 图片(WebP) | 50-100KB | 0.5-1s | 低 |
| 视频(MP4) | 2-5MB | 5-15s | 高 |
最佳实践:
// 移动端始终使用 mobImageUrl
if (isMobile) {
return (
<Picture
src={item.mobImageUrl.url}
alt={item.mobImageUrl.alt}
loading="lazy"
/>
)
}
// 桌面端根据需要使用视频
if (item.videoUrl) {
return <video src={item.videoUrl} autoPlay loop muted playsInline />
} else {
return <Picture src={item.imageUrl.url} alt={item.imageUrl.alt} />
}相关资源
Storybook 示例
查看组件的交互式文档和所有变体:
npm run storybook访问 http://localhost:6006/?path=/story/components-mediasceneswitcher--default
源代码
- 组件源码:
packages/ui/src/biz-components/MediaSceneSwitcher/index.tsx - 类型定义:
packages/ui/src/biz-components/MediaSceneSwitcher/types.ts - 样式文件:
packages/ui/src/biz-components/MediaSceneSwitcher/styles.css - 单元测试:
packages/ui/tests/MediaSceneSwitcher.test.tsx
依赖库
- Swiper: https://swiperjs.com/
- 版本要求:
^11.0.0 - 使用模块:
Pagination,Autoplay,Virtual
- 版本要求:
- @anker-in/headless-ui/base: 项目内部原子组件库
Picture: 响应式图片组件Heading: 标题组件Text: 文本组件
相关组件
- ImageTextFeature: 单图文特性展示
- ImageWithText: 图文组合展示
- MediaPlayerBase: 单视频播放器
- MediaPlayerMulti: 多媒体组合播放器
- MediaPlayerSticky: 固定定位媒体播放器
- TabsWithMedia: 带媒体内容的标签页
设计资源
- Figma 设计稿: 查看设计
- 设计系统文档: 请查阅项目 Design System 文档
- Tailwind 配置:
packages/ui/tailwind.config.js
性能测试
使用 Lighthouse 测试页面性能:
npm run build
npm run preview
# 在 Chrome DevTools 中运行 Lighthouse性能目标:
- Performance: > 90
- Accessibility: 100
- Best Practices: > 95
- SEO: > 90