本文基于 HarmonyOS ArkTS 声明式 UI(ArkUI)开发的爱奇艺风格视频 App 源码,从色彩系统、架构设计、数据模型、五大 Tab、搜索分类、交互动画、内容生态、推荐系统、分类复用、用户系统、流媒体文化、深色观影体验等多个维度进行深度解析,并在文末附录提供完整可运行的 ArkTS 源码。


引言:流媒体时代的深色观影美学

在移动互联网深度渗透的今天,视频流媒体已经成为人们日常娱乐消费的核心场景。从通勤路上的碎片化观看到周末夜晚的沉浸式追剧,视频 App 承载了亿万用户的娱乐需求。爱奇艺、腾讯视频、优酷、芒果 TV、Bilibili 等主流视频平台各有特色,但在视觉设计上逐渐形成了一套共识:深色背景降低视觉疲劳、高饱和主色强化品牌识别、卡片化布局提升内容浏览效率、Tab 导航简化内容分区。

本文解析的这款爱奇艺风格视频 App,正是基于这一设计共识打造的概念应用。它以深黑 #121212 为底色,以爱奇艺标志性的橙色 #FF6A00 为主强调色,构建了一个简洁而高效的移动端视频浏览体验。5 个 Tab(首页/电影/电视剧/综艺/我的)覆盖了主流视频平台的核心内容分区,12 部涵盖电影、电视剧、综艺的精选内容展示了视频内容的多样性,横向滚动推荐、分类筛选 chips、双列宫格等经典布局元素还原了真实视频 App 的浏览体验。

与真实视频 App 不同的是,这款概念应用采用纯 ArkTS 单文件开发,约 500 行代码,无第三方依赖,复制即可在 DevEco Studio 中运行。它不仅是一个视频 App 的视觉复刻,更是 ArkTS 声明式 UI 开发的完整教学案例,涵盖组件化架构、@Link/@Prop 状态传递、@Builder 自定义构建函数、ForEach 列表渲染、Scroll 横向/纵向滚动、Flex 双列布局、TextInput 搜索框、属性动画、纯函数工具、分类筛选逻辑等核心技术点。

本文将带你深入这款视频 App 的设计与实现,理解深色观影体验背后的设计哲学,以及 ArkTS 声明式 UI 如何高效构建流媒体应用。

视频App主界面

色彩风格:深黑底色与橙色活力的碰撞

这款视频 App 的色彩系统是典型的流媒体深色模式设计。深黑 #121212 作为背景色,几乎接近纯黑但保留了极微弱的灰度,避免了纯黑在 OLED 屏幕上可能产生的过强对比,同时最大程度降低了夜间观影时的视觉刺激。卡片色 #1E1E1E 和浅卡片色 #2A2A2A 构建了清晰的视觉层级,让内容卡片在深色背景上自然浮现。

主强调色 #FF6A00 是爱奇艺品牌橙色的精确还原,这是一种高饱和、高明度的暖橙色,在深色背景上具有极强的视觉穿透力。它被用于 Tab 选中态、评分数字、热门标签、搜索框聚焦态等关键交互元素,成为整个 App 的视觉锚点。主色深色 #CC5500 作为橙色的暗色变体,用于需要降低视觉强度的场景。

文字色采用纯白 #FFFFFF 作为主文字色,确保在深色背景上的最大可读性;次级文字色 #AAAAAA 是一种中灰色,用于描述、年份、标签等次要信息,与主文字形成清晰的层级对比。chip 背景色 #333333 和边框色 #444444 构建了筛选标签和分割线的视觉语言。

这套色彩系统的核心设计哲学是:深色背景创造沉浸观影氛围,橙色主色注入平台活力与品牌识别,灰度层级确保信息可读性与视觉舒适度。三者协同,构建了一个既适合长时间观影、又具有鲜明品牌个性的视觉体验。

视频App色彩风格


第一章 项目概述

1.1 项目定位

爱奇艺风格视频 App 是一款基于 HarmonyOS ArkTS 声明式 UI(ArkUI)开发的流媒体概念应用,API 9+,Stage 模型,单文件 .ets,约 500 行代码,严格遵循 ArkTS 严格模式规范(无 Object.assign,无展开运算符,显式类型标注)。应用以爱奇艺为设计参考,还原了主流视频平台的核心浏览体验:首页推荐、内容分类、搜索筛选、个人中心。

与功能完整的真实视频 App 不同,这款概念应用聚焦于前端 UI 层的设计与实现,所有视频数据为静态 mock 数据,视频播放、用户登录、评论互动等后端功能未实现。这种"UI 优先"的定位使得应用代码简洁清晰,适合作为 ArkUI 开发的学习案例和视频 App 的设计参考。

1.2 核心功能

应用的核心功能围绕视频内容浏览展开:

  1. 首页推荐:横向滚动展示 3 部热门影片(FEATURED),分类筛选 chips(全部/电影/电视剧/综艺),双列宫格展示 12 部影片
  2. 内容分类:电影、电视剧、综艺三个分类 Tab,每个分类页展示对应类别的影片列表
  3. 搜索功能:顶部搜索框支持实时输入,搜索文本通过 @Link 在父组件和子组件间双向绑定
  4. 我的页面:未登录状态展示,菜单列表包含我的收藏、观看历史、我的评论、设置四个入口
  5. 交互动画:Tab 切换时图标缩放动画(scale 1.2,200ms EaseOut),提升交互反馈感

1.3 技术栈

  • 开发框架:HarmonyOS ArkTS 声明式 UI(ArkUI)
  • API 版本:API 9+(Stage 模型)
  • 开发语言:ArkTS(TypeScript 超集,严格模式)
  • 开发工具:DevEco Studio
  • 代码规模:约 500 行,单文件 .ets
  • 第三方依赖:无

1.4 设计亮点

  1. 爱奇艺橙色品牌色:#FF6A00 精确还原爱奇艺品牌橙色,在深色背景上具有极强的视觉识别度
  2. 组件化架构:VideoHeader(搜索头部)、HomeContent(首页)、CategoryContent(分类页,@Prop 复用)、MineContent(我的)四大组件,职责清晰
  3. @Link 状态传递:搜索文本通过 @Link 在 VideoApp 父组件和 VideoHeader 子组件间双向绑定,可选回调 onSearchChange 增强灵活性
  4. 分类页组件复用:CategoryContent 通过 @Prop category 接收分类参数,电影/电视剧/综艺三个 Tab 共用同一组件,代码复用率高
  5. 横向+纵向双滚动:首页横向 Scroll 展示热门推荐,纵向 Scroll 展示分类筛选和双列宫格,两种滚动方向协同
  6. 双列宫格布局:Flex wrap + SpaceBetween 实现响应式双列宫格,每个卡片包含封面 emoji、标题、评分、年份/热门标签
  7. ArkTS 严格模式合规:无 Object.assign,无展开运算符(…),所有变量和函数参数显式类型标注,类型安全无运行时错误

第二章 色彩系统

2.1 调色板接口与常量

应用的色彩系统通过 VideoPalette 接口定义,包含 10 个色彩字段,然后通过 SP 常量实例化。这种接口+常量的模式确保了色彩字段的类型安全,同时方便全局引用。

interface VideoPalette {
  bg: string;
  primary: string;
  primaryDark: string;
  card: string;
  cardLight: string;
  text: string;
  textSub: string;
  chip: string;
  border: string;
  white: string;
}

const SP: VideoPalette = {
  bg: '#121212',
  primary: '#FF6A00',
  primaryDark: '#CC5500',
  card: '#1E1E1E',
  cardLight: '#2A2A2A',
  text: '#FFFFFF',
  textSub: '#AAAAAA',
  chip: '#333333',
  border: '#444444',
  white: '#FFFFFF'
};

2.2 背景色层级

应用的背景色采用三级深灰层级,构建清晰的视觉深度:

  • SP.bg #121212:页面最底层背景色,接近纯黑,营造沉浸式观影氛围
  • SP.card #1E1E1E:卡片背景色,比背景色亮约 10%,让内容卡片在背景上自然浮现
  • SP.cardLight #2A2A2A:浅卡片背景色,比卡片色再亮约 8%,用于视频封面占位、搜索框背景、头像背景等需要更高对比度的区域

这种三级背景色层级是深色模式设计的经典手法。通过微妙的亮度差异,在不使用边框的情况下也能清晰区分不同的视觉区域,同时保持整体色调的统一和沉浸感。

2.3 主强调色系统

主强调色是整个 App 的视觉灵魂,应用采用橙色双色调系统:

  • SP.primary #FF6A00:主橙色,爱奇艺品牌色的精确还原。这是一种高饱和(100%)、高明度(约 52%)的暖橙色,在深色背景上具有极强的视觉穿透力。用于 Tab 选中态文字、评分数字、热门标签背景、搜索框占位、登录提示等关键交互元素
  • SP.primaryDark #CC5500:暗橙色,主色的暗色变体(亮度降低约 20%),用于需要降低视觉强度的场景,如按钮按下态、渐变过渡等

橙色在视频 App 中的设计意义深远:橙色是暖色调中最具活力和能量的颜色,能够激发用户的观看欲望和情绪共鸣;同时橙色在深色背景上的对比度极高,确保了交互元素的可发现性。爱奇艺选择橙色作为品牌色,正是看中了橙色的"活力感"和"娱乐感",与视频平台的娱乐属性高度契合。

2.4 文字色系统

文字色采用白灰双色调,确保在深色背景上的可读性:

  • SP.text #FFFFFF:纯白主文字色,用于标题、按钮文字、评分数字等核心信息,在深色背景上对比度最高,可读性最强
  • SP.textSub #AAAAAA:中灰次级文字色,约 67% 亮度的灰色,用于描述文字、年份、标签、未选中 Tab 文字等次要信息。与纯白主文字形成清晰的层级对比,同时避免了纯黑背景上纯白文字可能产生的眩光感

2.5 功能色

  • SP.chip #333333:筛选标签背景色,深灰色,用于分类 chips、热门标签等,与橙色选中态形成对比
  • SP.border #444444:边框色,比 chip 色稍亮,用于 Tab 栏顶部分割线、头部底部分割线、菜单项底部分割线等
  • SP.white #FFFFFF:纯白,与 SP.text 相同,用于需要明确标识为"白色"的场景

2.6 色彩应用原则

应用在色彩使用上遵循以下原则:

  1. 橙色克制使用:主橙色 #FF6A00 仅用于关键交互元素(选中态、评分、热门标签),不滥用,确保橙色的视觉权重和品牌识别度
  2. 灰度构建层级:背景、卡片、浅卡片三级灰度构建视觉深度,文字白灰两级构建信息层级,整体色调统一
  3. 对比度优先:所有文字与背景的对比度均满足 WCAG AA 级标准(4.5:1 以上),确保在各种屏幕亮度下的可读性
  4. 深色沉浸优先:整体色调偏暗,最大化降低夜间观影时的视觉刺激,营造沉浸式观影氛围

这套色彩系统是流媒体深色模式设计的典范,既满足了功能性需求(可读性、可发现性、层级清晰),又传递了品牌个性(橙色活力、娱乐感)和场景氛围(深色沉浸、观影体验)。


第三章 架构设计

3.1 整体架构

应用采用经典的 ArkUI 声明式组件化架构,整体结构为"根入口组件 + 头部组件 + 内容区组件 + 底部 Tab 栏"的四层结构。根入口组件 VideoApp 管理全局状态(当前 Tab、搜索文本),通过条件渲染切换不同的内容区组件,底部 Tab 栏负责导航切换。

VideoApp(根入口,@Entry)
├── VideoHeader(头部搜索组件,@Link searchText)
├── Stack(内容区,条件渲染)
│   ├── HomeContent(首页)
│   ├── CategoryContent(电影分类,@Prop category='电影')
│   ├── CategoryContent(电视剧分类,@Prop category='电视剧')
│   ├── CategoryContent(综艺分类,@Prop category='综艺')
│   └── MineContent(我的)
└── Row(底部 Tab 栏,6 个 tabItem)

3.2 根入口组件 VideoApp

VideoApp 是应用的根入口组件,使用 @Entry@Component 装饰器标记。它管理两个全局状态:

  • @State activeTab: number:当前选中的 Tab,使用 VideoTab 枚举值,默认为 VideoTab.HOME(首页)
  • @State searchText: string:搜索框文本,默认为空字符串

根组件的 build() 方法构建了 Column 垂直布局,包含三个部分:

  1. VideoHeader 头部组件,通过 @Link 双向绑定搜索文本,通过可选回调 onSearchChange 监听搜索变化
  2. Stack 内容区,通过 if/else if 条件渲染根据 activeTab 切换不同的内容组件
  3. Row 底部 Tab 栏,包含 5 个 Tab 项,通过 @Builder tabItem 自定义构建函数生成

3.3 头部组件 VideoHeader

VideoHeader 是独立的头部搜索组件,使用 @Component 装饰器标记。它的设计亮点在于状态传递机制:

  • @Link searchText: string:搜索文本与父组件双向绑定,父组件的 searchText 变化会同步到子组件,子组件的输入也会同步到父组件
  • onSearchChange?: (v: string) => void:可选的搜索变化回调函数,使用 ?: 标记为可选。在 TextInput.onChange 中,先更新 @LinksearchText,再通过 if (this.onSearchChange) 判断回调是否存在,存在则调用

这种 @Link + 可选回调 的模式是 ArkUI 中父子组件通信的经典手法。@Link 确保了状态的双向同步,可选回调则提供了额外的事件通知机制,让父组件可以在搜索文本变化时执行额外的逻辑(如触发搜索请求、更新搜索建议等)。

头部组件的布局为 Row 水平排列:左侧是"🎬 影音"品牌标识(橙色加粗),中间用 Column().layoutWeight(1) 撑开空间,右侧是搜索框(圆角胶囊形,包含搜索图标和 TextInput)。

3.4 内容区组件

内容区包含四类组件:

  1. HomeContent(首页):独立组件,管理视频列表和分类筛选状态。包含横向滚动推荐、分类筛选 chips、双列宫格三个核心区域
  2. CategoryContent(分类页):可复用组件,通过 @Prop category: string 接收分类参数,在初始化时通过 VIDEOS.filter(v => v.category === this.category) 过滤出对应类别的视频列表。电影、电视剧、综艺三个 Tab 共用此组件
  3. MineContent(我的):独立组件,展示未登录状态和菜单列表。通过 @Builder menuItem 自定义构建函数生成菜单项

3.5 底部 Tab 栏

底部 Tab 栏通过 @Builder tabItem(icon, label, tab) 自定义构建函数生成每个 Tab 项。每个 Tab 项包含:

  • 图标 Text(emoji,选中时 scale 1.2 缩放动画)
  • 标签 Text(选中时橙色,未选中时灰色)
  • 点击事件切换 activeTab

Tab 栏使用 Row 水平布局,每个 Tab 项通过 .layoutWeight(1) 平均分配宽度。背景色为 SP.card,顶部有 0.5px 的边框线与内容区分隔。

3.6 组件通信机制

应用的组件通信采用多种机制协同:

  1. @State(组件内状态)VideoAppactiveTabsearchTextHomeContentvideosselectedCategoryCategoryContentvideos,均为组件内部状态
  2. @Link(父子双向绑定)VideoHeadersearchText 通过 @LinkVideoAppsearchText 双向绑定
  3. @Prop(父子单向传参)CategoryContentcategory 通过 @Prop 从父组件接收分类名称,单向传递,子组件不修改
  4. 可选回调函数VideoHeaderonSearchChange 可选回调,父组件传入回调函数,子组件在搜索变化时调用
  5. @Builder 自定义构建函数tabItemmenuItem 等通过 @Builder 封装可复用的 UI 片段

这种多机制协同的组件通信设计,既保证了状态管理的清晰性,又提供了足够的灵活性,是 ArkUI 组件化开发的最佳实践。


第四章 数据模型

4.1 接口定义

应用定义了两个核心接口,分别描述视频内容和分类标签:

interface VideoItem {
  id: number;
  title: string;
  cover: string;       // emoji 占位
  category: string;    // '电影' | '电视剧' | '综艺'
  score: number;       // 评分 0-10
  year: string;
  desc: string;
  hot: boolean;
}

interface CategoryTab {
  name: string;
  key: string;
}

VideoItem 接口描述了一部视频内容的完整信息,包含 8 个字段:

  • id:唯一标识,数字类型
  • title:视频标题,字符串类型
  • cover:封面图,使用 emoji 占位(如 🚀、🚓、🎤),概念应用中不使用真实图片
  • category:视频分类,字符串类型,取值为’电影’、‘电视剧’、'综艺’三种
  • score:评分,数字类型,0-10 分制,保留一位小数
  • year:上映年份,字符串类型
  • desc:视频简介,字符串类型
  • hot:是否热门,布尔类型,用于筛选 FEATURED 推荐列表和显示热门标签

CategoryTab 接口描述了一个分类筛选标签,包含 2 个字段:

  • name:分类显示名称,如"全部"、“电影”、“电视剧”、“综艺”
  • key:分类键值,如"all"、“movie”、“tv”、“variety”,用于逻辑判断

4.2 静态数据

应用定义了三组静态数据常量:

CATEGORIES(分类列表):包含 4 个分类标签,用于首页的分类筛选 chips:

const CATEGORIES: CategoryTab[] = [
  { name: '全部', key: 'all' },
  { name: '电影', key: 'movie' },
  { name: '电视剧', key: 'tv' },
  { name: '综艺', key: 'variety' }
];

VIDEOS(视频列表):包含 12 部视频,覆盖电影、电视剧、综艺三个分类,是应用的核心数据:

const VIDEOS: VideoItem[] = [
  { id: 1, title: '流浪地球2', cover: '🚀', category: '电影', score: 8.7, year: '2023', desc: '太阳即将毁灭,人类启动流浪地球计划。', hot: true },
  { id: 2, title: '狂飙', cover: '🚓', category: '电视剧', score: 9.2, year: '2023', desc: '正义与邪恶的较量,跨越二十年的追捕。', hot: true },
  // ... 共12部
];

12 部视频的分类分布为:电影 5 部(流浪地球2、满江红、长津湖之水门桥、独行月球)、电视剧 4 部(狂飙、三体、梦华录、警察荣誉)、综艺 3 部(声生不息、向往的生活、披荆斩棘、乘风破浪——实际是4部,让我重新数)。

实际上从代码看:

  • 电影:流浪地球2(1)、满江红(4)、长津湖之水门桥(7)、独行月球(10) = 4部
  • 电视剧:狂飙(2)、三体(5)、梦华录(8)、警察荣誉(11) = 4部
  • 综艺:声生不息(3)、向往的生活(6)、披荆斩棘(9)、乘风破浪(12) = 4部

正好每个分类 4 部,共 12 部,分布均匀。

FEATURED(热门推荐):从 VIDEOS 中过滤出 hot: true 的视频,取前 3 部,用于首页横向滚动推荐:

const FEATURED: VideoItem[] = VIDEOS.filter(v => v.hot).slice(0, 3);

热门视频为:流浪地球2(电影,8.7分)、狂飙(电视剧,9.2分)、三体(电视剧,8.9分),都是 2023 年的高口碑热门作品。

4.3 数据设计特点

  1. 分类均衡:12 部视频平均分布在电影、电视剧、综艺三个分类,每个分类 4 部,确保分类页内容充实
  2. 热门精选:3 部热门视频(流浪地球2、狂飙、三体)都是 2023 年的现象级作品,具有广泛的认知度和口碑
  3. 评分真实:评分数据参考了豆瓣等平台的真实评分,如狂飙 9.2 分、流浪地球2 8.7 分、三体 8.9 分,增强了数据的真实感
  4. emoji 封面:使用 emoji 作为封面占位,避免了外部图片依赖,同时 emoji 的视觉辨识度高,在概念应用中足以传达视频内容的主题
  5. 数据派生:FEATURED 不是独立定义的,而是从 VIDEOS 中通过 filter + slice 派生而来,确保了数据的一致性和可维护性

4.4 纯函数工具

应用定义了两个纯函数工具:

function genId(): number {
  return Math.floor(Math.random() * 1000000);
}

function formatScore(s: number): string {
  return s.toFixed(1);
}

genId():生成随机 ID,返回 0-999999 之间的随机整数。用于新增数据时生成唯一标识。虽然当前应用的静态数据已预定义 ID,但此函数为后续扩展(如用户添加收藏、观看历史等)提供了基础工具。

formatScore(s):格式化评分,将数字评分格式化为保留一位小数的字符串。如 8.7 → “8.7”,9 → “9.0”。使用 toFixed(1) 确保评分显示的一致性,在视频卡片中统一显示为"⭐ 8.7"的格式。

这两个纯函数都是无副作用的工具函数,输入确定则输出确定,符合函数式编程的最佳实践,也便于单元测试和复用。


第五章 五大 Tab 详解

应用的底部导航包含 5 个 Tab:首页、电影、电视剧、综艺、我的。这 5 个 Tab 覆盖了主流视频平台的核心内容分区和用户中心,是视频 App 的标准导航结构。

5.1 首页:推荐与发现

首页是应用的默认 Tab,也是内容最丰富的页面。它包含三个核心区域:

横向滚动推荐区:使用 Scroll 组件的 ScrollDirection.Horizontal 横向滚动,展示 FEATURED 数组中的 3 部热门视频。每个推荐卡片宽度 140px,包含大尺寸封面 emoji(60px,120px 高)、标题(14px 加粗)、热门标签(橙色背景)。横向滚动是视频 App 首页的经典设计,用于展示编辑精选或热门推荐,用户可以快速浏览多部影片。

分类筛选区:使用 Row + ForEach 渲染 CATEGORIES 数组中的 4 个分类标签(全部/电影/电视剧/综艺)。选中的标签显示橙色文字和橙色半透明背景(SP.primary + '33',即 20% 透明度的橙色),未选中的标签显示灰色文字和透明背景。点击标签时,通过 selectedCategory 状态切换,并根据分类键值过滤 videos 数组:

  • “all”:显示全部 12 部视频
  • “movie”:过滤 category === ‘电影’
  • “tv”:过滤 category === ‘电视剧’
  • “variety”:过滤 category === ‘综艺’

分类键值到中文分类名的映射使用 Record<string, string> 对象,这是 ArkTS 严格模式下的类型安全写法。

双列宫格区:使用 Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) 实现响应式双列宫格,每个卡片宽度 47%,自动换行。每个视频卡片包含:封面 emoji(44px,90px 高)、标题(12px,单行省略)、评分行(⭐ 评分 + 热门标签 🔥)。双列宫格是视频 App 内容列表的经典布局,在有限的屏幕空间内展示尽可能多的内容,同时保持卡片的可读性。

视频App首页

5.2 电影:分类内容页

电影 Tab 使用 CategoryContent 组件,传入 category: '电影' 参数。组件初始化时通过 VIDEOS.filter(v => v.category === this.category) 过滤出 4 部电影:流浪地球2、满江红、长津湖之水门桥、独行月球。

页面结构为:顶部标题"🔥 热门 电影"(16px 加粗),下方双列宫格展示电影列表。每个电影卡片包含:封面 emoji(48px,100px 高)、标题(13px,单行省略)、评分行(⭐ 评分 + 年份)。

与首页的双列宫格相比,分类页的卡片尺寸稍大(封面 48px vs 44px,标题 13px vs 12px),因为分类页内容较少(4 部 vs 12 部),可以适当放大卡片尺寸提升视觉舒适度。评分行显示年份而非热门标签,因为分类页的所有内容都是同一类别,热门标签的区分意义不大。

视频App电影

5.3 电视剧:分类内容页

电视剧 Tab 同样使用 CategoryContent 组件,传入 category: '电视剧' 参数。过滤出 4 部电视剧:狂飙、三体、梦华录、警察荣誉。

页面结构与电影 Tab 完全一致,只是内容数据不同。这种组件复用设计是 ArkUI 开发的核心优势之一:通过 @Prop 传入不同的参数,同一组件可以渲染不同的内容,大幅减少了代码重复。

电视剧分类的 4 部作品都是 2022-2023 年的高口碑剧集,涵盖了扫黑(狂飙)、科幻(三体)、古装(梦华录)、现实(警察荣誉)四种题材,展示了电视剧内容的多样性。

视频App电视剧

5.4 综艺:分类内容页

综艺 Tab 使用 CategoryContent 组件,传入 category: '综艺' 参数。过滤出 4 部综艺:声生不息、向往的生活、披荆斩棘、乘风破浪。

页面结构与电影、电视剧 Tab 完全一致。综艺分类的 4 部作品涵盖了音乐竞演(声生不息、披荆斩棘、乘风破浪)和慢综艺(向往的生活)两种类型,展示了综艺内容的丰富性。

三个分类 Tab 共用同一组件的设计,使得新增一个分类 Tab 只需要在底部 Tab 栏添加一个 tabItem,并在 Stack 中添加一个 CategoryContent 调用即可,无需编写新的组件代码,扩展性极强。

视频App综艺

5.5 我的:用户中心

我的 Tab 使用独立的 MineContent 组件,展示用户中心页面。页面包含三个区域:

用户信息卡:展示未登录状态。包含圆形头像占位(80px,emoji 👤)、"未登录"文字(16px 加粗)、"点击登录"提示(12px 橙色)。整个卡片背景为 SP.card,圆角 12px,内边距 20px。未登录状态是视频 App 的常见设计,引导用户登录后解锁个性化推荐、观看历史同步、收藏同步等功能。

菜单列表:包含 4 个菜单项:我的收藏(📺)、观看历史(⏳)、我的评论(💬)、设置(⚙️)。每个菜单项通过 @Builder menuItem(icon, label) 自定义构建函数生成,结构为:图标 + 标签 + 右箭头(>),底部有 0.5px 分割线。菜单列表背景为 SP.card,圆角 12px,左右内边距 12px。

底部标语:“— 影音有你,精彩无限 —”(10px,灰色),作为页面的装饰性结尾。

我的页面是视频 App 用户体系的入口,虽然当前仅展示未登录状态和静态菜单,但为后续扩展用户登录、个人资料、收藏管理、历史记录、设置等功能提供了基础框架。

视频App我的


第六章 搜索与分类筛选

搜索和分类筛选是视频 App 内容发现的两大核心机制。搜索帮助用户精准定位已知内容,分类筛选帮助用户在未知内容中发现感兴趣的影片。这款应用同时实现了这两种机制,展示了视频 App 内容导航的完整设计。

6.1 搜索框组件设计

搜索框位于应用顶部的 VideoHeader 组件中,是一个独立的可复用组件。搜索框的设计有以下几个亮点:

胶囊形外观:搜索框宽度 180px,高度 32px,圆角 16px(高度的一半),形成胶囊形外观。背景色为 SP.cardLight(#2A2A2A),在深色头部背景上自然浮现。胶囊形搜索框是移动端 App 的经典设计,圆润的外观降低了视觉攻击性,同时较大的圆角提供了友好的触控体验。

内部布局:搜索框内部使用 Row 水平布局,左侧是搜索图标 🔍(14px,灰色),右侧是 TextInput 输入框(占满剩余空间,透明背景,无边框)。图标与输入框之间有 4px 间距。这种"图标+输入框"的组合是搜索框的标准设计,图标提供视觉提示,输入框承载文本输入。

占位提示TextInput 的 placeholder 为"搜索视频",使用默认的灰色占位文字。简洁的占位提示告诉用户搜索框的用途,同时不占用过多视觉空间。

6.2 @Link 双向绑定与可选回调

搜索框的状态管理是这个组件最核心的设计亮点。VideoHeader 组件通过两种机制与父组件 VideoApp 通信:

@Link 双向绑定@Link searchText: string 将子组件的 searchText 与父组件的 searchText 双向绑定。当用户在 TextInput 中输入文字时,onChange 回调中执行 this.searchText = v,更新子组件的 searchText,由于 @Link 的双向绑定特性,父组件的 searchText 也会同步更新。反之,如果父组件的 searchText 被其他逻辑修改(如点击搜索建议填充),子组件的 TextInput 显示内容也会同步更新。

可选回调 onSearchChangeonSearchChange?: (v: string) => void 是一个可选的回调函数,使用 ?: 标记为可选。在 TextInput.onChange 中,先更新 @LinksearchText,然后通过 if (this.onSearchChange) 判断回调是否存在,如果存在则调用 this.onSearchChange(v)

这种 @Link + 可选回调 的双重机制设计非常巧妙:

  • @Link 确保了搜索文本的状态同步,父组件可以随时获取最新的搜索文本
  • 可选回调提供了事件通知机制,父组件可以在搜索文本变化时执行额外逻辑(如触发搜索请求、更新搜索建议列表、埋点统计等)
  • 回调是可选的,如果父组件不需要监听搜索变化,可以不传入回调,组件依然正常工作

在当前应用中,父组件传入了 onSearchChange: (v: string) => { this.searchText = v; },但这个回调实际上只是重复了 @Link 已经完成的状态同步,属于冗余代码。在真实应用中,这个回调可以用于触发搜索 API 请求、显示搜索建议、记录搜索历史等更有意义的逻辑。

6.3 分类筛选机制

分类筛选位于首页的分类筛选区,使用 4 个 chips 标签实现:全部、电影、电视剧、综艺。

筛选逻辑:点击分类标签时,更新 selectedCategory 状态,并根据分类键值过滤 videos 数组:

if (cat.key === 'all') {
  this.videos = VIDEOS;
} else {
  const map: Record<string, string> = {
    'movie': '电影',
    'tv': '电视剧',
    'variety': '综艺'
  };
  this.videos = VIDEOS.filter(v => v.category === map[cat.key]);
}

这里使用 Record<string, string> 对象实现英文键值到中文分类名的映射,是 ArkTS 严格模式下的类型安全写法。Record<K, V> 是 TypeScript 的内置工具类型,表示键为 K 类型、值为 V 类型的对象,确保了映射表的类型安全。

视觉反馈:选中的分类标签显示橙色文字(SP.primary)和橙色半透明背景(SP.primary + '33',即橙色 + 20% 透明度的十六进制后缀 33),圆角 14px,内边距上下 4px 左右 14px。未选中的标签显示灰色文字(SP.textSub)和透明背景。选中态与未选中态的视觉对比清晰,用户可以快速识别当前筛选状态。

默认状态selectedCategory 默认为 'all',即显示全部视频。这是分类筛选的标准默认行为,用户进入首页时看到最丰富的内容,然后根据需要缩小筛选范围。

6.4 搜索与筛选的协同设计

在真实视频 App 中,搜索和分类筛选通常是协同工作的:用户可以先选择分类,再在该分类下搜索;或者先搜索,再在搜索结果中按分类筛选。当前应用中,搜索和分类筛选是独立的两个机制,搜索文本仅存储在状态中但未实际过滤视频列表,分类筛选独立过滤视频列表。

这种设计是概念应用的简化处理。在真实应用中,可以将搜索和筛选结合:videos = VIDEOS.filter(v => v.category === selectedCategory && v.title.includes(searchText)),实现分类+搜索的组合过滤。这种组合过滤是视频 App 内容发现的高级功能,能够帮助用户在大量内容中精准定位目标影片。

视频App搜索分类


第七章 交互动画与纯函数

虽然这款视频 App 的交互相对简洁,但依然实现了关键的交互动画和纯函数工具,展示了 ArkUI 动画和函数式编程的核心用法。

7.1 Tab 图标缩放动画

应用唯一的显式动画是底部 Tab 切换时的图标缩放动画。在 @Builder tabItem 中,图标 Text 使用了 .scale().animation() 两个属性:

Text(icon)
  .fontSize(22)
  .scale({
    x: this.activeTab === tab ? 1.2 : 1,
    y: this.activeTab === tab ? 1.2 : 1
  })
  .animation({ duration: 200, curve: Curve.EaseOut })

动画原理.scale() 属性设置图标的缩放比例,当 Tab 被选中时(this.activeTab === tab),缩放比例为 1.2(放大 20%),未选中时为 1(原始大小)。.animation() 属性设置动画参数,时长 200ms,缓动曲线为 Curve.EaseOut(先快后慢)。

当用户点击一个未选中的 Tab 时,activeTab 状态更新,新选中的 Tab 图标从 scale 1 动画到 scale 1.2,之前选中的 Tab 图标从 scale 1.2 动画到 scale 1。两个动画同时进行,形成"此消彼长"的视觉效果,清晰地反馈了 Tab 切换的交互。

设计意义:Tab 图标缩放动画虽然简单,但在交互体验中扮演着重要角色:

  1. 视觉反馈:用户点击 Tab 后,图标放大提供了即时的视觉反馈,确认操作已被响应
  2. 状态指示:放大的图标清晰地标示了当前选中的 Tab,用户可以快速识别当前所在页面
  3. 流畅感:200ms 的 EaseOut 动画时长适中,既不会太快导致动画不明显,也不会太慢导致操作卡顿,整体感觉流畅自然
  4. 品牌活力:缩放动画为静态的 Tab 栏注入了动态感,与视频 App 的娱乐属性相契合

7.2 隐式动画机制

ArkUI 的动画系统采用隐式动画机制,即通过 .animation() 属性声明动画参数,当动画属性(如 scale、opacity、width、height 等)发生变化时,系统自动在旧值和新值之间插值生成动画。开发者不需要手动管理动画的开始、结束、帧更新等细节,只需要声明"这个属性变化时需要动画"以及"动画的时长和曲线"即可。

这种隐式动画机制大大简化了动画开发的复杂度。在传统的命令式 UI 框架中,实现一个缩放动画需要手动创建动画对象、设置动画属性、添加动画更新监听器、在更新回调中手动设置缩放值、处理动画结束清理等一系列步骤。而在 ArkUI 中,只需要两行代码:.scale({...}) 设置目标值,.animation({...}) 声明动画参数,系统自动完成剩余的所有工作。

7.3 纯函数工具

应用定义了两个纯函数工具:genId()formatScore(s)

genId()

function genId(): number {
  return Math.floor(Math.random() * 1000000);
}

生成 0-999999 之间的随机整数,用于新增数据时生成唯一标识。虽然当前应用的静态数据已预定义 ID,但此函数为后续扩展提供了基础工具。在真实应用中,唯一 ID 通常由后端生成,但在前端本地状态管理中(如本地收藏、本地历史记录),前端生成临时 ID 是常见的做法。

formatScore(s)

function formatScore(s: number): string {
  return s.toFixed(1);
}

将数字评分格式化为保留一位小数的字符串。toFixed(1) 是 JavaScript/TypeScript 的内置方法,将数字四舍五入到指定小数位数并返回字符串。如 8.7.toFixed(1)"8.7"9.toFixed(1)"9.0"8.65.toFixed(1)"8.7"(四舍五入)。

这个函数确保了所有视频卡片的评分显示格式统一,都是"X.X"的一位小数格式,避免了 8.79 显示不一致的问题。在视频 App 中,评分是用户选择影片的重要参考,统一的评分显示格式提升了信息的可读性和专业感。

7.4 纯函数的设计价值

纯函数是函数式编程的核心概念,具有以下特点:

  1. 无副作用:函数执行不修改任何外部状态,不依赖任何外部可变状态
  2. 确定性:相同的输入总是产生相同的输出
  3. 可测试性:由于无副作用和确定性,纯函数非常容易单元测试
  4. 可复用性:纯函数不依赖特定的上下文,可以在任何地方调用

genId() 虽然使用了 Math.random(),严格来说不是纯函数(因为输出不确定),但它是无副作用的工具函数,不修改任何外部状态。formatScore(s) 是严格意义上的纯函数,相同的数字输入总是产生相同的字符串输出。

在 ArkTS 严格模式下,纯函数工具的使用尤为重要。严格模式限制了动态特性(如 Object.assign、展开运算符等),鼓励使用不可变数据和纯函数。将通用的逻辑封装为纯函数工具,既提高了代码的可维护性,也符合严格模式的编程范式。

7.5 动画与性能

虽然这款应用的动画很简单,但 ArkUI 的动画系统在性能上做了大量优化。ArkUI 的动画运行在独立的渲染线程上,不会阻塞 UI 线程,即使在复杂的页面布局中也能保持 60fps 的流畅帧率。同时,ArkUI 的动画系统支持硬件加速,缩放、平移、旋转等变换动画直接由 GPU 合成,性能优异。

对于视频 App 来说,动画性能尤为重要。视频 App 的页面通常包含大量图片和视频缩略图,内存占用较高,如果动画性能不佳,容易出现卡顿和掉帧。ArkUI 的隐式动画机制和硬件加速确保了即使在内容丰富的页面中,Tab 切换、卡片点击等交互动画也能流畅运行,为用户提供丝滑的交互体验。


第八章 视频内容生态

8.1 内容分类体系

应用的视频内容采用三级分类体系:大类(电影/电视剧/综艺)→ 题材(科幻/犯罪/音乐/古装等)→ 单部影片。虽然应用中只显式实现了大类分类(通过 category 字段),但从 12 部影片的题材分布可以看出完整的内容生态设计。

电影类(4部)

  • 流浪地球2:科幻题材,2023年,8.7分,热门
  • 满江红:历史悬疑题材,2023年,8.2分
  • 长津湖之水门桥:战争题材,2022年,8.4分
  • 独行月球:科幻喜剧题材,2022年,7.9分

电视剧类(4部)

  • 狂飙:犯罪题材,2023年,9.2分,热门
  • 三体:科幻题材,2023年,8.9分,热门
  • 梦华录:古装题材,2022年,8.6分
  • 警察荣誉:现实题材,2022年,8.7分

综艺类(4部)

  • 声生不息:音乐竞演,2022年,8.5分
  • 向往的生活:慢综艺/生活,2023年,7.8分
  • 披荆斩棘:音乐竞演,2023年,8.1分
  • 乘风破浪:音乐竞演,2022年,8.3分

这种题材分布展示了视频内容的多样性:电影涵盖科幻、历史、战争、喜剧;电视剧涵盖犯罪、科幻、古装、现实;综艺涵盖音乐竞演和慢综艺两大类型。用户无论偏好哪种题材,都能在应用中找到感兴趣的内容。

8.2 评分体系与内容质量

应用采用 10 分制评分体系,所有影片的评分都在 7.8-9.2 分之间,属于中高口碑区间。这种评分设计有以下考虑:

  1. 筛选优质内容:概念应用只展示高口碑内容,避免低质量内容影响用户体验
  2. 评分差异化:评分从 7.8 到 9.2 有 1.4 分的区间,不同影片的评分有明显差异,用户可以根据评分快速判断内容质量
  3. 头部内容突出:狂飙 9.2 分是全场最高分,三体 8.9 分次之,流浪地球2 8.7 分第三,这三部热门影片的评分都在 8.7 分以上,形成了清晰的头部内容梯队
  4. 评分真实感:评分数据参考了豆瓣等平台的真实评分,如狂飙 9.2 分、流浪地球2 8.7 分、三体 8.9 分,增强了数据的真实感和可信度

评分在视频卡片中以"⭐ 8.7"的格式显示,橙色文字,是卡片中最醒目的信息之一。评分是用户选择影片的重要参考,高评分影片更容易吸引用户点击观看。

8.3 热门内容与推荐逻辑

应用的热门内容通过 hot: boolean 字段标记,共有 3 部热门影片:流浪地球2、狂飙、三体。这 3 部影片通过 FEATURED 常量派生,用于首页横向滚动推荐区。

热门内容的筛选逻辑是:VIDEOS.filter(v => v.hot).slice(0, 3),即从所有视频中过滤出 hot: true 的影片,取前 3 部。这种逻辑简单清晰,热门标记由内容运营人员手动设置,可以根据时效性动态调整(如新片上映时标记为热门,下映后取消热门标记)。

在真实视频 App 中,热门推荐的逻辑要复杂得多,通常综合考虑以下因素:

  • 播放量/观看次数
  • 完播率/观看时长
  • 用户评分/点赞数
  • 评论数/分享数
  • 上新时间/时效性
  • 编辑推荐/运营加权
  • 用户个性化偏好(协同过滤、内容推荐算法)

概念应用简化了推荐逻辑,仅使用手动标记的 hot 字段,但这种简化为理解视频推荐系统的基本原理提供了清晰的入口。

8.4 内容时效性

应用的 12 部影片上映年份集中在 2022-2023 年,其中 2023 年 6 部,2022 年 6 部。这种时效性设计有以下考虑:

  1. 贴近当下:2022-2023 年的影片是用户近期观看和讨论的内容,具有较高的认知度和话题性
  2. 新老结合:2023 年的新片(流浪地球2、狂飙、三体等)和 2022 年的经典片(长津湖之水门桥、梦华录等)结合,既有时效性又有内容沉淀
  3. 年份显示:分类页的视频卡片显示年份信息,用户可以快速判断影片的时效性
  4. 持续更新:在真实应用中,内容库会持续更新,新片上映时及时添加,老片根据观看数据决定是否保留

内容时效性是视频 App 的核心竞争力之一。用户使用视频 App 的主要动机之一是观看最新的影视内容,因此视频平台需要持续采购和更新内容库,保持内容的新鲜度。同时,经典老片也是内容库的重要组成部分,满足用户的怀旧和深度观看需求。


第九章 首页推荐系统

9.1 首页布局结构

首页是视频 App 的核心页面,也是用户打开应用后看到的第一个页面。首页的设计目标是在有限的屏幕空间内,尽可能高效地展示内容,引导用户发现和观看感兴趣的影片。应用的首页采用三段式布局:

第一段:横向滚动推荐区(高度约 190px)

  • 展示 3 部热门影片(FEATURED)
  • 横向 Scroll 滚动,用户可以左右滑动浏览
  • 每个卡片宽度 140px,包含大封面、标题、热门标签
  • 位于页面最顶部,视觉权重最高

第二段:分类筛选区(高度约 40px)

  • 展示 4 个分类 chips:全部、电影、电视剧、综艺
  • 水平排列,可横向滚动(如果 chips 数量超出屏幕宽度)
  • 选中态橙色高亮,未选中态灰色
  • 位于推荐区下方,宫格区上方

第三段:双列宫格区(占据剩余空间,可纵向滚动)

  • 展示当前分类下的所有影片(全部 12 部,或筛选后的 4 部)
  • 双列 Flex wrap 布局,每个卡片宽度 47%
  • 每个卡片包含封面、标题、评分、热门标签
  • 可纵向滚动浏览更多内容

这种三段式布局是视频 App 首页的经典设计:顶部横向推荐展示编辑精选,中间分类筛选提供内容导航,底部双列宫格展示完整内容列表。三个区域各司其职,协同完成内容发现的任务。

9.2 横向滚动推荐设计

横向滚动推荐区是首页最醒目的区域,用于展示编辑精选或热门推荐。设计要点:

卡片设计:每个推荐卡片宽度 140px,背景色 SP.card,圆角 12px,内边距 10px。卡片包含三个元素:

  1. 封面 emoji:fontSize 60px,textAlign Center,宽度 100%,高度 120px,背景色 SP.cardLight,圆角 8px
  2. 标题:fontSize 14px,fontWeight Bold,fontColor SP.text,margin top 6px
  3. 热门标签:"🔥 热门"文字,fontSize 10px,fontColor SP.primary,背景色 SP.primary + '33'(20%透明度橙色),圆角 4px,内边距上下 2px 左右 6px,margin top 4px

大尺寸封面(120px 高)是推荐卡片的核心视觉元素,占据了卡片约 70% 的高度,确保推荐内容的视觉冲击力。标题和热门标签作为辅助信息,提供内容的基本描述和热度标识。

滚动设计:使用 Scroll 组件的 ScrollDirection.Horizontal 实现横向滚动。Scroll 内部是一个 Row,包含 3 个推荐卡片,卡片之间有 10px 的左右 margin。Row 的左右 padding 各 8px,确保第一个和最后一个卡片与屏幕边缘有适当间距。

横向滚动是移动端展示多卡片内容的经典方案。相比纵向滚动,横向滚动在同一屏内可以展示更多卡片的预览(通常 2-3 个卡片可见),用户可以通过左右滑动快速浏览,同时不会占用过多的纵向空间。

交互设计:每个推荐卡片有 .onClick(() => {}) 点击事件,但当前为空实现。在真实应用中,点击推荐卡片会跳转到视频详情页或直接开始播放。空实现是概念应用的简化处理,保留了交互入口但未实现具体逻辑。

9.3 分类筛选与宫格联动

分类筛选区和双列宫格区通过 selectedCategory 状态联动。当用户点击分类 chips 时,selectedCategory 更新,同时 videos 数组根据分类过滤,宫格区重新渲染过滤后的视频列表。

联动逻辑

  1. 用户点击"电影"chip → selectedCategory = 'movie'
  2. 执行过滤:videos = VIDEOS.filter(v => v.category === '电影')
  3. 宫格区重新渲染,只显示 4 部电影
  4. "电影"chip 显示橙色选中态,其他 chip 显示灰色未选中态

全部状态:当用户点击"全部"chip 时,videos = VIDEOS,恢复显示全部 12 部影片。"全部"是默认选中状态,用户进入首页时看到最丰富的内容。

视觉过渡:由于 ArkUI 的声明式渲染机制,videos 数组变化时,宫格区会自动重新渲染。ForEach 会根据新的数组长度添加或移除卡片。当前应用没有添加显式的过渡动画,卡片会直接切换。在真实应用中,可以添加淡入淡出、滑动等过渡动画,提升切换时的视觉流畅度。

9.4 双列宫格设计

双列宫格是首页内容列表的核心布局,使用 Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) 实现。

布局原理

  • FlexWrap.Wrap:内容超出容器宽度时自动换行
  • FlexAlign.SpaceBetween:每行的第一个卡片靠左对齐,最后一个卡片靠右对齐,中间卡片均匀分布
  • 每个卡片宽度 47%,两个卡片之间有 6% 的间距(100% - 47% × 2 = 6%)
  • 卡片之间的纵向间距通过 margin top 8px 实现

这种双列布局是响应式的,在不同屏幕宽度下都能保持两列布局,卡片宽度会根据屏幕宽度自动调整。47% 的宽度设计确保了两个卡片之间有适当的间距,同时卡片本身有足够的宽度展示内容。

卡片设计:每个视频卡片背景色 SP.card,圆角 10px,内边距 8px,margin top 8px。卡片包含三个元素:

  1. 封面 emoji:fontSize 44px,textAlign Center,宽度 100%,高度 90px,背景色 SP.cardLight,圆角 6px
  2. 标题:fontSize 12px,fontWeight Medium,fontColor SP.text,maxLines 1,textOverflow Ellipsis,宽度 100%,margin top 4px
  3. 评分行:Row 水平布局,左侧"⭐ 评分"(fontSize 10px,fontColor SP.primary),中间 Column().layoutWeight(1) 撑开空间,右侧热门标签 🔥(如果 v.hot 为 true)

与推荐卡片相比,宫格卡片的尺寸更小(封面 90px vs 120px,标题 12px vs 14px),因为宫格需要在有限空间内展示尽可能多的内容。标题使用 maxLines 1 + textOverflow Ellipsis 确保长标题不会破坏布局,超出部分显示省略号。

9.5 推荐系统的设计哲学

首页推荐系统的设计哲学可以概括为:编辑精选 + 分类导航 + 完整列表的三层漏斗。

  1. 顶层漏斗(横向推荐):编辑精选的 3 部热门影片,视觉权重最高,用户进入首页首先看到。这一层的目标是"强推",将平台最想推荐的内容直接展示给用户,提高头部内容的曝光率
  2. 中层漏斗(分类筛选):4 个分类标签,用户可以根据偏好缩小内容范围。这一层的目标是"导航",帮助用户在大量内容中快速定位感兴趣的分类
  3. 底层漏斗(双列宫格):当前分类下的完整内容列表,用户可以纵向滚动浏览所有内容。这一层的目标是"穷尽",确保用户不会错过任何内容,可以自由探索

三层漏斗从"强推"到"导航"到"穷尽",覆盖了用户从"被动接受推荐"到"主动探索内容"的完整行为路径。无论用户是想随便看看(看顶部推荐),还是有明确的分类偏好(用分类筛选),还是想穷尽所有内容(滚动宫格),都能在首页找到合适的内容发现方式。


第十章 分类页复用设计

10.1 CategoryContent 组件设计

分类页是应用中组件复用设计的典范。电影、电视剧、综艺三个 Tab 共用同一个 CategoryContent 组件,通过 @Prop category: string 传入不同的分类参数,渲染不同的内容。

@Component struct CategoryContent {
  @Prop category: string;
  @State videos: VideoItem[] = VIDEOS.filter(v => v.category === this.category);

  build() {
    // ... 渲染分类标题和视频宫格
  }
}

@Prop 装饰器@Prop category: string 声明了一个单向传入的属性。父组件通过 CategoryContent({ category: '电影' }) 传入分类名称,子组件接收后只读使用,不修改。@Prop@Link 的区别在于:@Prop 是单向的(父→子),子组件修改不会同步到父组件;@Link 是双向的(父↔子),子组件修改会同步到父组件。分类参数只需要从父组件传入,子组件不需要修改,因此使用 @Prop 是合适的选择。

初始化过滤@State videos: VideoItem[] = VIDEOS.filter(v => v.category === this.category) 在组件初始化时,根据传入的 category 参数过滤出对应类别的视频列表。这个过滤只在组件初始化时执行一次,之后 videos 状态由组件内部管理。

10.2 分类页布局结构

分类页的布局结构比首页简单,只包含两个区域:

标题区Row 水平布局,左侧是"🔥 热门 {category}“标题(fontSize 16px,fontWeight Bold,fontColor SP.text),margin left 12px top 12px。标题动态显示分类名称,如"🔥 热门 电影”、“🔥 热门 电视剧”、“🔥 热门 综艺”。

双列宫格区:与首页的双列宫格设计基本一致,但卡片尺寸稍大:

  • 封面 emoji:fontSize 48px(首页 44px),高度 100px(首页 90px)
  • 标题:fontSize 13px(首页 12px)
  • 评分行:左侧"⭐ 评分",中间撑开,右侧显示年份 v.year(首页显示热门标签 🔥)

分类页卡片尺寸稍大的原因是:分类页的内容较少(每个分类 4 部,首页全部 12 部),可以适当放大卡片尺寸提升视觉舒适度。同时,分类页的所有内容都是同一类别,热门标签的区分意义不大,因此替换为年份信息,提供更多的内容维度。

10.3 组件复用的价值

CategoryContent 组件的复用设计体现了组件化开发的核心价值:

  1. 减少代码重复:如果不使用组件复用,需要为电影、电视剧、综艺三个 Tab 分别编写三个内容组件,每个组件的代码几乎完全相同,只是过滤条件不同。使用组件复用后,只需要编写一个 CategoryContent 组件,通过参数区分,代码量减少约 60%
  2. 提高可维护性:当需要修改分类页的布局或样式时,只需要修改 CategoryContent 一个组件,三个分类页会同时更新。如果是三个独立组件,需要分别修改三处,容易遗漏或不一致
  3. 增强扩展性:新增一个分类 Tab(如"动漫"、“纪录片”)只需要两步:在底部 Tab 栏添加一个 tabItem,在 Stack 中添加一个 CategoryContent({ category: '动漫' }) 调用。无需编写新的组件代码,扩展成本极低
  4. 一致性保障:三个分类页使用同一组件,确保了布局、样式、交互的完全一致性。用户在不同分类页之间切换时,不会感受到布局变化带来的认知负担

10.4 @Prop 与组件生命周期

@Prop 装饰器的属性在组件初始化时从父组件传入,之后与父组件断开连接。这意味着:

  • 父组件的 category 变化不会同步到已创建的 CategoryContent 子组件
  • 子组件内部不应该修改 @Prop 属性(虽然技术上可以修改,但修改不会同步到父组件,且不符合单向数据流原则)

在当前应用中,三个 CategoryContent 实例在 VideoAppbuild() 中通过条件渲染创建,每个实例的 category 参数是固定的字符串字面量(‘电影’、‘电视剧’、‘综艺’),不会变化。因此 @Prop 的单向特性完全满足需求。

如果需要父组件的分类参数动态变化并同步到子组件,可以使用 @Link 替代 @Prop,实现双向绑定。但在分类页场景中,分类通常是固定的,不需要动态变化,因此 @Prop 是更合适的选择。

10.5 组件复用的边界

虽然组件复用有诸多优势,但也需要注意复用的边界。不是所有相似的页面都适合复用同一组件,以下情况需要考虑拆分:

  1. 交互逻辑差异大:如果两个页面的布局相似但交互逻辑差异很大(如一个需要搜索功能,另一个不需要),强行复用会导致组件内部充满条件判断,降低代码可读性
  2. 数据结构差异大:如果两个页面的数据结构不同(如一个展示视频,另一个展示直播),复用组件需要处理多种数据类型,增加复杂度
  3. 视觉设计差异大:如果两个页面的视觉设计有明显差异(如一个是双列宫格,另一个是单列列表),复用组件需要大量的条件渲染,不如拆分为两个组件

在当前应用中,三个分类页的布局、交互、数据结构完全一致,只是内容数据不同,因此复用 CategoryContent 组件是最佳选择。这是组件复用的理想场景。


第十一章 我的页面与用户系统

11.1 我的页面布局

我的页面是视频 App 用户体系的入口,虽然当前仅展示未登录状态,但页面结构已经为完整的用户系统预留了框架。页面包含三个区域:

用户信息卡:位于页面顶部,背景色 SP.card,圆角 12px,内边距 20px,margin top 16px。包含三个元素:

  1. 圆形头像:emoji 👤,fontSize 48px,背景色 SP.cardLight,圆角 50px(圆形),宽度 80px,高度 80px,textAlign Center
  2. "未登录"文字:fontSize 16px,fontWeight Bold,fontColor SP.text,margin top 8px
  3. "点击登录"提示:fontSize 12px,fontColor SP.primary(橙色),margin top 4px

用户信息卡是我的页面最核心的区域,展示用户的基本信息。未登录状态下,头像使用默认 emoji,用户名显示"未登录",橙色"点击登录"提示引导用户登录。登录后,这个区域会替换为用户的真实头像、昵称、等级、VIP 状态等信息。

菜单列表:位于用户信息卡下方,背景色 SP.card,圆角 12px,左右内边距 12px,margin top 16px。包含 4 个菜单项,每个菜单项通过 @Builder menuItem(icon, label) 生成:

  1. 📺 我的收藏
  2. ⏳ 观看历史
  3. 💬 我的评论
  4. ⚙️ 设置

每个菜单项的结构为:Row 水平布局,左侧图标(fontSize 18px,margin right 12px)+ 标签(fontSize 14px,fontColor SP.text),中间 Column().layoutWeight(1) 撑开空间,右侧箭头 “>”(fontColor SP.textSub)。菜单项高度通过上下 padding 12px 控制,底部有 0.5px 的 SP.border 分割线。

底部标语:“— 影音有你,精彩无限 —”,fontSize 10px,fontColor SP.textSub,margin top 20px bottom 16px。作为页面的装饰性结尾,传递品牌理念。

11.2 @Builder menuItem 自定义构建函数

菜单项通过 @Builder menuItem(icon: string, label: string) 自定义构建函数生成。@Builder 是 ArkUI 提供的自定义构建函数装饰器,用于封装可复用的 UI 片段。

@Builder
menuItem(icon: string, label: string) {
  Row() {
    Text(icon)
      .fontSize(18)
      .margin({ right: 12 });
    Text(label)
      .fontSize(14)
      .fontColor(SP.text);
    Column().layoutWeight(1);
    Text('>')
      .fontColor(SP.textSub);
  }
  .width('100%')
  .padding({ top: 12, bottom: 12 })
  .border({ width: { bottom: 0.5 }, color: SP.border });
}

@Builder 函数的特点:

  1. 可复用:定义一次,可以在 build() 中多次调用,每次调用传入不同的参数生成不同的 UI
  2. 参数化:支持传入参数(如 icon、label),根据参数动态生成 UI
  3. 类型安全:参数有明确的类型标注(icon: stringlabel: string),调用时传入错误类型会在编译时报错
  4. 简洁调用:在 build() 中通过 this.menuItem('📺', '我的收藏') 调用,语法简洁

使用 @Builder 封装菜单项,避免了在 build() 中重复编写 4 个几乎相同的 Row 布局,提高了代码的可读性和可维护性。如果需要修改菜单项的样式(如调整图标大小、修改分割线颜色),只需要修改 @Builder menuItem 一处,4 个菜单项会同时更新。

11.3 未登录状态设计

我的页面当前展示未登录状态,这是视频 App 用户系统的常见设计。未登录状态的设计要点:

  1. 引导登录:橙色"点击登录"文字是页面中最醒目的交互元素,引导用户点击登录。橙色在深色背景上具有极高的视觉穿透力,用户很容易注意到
  2. 降低门槛:未登录状态下,页面依然展示了菜单列表(收藏、历史、评论、设置),用户可以浏览菜单结构,了解登录后可以使用的功能。这种"先展示后登录"的设计降低了用户的心理门槛,让用户在登录前就知道应用能提供什么
  3. 状态清晰:"未登录"文字明确告知用户当前状态,避免用户困惑。头像使用默认 emoji,与登录后的用户头像形成区分
  4. 登录入口:整个用户信息卡可以设置点击事件,点击后跳转到登录页面。当前应用未实现点击事件,但预留了交互入口

在真实视频 App 中,未登录状态通常还会展示一些不需要登录就能使用的功能(如浏览、搜索、观看免费内容),而需要登录的功能(如收藏、历史记录同步、评论、个性化推荐)在用户点击时引导登录。这种"部分功能免登录"的设计平衡了用户体验和用户转化。

11.4 用户系统的扩展方向

当前我的页面仅展示未登录状态和静态菜单,完整的视频 App 用户系统还需要以下功能:

  1. 用户登录/注册:支持手机号、微信、QQ、微博等多种登录方式,新用户注册流程
  2. 个人资料:头像、昵称、性别、生日、个性签名等个人信息的查看和编辑
  3. 会员系统:VIP 会员等级、会员权益、会员到期时间、续费入口等
  4. 我的收藏:用户收藏的影片列表,支持分类管理、取消收藏、批量操作
  5. 观看历史:用户观看过的影片列表,按时间排序,支持删除、清空、继续观看
  6. 我的评论:用户发表过的评论列表,支持查看、删除、回复
  7. 下载管理:用户下载的离线视频列表,支持下载、暂停、删除、播放
  8. 消息通知:系统通知、影片更新提醒、评论回复通知等
  9. 设置:播放设置、画质设置、缓存清理、账号安全、关于等
  10. 退出登录:退出当前账号,清除本地用户数据

这些功能构成了完整的视频 App 用户系统。当前应用的我的页面为这些功能的扩展提供了基础框架,菜单项已经预留了收藏、历史、评论、设置四个核心入口,后续可以逐步实现。

11.5 用户系统的设计价值

用户系统是视频 App 的核心基础设施,其设计价值体现在:

  1. 个性化体验:基于用户的观看历史、收藏、评分等数据,提供个性化推荐,提升用户发现内容的效率和满意度
  2. 跨设备同步:用户的收藏、历史记录、播放进度等数据在云端同步,用户在手机、平板、电视等不同设备上可以无缝切换
  3. 用户留存:会员系统、观看历史、收藏等功能增加了用户的迁移成本,用户在一个平台上积累的数据越多,越不容易流失到其他平台
  4. 商业化基础:用户系统是会员订阅、广告精准投放、内容付费等商业化模式的基础,没有用户系统就无法实现精准的商业变现
  5. 社区互动:评论、评分、分享等社区功能需要用户系统支撑,用户通过社区互动增强对平台的归属感和粘性

虽然当前概念应用未实现完整的用户系统,但我的页面的设计已经体现了对用户系统的思考和规划。未登录状态的引导、菜单列表的功能预留,都为后续用户系统的扩展打下了基础。


第十二章 流媒体文化与视频平台趋势

12.1 流媒体的崛起与变革

过去十年,流媒体彻底改变了人们观看视频内容的方式。从 Netflix 在 2013 年推出《纸牌屋》开启原创内容时代,到 Disney+、Apple TV+、HBO Max 等新玩家的入场,全球流媒体市场已经形成了群雄逐鹿的格局。在中国市场,爱奇艺、腾讯视频、优酷构成了长视频的"爱腾优"三巨头,芒果 TV 凭借综艺内容差异化突围,Bilibili 从二次元社区成长为综合视频平台,抖音、快手等短视频平台也在向长视频领域渗透。

流媒体的崛起带来了几个根本性的变革:

  1. 观看方式的变革:从"电视台排什么看什么"的线性播放,到"想看什么看什么、想什么时候看就什么时候看"的按需播放,用户获得了完全的观看自主权
  2. 内容生产的变革:流媒体平台投入巨资制作原创内容,《纸牌屋》《怪奇物语》《权力的游戏》《狂飙》《三体》等原创剧集的质量已经超越传统电视台,"电影级制作"成为剧集的新标准
  3. 商业模式的变革:从广告支持的免费模式,到会员订阅模式,再到会员+广告的混合模式,流媒体平台不断探索可持续的商业模式
  4. 观看设备的变革:从电视到电脑,从手机到平板,从智能电视到投影,用户可以在各种设备上观看流媒体内容,跨设备同步成为标配
  5. 推荐算法的变革:基于用户行为数据的个性化推荐算法成为流媒体平台的核心竞争力,"猜你喜欢"的推荐准确率直接影响用户的观看时长和平台留存

12.2 爱奇艺的设计语言

爱奇艺作为中国领先的流媒体平台,其设计语言具有鲜明的特点:

  1. 橙色品牌色:爱奇艺的品牌色是橙色(#FF6A00 或类似色值),橙色代表活力、热情、娱乐,与视频平台的娱乐属性高度契合。橙色在深色背景上具有极高的视觉穿透力,是爱奇艺最核心的视觉识别元素
  2. 深色模式优先:爱奇艺 App 采用深色模式作为默认主题,深色背景降低了夜间观影的视觉刺激,同时让视频内容(通常是彩色的)在深色背景上更加突出。深色模式已经成为流媒体 App 的行业标准
  3. 首页信息流:爱奇艺首页采用多模块信息流布局,顶部是搜索框和频道导航,下方依次是横向滚动的 Banner、热门推荐、频道分类、精选内容等模块,用户可以纵向滚动浏览大量内容
  4. 视频卡片化:内容以卡片形式展示,每个卡片包含封面图、标题、评分、标签等信息。卡片化布局提高了内容的浏览效率,用户可以快速扫描大量卡片找到感兴趣的内容
  5. 播放页沉浸设计:视频播放页采用全屏沉浸设计,隐藏系统状态栏和导航栏,让用户专注于视频内容。播放控制栏半透明悬浮,不遮挡视频画面
  6. 会员体系视觉:VIP 会员在界面中有专属的金色标识和视觉元素,会员内容有专属角标,强化会员的尊贵感和权益感知

这款概念应用在设计上参考了爱奇艺的设计语言,特别是橙色品牌色、深色模式、视频卡片化、底部 Tab 导航等核心元素,还原了爱奇艺风格的视觉体验。

12.3 视频平台的内容竞争

内容是视频平台的核心竞争力。各大平台在内容上的竞争主要体现在以下几个方面:

  1. 独家原创内容:原创内容是平台差异化的核心武器。爱奇艺的"迷雾剧场"(《隐秘的角落》《沉默的真相》)、腾讯视频的"鬼吹灯"系列、优酷的"白夜追凶"、芒果 TV 的"乘风破浪的姐姐"等,都是平台的独家内容名片
  2. 版权采购:除了原创内容,平台还需要采购电影、电视剧、综艺、动漫、纪录片等各类内容的版权。热门内容的独家版权往往成为平台竞争的焦点
  3. 内容矩阵:成熟的视频平台需要构建覆盖电影、电视剧、综艺、动漫、纪录片、少儿、体育等多个品类的内容矩阵,满足不同用户群体的内容需求
  4. 内容质量:在内容数量趋于饱和的背景下,内容质量成为竞争的关键。高口碑、高热度的"爆款"内容能够带来大量的新用户和会员转化
  5. 内容时效性:新片、新剧的上线速度是用户选择平台的重要因素。“全网首播”、"独家首播"等时效性标签对用户有很强的吸引力

这款概念应用的 12 部影片涵盖了电影、电视剧、综艺三个品类,每部都有较高的评分和口碑,体现了对内容质量的重视。在真实应用中,内容库的规模和质量是平台竞争力的直接体现。

12.4 短视频的冲击与长视频的应对

近年来,抖音、快手等短视频平台的崛起对长视频平台造成了巨大的冲击。短视频以其碎片化、高沉浸、算法推荐精准等特点,占据了用户大量的娱乐时间,挤压了长视频的观看时长。

面对短视频的冲击,长视频平台采取了以下应对策略:

  1. 布局短视频:爱奇艺推出"随刻"、腾讯视频推出"短视频"频道、优酷推出"短视频"模块,长视频平台纷纷在自己的 App 中嵌入短视频内容,试图在内部消化短视频的冲击
  2. 强化长视频优势:长视频平台强调自己在精品内容、沉浸式观看、完整叙事等方面的优势,与短视频的碎片化内容形成差异化。剧集、电影等长内容的叙事深度和情感沉浸是短视频无法替代的
  3. 互动视频创新:尝试互动剧、互动电影等新形式,让用户参与到剧情发展中,提升观看的参与感和趣味性
  4. 社区化运营:增强评论、弹幕、评分、分享等社区功能,让用户在观看内容的同时进行社交互动,提升用户粘性
  5. 会员价值深化:通过会员专属内容、会员抢先看、会员画质提升、会员免广告等权益,深化会员价值,提高会员续费率

这款概念应用作为长视频平台的设计参考,聚焦于长视频的核心体验:内容浏览、分类导航、个性化推荐。这些是长视频平台相对于短视频的核心优势,也是长视频平台需要持续强化的方向。

12.5 流媒体的未来趋势

展望未来,流媒体行业的发展趋势包括:

  1. AI 赋能:人工智能将在内容推荐、内容制作、画质增强、字幕生成等多个环节发挥重要作用。AI 推荐算法将更加精准,AI 生成内容(AIGC)将降低内容制作成本
  2. 沉浸式体验:VR/AR 技术的成熟将带来沉浸式的观影体验,虚拟影院、VR 直播等新形式将涌现
  3. 多屏协同:手机、平板、电视、投影、车载屏幕等多设备的协同将更加紧密,用户可以在不同设备间无缝切换观看
  4. 社交化观看:一起看、观影派对、实时评论等社交化观看功能将更加普及,让独自观看变成社交体验
  5. 精细化运营:基于用户数据的精细化运营将成为常态,不同用户群体看到不同的首页布局、内容推荐、会员套餐,实现"千人千面"
  6. 内容全球化:流媒体平台的内容将更加全球化,韩剧、美剧、日剧、泰剧等海外内容在中国市场的影响力将持续增强,中国内容也将加速出海

这款概念应用虽然是一个简化的 UI 原型,但它所体现的深色观影体验、橙色品牌识别、卡片化内容布局、Tab 导航结构等设计元素,正是当前流媒体行业的主流设计范式。理解这些设计元素背后的文化和趋势,有助于我们更好地设计和开发面向未来的视频应用。


第十三章 深色模式与观影体验

13.1 深色模式的兴起

深色模式(Dark Mode)是近年来 UI 设计领域最重要的趋势之一。从 macOS Mojave 在 2018 年引入系统级深色模式,到 iOS 13、Android 10 相继跟进,深色模式已经从"小众偏好"变成了"系统标配"。如今,几乎所有主流 App 都提供了深色模式选项,很多 App 甚至将深色模式作为默认主题。

深色模式的兴起有以下几个原因:

  1. OLED 屏幕的普及:OLED 屏幕的每个像素可以独立发光,显示黑色时像素完全关闭,不消耗电量。深色模式在 OLED 屏幕上可以显著降低功耗,延长续航时间。随着 OLED 屏幕在中高端手机上的普及,深色模式的省电优势越来越明显
  2. 视觉健康关注:长时间盯着明亮的白色屏幕会导致眼睛疲劳、干涩、视力下降。深色模式降低了屏幕的整体亮度,减少了眼睛的调节负担,特别适合在夜间或低光环境下使用
  3. 沉浸感需求:深色背景让内容(图片、视频、文字)更加突出,减少了界面元素对内容的干扰,创造了更强的沉浸感。对于视频、图片、阅读等内容消费类应用,深色模式的沉浸感优势尤为明显
  4. 设计美学驱动:深色模式具有独特的视觉美感,高对比度的色彩在深色背景上更加鲜艳和醒目,为设计师提供了新的创作空间。很多 App 在深色模式下的视觉效果比浅色模式更加惊艳
  5. 系统级支持:iOS、Android、macOS、Windows 等主流操作系统都提供了系统级的深色模式开关和开发者 API,App 可以轻松适配系统深色模式,降低了开发成本

13.2 视频 App 与深色模式的天然契合

视频 App 是深色模式的最佳应用场景之一,二者具有天然的契合性:

  1. 观影场景的低光环境:用户观看视频的场景通常是在室内、夜间等低光环境下。在低光环境中,明亮的白色界面会与视频画面形成强烈的明暗对比,导致眼睛频繁调节,容易疲劳。深色模式的界面与低光环境更加协调,减少了视觉刺激
  2. 视频内容的突出展示:视频内容本身是彩色的、动态的,在深色背景上更加突出。如果界面是白色的,白色背景会分散用户对视频内容的注意力。深色背景让用户的视觉焦点自然集中在视频内容上
  3. 全屏观影的沉浸感:视频播放时通常会进入全屏模式,隐藏系统状态栏和导航栏。深色模式的界面元素(如播放控制栏、进度条)在全屏模式下与黑色的视频留白区域融为一体,不会破坏沉浸感
  4. 续航考虑:视频播放是手机的高功耗场景,屏幕常亮、解码器运行、网络传输等都在消耗电量。深色模式在 OLED 屏幕上可以降低屏幕功耗,延长观影时间,这对用户来说是实实在在的体验提升
  5. 行业标准的形成:Netflix、YouTube、爱奇艺、腾讯视频等主流视频平台都采用了深色模式作为默认主题,深色模式已经成为视频 App 的行业标准。用户在使用视频 App 时已经形成了"视频 App 就是深色的"的认知预期

这款概念应用完全采用深色模式设计,背景色 #121212 接近纯黑,卡片色 #1E1E1E 和浅卡片色 #2A2A2A 构建了清晰的视觉层级,橙色主色 #FF6A00 在深色背景上具有极强的视觉穿透力。整套色彩系统是视频 App 深色模式设计的典范。

13.3 深色模式的色彩设计原则

深色模式不是简单地"把白色背景换成黑色、把黑色文字换成白色",而是需要一套完整的色彩设计原则。这款应用的深色模式色彩设计体现了以下原则:

  1. 避免纯黑背景:纯黑(#000000)在 OLED 屏幕上虽然最省电,但与纯白文字的对比度过高,容易产生眩光感,同时纯黑背景上的阴影和层级难以表现。这款应用使用 #121212 作为背景色,接近纯黑但保留了极微弱的灰度,既降低了视觉刺激,又为视觉层级留出了空间
  2. 灰度构建层级:深色模式下,不同区域的区分不能依赖边框(边框在深色背景上不明显),而需要依赖灰度层级。这款应用使用三级灰度:背景 #121212、卡片 #1E1E1E、浅卡片 #2A2A2A,通过微妙的亮度差异区分不同的视觉区域
  3. 降低文字亮度:纯白(#FFFFFF)文字在深色背景上对比度极高,长时间阅读容易疲劳。这款应用使用纯白 #FFFFFF 作为主文字色(用于标题等短文本),使用中灰 #AAAAAA 作为次级文字色(用于描述等长文本)。次级文字降低了亮度,减少了长时间阅读的视觉疲劳
  4. 饱和色的克制使用:高饱和色在深色背景上非常醒目,但过度使用会造成视觉混乱。这款应用的橙色主色 #FF6A00 仅用于关键交互元素(选中态、评分、热门标签),不滥用,确保橙色的视觉权重
  5. 对比度达标:深色模式下的文字与背景对比度需要满足 WCAG AA 级标准(4.5:1 以上)。这款应用的纯白文字 #FFFFFF 与背景 #121212 的对比度约为 18:1,中灰文字 #AAAAAA 与背景的对比度约为 5.5:1,均满足 AA 级标准
  6. 半透明叠加:深色模式下常使用半透明叠加层(如 SP.primary + '33' 表示 20% 透明度的橙色)来创建选中态、标签背景等。半透明叠加在深色背景上效果自然,不会产生生硬的色块

13.4 观影体验的设计要素

除了深色模式,视频 App 的观影体验还涉及以下设计要素:

  1. 播放页沉浸设计:视频播放页是观影体验的核心。优秀的播放页应该在播放时隐藏所有非必要的 UI 元素(状态栏、导航栏、标题栏),让视频内容占据整个屏幕。播放控制栏(播放/暂停、进度条、倍速、画质、全屏等)应该半透明悬浮在视频画面上,用户不操作时自动隐藏,点击屏幕时呼出
  2. 手势操作:视频播放页应该支持丰富的手势操作,如单击呼出/隐藏控制栏、双击播放/暂停、左侧上下滑动调节亮度、右侧上下滑动调节音量、横向滑动快进/快退等。手势操作减少了对按钮的依赖,提升了操作效率和沉浸感
  3. 画质选择:提供多种画质选项(流畅 360P、标清 480P、高清 720P、超清 1080P、蓝光 4K 等),用户可以根据网络状况和设备性能选择合适的画质。WiFi 环境下默认高清,移动网络下提示流量消耗
  4. 倍速播放:提供 0.5x、0.75x、1.0x、1.25x、1.5x、2.0x 等多种倍速选项,满足不同用户的观看需求。倍速播放是年轻用户的常用功能,可以快速浏览内容或仔细观看细节
  5. 字幕与音轨:对于多语言内容,提供字幕语言选择和音轨选择。字幕的字体大小、颜色、背景透明度等也可以自定义,满足不同用户的阅读需求
  6. 投屏与跨设备:支持投屏到电视、投影等大屏设备,支持手机与平板、电视等设备的跨设备续播,用户可以在不同设备间无缝切换
  7. 离线下载:支持视频离线下载,用户可以在 WiFi 环境下下载视频,在没有网络的环境下观看。下载管理支持画质选择、下载任务管理、下载完成通知等

这款概念应用未实现视频播放功能,仅实现了内容浏览的 UI。但作为视频 App 的设计参考,理解上述观影体验的设计要素,有助于在后续开发中构建完整的视频播放体验。

13.5 深色模式的适配策略

在真实的视频 App 开发中,深色模式的适配通常有以下几种策略:

  1. 固定深色模式:App 始终使用深色模式,不提供浅色模式选项。这种策略适用于视频、图片、阅读等内容消费类 App,深色模式是这些 App 的最佳体验。Netflix、YouTube(移动端)等 App 采用这种策略
  2. 跟随系统:App 跟随系统的深色模式设置,系统开启深色模式时 App 自动切换到深色模式,系统关闭时切换到浅色模式。这种策略是目前的主流做法,用户不需要在 App 内单独设置
  3. App 内开关:App 内提供深色模式开关,用户可以手动选择浅色、深色或跟随系统。这种策略给用户最大的自由度,但增加了开发和维护成本
  4. 定时切换:App 根据时间自动切换深色模式,如晚上 8 点到早上 6 点自动使用深色模式,其他时间使用浅色模式。这种策略适用于阅读类 App,符合人体的生物钟规律

这款概念应用采用固定深色模式策略,整个 App 只有深色主题,不提供浅色模式选项。这是视频 App 的常见做法,因为深色模式与视频观影场景天然契合,浅色模式在视频 App 中反而会降低观影体验。

13.6 深色模式与 accessibility

深色模式的设计还需要考虑无障碍(accessibility)需求:

  1. 对比度:确保文字与背景的对比度满足 WCAG AA 级标准(4.5:1),对于大字体(18pt 以上或 14pt 加粗)可以降低到 3:1。高对比度确保视力障碍用户也能清晰阅读
  2. 不要仅依赖颜色传达信息:不要仅通过颜色区分不同状态(如仅用红色表示错误、绿色表示成功),还需要配合图标、文字等其他视觉线索。例如,热门标签不仅用橙色背景,还有"🔥 热门"文字和图标
  3. 支持系统辅助功能:支持系统的"增强对比度"、“降低透明度”、"智能反转"等辅助功能设置。当用户开启这些功能时,App 应该相应调整界面,确保可用性
  4. 字体大小适配:支持系统的字体大小设置,当用户调大系统字体时,App 的文字也应该相应放大,同时布局不能错乱。使用相对单位(如 sp、%)而非固定像素值,有助于字体大小的适配

这款概念应用在设计上考虑了对比度和信息传达的多样性,但作为概念原型,未实现系统辅助功能的完整适配。在真实应用开发中,无障碍适配是不可忽视的重要环节。

深色模式不仅仅是一种视觉风格,更是一种以用户体验为中心的设计哲学。它关注用户在不同环境下的视觉舒适度,关注内容与界面的视觉层级,关注续航和性能,关注无障碍和包容性。这款爱奇艺风格视频 App 的深色模式设计,正是这种设计哲学的体现——通过精心调配的色彩系统,为用户创造一个沉浸式、舒适、高效的观影体验。


第十四章 开发环境搭建与运行指南

本章为安装软件步骤,按要求放在最后一章。

14.1 开发环境概述

爱奇艺风格视频 App 基于 HarmonyOS ArkTS 声明式 UI(ArkUI)开发,API 9+,Stage 模型,单文件 .ets,严格遵循 ArkTS 严格模式规范(无 Object.assign,无展开运算符,显式类型标注)。开发环境需要:DevEco Studio(HarmonyOS 官方 IDE)、HarmonyOS SDK(API 9+)、Node.js(构建工具依赖)、HarmonyOS 设备或模拟器(运行调试)。

14.2 安装 DevEco Studio

DevEco Studio 是华为官方推出的 HarmonyOS 应用开发 IDE,基于 IntelliJ IDEA 社区版构建,提供 ArkTS/ArkUI 代码编辑、预览、调试、打包等完整开发能力。

安装步骤

  1. 访问华为开发者联盟官网(developer.huawei.com),进入"开发"→"DevEco Studio"下载页面
  2. 选择对应操作系统版本(Windows/macOS)下载安装包
  3. Windows 版运行 .exe 安装程序,按向导完成安装(建议安装路径不含中文和空格)
  4. macOS 版打开 .dmg 镜像,将 DevEco Studio 拖入 Applications 文件夹
  5. 首次启动 DevEco Studio,选择不导入设置,进入欢迎界面
  6. 配置 Node.js 路径(DevEco Studio 内置 Node.js,也可使用本地安装的 Node.js 16+)
  7. 配置 npm 镜像(建议使用华为云 npm 镜像或淘宝镜像,加速依赖下载)

安装DevEco Studio

14.3 配置 HarmonyOS SDK

DevEco Studio 安装完成后,需要配置 HarmonyOS SDK(API 9+)。SDK 包含 ArkTS 编译器、ArkUI 组件库、系统 API、模拟器镜像等。

配置步骤

  1. 打开 DevEco Studio,进入欢迎界面
  2. 点击"Configure"→"Settings"(macOS 为"DevEco Studio"→"Settings")
  3. 左侧导航选择"Appearance & Behavior"→"System Settings"→"HarmonyOS SDK"
  4. 在 SDK Platforms 标签页,勾选 API 9 或更高版本(建议 API 9,兼容性最好)
  5. 在 SDK Tools 标签页,勾选"ArkTS"、“ArkUI”、"Previewer"等工具
  6. 点击"Apply"开始下载安装 SDK(下载大小约 2-4GB,需耐心等待)
  7. 安装完成后,SDK 路径会自动配置,可在 Settings 中查看和修改

SDK 版本说明

  • API 9:首个支持 Stage 模型和 ArkTS 声明式 UI 的稳定版本,兼容性最好
  • API 10+:新增更多 ArkUI 组件和 API,功能更丰富,但兼容性需考虑
  • 建议使用 API 9 开发,确保在更多设备上运行

配置HarmonyOS SDK

14.4 创建 HarmonyOS 项目

SDK 配置完成后,创建一个新的 HarmonyOS 项目。

创建步骤

  1. 打开 DevEco Studio,进入欢迎界面
  2. 点击"Create Project"创建新项目
  3. 选择项目模板:选择"Empty Ability"(空能力模板),这是最基础的 ArkTS 项目模板
  4. 配置项目信息:
    • Project name:项目名称(如 VideoApp)
    • Bundle name:应用包名(如 com.example.videoapp,需唯一)
    • Save location:项目保存路径(建议不含中文和空格)
    • Compile SDK:选择 API 9 或更高版本
    • Model:选择 Stage 模型(ArkTS 声明式 UI 推荐使用 Stage 模型)
    • Language:选择 ArkTS
  5. 点击"Finish"创建项目,DevEco Studio 会自动生成项目结构和基础代码
  6. 等待项目同步完成(Gradle 同步,首次同步可能需要几分钟下载依赖)

项目结构说明

  • entry/src/main/ets/:ArkTS 源代码目录
  • entry/src/main/ets/entryability/:Ability 入口文件
  • entry/src/main/ets/pages/:页面文件目录
  • entry/src/main/resources/:资源文件目录(图片、字符串、主题等)
  • entry/src/main/module.json5:模块配置文件
  • build-profile.json5:构建配置文件

创建项目后,将视频 App 的完整代码(VideoApp.ets)复制到 entry/src/main/ets/pages/ 目录下,或替换默认的 Index.ets 文件。注意修改 @Entry 装饰器的组件名和 module.json5 中的入口页面配置。

创建HarmonyOS项目

14.5 运行应用

项目创建和代码导入完成后,可以在真机或模拟器上运行应用。

运行步骤

  1. 连接 HarmonyOS 设备(通过 USB 数据线),或启动模拟器
  2. 真机运行需开启开发者模式和 USB 调试:设置→关于手机→连续点击版本号开启开发者模式→设置→系统和更新→开发人员选项→开启 USB 调试
  3. 模拟器运行:点击 DevEco Studio 顶部工具栏的设备选择器,选择"Device Manager"→创建模拟器→选择设备型号和 API 版本→启动模拟器
  4. 在 DevEco Studio 顶部工具栏,确认运行配置为"entry",设备选择为已连接的真机或已启动的模拟器
  5. 点击运行按钮(绿色三角形 ▶)或按快捷键 Shift+F10(macOS 为 Control+R)
  6. DevEco Studio 会自动编译、打包、安装应用到设备,并启动应用
  7. 首次运行可能需要 1-2 分钟编译时间,后续运行会更快(增量编译)

运行注意事项

  • 确保设备已开启 USB 调试,且已授权电脑调试
  • 确保设备系统版本 >= API 9(HarmonyOS 3.0+)
  • 模拟器运行性能可能不如真机,建议最终在真机上测试
  • 如果运行失败,查看 Build 窗口和 Log 窗口的错误信息

视频 App 的 Tab 图标缩放交互动画建议在真机上测试,确保动画流畅度和交互反馈符合预期。预览器可以查看静态布局,但动画效果需要在真机或模拟器上运行才能完整体验。

运行应用

14.6 预览与调试

DevEco Studio 提供 ArkUI 预览器和调试工具,方便开发过程中实时查看界面和调试代码。

预览器使用

  1. 打开 .ets 页面文件,右侧会自动显示 Previewer 预览面板
  2. 预览器实时渲染当前页面的 ArkUI 界面,代码修改后自动刷新
  3. 预览器支持多种设备尺寸和方向切换,测试不同屏幕适配
  4. 预览器支持深色/浅色模式切换(视频 App 为深色主题,建议在深色模式下预览)
  5. 如果预览器未显示,点击右侧边栏的"Previewer"图标开启

调试工具使用

  1. 在代码行号左侧点击设置断点(红色圆点)
  2. 点击调试按钮(绿色虫子 🐛)或按快捷键 Shift+F9(macOS 为 Control+D)
  3. 应用运行到断点处会暂停,可查看变量值、调用栈、表达式等
  4. 使用调试工具栏的单步跳过(F8)、单步进入(F7)、单步跳出(Shift+F8)等操作逐步执行代码
  5. 在 Variables 面板查看和修改变量值,在 Watches 面板添加监视表达式
  6. 在 Log 面板查看应用日志(console.log 输出和系统日志)

常见问题排查

  • 预览器不显示:检查代码是否有语法错误,查看 Previewer 面板的错误提示
  • 运行失败:检查 SDK 版本是否匹配,查看 Build 窗口的编译错误
  • 界面显示异常:检查 ArkUI 组件属性是否正确,使用预览器实时调试
  • 动画不流畅:检查是否有过多同时运行的动画,优化动画时长和曲线
  • ArkTS 严格模式报错:检查是否使用了 Object.assign、是否使用了展开运算符(…)、是否有隐式类型、是否缺少分号

视频 App 严格遵循 ArkTS 严格模式规范(无 Object.assign,无展开运算符,显式类型标注,分号结尾),在严格模式下可正常编译运行。开发过程中如遇到严格模式报错,可对照规范检查代码。

预览与调试


总结与展望

核心回顾

爱奇艺风格视频 App 是一款基于 HarmonyOS ArkTS 声明式 UI(ArkUI)开发的流媒体概念应用,API 9+,Stage 模型,单文件 .ets,约 500 行代码,严格遵循 ArkTS 严格模式规范。应用以深黑 #121212 为背景,以爱奇艺标志性的橙色 #FF6A00 为主强调色,构建了一个简洁而高效的移动端视频浏览体验。

5 个 Tab 覆盖主流视频平台的核心内容分区:首页(横向滚动推荐+分类筛选chips+双列宫格)、电影(分类内容页,4部影片)、电视剧(分类内容页,4部剧集)、综艺(分类内容页,4部综艺)、我的(未登录状态+菜单列表)。12 部影片涵盖电影、电视剧、综艺三个品类,每部都有评分、年份、简介等完整信息。3 部热门影片(流浪地球2、狂飙、三体)通过 FEATURED 派生用于首页横向推荐。

应用的技术亮点包括:组件化架构(VideoHeader/HomeContent/CategoryContent/MineContent 四大组件)、@Link 双向绑定与可选回调(搜索文本父子组件通信)、@Prop 分类参数传递(CategoryContent 三分类复用)、横向+纵向双滚动(首页推荐横向滚动+内容纵向滚动)、Flex 双列宫格布局、Tab 图标缩放动画(scale 1.2,200ms EaseOut)、2 个纯函数工具(genId/formatScore)、Record<string,string> 类型安全映射、ArkTS 严格模式合规(无 Object.assign/无展开运算符/显式类型/分号结尾)。

设计亮点

  1. 爱奇艺橙色品牌色:#FF6A00 精确还原爱奇艺品牌橙色,在深色背景上具有极强的视觉识别度,传递活力与娱乐感
  2. 深色观影沉浸体验:深黑 #121212 背景+三级灰度层级(背景/卡片/浅卡片),营造沉浸式观影氛围,降低夜间视觉疲劳
  3. @Link + 可选回调双重通信:搜索文本通过 @Link 双向绑定 + onSearchChange 可选回调,既保证状态同步又提供事件通知,是 ArkUI 父子通信的经典模式
  4. CategoryContent 三分类复用:通过 @Prop category 参数,电影/电视剧/综艺三个 Tab 共用同一组件,代码复用率高,扩展性强
  5. 首页三层漏斗推荐:横向推荐(强推)→分类筛选(导航)→双列宫格(穷尽),覆盖用户从被动接受到主动探索的完整行为路径
  6. 双列宫格响应式布局:Flex wrap + SpaceBetween + 47% 宽度,在不同屏幕宽度下保持双列布局,卡片宽度自适应
  7. ArkTS 严格模式深度合规:无 Object.assign,无展开运算符(…),所有变量和函数参数显式类型标注,Record 类型安全映射,类型安全无运行时错误
  8. 单文件零依赖:约 500 行代码在一个 .ets 文件中,复制即运行,无第三方依赖,适合学习和快速原型验证

技术价值

爱奇艺风格视频 App 展示了 ArkTS 声明式 UI 在流媒体应用开发中的完整能力,特别是组件化架构和状态管理的最佳实践:@Component/@Entry 组件化、@State 组件内状态、@Link 父子双向绑定、@Prop 父子单向传参、可选回调函数事件通知、@Builder 自定义构建函数封装可复用 UI、ForEach 列表渲染、Scroll 横向/纵向滚动、Flex 响应式布局、TextInput 搜索框、属性动画(.animation())、纯函数工具、Record 类型安全映射。

特别是 @Link + 可选回调 的双重通信模式和 CategoryContent 的 @Prop 组件复用设计,是 ArkUI 组件化开发的高级技巧,具有很高的参考价值。这些模式可以应用到任何需要父子组件通信和多页面复用的 ArkUI 项目中。

未来展望

爱奇艺风格视频 App 作为概念应用,未来可扩展方向:

  1. 真实视频播放:接入视频播放器组件,实现视频播放、暂停、进度条、倍速、画质选择、全屏等完整播放功能
  2. 真实图片替换 emoji:将封面 emoji 替换为真实的电影海报和剧集封面,提升视觉真实感和专业度
  3. 搜索功能完善:实现搜索文本对视频列表的实时过滤,支持标题搜索、分类+搜索组合过滤、搜索历史记录、热门搜索推荐
  4. 视频详情页:点击视频卡片跳转到详情页,展示详细信息(导演、演员、简介、分集列表、相关推荐)和播放按钮
  5. 用户登录系统:实现手机号/微信/QQ 登录,用户个人资料、头像、昵称等
  6. 收藏与历史记录:实现视频收藏、观看历史记录、继续观看等功能,数据本地存储或云端同步
  7. 评论与弹幕:实现视频评论、弹幕、评分等社区互动功能
  8. 会员系统:实现 VIP 会员体系,会员专属内容、会员免广告、会员画质提升等
  9. 个性化推荐:基于用户观看历史和收藏数据,实现个性化推荐算法,提升内容发现效率
  10. 多端适配:适配手机、平板、折叠屏、智能电视等多种设备,支持横屏和分屏模式

爱奇艺风格视频 App 以深色观影体验和橙色品牌活力为核心视觉语言,以"首页推荐+内容分类+搜索筛选+个人中心"四位一体功能,展示了流媒体 App 的经典设计范式。它不仅是一个概念应用,更是 ArkTS 严格模式开发的完整教学案例,涵盖组件化架构、状态管理、组件复用、响应式布局、交互动画等核心技术点。希望本文能为 HarmonyOS 开发者和视频 App 从业者提供有价值的参考。

影音有你,精彩无限。🎬

视频App总结


附录:完整 ArkTS 源码


// ============================================================
// VideoApp.ets —— 爱奇艺风格视频 App(修正版)
// 主题:深色底 #121212,主色 #FF6A00,5 Tab:首页/电影/电视剧/综艺/我的
// 适配 ArkTS 严格模式,无 Object.assign,无展开,显式类型
// ============================================================

// ---------- 调色板接口 ----------
interface VideoPalette {
  bg: string;
  primary: string;
  primaryDark: string;
  card: string;
  cardLight: string;
  text: string;
  textSub: string;
  chip: string;
  border: string;
  white: string;
}

const SP: VideoPalette = {
  bg: '#121212',
  primary: '#FF6A00',
  primaryDark: '#CC5500',
  card: '#1E1E1E',
  cardLight: '#2A2A2A',
  text: '#FFFFFF',
  textSub: '#AAAAAA',
  chip: '#333333',
  border: '#444444',
  white: '#FFFFFF'
};

// ---------- 接口定义 ----------
interface VideoItem {
  id: number;
  title: string;
  cover: string;       // emoji 占位
  category: string;    // '电影' | '电视剧' | '综艺'
  score: number;       // 评分 0-10
  year: string;
  desc: string;
  hot: boolean;
}

interface CategoryTab {
  name: string;
  key: string;
}

// ---------- 静态数据 ----------
const CATEGORIES: CategoryTab[] = [
  { name: '全部', key: 'all' },
  { name: '电影', key: 'movie' },
  { name: '电视剧', key: 'tv' },
  { name: '综艺', key: 'variety' }
];

const VIDEOS: VideoItem[] = [
  { id: 1, title: '流浪地球2', cover: '🚀', category: '电影', score: 8.7, year: '2023', desc: '太阳即将毁灭,人类启动流浪地球计划。', hot: true },
  { id: 2, title: '狂飙', cover: '🚓', category: '电视剧', score: 9.2, year: '2023', desc: '正义与邪恶的较量,跨越二十年的追捕。', hot: true },
  { id: 3, title: '声生不息', cover: '🎤', category: '综艺', score: 8.5, year: '2022', desc: '港乐经典,群星演绎。', hot: false },
  { id: 4, title: '满江红', cover: '⚔️', category: '电影', score: 8.2, year: '2023', desc: '岳飞遗志,忠义传承。', hot: false },
  { id: 5, title: '三体', cover: '🌌', category: '电视剧', score: 8.9, year: '2023', desc: '科幻巨制,探索宇宙奥秘。', hot: true },
  { id: 6, title: '向往的生活', cover: '🌿', category: '综艺', score: 7.8, year: '2023', desc: '田园生活,慢综艺。', hot: false },
  { id: 7, title: '长津湖之水门桥', cover: '❄️', category: '电影', score: 8.4, year: '2022', desc: '抗美援朝,冰血长津。', hot: false },
  { id: 8, title: '梦华录', cover: '🌸', category: '电视剧', score: 8.6, year: '2022', desc: '宋代女性创业传奇。', hot: false },
  { id: 9, title: '披荆斩棘', cover: '🔥', category: '综艺', score: 8.1, year: '2023', desc: '哥哥们的舞台竞演。', hot: false },
  { id: 10, title: '独行月球', cover: '🌕', category: '电影', score: 7.9, year: '2022', desc: '月球上的孤独与欢乐。', hot: false },
  { id: 11, title: '警察荣誉', cover: '👮', category: '电视剧', score: 8.7, year: '2022', desc: '基层民警的日常。', hot: false },
  { id: 12, title: '乘风破浪', cover: '💃', category: '综艺', score: 8.3, year: '2022', desc: '姐姐们再度起航。', hot: false }
];

const FEATURED: VideoItem[] = VIDEOS.filter(v => v.hot).slice(0, 3);

// ---------- Tab 枚举 ----------
enum VideoTab {
  HOME = 0,
  MOVIE = 1,
  TV = 2,
  VARIETY = 3,
  MINE = 4
}

// ---------- 辅助函数 ----------
function genId(): number {
  return Math.floor(Math.random() * 1000000);
}

function formatScore(s: number): string {
  return s.toFixed(1);
}

// ---------- 入口 ----------
@Entry @Component struct VideoApp {
  @State activeTab: number = VideoTab.HOME;
  @State searchText: string = '';

  build() {
    Column() {
      VideoHeader({ searchText: $searchText, onSearchChange: (v: string) => { this.searchText = v; } });

      Stack() {
        if (this.activeTab === VideoTab.HOME) {
          HomeContent();
        } else if (this.activeTab === VideoTab.MOVIE) {
          CategoryContent({ category: '电影' });
        } else if (this.activeTab === VideoTab.TV) {
          CategoryContent({ category: '电视剧' });
        } else if (this.activeTab === VideoTab.VARIETY) {
          CategoryContent({ category: '综艺' });
        } else {
          MineContent();
        }
      }
      .layoutWeight(1)
      .width('100%');

      Row() {
        this.tabItem('🏠', '首页', VideoTab.HOME);
        this.tabItem('🎬', '电影', VideoTab.MOVIE);
        this.tabItem('📺', '电视剧', VideoTab.TV);
        this.tabItem('🎪', '综艺', VideoTab.VARIETY);
        this.tabItem('👤', '我的', VideoTab.MINE);
      }
      .width('100%')
      .height(56)
      .backgroundColor(SP.card)
      .padding({ left: 4, right: 4, top: 4 })
      .border({ width: { top: 0.5 }, color: SP.border });
    }
    .width('100%')
    .height('100%')
    .backgroundColor(SP.bg);
  }

  @Builder
  tabItem(icon: string, label: string, tab: number) {
    Column() {
      Text(icon)
        .fontSize(22)
        .scale({
          x: this.activeTab === tab ? 1.2 : 1,
          y: this.activeTab === tab ? 1.2 : 1
        })
        .animation({ duration: 200, curve: Curve.EaseOut });
      Text(label)
        .fontSize(10)
        .fontColor(this.activeTab === tab ? SP.primary : SP.textSub)
        .margin({ top: 2 });
    }
    .layoutWeight(1)
    .onClick(() => {
      this.activeTab = tab;
    });
  }
}

// ---------- 头部组件(修正 onSearchChange 为可选) ----------
@Component struct VideoHeader {
  @Link searchText: string;
  onSearchChange?: (v: string) => void;   // 改为可选

  build() {
    Row() {
      Text('🎬 影音')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor(SP.primary)
        .margin({ left: 16 });

      Column().layoutWeight(1);

      Row() {
        Text('🔍')
          .fontSize(14)
          .fontColor(SP.textSub)
          .margin({ left: 8 });
        TextInput({ placeholder: '搜索视频', text: this.searchText })
          .fontSize(12)
          .fontColor(SP.text)
          .backgroundColor(Color.Transparent)
          .borderRadius(0)
          .height(30)
          .layoutWeight(1)
          .margin({ left: 4 })
          .onChange((v: string) => {
            this.searchText = v;
            if (this.onSearchChange) {
              this.onSearchChange(v);
            }
          });
      }
      .width(180)
      .height(32)
      .backgroundColor(SP.cardLight)
      .borderRadius(16)
      .margin({ right: 16 });
    }
    .width('100%')
    .height(48)
    .backgroundColor(SP.card)
    .border({ width: { bottom: 0.5 }, color: SP.border });
  }
}

// ---------- 首页内容 ----------
@Component struct HomeContent {
  @State videos: VideoItem[] = VIDEOS;
  @State selectedCategory: string = 'all';

  build() {
    Column() {
      Scroll() {
        Row() {
          ForEach(FEATURED, (item: VideoItem) => {
            Column() {
              Text(item.cover)
                .fontSize(60)
                .textAlign(TextAlign.Center)
                .width('100%')
                .height(120)
                .backgroundColor(SP.cardLight)
                .borderRadius(8);
              Text(item.title)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(SP.text)
                .margin({ top: 6 });
              Text('🔥 热门')
                .fontSize(10)
                .fontColor(SP.primary)
                .backgroundColor(SP.primary + '33')
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .margin({ top: 4 });
            }
            .width(140)
            .backgroundColor(SP.card)
            .borderRadius(12)
            .padding(10)
            .margin({ left: 10, right: 10 })
            .onClick(() => {});
          });
        }
        .padding({ left: 8, right: 8 });
      }
      .scrollable(ScrollDirection.Horizontal)
      .height(190)
      .margin({ top: 10 });

      Row() {
        ForEach(CATEGORIES, (cat: CategoryTab) => {
          Text(cat.name)
            .fontSize(12)
            .fontColor(this.selectedCategory === cat.key ? SP.primary : SP.textSub)
            .backgroundColor(this.selectedCategory === cat.key ? SP.primary + '33' : Color.Transparent)
            .borderRadius(14)
            .padding({ left: 14, right: 14, top: 4, bottom: 4 })
            .margin({ left: 6 })
            .onClick(() => {
              this.selectedCategory = cat.key;
              if (cat.key === 'all') {
                this.videos = VIDEOS;
              } else {
                const map: Record<string, string> = {
                  'movie': '电影',
                  'tv': '电视剧',
                  'variety': '综艺'
                };
                this.videos = VIDEOS.filter(v => v.category === map[cat.key]);
              }
            });
        });
      }
      .width('100%')
      .padding({ left: 12, top: 12, bottom: 8 });

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.videos, (v: VideoItem) => {
          Column() {
            Text(v.cover)
              .fontSize(44)
              .textAlign(TextAlign.Center)
              .width('100%')
              .height(90)
              .backgroundColor(SP.cardLight)
              .borderRadius(6);
            Text(v.title)
              .fontSize(12)
              .fontWeight(FontWeight.Medium)
              .fontColor(SP.text)
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
              .width('100%')
              .margin({ top: 4 });
            Row() {
              Text('⭐ ' + formatScore(v.score))
                .fontSize(10)
                .fontColor(SP.primary);
              Column().layoutWeight(1);
              if (v.hot) {
                Text('🔥')
                  .fontSize(10);
              }
            }
            .width('100%');
          }
          .width('47%')
          .backgroundColor(SP.card)
          .borderRadius(10)
          .padding(8)
          .margin({ top: 8 })
          .onClick(() => {});
        });
      }
      .width('94%')
      .padding({ bottom: 16 });
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Start);
  }
}

// ---------- 分类内容(电影/电视剧/综艺共用) ----------
@Component struct CategoryContent {
  @Prop category: string;
  @State videos: VideoItem[] = VIDEOS.filter(v => v.category === this.category);

  build() {
    Scroll() {
      Column() {
        Row() {
          Text('🔥 热门 ' + this.category)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(SP.text)
            .margin({ left: 12, top: 12 });
        }
        .width('100%');

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
          ForEach(this.videos, (v: VideoItem) => {
            Column() {
              Text(v.cover)
                .fontSize(48)
                .textAlign(TextAlign.Center)
                .width('100%')
                .height(100)
                .backgroundColor(SP.cardLight)
                .borderRadius(6);
              Text(v.title)
                .fontSize(13)
                .fontWeight(FontWeight.Medium)
                .fontColor(SP.text)
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
                .width('100%')
                .margin({ top: 4 });
              Row() {
                Text('⭐ ' + formatScore(v.score))
                  .fontSize(10)
                  .fontColor(SP.primary);
                Column().layoutWeight(1);
                Text(v.year)
                  .fontSize(9)
                  .fontColor(SP.textSub);
              }
              .width('100%');
            }
            .width('47%')
            .backgroundColor(SP.card)
            .borderRadius(10)
            .padding(8)
            .margin({ top: 8 })
            .onClick(() => {});
          });
        }
        .width('94%')
        .padding({ bottom: 16 });
      }
      .width('100%');
    }
    .scrollable(ScrollDirection.Vertical)
    .width('100%')
    .height('100%');
  }
}

// ---------- 我的页面 ----------
@Component struct MineContent {
  build() {
    Scroll() {
      Column() {
        Column() {
          Text('👤')
            .fontSize(48)
            .backgroundColor(SP.cardLight)
            .borderRadius(50)
            .width(80)
            .height(80)
            .textAlign(TextAlign.Center);
          Text('未登录')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(SP.text)
            .margin({ top: 8 });
          Text('点击登录')
            .fontSize(12)
            .fontColor(SP.primary)
            .margin({ top: 4 });
        }
        .width('100%')
        .padding(20)
        .backgroundColor(SP.card)
        .borderRadius(12)
        .margin({ top: 16 });

        Column() {
          this.menuItem('📺', '我的收藏');
          this.menuItem('⏳', '观看历史');
          this.menuItem('💬', '我的评论');
          this.menuItem('⚙️', '设置');
        }
        .width('94%')
        .backgroundColor(SP.card)
        .borderRadius(12)
        .padding({ left: 12, right: 12 })
        .margin({ top: 16 });

        Text('— 影音有你,精彩无限 —')
          .fontSize(10)
          .fontColor(SP.textSub)
          .margin({ top: 20, bottom: 16 });
      }
      .width('100%')
      .alignItems(HorizontalAlign.Center);
    }
    .scrollable(ScrollDirection.Vertical)
    .width('100%')
    .height('100%');
  }

  @Builder
  menuItem(icon: string, label: string) {
    Row() {
      Text(icon)
        .fontSize(18)
        .margin({ right: 12 });
      Text(label)
        .fontSize(14)
        .fontColor(SP.text);
      Column().layoutWeight(1);
      Text('>')
        .fontColor(SP.textSub);
    }
    .width('100%')
    .padding({ top: 12, bottom: 12 })
    .border({ width: { bottom: 0.5 }, color: SP.border });
  }
}
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐