一、技术前言

在这里插入图片描述

在移动互联网视频消费持续井喷的当下,影视追剧类应用已经从单纯的内容播放器演变为集内容发现、追剧管理、社交互动、智能辅助于一体的综合性娱乐平台。用户的核心诉求早已超越了"能看视频"这一基本底线,转而追求"追得省心、看得沉浸、管理得高效"三位一体的进阶体验。HarmonyOS ArkUI 框架以其声明式 UI 编程范式、高效的状态驱动渲染引擎以及丰富的系统能力套件(Kit),为构建此类复杂交互场景的应用提供了坚实的技术基座。在 ArkTS 语言体系中,@Entry@Component@State@Builder@Observed 等装饰器协同工作,让开发者能够以接近自然描述的方式声明界面结构与数据流,由框架底层负责高效的差分渲染和状态同步,从而将开发者从繁琐的命令式 DOM 操作中彻底解放出来。

在这里插入图片描述

HarmonyOS 6.1.1 版本的 Speech Kit 引入了一项对影视追剧场景具有里程碑意义的新特性——AICaptionComponent(AI 字幕组件)全面增强了语言配置能力。在这一版本中,AICaptionOptions 接口新增了四大关键字段:sourceLanguage 用于指定字幕识别的源语言,取值范围为 'zh'(中文)和 'en'(英文),决定了 AI 语音识别引擎对输入音频流的语种假设;targetLanguage 用于指定字幕的输出目标语言,取值范围为 'zh''en' 以及 'zh-en'(中英双语对照),当源语言为英文时支持翻译为中文或双语展示,当源语言为中文时则锁定为中文输出,无翻译方向;fontSizeAICaptionFontSize 枚举类型,提供 SMALLNORMALBIGLARGE 四档字幕字号选项,满足不同视力和屏幕距离下的可读性需求;fontColor 则接受 ResourceColor 类型,允许开发者自由设定字幕文字颜色,从经典白到暖阳黄、薄荷绿等多种预设色均可灵活配置。这四大字段共同构成了 AI 字幕服务在外观和语言两个维度上的完整可定制体系。

在这里插入图片描述
AICaptionComponent 的运行机制值得深入理解。该组件通过 AICaptionController 控制器实例与底层 AI 语音识别服务建立通信通道,开发者通过控制器的 writeAudio() 方法向服务持续写入 PCM 格式的音频数据流(AudioData 类型,包含一个 Uint8Array 字节数组字段),AI 引擎在服务端实时进行语音识别和翻译,将生成的字幕文本通过 isShown 双向绑定字段驱动组件渲染到界面。组件的 options 参数接受上述 AICaptionOptions 配置对象,其中 onPrepared 回调在字幕服务初始化完成时触发,onError 回调在服务异常时触发并携带 BusinessError 错误信息,这两个回调为开发者提供了完整的运行时状态感知能力。在本应用中,这一机制被设计为一个完整的交互闭环:用户设置语言和外观参数 → buildCaptionOptions() 方法实时组装配置 → 组件 options 联动重建 → 用户开启字幕并写入演示音频 → 字幕实时渲染到预览区。

在这里插入图片描述
从业务场景来看,"追剧场·影视追剧平台"定位为垂直影视追剧类应用,其设计理念围绕"追剧全生命周期"展开。应用设计了 4 个功能维度各异的 Tab 页面:影库 Tab 以横滑热播海报大卡墙和三列片型宫格入口呈现内容发现能力,支持新建、编辑、删除片单的完整 CRUD 操作;追剧 Tab 以进度条清单和月度观影时长柱状图呈现追剧管理能力,让用户对"在追什么、看到哪里、何时更新"一目了然;AI字幕 Tab 作为本应用的技术亮点页面,完整展示了 Speech Kit 6.1.1 新特性的预览、语言设置、外观设置、实时代码预览和场景推荐五大区块;我的 Tab 以鎏金黄渐变大卡呈现用户 VIP 信息和观影统计,搭配功能清单行提供入口管理。四个 Tab 的布局风格完全不同,避免了视觉同质化。

在这里插入图片描述

在视觉设计层面,本应用采用了影院黑(#101014)作为全局背景色,搭配鎏金黄(#F0B429)作为主强调色,辅以暗金(#C98F14)、卡面灰(#1B1B22)、芯片灰(#262630)等过渡色,构建出一套层次分明、对比鲜明的深色系视觉语言。这种配色方案并非随意选择——影院黑模拟了电影院熄灯后的沉浸环境,能够有效降低视觉疲劳,尤其在夜间追剧场景下体验更佳;鎏金黄则呼应了电影院灯光、胶片金属质感和 VIP 金卡的经典意象,在深色背景上具有极高的视觉识别度和情感温度。此外,应用通过 setInterval 驱动的呼吸动画(breath 状态每秒翻转),让封面海报的透明度、柱状图柱高、进度条颜色、状态指示灯等多个元素产生有节奏的微动效,为静态界面注入了生命力。

在这里插入图片描述
下面,我们将从代码的第一行开始,逐段、逐块地深入分析这个影视追剧平台的完整技术实现。


二、整体架构流程图

弹窗系统

内容区域 Scroll

头部区域

片单 CRUD 方法

AI 字幕核心方法

辅助函数层

数据模型层

颜色与常量层

状态管理层

入口组件

底部导航

tabBar
单排 4 Tab

Page1118
@Entry @Component struct

currentTab: number
当前激活 Tab 索引

breath: boolean
呼吸动画开关

cateIdx: number
头部片型 chips 选中

addModal / editModal / delModal
三弹窗开关

dramaList / followList
sceneList / statList
四组 @State 数据

srcLang / tgtLang
captionSize / captionColor
AI 字幕四大字段

captionShown / captionReady
captionFed / captionErrMsg
字幕运行时状态

ColorPalette 接口
14 色深色主题色板

TAB_LIST / CATE_TAGS
GENRE_GRID / SRC_LANGS
TGT_LANGS_EN / SIZE_OPTIONS
CAPTION_FONT_COLORS
月度图表常量

DramaItem @Observed
影库条目

FollowItem @Observed
追剧条目

CaptionScene @Observed
字幕场景条目

UserStat @Observed
我的页功能条目

genreColor
片型颜色映射

dayColor
更新日颜色映射

sizeName / langName / colorName
枚举转展示名

buildCaptionOptions
组装 AICaptionOptions

switchSourceLang
源语言联动目标语言

feedDemoAudio
生成 PCM 写入音频流

openEditDrama
打开编辑弹窗回填

saveDrama
新建片单保存

updateDrama
编辑保存刷新数组

delDrama
删除片单确认

headerMain
渐变 Banner + 搜索条 + 片型 chips

tabDrama
Tab0 横滑海报大卡 + 片型宫格

tabFollow
Tab1 进度条清单 + 月度柱状图

tabCaption
Tab2 AI 字幕五区块特性页

tabMine
Tab3 VIP 渐变大卡 + 功能清单

chartCard
月度观影柱状图

modalOverlay
全屏遮罩

panelAdd
新建片单弹窗

panelEdit
编辑片单弹窗

panelDel
删除确认弹窗

上图展示了本应用的完整技术架构层次。从上至下,最外层是 @Entry @Component 装饰的入口结构体,它持有全部状态变量和业务方法;中间层分为颜色常量、数据模型、辅助函数三大支撑模块,为 UI 层提供数据契约和工具函数;核心业务方法包含 AI 字幕配置组装、语言联动逻辑、音频流写入、片单增删改查四大功能组;UI 层由头部、四个 Tab 内容区、底部导航和弹窗系统共同构成,通过 currentTab 状态驱动 Tab 切换,通过弹窗开关驱动模态层挂载。


三、颜色系统与主题设计

3.1 ColorPalette 色板接口

/** 主题色板接口:集中声明页面所有颜色字段(影院黑+鎏金黄深色系) */
interface ColorPalette {
  bg: string;
  card: string;
  chip: string;
  title: string;
  sub: string;
  text3: string;
  gold: string;
  goldD: string;
  red: string;
  blue: string;
  green: string;
  line: string;
  tabOn: string;
  mask: string;
}

这段代码定义了 ColorPalette 接口,它是整个应用颜色体系的类型契约。在 ArkTS 的类型系统中,interface 不仅仅用于约束对象的结构,更承担着"设计文档"的角色——任何阅读这份接口定义的开发者,都能在数秒内掌握应用使用的全部颜色维度。

接口中声明了 14 个颜色字段,覆盖了从全局背景(bg)到卡片底色(card)、芯片底色(chip),从主标题色(title)到次级文本色(sub)、三级弱化色(text3),从主强调金色(gold)到暗金渐变色(goldD),以及用于不同语义场景的红/蓝/绿状态色、分割线色(line)、Tab 选中色(tabOn)和弹窗遮罩色(mask)。这种"全量集中声明"的设计模式带来了三个显著优势:第一,主题修改时只需改一个常量对象,全局生效;第二,类型检查能在编译期捕获拼写错误和遗漏;第三,颜色语义化命名让 UI 代码可读性极高——COLORS.gold'#F0B429' 更直观地传达了"这是鎏金黄强调色"的设计意图。

3.2 深色主题色板常量

/** 深色主题色板常量(追剧场 · 影院黑 + 鎏金黄) */
const COLORS: ColorPalette = {
  bg: '#101014',
  card: '#1B1B22',
  chip: '#262630',
  title: '#F5F2EA',
  sub: '#C4BCA8',
  text3: '#7F796A',
  gold: '#F0B429',
  goldD: '#C98F14',
  red: '#FF5A45',
  blue: '#6BA8FF',
  green: '#6ED491',
  line: '#2E2E3A',
  tabOn: '#F0B429',
  mask: 'rgba(6,6,10,0.72)'
};

这是 COLORS 常量的实际赋值,严格遵循 ColorPalette 接口约束。我们可以从中解读出这套深色主题的精心设计逻辑。

背景色 #101014 是一种接近纯黑但略带暖灰调的颜色,比纯黑 #000000 更加柔和,减少了 OLED 屏幕在暗环境下从纯黑到内容色之间的突兀跳跃感。卡片底色 #1B1B22 比背景色亮约 11 个亮度阶,形成"卡片浮于背景"的层次效果;芯片色 #262630 再亮一阶,用于按钮、标签等更内层的交互元素。主标题色 #F5F2EA 并非纯白,而是带有一丝暖象牙色调,与鎏金黄主题色形成色温呼应,避免了冷白与暖金之间的割裂感。次级文本 #C4BCA8 和三级文本 #7F796A 也在暖灰色系中递进,形成了自然的文本层级。

鎏金黄 #F0B429 是整套主题的灵魂色,用于 Tab 选中态、进度百分比、强调按钮、渐变终点等所有需要视觉聚焦的场景;暗金 #C98F14 作为渐变起点和暗态按钮底色,与鎏金黄形成从深到浅的过渡。值得注意的是,弹窗遮罩色使用了 rgba(6,6,10,0.72) 而非纯黑半透明,这个微调让遮罩保留了影院黑的主题温度,避免了冷灰遮罩与暖色卡片的违和感。


四、常量定义与数据规范

4.1 Tab 导航元数据

/** Tab 元数据接口:底部导航图标 + 标签 */
interface TabMeta {
  icon: string;
  label: string;
}

/** 底部导航 Tab 常量列表(4 Tab 单排) */
const TAB_LIST: TabMeta[] = [
  { icon: '🎞', label: '影库' },
  { icon: '📌', label: '追剧' },
  { icon: '🗣', label: 'AI字幕' },
  { icon: '👤', label: '我的' }
];

TabMeta 接口定义了底部导航每一项的数据结构:icon 存储 emoji 图标字符串,label 存储中文标签文本。TAB_LIST 常量数组包含 4 个 Tab 元数据项,分别对应影库、追剧、AI 字幕、我的四个功能页面。

选择 emoji 而非图标字体或 SVG 资源作为 Tab 图标,是一个兼顾开发效率和视觉表现力的务实决策。Emoji 字符在所有平台上都有内置渲染支持,无需引入额外的字体文件或图片资源,有效控制了应用包体积。同时,emoji 的彩色渲染特性让深色主题下的 Tab 栏天然具备了色彩辨识度——选中态通过 fontSize 放大和 fontColor 鎏金黄高亮进一步强化视觉聚焦,未选中态则通过 opacity 降低至 0.65 形成弱化层次。这个 TAB_LIST 数组在底部导航 tabBar() Builder 中被 ForEach 遍历渲染,currentTab 状态与 idx 索引的比较驱动了选中态的样式切换。

4.2 片型分类与宫格入口

/** 头部横滑片型 chips 文案 */
const CATE_TAGS: string[] = ['热播', '悬疑', '古装', '都市', '甜宠', '谍战', '科幻', '喜剧'];

/** 片型宫格入口接口(影库 Tab 三列片型宫格) */
interface GenreEntry {
  icon: string;   // 片型图标
  name: string;   // 片型名
  count: string;  // 片库数量文本
}

/** 片型宫格入口 Mock 数据(6 个) */
const GENRE_GRID: GenreEntry[] = [
  { icon: '🕵', name: '悬疑烧脑', count: '128 部' },
  { icon: '🏮', name: '古装传奇', count: '96 部' },
  { icon: '🌃', name: '都市生活', count: '85 部' },
  { icon: '🍬', name: '甜宠恋爱', count: '72 部' },
  { icon: '🕶', name: '谍战风云', count: '64 部' },
  { icon: '🚀', name: '科幻未来', count: '58 部' }
];

这段代码定义了两组与片型分类相关的常量数据。CATE_TAGS 是一个纯字符串数组,包含 8 个片型标签文案,用于头部横滑 chips 区域。这些 chips 通过 Scroll 组件横向滚动展示,用户点击后 cateIdx 状态更新,选中的 chip 背景变为芯片灰深色、文字变为鎏金黄,形成清晰的选择反馈。这组分类标签覆盖了国内影视剧市场最主流的八大类型,能够满足绝大多数用户的分类筛选需求。

GenreEntry 接口和 GENRE_GRID 数组则用于影库 Tab 的片型宫格入口。每个 GenreEntry 包含三个字段:icon 是 emoji 图标,name 是片型名称(比 chips 中的简短标签更具描述性,如"悬疑烧脑"而非"悬疑"),count 是该类型片库的剧集数量文案。6 个宫格入口通过 FlexSpaceBetween 对齐和 31.5% 宽度实现了三列布局,每个宫格卡片内部纵向排列图标、名称和数量,形成"图标在上、名称在中、数量在下"的信息层级。emoji 图标的透明度受 breath 状态驱动每秒微调,为静态宫格注入了呼吸般的微妙动感。

4.3 字幕语言与样式常量(Speech Kit 6.1.1)

/** 源语言选项(取值范围:['zh','en']) */
interface LangOption {
  code: string;    // 语言码
  name: string;    // 展示名
}

/** 源语言选项列表 */
const SRC_LANGS: LangOption[] = [
  { code: 'zh', name: '中文' },
  { code: 'en', name: '英文' }
];

/** 英文源时的目标语言选项(中文源时目标语言锁定 'zh') */
const TGT_LANGS_EN: LangOption[] = [
  { code: 'zh', name: '中文' },
  { code: 'en', name: '英文' },
  { code: 'zh-en', name: '中英双语' }
];

这段代码定义了 AI 字幕语言配置所需的两套语言选项常量。LangOption 接口将语言码(code)和展示名(name)配对存储,使得 UI 渲染时可以直接使用 name 展示给用户,同时用 code 传递给 AICaptionOptions 配置。

SRC_LANGS 定义了源语言的两个选项:中文 'zh' 和英文 'en',这与 Speech Kit 6.1.1 中 sourceLanguage 字段的取值范围完全对应。源语言的选择决定了 AI 语音识别引擎对输入音频流的语种假设——选择 'zh' 意味着引擎将音频视为中文语音进行识别,选择 'en' 则视为英文语音。TGT_LANGS_EN 则定义了当源语言为英文时可选的目标语言:中文(翻译)、英文(原样输出)、中英双语(对照展示)三种模式。当源语言切换为中文时,目标语言会被 switchSourceLang() 方法锁定为 'zh',因为中文源没有翻译需求。这种"源语言联动目标语言"的设计避免了无效的语言组合(如"中文源→英文目标"),提升了配置的合理性和用户体验。

/** 字号选项接口(AICaptionFontSize 枚举四档) */
interface SizeOption {
  size: AICaptionFontSize;  // 字号枚举值
  name: string;             // 展示名
}

/** 字幕字号四档选项 */
const SIZE_OPTIONS: SizeOption[] = [
  { size: AICaptionFontSize.SMALL, name: '小号' },
  { size: AICaptionFontSize.NORMAL, name: '标准' },
  { size: AICaptionFontSize.BIG, name: '大号' },
  { size: AICaptionFontSize.LARGE, name: '超大' }
];

/** 字幕字体颜色预设(fontColor 为 ResourceColor) */
const CAPTION_FONT_COLORS: string[] = ['#FFFFFF', '#FFE9B0', '#9CE8B5', '#9CD0FF', '#FFB3C1'];

这部分代码定义了 AI 字幕外观配置所需的字号和颜色常量。SizeOption 接口将 AICaptionFontSize 枚举值与中文展示名配对,SIZE_OPTIONS 数组包含 SMALLNORMALBIGLARGE 四个枚举值,分别对应小号、标准、大号、超大四档字幕字号。这四档字号对应了从手机近距离观看到远距离投屏观看的不同视觉需求,用户可以根据实际场景灵活选择。

CAPTION_FONT_COLORS 数组定义了 5 个预设字幕颜色:经典白 #FFFFFF(最通用的字幕色,适合大多数深色视频背景)、暖阳黄 #FFE9B0(暖色调,与鎏金黄主题呼应,适合古装剧和文艺片)、薄荷绿 #9CE8B5(清新色调,适合轻松追剧场景)、云朵蓝 #9CD0FF(冷色调,适合科幻剧和科技感场景)、樱花粉 #FFB3C1(柔和色调,适合甜宠剧和浪漫场景)。这五种颜色的选取并非随意——它们在深色视频背景上都能保持足够的对比度和可读性,同时又各自带有不同的情感色彩,让字幕本身也能成为观影氛围的一部分。fontColor 字段接受 ResourceColor 类型,这些十六进制字符串值可以直接作为颜色值传入。

4.4 月度观影数据常量

/** 月度观影时长柱状图月份索引 */
const MONTH_IDX: number[] = [0, 1, 2, 3, 4, 5];
/** 月度柱状图月份名称 */
const MONTH_LABELS: string[] = ['3月', '4月', '5月', '6月', '7月', '8月'];
/** 月度观影时长数值(小时) */
const MONTH_HOURS: number[] = [42, 56, 48, 64, 78, 86];
/** 月度柱状图最大值(小时) */
const MONTH_MAX: number = 100;

这段代码定义了追剧 Tab 中月度观影时长柱状图所需的四组常量数据。MONTH_IDX 是月份索引数组,用于 ForEach 的遍历键值;MONTH_LABELS 是月份名称数组,作为柱状图 X 轴标签;MONTH_HOURS 是各月份的观影时长数值(单位:小时),从 3 月的 42 小时到 8 月的 86 小时,呈现明显的逐月增长趋势,暗示用户追剧热度持续攀升;MONTH_MAX 是柱状图的满刻度值 100 小时,用于计算柱高比例。

这四组常量通过索引关联——MONTH_HOURS[i] 对应 MONTH_LABELS[i] 的月份。在 chartCard() Builder 中,ForEach 遍历 MONTH_IDX,通过索引 iMONTH_HOURSMONTH_LABELS 中取出对应数据,计算柱高 Math.max(20, MONTH_HOURS[i] / MONTH_MAX * 110 * (this.breath ? 1.05 : 0.95))。其中 breath 状态在每秒翻转时让柱高在 0.95 倍和 1.05 倍之间微调,形成"柱状图呼吸"的动效,Math.max(20, ...) 保证即使最小值也有 20px 的可见柱高。


五、辅助函数体系

5.1 片型颜色映射

/** 片型颜色映射:悬疑甜宠红 / 古装喜剧金 / 谍战科幻蓝 / 都市绿 */
function genreColor(g: string): string {
  if (g === '悬疑' || g === '甜宠') { return COLORS.red; }
  if (g === '古装' || g === '喜剧') { return COLORS.gold; }
  if (g === '谍战' || g === '科幻') { return COLORS.blue; }
  if (g === '都市') { return COLORS.green; }
  return COLORS.sub;
}

genreColor 函数实现了片型到主题色的语义映射逻辑。函数接收一个片型字符串参数 g,返回对应的颜色值。映射规则遵循色彩心理学原理:悬疑和甜宠类映射为红色 COLORS.red,红色在视觉心理中关联紧张、刺激和热情,与悬疑剧的悬念感和甜宠剧的心动感相契合;古装和喜剧类映射为鎏金黄 COLORS.gold,金色暗示历史厚重感和欢乐氛围;谍战和科幻类映射为蓝色 COLORS.blue,蓝色关联冷静、理性和科技感;都市类映射为绿色 COLORS.green,绿色暗示生活气息和自然感。当传入未识别的片型时,返回次级文本色 COLORS.sub 作为兜底。

这个函数在影库 Tab 的横滑海报大卡中被调用——每个海报底部的类型角标 Text(item.genre)fontColor 通过 genreColor(item.genre) 获取颜色值,使得不同类型的剧集在视觉上即可被快速区分。这种"语义化颜色映射"的设计模式让颜色不再是静态的装饰,而是承载了信息编码的功能。

5.2 更新日颜色映射

/** 更新日颜色映射:今晚更新金 / 已完结弱化 / 周更蓝 */
function dayColor(day: string): string {
  if (day === '今晚更新') { return COLORS.gold; }
  if (day === '已完结') { return COLORS.text3; }
  return COLORS.blue;
}

dayColor 函数实现了追剧更新日到颜色的映射。函数接收更新日字符串参数 day,返回对应的颜色值。"今晚更新"映射为鎏金黄 COLORS.gold,因为今晚即将更新的剧集是最需要用户关注的,金色高亮形成视觉优先级提醒;“已完结"映射为三级弱化色 COLORS.text3,因为已完结的剧集不再需要追更提醒,弱化处理降低了视觉干扰;其他常规更新日(如"周三更新”、“周四更新”)映射为蓝色 COLORS.blue,表示信息性提醒。

这个函数在追剧 Tab 的进度清单中被调用——每条追剧记录的更新日胶囊标签 Text(item.day)fontColor 通过 dayColor(item.day) 获取颜色值。配合 COLORS.chip 背景色和圆角胶囊样式,形成了清晰的信息层级:金色最醒目(今晚必看)、蓝色次之(常规追更)、灰色最弱(已完结归档)。

5.3 字幕枚举与语言转换函数

/** 字号枚举转展示名(options 代码预览用) */
function sizeName(size: AICaptionFontSize): string {
  if (size === AICaptionFontSize.SMALL) { return 'SMALL'; }
  if (size === AICaptionFontSize.BIG) { return 'BIG'; }
  if (size === AICaptionFontSize.LARGE) { return 'LARGE'; }
  return 'NORMAL';
}

/** 语言码转展示名(如 'zh-en' → '中英双语') */
function langName(code: string): string {
  if (code === 'zh') { return '中文'; }
  if (code === 'en') { return '英文'; }
  return '中英双语';
}

/** 字幕颜色预设转中文名(按 CAPTION_FONT_COLORS 下标顺序) */
function colorName(c: string): string {
  if (c === CAPTION_FONT_COLORS[0]) { return '经典白'; }
  if (c === CAPTION_FONT_COLORS[1]) { return '暖阳黄'; }
  if (c === CAPTION_FONT_COLORS[2]) { return '薄荷绿'; }
  if (c === CAPTION_FONT_COLORS[3]) { return '云朵蓝'; }
  return '樱花粉';
}

这三个辅助函数共同服务于 AI 字幕 Tab 的信息展示需求。sizeName 函数将 AICaptionFontSize 枚举值转换为对应的英文字符串,这个转换并非用于直接展示给用户(用户看到的是 SIZE_OPTIONS 中的中文名),而是用于"options 实时代码预览卡"区块中以代码语法形式展示当前配置——例如当用户选择"大号"字号时,预览卡中会显示 fontSize: BIG 而非 fontSize: 大号,因为 BIG 才是 ArkTS 代码中实际使用的枚举标识符。这种"代码预览"设计让用户能直观理解自己的设置如何映射到实际代码。

langName 函数将语言码转换为中文名,用于"当前语言组合摘要"行和场景推荐列表中的语言组合展示。当语言码为 'zh-en' 时返回"中英双语",为 'zh' 返回"中文",为 'en' 返回"英文"。colorName 函数将颜色十六进制值转换为诗意化的中文名——经典白、暖阳黄、薄荷绿、云朵蓝、樱花粉,这些名称不仅描述了颜色本身,还赋予了情感色彩,让用户在选择字幕颜色时不仅是在做技术配置,更像是在选择一种观影氛围。


六、数据模型层

6.1 DramaItem 影库条目模型

/** 影库条目(影库 Tab 横滑热播海报大卡) */
@Observed export class DramaItem {
  title: string;     // 剧名
  cover: string;     // emoji 海报
  genre: string;     // 类型(如"悬疑")
  score: string;      // 评分文本(如"9.2")
  episodes: string;  // 集数文本(如"32 集")
  heat: string;      // 热度文本(如"386万")

  constructor(title: string, cover: string, genre: string, score: string, episodes: string, heat: string) {
    this.title = title;
    this.cover = cover;
    this.genre = genre;
    this.score = score;
    this.episodes = episodes;
    this.heat = heat;
  }
}

DramaItem 是影库 Tab 的核心数据模型,使用 @Observed 装饰器声明。@Observed 是 ArkUI 状态管理框架中的重要装饰器,它使得被装饰类的实例属性变化能够被框架感知并驱动 UI 更新——当 dramaList 数组中某个 DramaItem 实例的 titlegenre 属性被修改时,引用该属性的 UI 组件会自动刷新。

模型包含 6 个字段:title 是剧名字符串;cover 是 emoji 海报图标(使用 emoji 模拟真实海报图片,降低资源依赖);genre 是类型标签(如"悬疑"、“古装”),用于传递给 genreColor() 函数获取语义色;score 是评分文本字符串(如"9.2");episodes 是集数文本(如"32 集");heat 是热度文本(如"386万")。构造函数按位赋值,确保每个实例在创建时所有字段都有明确的初始值。export 关键字使该类可以在模块外被引用,方便代码拆分和复用。

/** 本周热播片库 Mock 数据(8 条) */
const DRAMA_LIST: Array<DramaItem> = [
  new DramaItem('雾岛回声', '🌫', '悬疑', '9.2', '32 集', '386万'),
  new DramaItem('长安十二烛影', '🏮', '古装', '8.9', '40 集', '312万'),
  new DramaItem('深水警报', '🌊', '谍战', '8.7', '36 集', '268万'),
  new DramaItem('甜度超标', '🍬', '甜宠', '8.4', '24 集', '241万'),
  new DramaItem('霓虹刑侦录', '🌃', '悬疑', '8.8', '28 集', '226万'),
  new DramaItem('急诊室六点钟', '🏥', '都市', '8.3', '44 集', '198万'),
  new DramaItem('火星直播间', '🚀', '科幻', '8.6', '20 集', '176万'),
  new DramaItem('巷口食堂', '🍜', '喜剧', '8.5', '30 集', '154万')
];

DRAMA_LIST 是 8 条影库 Mock 数据,覆盖了悬疑、古装、谍战、甜宠、都市、科幻、喜剧 7 种类型。每条数据通过 DramaItem 构造函数创建,剧名均为虚构但富有画面感的名称(如"雾岛回声"、“长安十二烛影”),评分从 8.3 到 9.2 分布合理,热度从 154 万到 386 万递减排列,模拟了热播榜的自然排序。这组数据在组件中被赋值给 @State dramaList,作为影库 Tab 横滑海报大卡的数据源,同时支持新建、编辑、删除等 CRUD 操作的动态修改。

6.2 FollowItem 追剧条目模型

/** 追剧条目(追剧 Tab 进度条清单) */
@Observed export class FollowItem {
  title: string;    // 剧名
  cover: string;    // emoji 封面
  seen: string;     // 已看到集数文本(如"看到 24 集")
  total: string;    // 总集数文本
  percent: number;  // 追剧进度 0~100
  day: string;      // 更新日(如"周三更新")

  constructor(title: string, cover: string, seen: string, total: string, percent: number, day: string) {
    this.title = title;
    this.cover = cover;
    this.seen = seen;
    this.total = total;
    this.percent = percent;
    this.day = day;
  }
}

FollowItem 是追剧 Tab 的核心数据模型,同样使用 @Observed 装饰器。模型包含 6 个字段:titlecoverDramaItem 一致;seen 是已观看集数文案(如"看到 24 集");total 是总集数文本(如"32 集");percent 是追剧进度百分比(0~100 的数值类型,用于驱动 Progress 组件的进度条渲染);day 是更新日文案(如"周三更新"、“今晚更新”、“已完结”),传递给 dayColor() 函数获取语义色。

percent 字段是数值类型 number,这是与 DramaItem 中全字符串字段的一个关键区别——因为 Progress 组件的 value 参数需要数值类型来驱动进度条宽度。当 percent 为 100 时表示已追完,进度条满格;为 22 时表示刚起步,进度条仅填充约五分之一。这个数值字段的存在使得追剧进度能够以直观的视觉形式呈现,比纯文本描述(如"看到 8/36 集")更加一目了然。

/** 我的追剧清单 Mock 数据(8 条) */
const FOLLOW_LIST: Array<FollowItem> = [
  new FollowItem('雾岛回声', '🌫', '看到 24 集', '32 集', 75, '周三更新'),
  new FollowItem('长安十二烛影', '🏮', '看到 31 集', '40 集', 78, '已完结'),
  new FollowItem('深水警报', '🌊', '看到 8 集', '36 集', 22, '周四更新'),
  new FollowItem('甜度超标', '🍬', '看到 19 集', '24 集', 79, '今晚更新'),
  new FollowItem('火星直播间', '🚀', '看到 20 集', '20 集', 100, '已完结'),
  new FollowItem('霓虹刑侦录', '🌃', '看到 12 集', '28 集', 43, '周五更新'),
  new FollowItem('巷口食堂', '🍜', '看到 5 集', '30 集', 17, '日更'),
  new FollowItem('急诊室六点钟', '🏥', '看到 36 集', '44 集', 82, '周一更新')
];

FOLLOW_LIST 包含 8 条追剧 Mock 数据,与 DRAMA_LIST 共享了部分剧名和封面 emoji(如"雾岛回声"、“长安十二烛影"等),模拟了"同一部剧在影库和追剧两个 Tab 中都存在"的真实场景。进度值从 17% 到 100% 分布,更新日覆盖了"今晚更新”、“周三更新”、“周四更新”、“周五更新”、“周一更新”、"日更"和"已完结"等多种状态,全面展示了追剧进度清单在不同更新状态下的视觉表现。

6.3 CaptionScene 字幕场景模型

/** 字幕场景条目(AI字幕 Tab 场景推荐列表) */
@Observed export class CaptionScene {
  scene: string;   // 场景名
  desc: string;    // 场景说明
  src: string;     // 推荐源语言
  tgt: string;     // 推荐目标语言

  constructor(scene: string, desc: string, src: string, tgt: string) {
    this.scene = scene;
    this.desc = desc;
    this.src = src;
    this.tgt = tgt;
  }
}

CaptionScene 是 AI 字幕 Tab 场景推荐列表的数据模型。模型包含 4 个字段:scene 是场景名称(如"生肉美剧速看");desc 是场景说明文案(如"无字幕资源 AI 补中文字幕");src 是该场景推荐的源语言码;tgt 是推荐的目标语言码。后两个字段使得用户点击"套用"按钮时,能够一键将场景预设的语言组合应用到当前 AI 字幕配置中。

/** 字幕场景 Mock 数据(5 条:影视追剧行业相关) */
const SCENE_LIST: Array<CaptionScene> = [
  new CaptionScene('生肉美剧速看', '无字幕资源 AI 补中文字幕', 'en', 'zh'),
  new CaptionScene('双语台词学习', '原文译文对照练台词', 'en', 'zh-en'),
  new CaptionScene('国产剧速记', '静音追剧也能看懂剧情', 'zh', 'zh'),
  new CaptionScene('英语磨耳朵', '纯英文字幕沉浸式看剧', 'en', 'en'),
  new CaptionScene('纪录片精读', '旁白原文译文对照理解', 'en', 'zh')
];

SCENE_LIST 包含 5 条字幕场景 Mock 数据,每条都对应一个真实的影视追剧使用场景。"生肉美剧速看"针对无字幕的英文原声资源,AI 自动补中文字幕;"双语台词学习"适合英语学习者,中英对照展示;"国产剧速记"用于静音环境下追剧看字幕;"英语磨耳朵"适合纯英文字幕的沉浸式英语学习;"纪录片精读"针对英文旁白纪录片的中英对照理解。这 5 个场景覆盖了影视追剧 + 语言学习的交叉领域,与"追剧场"应用定位高度吻合。

6.4 UserStat 用户功能清单模型

/** 我的页功能清单条目 */
@Observed export class UserStat {
  icon: string;    // 功能图标
  label: string;   // 功能名
  value: string;   // 状态/数值文本
  arrow: boolean;  // 是否显示右箭头

  constructor(icon: string, label: string, value: string, arrow: boolean) {
    this.icon = icon;
    this.label = label;
    this.value = value;
    this.arrow = arrow;
  }
}

UserStat 是我的 Tab 功能清单行的数据模型。4 个字段中,icon 是功能图标 emoji,label 是功能名称(如"我的片单"、“缓存剧集”),value 是状态或数值文案(如"12 个"、“36 集 · 1080P 高清”),arrow 是布尔值控制是否显示右侧箭头指示符 arrow 字段的设计使得某些纯信息性行可以不显示箭头(避免误导用户以为可点击跳转),而功能性入口行则显示箭头引导点击。

/** 我的页功能清单 Mock 数据(8 条) */
const STAT_LIST: Array<UserStat> = [
  new UserStat('🎞', '我的片单', '12 个', true),
  new UserStat('⬇', '缓存剧集', '36 集 · 1080P 高清', true),
  new UserStat('🕘', '观看历史', '89 条', true),
  new UserStat('❤', '想看清单', '48 部', true),
  new UserStat('🔔', '更新提醒', '每晚 19:30 推送', true),
  new UserStat('🗣', 'AI 字幕偏好', '源 en · 目标 zh', true),
  new UserStat('🎁', '追剧 VIP 特权', '2026-11-18 到期', true),
  new UserStat('⚙', '播放与画质设置', '4K 超清 · 杜比音效', true)
];

STAT_LIST 包含 8 条功能清单 Mock 数据,覆盖了片单管理、缓存、历史、想看、提醒、字幕偏好、VIP 和播放设置等影视追剧应用的核心功能入口。值得注意的是"AI 字幕偏好"一项的 value 为"源 en · 目标 zh",这与 AI 字幕 Tab 中的语言配置形成了跨 Tab 的信息呼应——用户在 AI 字幕 Tab 设置的语言偏好,在"我的"页也能看到摘要展示,体现了应用内部状态的一致性设计。


七、组件主体与状态管理

7.1 组件声明与基础状态变量

/** 1118 追剧场 · 影视追剧平台主页面 */
@Entry
@Component
struct Page1118 {
  /** 当前选中 Tab 索引 */
  @State currentTab: number = 0;
  /** 呼吸动画开关(每秒翻转,联动柱状图与封面) */
  @State breath: boolean = false;
  /** 呼吸动画定时器句柄 */
  @State timer: number = -1;
  /** 头部片型 chips 选中索引 */
  @State cateIdx: number = 0;
  /** 新建片单弹窗开关 */
  @State addModal: boolean = false;
  /** 编辑片单弹窗开关 */
  @State editModal: boolean = false;
  /** 删除片单确认弹窗开关 */
  @State delModal: boolean = false;
  /** 当前编辑的片单索引 */
  @State editIdx: number = 0;
  /** 当前删除的片单索引 */
  @State delIdx: number = 0;
  /** 影库热播数据 */
  @State dramaList: Array<DramaItem> = DRAMA_LIST;
  /** 追剧进度清单数据 */
  @State followList: Array<FollowItem> = FOLLOW_LIST;
  /** 字幕场景列表数据 */
  @State sceneList: Array<CaptionScene> = SCENE_LIST;
  /** 我的页功能清单数据 */
  @State statList: Array<UserStat> = STAT_LIST;
  /** 新建表单:片单名称 */
  @State formName: string = '';
  /** 新建表单:片单类型 */
  @State formTag: string = '';
  /** 编辑表单:片单名称 */
  @State editName: string = '';
  /** 编辑表单:片单类型 */
  @State editTag: string = '';

这段代码声明了应用的主入口组件 Page1118,使用 @Entry@Component 两个装饰器——@Entry 标记该组件为页面入口,@Component 声明它是一个自定义组件。struct 关键字是 ArkTS 特有的结构体声明方式,在 ArkUI 中 struct 等价于组件类。

状态变量使用 @State 装饰器声明,共分为四组:第一组是 UI 导航状态,包括 currentTab(当前 Tab 索引,0~3)、cateIdx(头部 chips 选中索引);第二组是弹窗状态,包括 addModaleditModaldelModal 三个布尔开关和 editIdxdelIdx 两个操作索引;第三组是数据源状态,包括 dramaListfollowListsceneListstatList 四组数组,初始化时分别引用对应的 Mock 常量;第四组是表单状态,包括 formNameformTag(新建表单)和 editNameeditTag(编辑表单)。

@State 装饰器的核心作用是建立状态与 UI 的响应式绑定——当 currentTab 从 0 变为 2 时,build() 方法中的 if-else 条件分支会重新执行,渲染 AI 字幕 Tab 的内容;当 addModalfalse 变为 true 时,Stack 中的条件渲染分支会挂载 panelAdd 弹窗。breath 状态由 setInterval 每秒翻转,驱动了多处微动效:海报透明度、柱状图柱高、进度条颜色、状态指示灯等,形成了全局统一的呼吸节奏。timer 存储定时器句柄用于 aboutToDisappear 时清理,防止内存泄漏。

7.2 AI 字幕控制器与状态

  // --- AI 字幕状态(6.1.1 特性:源语言/目标语言/字体大小/字体颜色) ---
  /** 字幕控制器(writeAudio 写入音频流) */
  private captionController: AICaptionController = new AICaptionController();
  /** 字幕显示状态(@Link 双向绑定到 AICaptionComponent.isShown) */
  @State captionShown: boolean = false;
  /** sourceLanguage:字幕源语言 */
  @State srcLang: string = 'zh';
  /** targetLanguage:字幕目标语言 */
  @State tgtLang: string = 'zh';
  /** fontSize:字体大小(AICaptionFontSize 枚举) */
  @State captionSize: AICaptionFontSize = AICaptionFontSize.NORMAL;
  /** fontColor:字体颜色(默认经典白,取自预设数组) */
  @State captionColor: string = CAPTION_FONT_COLORS[0];
  /** onPrepared 回调置 true(字幕服务就绪) */
  @State captionReady: boolean = false;
  /** onError 错误信息 */
  @State captionErrMsg: string = '';
  /** 已写入音频块计数 */
  @State captionFed: number = 0;

这段代码声明了 AI 字幕功能所需的全部状态变量。首先是 captionController,它是 AICaptionController 类的实例,声明为 private(组件内部使用,不对外暴露)。AICaptionController 是 Speech Kit 提供的字幕控制器,负责与底层 AI 语音识别服务建立通信通道,其 writeAudio() 方法接收 AudioData 类型的音频数据块,将音频流持续推送给识别引擎。

captionShown 是字幕显示开关,与 AICaptionComponentisShown 参数形成双向绑定——当 captionShowntrue 时字幕组件可见,为 false 时隐藏。srcLangtgtLang 分别对应 AICaptionOptionssourceLanguagetargetLanguage 字段,初始值均为 'zh'(中文源→中文目标)。captionSizeAICaptionFontSize 枚举类型,初始值为 NORMAL(标准字号)。captionColor 是字幕颜色字符串,初始值取自 CAPTION_FONT_COLORS[0](即经典白 #FFFFFF)。

captionReady 是服务就绪标志,由 onPrepared 回调置为 true,用于驱动预览卡中"已就绪/初始化中"状态标签的显示。captionErrMsg 存储 onError 回调返回的错误信息,当不为空字符串时在预览卡底部显示错误提示行。captionFed 是已写入音频块的计数器,每次 feedDemoAudio() 成功调用后自增,在预览卡的"写入演示音频"按钮旁以 ×N 形式展示写入次数。这 9 个状态变量共同构成了 AI 字幕功能的完整运行时状态管理体系。

7.3 buildCaptionOptions 方法

  /** 组装 AICaptionOptions:体现 6.1.1 新增的源语言/目标语言/字体大小/字体颜色 */
  buildCaptionOptions(): AICaptionOptions {
    const opts: AICaptionOptions = {
      initialOpacity: 1,
      sourceLanguage: this.srcLang,        // ★ 6.1.1:字幕源语言('zh' | 'en')
      targetLanguage: this.tgtLang,        // ★ 6.1.1:字幕目标语言('zh' | 'en' | 'zh-en')
      fontSize: this.captionSize,          // ★ 6.1.1:字体大小(AICaptionFontSize 枚举)
      fontColor: this.captionColor,        // ★ 6.1.1:字体颜色(ResourceColor)
      onPrepared: () => {
        this.captionReady = true;
        this.captionErrMsg = '';
      },
      onError: (error: BusinessError) => {
        this.captionErrMsg = '字幕服务异常 ' + error.code + ':' + error.message;
      }
    };
    return opts;
  }

buildCaptionOptions 是 AI 字幕功能的核心方法,负责将组件当前的语言和外观状态组装为 AICaptionOptions 配置对象。该方法每次被调用时都会读取最新的 srcLangtgtLangcaptionSizecaptionColor 状态值,确保配置对象始终与用户的最新设置同步。

配置对象包含 7 个字段:initialOpacity 设为 1,表示字幕组件初始不透明度(完全可见);sourceLanguage 映射 this.srcLang,取值为 'zh''en'targetLanguage 映射 this.tgtLang,取值为 'zh''en''zh-en'fontSize 映射 this.captionSize,取值为 AICaptionFontSize 枚举的四档之一;fontColor 映射 this.captionColor,取值为颜色十六进制字符串。这四个字段正是 HarmonyOS 6.1.1 版本新增的核心配置能力。

两个回调函数实现了运行时状态感知闭环:onPrepared 在字幕服务初始化完成时触发,将 captionReady 置为 true 并清空错误信息,驱动预览卡的状态标签从"初始化中"变为"已就绪";onError 在服务异常时触发,接收 BusinessError 类型的错误对象,将其 codemessage 拼接为错误信息字符串存入 captionErrMsg,驱动预览卡底部显示红色错误提示。这个方法在 tabCaption() Builder 的 AICaptionComponent 组件构造中被调用,每次状态变化时都会重新执行,保证配置的实时性。

7.4 switchSourceLang 语言联动

  /** 切换源语言时联动目标语言(中文源时目标语言仅支持 'zh') */
  switchSourceLang(code: string) {
    this.srcLang = code;
    if (code === 'zh') {
      this.tgtLang = 'zh';        // 中文源:无翻译方向,锁定中文
    } else {
      this.tgtLang = 'zh-en';     // 英文源:默认双语,可再选 zh/en
    }
  }

switchSourceLang 方法处理源语言切换时的目标语言联动逻辑。当用户选择源语言为中文 'zh' 时,目标语言被锁定为 'zh'——因为中文语音识别后已经是中文文本,不存在翻译需求,Speech Kit 也不支持"中文→英文"的翻译方向。当用户选择源语言为英文 'en' 时,目标语言默认设为 'zh-en'(中英双语),因为英文源最常见的追剧需求是"既看英文原文字幕又看中文翻译",双语模式是最佳默认值,用户随后还可以手动切换为纯中文或纯英文。

这种联动设计避免了无效的语言组合,是典型的"防御性编程"思维——与其让用户先选错组合再收到错误提示,不如在源头就消除错误可能。方法在两个场景下被调用:一是在语言设置卡中用户点击源语言选项时,二是在场景推荐列表中用户点击"套用"按钮时(场景预设的 src 值被传入),两条路径共享同一套联动逻辑,保证了状态一致性。

7.5 feedDemoAudio 音频写入

  /** 演示写入音频流:生成 640 字节 PCM 块(16kHz/16bit/单声道 ≈ 20ms)调用 writeAudio */
  feedDemoAudio() {
    const block = new Uint8Array(640);
    for (let i = 0; i < 640; i += 2) {
      const t = (i / 2) / 16000;
      const v = Math.round(Math.sin(2 * Math.PI * 440 * t) * 6000);
      block[i] = v & 0xFF;
      block[i + 1] = (v >> 8) & 0xFF;
    }
    try {
      const audioData: AudioData = { data: block };
      this.captionController.writeAudio(audioData);
      this.captionFed++;
    } catch (e) {
      this.captionErrMsg = '音频写入失败';
    }
  }

feedDemoAudio 方法生成一个模拟的 PCM 音频数据块并写入字幕控制器。方法首先创建一个 640 字节的 Uint8Array,然后以 2 字节为单位(16bit 采样深度)填充正弦波数据。采样率设计为 16kHz(16000Hz),因此 640 字节包含 320 个采样点,时长约为 20 毫秒。波形频率为 440Hz(标准 A4 音高),振幅为 6000(16bit 范围 -32768~32767 内的适中值)。每个采样点的值通过 Math.sin(2 * Math.PI * 440 * t) * 6000 计算,然后以小端序拆分为低字节和高字节分别存入 block[i]block[i+1]

生成的 PCM 块包装为 AudioData 对象({ data: block }),通过 captionController.writeAudio() 方法写入。try-catch 块处理可能发生的写入异常——如果字幕服务未就绪或写入通道已关闭,writeAudio 可能抛出异常,此时将错误信息设为"音频写入失败"并展示在预览卡底部。成功写入后 captionFed 计数器自增,驱动按钮旁的 ×N 计数显示。这个方法虽然生成的是纯音而非语音,但在演示和测试场景下足以验证 writeAudio 接口的调用流程和音频通道的连通性。在实际生产环境中,开发者需要从麦克风采集或从视频音轨提取真实的 PCM 语音数据来替换这段演示代码。

7.6 片单增删改查方法

  /** 打开编辑片单弹窗(回填当前剧名与类型) */
  openEditDrama(idx: number) {
    this.editIdx = idx;
    this.editName = this.dramaList[idx].title;
    this.editTag = this.dramaList[idx].genre;
    this.editModal = true;
  }

  /** 保存新建片单(空字段用默认值兜底) */
  saveDrama() {
    const name = this.formName === '' ? '未命名片单' : this.formName;
    const tag = this.formTag === '' ? '自建' : this.formTag;
    this.dramaList.push(new DramaItem(name, '🎬', tag, '8.0', '更新中', '新入库'));
    this.formName = '';
    this.formTag = '';
    this.addModal = false;
  }

  /** 保存编辑片单(整体刷新数组引用以刷新横滑大卡墙) */
  updateDrama() {
    if (this.editIdx >= 0 && this.editIdx < this.dramaList.length) {
      if (this.editName !== '') {
        this.dramaList[this.editIdx].title = this.editName;
      }
      if (this.editTag !== '') {
        this.dramaList[this.editIdx].genre = this.editTag;
      }
      this.dramaList = this.dramaList.slice();
    }
    this.editModal = false;
  }

  /** 删除片单(确认弹窗回调) */
  delDrama() {
    if (this.delIdx >= 0 && this.delIdx < this.dramaList.length) {
      this.dramaList.splice(this.delIdx, 1);
    }
    this.delModal = false;
  }

这四个方法共同构成了影库片单的完整 CRUD 操作链。openEditDrama 负责打开编辑弹窗前的数据回填——将指定索引的片单 titlegenre 赋值给编辑表单状态变量 editNameeditTag,然后打开 editModal。这种"先回填后打开"的设计确保了用户在弹窗中看到的初始值就是当前片单的实际数据,避免了空白表单带来的困惑。

saveDrama 处理新建片单的保存逻辑,对空字段进行默认值兜底(formName 为空时用"未命名片单",formTag 为空时用"自建"),然后通过 push 方法向 dramaList 数组追加新的 DramaItem 实例。新建的片单封面固定为 🎬 emoji,评分默认"8.0",集数默认"更新中",热度默认"新入库"。保存后清空表单并关闭弹窗。

updateDrama 处理编辑保存,采用了一种关键的 ArkUI 响应式触发技巧——在修改了数组元素的属性后,执行 this.dramaList = this.dramaList.slice() 创建一个新数组引用。这是因为 ArkUI 的 @State 对数组引用变化的检测比对数组元素属性变化的检测更可靠,通过 slice() 创建新引用可以确保 ForEach 重新渲染整个列表。delDrama 通过 splice 方法从数组中删除指定索引的元素,splice 方法会原地修改数组并触发 @State 的变化检测。

7.7 生命周期管理

  /** 生命周期:启动呼吸动画定时器(每 1000ms 翻转 breath) */
  aboutToAppear() {
    this.timer = setInterval(() => {
      this.breath = !this.breath;
    }, 1000);
  }

  /** 生命周期:销毁时清理定时器 */
  aboutToDisappear() {
    clearInterval(this.timer);
  }

这段代码展示了 ArkUI 组件生命周期的标准用法。aboutToAppear 是组件创建后、build() 执行前的生命周期回调,在此启动 setInterval 定时器,每 1000 毫秒翻转 breath 布尔值。由于 breath@State 装饰,每次翻转都会触发依赖该状态的 UI 组件重新渲染——海报透明度在 1.0 和 0.85 之间切换、柱状图柱高在 0.95 倍和 1.05 倍之间微调、进度条颜色在鎏金黄和暗金之间切换、状态指示灯透明度脉动,形成全局统一的呼吸节奏。

aboutToDisappear 是组件销毁前的生命周期回调,在此调用 clearInterval(this.timer) 清理定时器。这一步至关重要——如果组件销毁时未清理定时器,定时器会在后台持续运行,不断尝试修改已销毁组件的状态,导致内存泄漏和潜在的运行时错误。这种"创建-销毁"成对管理的模式是 ArkUI 动画编程的最佳实践。


八、页面构建与布局体系

8.1 build 主构建方法

  /** 页面主构建:Stack 包裹主内容与三层弹窗 */
  build() {
    Stack() {
      Column() {
        this.headerMain()
        Divider().strokeWidth(1).color(COLORS.line)
        Scroll() {
          Column() {
            if (this.currentTab === 0) {
              this.tabDrama()
            } else if (this.currentTab === 1) {
              this.tabFollow()
            } else if (this.currentTab === 2) {
              this.tabCaption()
            } else {
              this.tabMine()
            }
          }
          .padding({ left: 14, right: 14, top: 12, bottom: 12 })
        }
        .layoutWeight(1)
        .scrollBar(BarState.Off)
        this.tabBar()
      }
      .width('100%')
      .height('100%')

      if (this.addModal) {
        this.panelAdd(() => {
          this.addModal = false;
        })
      }
      if (this.editModal) {
        this.panelEdit(() => {
          this.editModal = false;
        })
      }
      if (this.delModal) {
        this.panelDel(() => {
          this.delModal = false;
        })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor(COLORS.bg)
    .alignContent(Alignment.Center)
  }

build() 是组件的构建方法,定义了整个页面的 DOM 结构。最外层是 Stack 容器,它允许子元素层叠排列——主内容 Column 在底层,三个弹窗面板按条件挂载在上层。StackalignContent(Alignment.Center) 让弹窗面板居中显示。

主内容 Column 从上到下依次排列:headerMain() 头部区域、Divider 分割线、Scroll 可滚动内容区、tabBar() 底部导航。Scroll 组件占据 layoutWeight(1) 的剩余空间,内部是一个 Column 容器,根据 currentTab 的值条件渲染四个 Tab Builder 之一。scrollBar(BarState.Off) 隐藏了滚动条,保持深色主题的简洁视觉。内容区设置了 14px 的左右内边距和 12px 的上下内边距,确保内容与屏幕边缘保持舒适间距。

Stack 的上层是三个条件渲染的弹窗面板,每个弹窗通过对应的 @State 布尔值控制挂载与卸载。panelAddpanelEditpanelDel 都接收一个 onClose 回调函数作为参数,在弹窗内部点击遮罩或取消按钮时调用该回调关闭弹窗。这种"回调参数"的设计模式实现了弹窗关闭逻辑的解耦——弹窗 Builder 本身不需要知道关闭后要做什么,只需调用传入的回调即可。

8.2 headerMain 头部区域

  /** 头部:顶部渐变 Banner(观影问候语+追剧日历提示)+ 搜索条 + 横滑片型 chips */
  @Builder
  headerMain() {
    Column({ space: 12 }) {
      // 渐变 Banner:观影问候语 + 追剧日历提示
      Column({ space: 10 }) {
        Row() {
          Column({ space: 5 }) {
            Text('晚上好,追剧的人').fontSize(16).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            Text('追剧日历 · 今晚 2 部更新,《雾岛回声》20:00 上线').fontSize(9).fontColor(COLORS.title).opacity(0.72)
              .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)

          Column() {
            Text('🎬').fontSize(22).opacity(this.breath ? 1 : 0.6)
          }
          .width(44).height(44).borderRadius(22).backgroundColor(COLORS.goldD)
          .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
        }
        .width('100%')

        Row({ space: 8 }) {
          Text('📌 在追 8 部').fontSize(9).fontColor(COLORS.title).opacity(0.9)
            .padding({ left: 8, right: 8, top: 3, bottom: 3 }).backgroundColor(COLORS.goldD).borderRadius(8)
          Text('🔥 今晚更新 2 集').fontSize(9).fontColor(COLORS.title).opacity(0.9)
            .padding({ left: 8, right: 8, top: 3, bottom: 3 }).backgroundColor(COLORS.goldD).borderRadius(8)
          Text('⭐ VIP 抢先看 6 集').fontSize(9).fontColor(COLORS.goldD)
            .padding({ left: 8, right: 8, top: 3, bottom: 3 }).backgroundColor(COLORS.gold).borderRadius(8)
        }
        .width('100%')
      }
      .width('100%').padding(16).borderRadius(14)
      .linearGradient({ angle: 135, colors: [[COLORS.goldD, 0], [COLORS.gold, 1]] })

headerMain 是头部区域的 Builder 函数,使用 @Builder 装饰器声明。@Builder 是 ArkUI 的 UI 构建函数装饰器,用于将可复用的 UI 片段封装为函数,在 build() 中通过 this.xxx() 调用。

头部区域从上到下分为三层。第一层是渐变 Banner——一个使用 linearGradient 设置了 135 度对角渐变(从暗金 #C98F14 到鎏金黄 #F0B429)的圆角容器,内部包含问候语行和状态标签行。问候语行左侧是"晚上好,追剧的人"标题和"追剧日历"提示文案,右侧是一个 44x44 的圆形图标容器,内部的 🎬 emoji 透明度受 breath 状态驱动在 1.0 和 0.6 之间脉动,形成"电影胶片呼吸"的动效。状态标签行包含三个胶囊标签:在追数量、今晚更新集数、VIP 抢先看,前两个使用暗金底色,第三个使用鎏金黄底色,通过颜色差异突出 VIP 特权信息。

      // 搜索条(右侧语音入口跳转 AI 字幕 Tab)
      Row({ space: 8 }) {
        Text('🔍').fontSize(14)
        Text('搜索剧名 / 演员 / 片单').fontSize(11).fontColor(COLORS.text3).layoutWeight(1)
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        Text('🎙').fontSize(14).onClick(() => {
          this.currentTab = 2;
        })
      }
      .width('100%').padding({ left: 14, right: 14, top: 10, bottom: 10 })
      .backgroundColor(COLORS.card).borderRadius(20)

第二层是搜索条,采用卡片底色和 20px 大圆角的胶囊形设计。左侧是放大镜 emoji,中间是灰色占位提示文案"搜索剧名 / 演员 / 片单",右侧是麦克风 emoji。麦克风图标的 onClick 事件将 currentTab 设为 2(AI 字幕 Tab),实现了从搜索条语音入口到 AI 字幕功能页的快速跳转——这是一个巧妙的产品设计,将"语音搜索"和"AI 字幕"在用户认知中关联起来,引导用户发现 AI 字幕功能。

      // 横滑片型 chips
      Scroll() {
        Row({ space: 8 }) {
          ForEach(CATE_TAGS, (tg: string, idx: number) => {
            Text(tg).fontSize(11)
              .fontColor(this.cateIdx === idx ? COLORS.gold : COLORS.sub)
              .padding({ left: 13, right: 13, top: 6, bottom: 6 })
              .backgroundColor(this.cateIdx === idx ? COLORS.chip : COLORS.card)
              .borderRadius(13)
              .onClick(() => {
                this.cateIdx = idx;
              })
          }, (tg: string) => tg)
        }
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
    }
    .width('100%')
    .padding({ left: 14, right: 14, top: 12, bottom: 12 })
    .linearGradient({ angle: 180, colors: [[COLORS.chip, 0], [COLORS.bg, 1]] })
  }

第三层是横滑片型 chips,使用 Scroll 组件横向滚动展示 CATE_TAGS 数组中的 8 个片型标签。每个 chip 通过 ForEach 渲染,选中态(cateIdx === idx)的文字为鎏金黄、背景为芯片灰深色,未选中态的文字为次级灰色、背景为卡片灰浅色,13px 的圆角形成胶囊造型。onClick 事件更新 cateIdx 状态,驱动选中态切换。整个头部区域的外层 Column 还应用了一个 180 度的垂直渐变(从芯片灰到背景色),让头部到内容区的过渡更加自然。

8.3 tabDrama 影库 Tab

  /** 影库 Tab:横滑热播海报大卡 + 片型宫格入口 */
  @Builder
  tabDrama() {
    Column({ space: 12 }) {
      // 1. 片库标题行(在库数量 + 新建片单入口)
      Row() {
        Text('🎞 本周热播片库').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text(this.dramaList.length.toString() + ' 部在库').fontSize(9).fontColor(COLORS.text3)
        Text('+ 新建').fontSize(9).fontColor(COLORS.gold)
          .padding({ left: 9, right: 9, top: 4, bottom: 4 })
          .backgroundColor(COLORS.chip).borderRadius(8)
          .onClick(() => {
            this.addModal = true;
          })
      }
      .width('100%')

影库 Tab 的构建从标题行开始。标题行采用 Row 水平布局:左侧"本周热播片库"标题,中间用 Column().layoutWeight(1) 撑开弹性空间,右侧依次是在库数量文本和"+ 新建"按钮。"+ 新建"按钮使用鎏金黄文字和芯片灰背景,点击后设置 addModal = true 打开新建片单弹窗。dramaList.length.toString() 动态显示当前在库剧集数量,当用户新建或删除片单时,这个数字会自动更新。

      // 2. 横滑热播海报大卡(Scroll 横向滑动,布局区别于其余 Tab)
      Scroll() {
        Row({ space: 12 }) {
          ForEach(this.dramaList, (item: DramaItem, idx: number) => {
            Column({ space: 8 }) {
              // 海报渐变块(emoji 海报 + 类型角标 + 评分角标)
              Column() {
                Text(item.cover).fontSize(38).opacity(this.breath ? 1 : 0.85)
                Column().layoutWeight(1)
                Row() {
                  Text(item.genre).fontSize(8).fontColor(genreColor(item.genre))
                    .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                    .backgroundColor(COLORS.chip).borderRadius(5)
                  Column().layoutWeight(1)
                  Text(item.score + ' 分').fontSize(9).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
                }
                .width('100%').margin({ bottom: 8 })
              }
              .width('100%').height(140).borderRadius(10)
              .linearGradient({ angle: 145, colors: [[COLORS.goldD, 0], [COLORS.gold, 1]] })

              // 剧名 + 集数热度
              Text(item.title).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
                .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
              Text(item.episodes + ' · 🔥 ' + item.heat).fontSize(9).fontColor(COLORS.sub)
                .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })

              // 编辑 / 删除迷你操作行
              Row({ space: 6 }) {
                Text('编辑').fontSize(8).fontColor(COLORS.sub)
                  .layoutWeight(1).textAlign(TextAlign.Center)
                  .padding({ top: 4, bottom: 4 }).backgroundColor(COLORS.chip).borderRadius(6)
                  .onClick(() => {
                    this.openEditDrama(idx);
                  })
                Text('删除').fontSize(8).fontColor(COLORS.red)
                  .layoutWeight(1).textAlign(TextAlign.Center)
                  .padding({ top: 4, bottom: 4 }).backgroundColor(COLORS.chip).borderRadius(6)
                  .onClick(() => {
                    this.delIdx = idx;
                    this.delModal = true;
                  })
              }
              .width('100%')
            }
            .width(176).padding(10).backgroundColor(COLORS.card).borderRadius(14)
          }, (item: DramaItem) => item.title + item.genre)
        }
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')

影库 Tab 的核心视觉区域是横滑热播海报大卡墙。Scroll 组件设置为横向滚动(ScrollDirection.Horizontal),内部 Row 容器通过 ForEach 遍历 dramaList 渲染每个剧集卡片。每张卡片宽度固定为 176px,采用卡片底色和 14px 圆角。

卡片内部从上到下分为四个层级。第一层是海报渐变块——高度 140px 的 Column,使用 145 度渐变(从暗金到鎏金黄)模拟影院海报的金属质感。海报块顶部是 38px 的 emoji 海报图标,透明度受 breath 驱动微调。海报块底部是类型角标和评分角标的水平排列:类型角标通过 genreColor() 函数获取语义色,评分角标使用白色粗体显示分数。

第二层是剧名和集数热度信息行,均设置了 maxLines(1)textOverflow(Ellipsis) 防止长文本溢出。第三层是编辑和删除操作行,两个按钮等宽分布,编辑按钮使用次级灰色文字,删除按钮使用红色文字以示危险操作。编辑按钮点击调用 openEditDrama(idx) 打开编辑弹窗,删除按钮点击设置 delIdxdelModal 打开删除确认弹窗。ForEach 的键值生成函数 (item: DramaItem) => item.title + item.genre 使用剧名+类型组合作为唯一标识,确保列表更新时能正确进行差分渲染。

      // 3. 片型宫格标题行
      Row() {
        Text('🗂 片型宫格').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text('按类型找剧').fontSize(9).fontColor(COLORS.text3)
      }
      .width('100%')

      // 4. 片型宫格入口(Flex 换行三列布局)
      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(GENRE_GRID, (g: GenreEntry) => {
          Column({ space: 6 }) {
            Text(g.icon).fontSize(24).opacity(this.breath ? 1 : 0.8)
            Text(g.name).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            Text(g.count).fontSize(8).fontColor(COLORS.text3)
          }
          .width('31.5%').padding({ top: 14, bottom: 14 }).backgroundColor(COLORS.card).borderRadius(12)
          .alignItems(HorizontalAlign.Center)
        }, (g: GenreEntry) => g.name)
      }
      .width('100%')

      // 5. 观影提示条(横滑与宫格之外的呼吸装饰行)
      Row({ space: 8 }) {
        Text('🍿').fontSize(14)
        Text('本周累计上新 12 部剧集,VIP 可抢先看 6 集').fontSize(9).fontColor(COLORS.sub)
          .layoutWeight(1).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        Text('›').fontSize(14).fontColor(COLORS.text3)
      }
      .width('100%').padding(10).backgroundColor(COLORS.card).borderRadius(10)
    }
    .width('100%')
  }

影库 Tab 的下半部分包含片型宫格入口和观影提示条。片型宫格使用 Flex 组件的 FlexWrap.Wrap 换行模式实现三列布局,每个宫格宽度设为 31.5%,通过 SpaceBetween 对齐在三个宫格之间均匀分配剩余空间。每个宫格内部纵向排列 emoji 图标(24px,透明度受 breath 微调)、片型名称(11px 粗体)和片库数量(8px 灰色),形成清晰的信息层级。

最底部是观影提示条,使用卡片底色和 10px 圆角的水平行设计,左侧爆米花 emoji 增添影院氛围,中间是提示文案,右侧箭头引导用户了解更多。整个影库 Tab 的布局结构与追剧 Tab、AI 字幕 Tab、我的 Tab 完全不同,实现了"四 Tab 四布局"的视觉差异化设计目标。

8.4 tabFollow 追剧 Tab

  /** 追剧 Tab:追剧进度清单(进度条布局)+ 月度观影时长柱状图 */
  @Builder
  tabFollow() {
    Column({ space: 12 }) {
      // 1. 追剧日历标题行
      Row() {
        Text('📌 我的追剧日历').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text(this.followList.length.toString() + ' 部在追 · 更新提醒已开启').fontSize(9).fontColor(COLORS.gold)
      }
      .width('100%')

      // 2. 追剧进度清单(封面 + 台词信息 + 线性进度条)
      ForEach(this.followList, (item: FollowItem, idx: number) => {
        Column({ space: 8 }) {
          Row({ space: 10 }) {
            // 圆形剧集封面
            Column() {
              Text(item.cover).fontSize(20)
            }
            .width(42).height(42).borderRadius(21).backgroundColor(COLORS.chip)
            .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)

            // 剧名 + 更新日胶囊 + 观看进度文本
            Column({ space: 3 }) {
              Row({ space: 6 }) {
                Text(item.title).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
                  .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
                Text(item.day).fontSize(8).fontColor(dayColor(item.day))
                  .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                  .backgroundColor(COLORS.chip).borderRadius(6)
              }

              Text(item.seen + ' / 共 ' + item.total).fontSize(9).fontColor(COLORS.sub)
                .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Start)

            // 进度百分比(鎏金黄高亮)
            Text(item.percent.toString() + '%').fontSize(12).fontColor(COLORS.gold).fontWeight(FontWeight.Bold)
          }
          .width('100%').alignItems(VerticalAlign.Center)

          // 线性进度条(percent 0~100,颜色随呼吸微调)
          Progress({ value: item.percent, total: 100, type: ProgressType.Linear })
            .width('100%')
            .color(this.breath ? COLORS.gold : COLORS.goldD)
            .backgroundColor(COLORS.chip)
            .style({ strokeWidth: 6 })
            .borderRadius(3)
        }
        .width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(12)
      }, (item: FollowItem) => item.title + item.day)

      // 3. 月度观影时长柱状图
      this.chartCard()
    }
    .width('100%')
  }

追剧 Tab 的布局结构清晰分为标题行、进度清单和柱状图三部分。标题行显示"我的追剧日历"和在追数量,右侧行用鎏金黄文字显示更新提醒状态。

进度清单通过 ForEach 遍历 followList 渲染每条追剧记录。每条记录是一个卡片容器,内部上半部分是 Row 水平布局:左侧是 42x42 的圆形封面容器(使用芯片灰底色和 21px 圆角),中间是剧名+更新日胶囊+观看进度文本的纵向信息列,右侧是鎏金黄粗体的进度百分比数值。下半部分是 Progress 线性进度条组件,value 参数取自 item.percenttotal 为 100,typeProgressType.Linear。进度条颜色受 breath 状态驱动在鎏金黄和暗金之间切换,strokeWidth: 6 控制进度条粗细,borderRadius: 3 让进度条端头圆滑。进度条底色使用 COLORS.chip,与卡片底色形成层次区分。清单底部调用 chartCard() 渲染月度观影时长柱状图。

8.5 tabCaption AI 字幕 Tab

  /** AI字幕 Tab:Speech Kit 6.1.1 特性页(预览/语言/外观/代码/场景 五区块) */
  @Builder
  tabCaption() {
    Column({ space: 12 }) {
      // 特性简介条
      Row({ space: 8 }) {
        Text('🗣').fontSize(16)
        Column({ space: 2 }) {
          Text('Speech Kit · 场景化语音服务').fontSize(11)
            .fontColor(COLORS.title).fontWeight(FontWeight.Bold)
          Text('HarmonyOS 6.1.1:AI字幕支持源语言 / 目标语言 / 字体颜色 / 字体大小')
            .fontSize(8).fontColor(COLORS.title).opacity(0.75)
            .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
      }
      .width('100%').padding(10).borderRadius(10)
      .linearGradient({ angle: 135, colors: [[COLORS.goldD, 0], [COLORS.gold, 1]] })

AI 字幕 Tab 是整个应用的技术亮点页面,由五个区块构成。顶部是特性简介条,使用与头部 Banner 相同的 135 度鎏金黄渐变,左侧是语音 emoji,右侧是"Speech Kit · 场景化语音服务"标题和"HarmonyOS 6.1.1:AI字幕支持源语言 / 目标语言 / 字体颜色 / 字体大小"特性说明文案,明确标注了四大新增字段。

      // ===== 区块 1:组件实时预览卡 =====
      Column({ space: 10 }) {
        Row() {
          Text('🎞 AI 字幕实时预览').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
          Column().layoutWeight(1)
          Text(this.captionReady ? '已就绪' : '初始化中').fontSize(9)
            .fontColor(this.captionReady ? COLORS.green : COLORS.gold)
            .opacity(this.captionReady ? 1 : (this.breath ? 1 : 0.55))
            .padding({ left: 8, right: 8, top: 3, bottom: 3 })
            .backgroundColor(COLORS.chip).borderRadius(8)
        }
        .width('100%')

        // AI 字幕组件(isShown @Link 双向绑定,options 实时重建)
        AICaptionComponent({
          isShown: this.captionShown,
          controller: this.captionController,
          options: this.buildCaptionOptions()
        })
        .width('100%')
        .height(110)
        .borderRadius(10)
        .border({ width: 1, color: COLORS.line })

        // 控制按钮行:开启/隐藏字幕 + 写入演示音频
        Row({ space: 10 }) {
          Text(this.captionShown ? '隐藏字幕' : '开启字幕').fontSize(12)
            .fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            .layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 9, bottom: 9 })
            .backgroundColor(this.captionShown ? COLORS.goldD : COLORS.gold)
            .borderRadius(10)
            .onClick(() => {
              this.captionShown = !this.captionShown;
            })

          Row({ space: 5 }) {
            Text('写入演示音频').fontSize(12).fontColor(COLORS.blue)
            Text('×' + this.captionFed.toString()).fontSize(9).fontColor(COLORS.blue)
          }
          .layoutWeight(1).justifyContent(FlexAlign.Center)
          .padding({ top: 9, bottom: 9 })
          .backgroundColor(COLORS.chip).borderRadius(10)
          .onClick(() => {
            this.feedDemoAudio();
          })
        }
        .width('100%')

        // 错误信息行(onError 回调触发显示)
        if (this.captionErrMsg !== '') {
          Text('⚠ ' + this.captionErrMsg).fontSize(9).fontColor(COLORS.red)
            .width('100%').maxLines(2)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
            .padding(8).backgroundColor(COLORS.chip).borderRadius(8)
        }
      }
      .width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)

区块 1 是组件实时预览卡,这是 AI 字幕功能的核心交互区域。卡片顶部标题行右侧有状态标签——当 captionReadytrue 时显示绿色"已就绪",为 false 时显示金色"初始化中"且透明度受 breath 驱动脉动。卡片中央是 AICaptionComponent 组件实例,传入三个参数:isShown 绑定 captionShown 状态控制字幕显隐,controller 绑定 captionController 实例,options 调用 buildCaptionOptions() 方法实时组装配置。组件高度设为 110px,使用线条色边框勾勒出预览区域边界。

组件下方是两个控制按钮:左侧"开启/隐藏字幕"按钮通过 captionShown 状态切换文字和底色(开启时金色、隐藏时暗金),点击翻转 captionShown 状态;右侧"写入演示音频"按钮调用 feedDemoAudio() 方法,旁边显示 ×N 写入计数。卡片底部是条件渲染的错误信息行,当 captionErrMsg 不为空时显示红色错误提示,包含警告符号和错误文本。

      // ===== 区块 2:语言设置卡(sourceLanguage / targetLanguage) =====
      Column({ space: 10 }) {
        Text('🌐 语言设置').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

        // 源语言标题行('zh' | 'en' 二选一)
        Row() {
          Text('源语言 sourceLanguage').fontSize(10).fontColor(COLORS.sub)
          Column().layoutWeight(1)
          Text("取值 'zh' | 'en'").fontSize(8).fontColor(COLORS.text3)
        }
        .width('100%')

        Row({ space: 8 }) {
          ForEach(SRC_LANGS, (l: LangOption) => {
            Text(l.name).fontSize(11)
              .fontColor(this.srcLang === l.code ? COLORS.title : COLORS.sub)
              .fontWeight(this.srcLang === l.code ? FontWeight.Bold : FontWeight.Normal)
              .layoutWeight(1).textAlign(TextAlign.Center)
              .padding({ top: 8, bottom: 8 })
              .backgroundColor(this.srcLang === l.code ? COLORS.gold : COLORS.chip)
              .borderRadius(10)
              .onClick(() => {
                this.switchSourceLang(l.code);
              })
          }, (l: LangOption) => l.code)
        }
        .width('100%')

区块 2 是语言设置卡,展示 sourceLanguagetargetLanguage 两个 6.1.1 新增字段的配置能力。源语言部分包含标题行(标注字段名和取值范围)和选项按钮行。两个选项按钮(中文、英文)通过 ForEach 遍历 SRC_LANGS 渲染,选中态使用鎏金黄底色和白色粗体文字,未选中态使用芯片灰底色和次级灰色文字。点击选项调用 switchSourceLang() 方法,实现源语言切换时目标语言的联动。

        // 目标语言标题行(中文源锁定 zh;英文源可选 zh / en / zh-en)
        Row() {
          Text('目标语言 targetLanguage').fontSize(10).fontColor(COLORS.sub)
          Column().layoutWeight(1)
          Text(this.srcLang === 'zh' ? '中文源已锁定' : "取值 'zh' | 'en' | 'zh-en'")
            .fontSize(8).fontColor(COLORS.text3)
        }
        .width('100%')

        if (this.srcLang === 'zh') {
          // 中文源:目标语言锁定提示行(不可选)
          Row({ space: 8 }) {
            Text('🔒').fontSize(13)
            Text('中文源锁定中文:无翻译方向,targetLanguage 固定为 zh')
              .fontSize(10).fontColor(COLORS.text3).layoutWeight(1)
              .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
          }
          .width('100%').padding(10).backgroundColor(COLORS.chip).borderRadius(10)
        } else {
          // 英文源:目标语言三选项
          Row({ space: 8 }) {
            ForEach(TGT_LANGS_EN, (l: LangOption) => {
              Text(l.name).fontSize(11)
                .fontColor(this.tgtLang === l.code ? COLORS.title : COLORS.sub)
                .fontWeight(this.tgtLang === l.code ? FontWeight.Bold : FontWeight.Normal)
                .layoutWeight(1).textAlign(TextAlign.Center)
                .padding({ top: 8, bottom: 8 })
                .backgroundColor(this.tgtLang === l.code ? COLORS.gold : COLORS.chip)
                .borderRadius(10)
                .onClick(() => {
                  this.tgtLang = l.code;
                })
            }, (l: LangOption) => l.code)
          }
          .width('100%')
        }

        // 当前语言组合摘要
        Row({ space: 6 }) {
          Circle().width(6).height(6).fill(COLORS.gold)
          Text('当前组合:源 ' + langName(this.srcLang) + ' → 目标 ' + langName(this.tgtLang))
            .fontSize(9).fontColor(COLORS.sub)
            .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        }
        .width('100%')
      }
      .width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)

目标语言部分采用条件渲染——当 srcLang'zh' 时显示锁定提示行(带锁图标和说明文案"中文源锁定中文:无翻译方向,targetLanguage 固定为 zh"),当 srcLang'en' 时显示三个选项按钮(中文、英文、中英双语)。目标语言标题行右侧的取值说明也会根据源语言动态切换文案。卡片底部是当前语言组合摘要行,使用金色小圆点引导,通过 langName() 函数将语言码转换为中文名展示,让用户对当前配置一目了然。

      // ===== 区块 3:外观设置卡(fontSize / fontColor) =====
      Column({ space: 10 }) {
        Text('🎨 外观设置').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

        // 字体大小标题行(AICaptionFontSize 四档)
        Row() {
          Text('字体大小 fontSize').fontSize(10).fontColor(COLORS.sub)
          Column().layoutWeight(1)
          Text('AICaptionFontSize').fontSize(8).fontColor(COLORS.text3)
        }
        .width('100%')

        Row({ space: 8 }) {
          ForEach(SIZE_OPTIONS, (s: SizeOption) => {
            Text(s.name).fontSize(11)
              .fontColor(this.captionSize === s.size ? COLORS.title : COLORS.sub)
              .fontWeight(this.captionSize === s.size ? FontWeight.Bold : FontWeight.Normal)
              .layoutWeight(1).textAlign(TextAlign.Center)
              .padding({ top: 8, bottom: 8 })
              .backgroundColor(this.captionSize === s.size ? COLORS.gold : COLORS.chip)
              .borderRadius(10)
              .onClick(() => {
                this.captionSize = s.size;
              })
          }, (s: SizeOption) => s.name)
        }
        .width('100%')

区块 3 是外观设置卡,展示 fontSizefontColor 两个 6.1.1 新增字段的配置能力。字体大小部分通过 ForEach 遍历 SIZE_OPTIONS 渲染四个选项按钮(小号、标准、大号、超大),选中态样式与语言选项一致。点击选项直接将 captionSize 设为对应的 AICaptionFontSize 枚举值,由于 buildCaptionOptions() 方法在每次 options 求值时都会读取最新的 captionSize,字号变化会实时反映到预览区的字幕组件上。

        // 字体颜色标题行(ResourceColor 五预设)
        Row() {
          Text('字体颜色 fontColor').fontSize(10).fontColor(COLORS.sub)
          Column().layoutWeight(1)
          Text('ResourceColor').fontSize(8).fontColor(COLORS.text3)
        }
        .width('100%')

        // 五个圆形色块(选中鎏金黄描边)
        Row({ space: 12 }) {
          ForEach(CAPTION_FONT_COLORS, (c: string) => {
            Circle().width(26).height(26).fill(c)
              .border({ width: 2, color: this.captionColor === c ? COLORS.gold : COLORS.line })
              .onClick(() => {
                this.captionColor = c;
              })
          }, (c: string) => c)
        }
        .width('100%').justifyContent(FlexAlign.SpaceBetween)

        // 当前选中颜色说明行
        Row() {
          Circle().width(10).height(10).fill(this.captionColor)
          Text(colorName(this.captionColor) + ' ' + this.captionColor)
            .fontSize(9).fontColor(COLORS.sub).margin({ left: 6 })
          Column().layoutWeight(1)
          Text('字幕原文与译文同步生效').fontSize(8).fontColor(COLORS.text3)
        }
        .width('100%')
      }
      .width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)

字体颜色部分通过 ForEach 遍历 CAPTION_FONT_COLORS 渲染五个圆形色块(26x26 的 Circle 组件),选中态使用 2px 鎏金黄描边,未选中态使用线条色描边。点击色块将 captionColor 设为对应颜色值。色块行使用 SpaceBetween 对齐让五个色块均匀分布。底部说明行通过 colorName() 函数将颜色值转换为诗意化的中文名(如"暖阳黄 #FFE9B0"),并提示"字幕原文与译文同步生效",说明 fontColor 同时作用于原文和译文文本。

      // ===== 区块 4:options 实时代码预览卡 =====
      Column({ space: 10 }) {
        Row() {
          Text('💻 AICaptionOptions 实时代码').fontSize(13)
            .fontColor(COLORS.title).fontWeight(FontWeight.Bold)
          Column().layoutWeight(1)
          Text('随设置联动').fontSize(8).fontColor(COLORS.gold)
        }
        .width('100%')

        // 深色代码块(monospace,键名鎏金黄高亮)
        Column({ space: 5 }) {
          Text('AICaptionOptions = {').fontSize(9).fontColor(COLORS.text3).fontFamily('monospace')
          Row({ space: 4 }) {
            Text('● sourceLanguage:').fontSize(9).fontColor(COLORS.gold).fontFamily('monospace')
            Text("'" + this.srcLang + "'").fontSize(9).fontColor(COLORS.blue).fontFamily('monospace')
          }
          .width('100%')
          Row({ space: 4 }) {
            Text('● targetLanguage:').fontSize(9).fontColor(COLORS.gold).fontFamily('monospace')
            Text("'" + this.tgtLang + "'").fontSize(9).fontColor(COLORS.blue).fontFamily('monospace')
          }
          .width('100%')
          Row({ space: 4 }) {
            Text('● fontSize:').fontSize(9).fontColor(COLORS.gold).fontFamily('monospace')
            Text(sizeName(this.captionSize)).fontSize(9).fontColor(COLORS.green).fontFamily('monospace')
          }
          .width('100%')
          Row({ space: 4 }) {
            Text('● fontColor:').fontSize(9).fontColor(COLORS.gold).fontFamily('monospace')
            Text("'" + this.captionColor + "'").fontSize(9)
              .fontColor(this.captionColor).fontFamily('monospace')
          }
          .width('100%')
          Text('}').fontSize(9).fontColor(COLORS.text3).fontFamily('monospace')
        }
        .width('100%').padding(12).backgroundColor(COLORS.bg).borderRadius(10)
        .alignItems(HorizontalAlign.Start)

        Text('★ 6.1.1 新增字段:sourceLanguage / targetLanguage / fontSize / fontColor')
          .fontSize(8).fontColor(COLORS.sub).width('100%')
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
      }
      .width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)

区块 4 是 options 实时代码预览卡,这是一个独具匠心的设计——将当前 AICaptionOptions 配置以代码语法形式实时展示在界面上。代码块使用 fontFamily('monospace') 等宽字体和背景色 COLORS.bg(影院黑),模拟代码编辑器的外观。每个字段以 Row 渲染:键名使用鎏金黄色(如 ● sourceLanguage:),值使用不同颜色——语言码用蓝色(如 'zh'),字号枚举用绿色(如 NORMAL),颜色值使用实际颜色值渲染(如选中暖阳黄时文字本身就是 #FFE9B0 色)。这种设计让用户能直观地看到自己的设置如何映射到实际代码结构,是极佳的技术教学和调试辅助。代码块底部标注"★ 6.1.1 新增字段:sourceLanguage / targetLanguage / fontSize / fontColor",明确标识了四个新字段。

      // ===== 区块 5:字幕场景推荐列表(左色条 + 点击套用语言组合) =====
      Column({ space: 8 }) {
        Row() {
          Text('🎬 字幕场景推荐').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
          Column().layoutWeight(1)
          Text('点击套用语言组合').fontSize(8).fontColor(COLORS.text3)
        }
        .width('100%')

        ForEach(this.sceneList, (item: CaptionScene, idx: number) => {
          Row({ space: 10 }) {
            // 左色条(金/蓝/红轮换)
            Column().width(4).height(42).borderRadius(2)
              .backgroundColor(idx % 3 === 0 ? COLORS.gold : (idx % 3 === 1 ? COLORS.blue : COLORS.red))

            Column({ space: 3 }) {
              Text(item.scene).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
                .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
              Text(item.desc).fontSize(9).fontColor(COLORS.sub)
                .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
              Text('源 ' + item.src + ' → 目标 ' + item.tgt).fontSize(9).fontColor(COLORS.gold)
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Start)

            Text('套用 ›').fontSize(9).fontColor(COLORS.gold)
          }
          .width('100%').padding(10).backgroundColor(COLORS.chip).borderRadius(10)
          .alignItems(VerticalAlign.Center)
          .onClick(() => {
            this.switchSourceLang(item.src);
            this.tgtLang = item.tgt;
          })
        }, (item: CaptionScene) => item.scene)
      }
      .width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)
    }
    .width('100%')
  }

区块 5 是字幕场景推荐列表,通过 ForEach 遍历 sceneList 渲染 5 个场景条目。每个条目左侧是 4px 宽的色条,颜色按索引取模 3 在鎏金黄、蓝色、红色之间轮换,形成视觉节奏感。色条右侧是场景名称、说明文案和语言组合信息的三行纵向布局,最右侧是"套用 ›"按钮。整个条目的 onClick 事件调用 switchSourceLang(item.src) 和直接设置 this.tgtLang = item.tgt,一键将场景预设的语言组合应用到当前配置。这种"一键套用"设计极大降低了用户的配置成本——用户无需理解源语言和目标语言的含义,只需选择符合自己使用场景的预设即可。

8.6 tabMine 我的 Tab

  /** 我的 Tab:用户信息+追剧VIP渐变大卡 + 功能清单行 */
  @Builder
  tabMine() {
    Column({ space: 12 }) {
      // 1. 用户信息 + 追剧 VIP 渐变大卡(鎏金黄渐变)
      Column({ space: 12 }) {
        Row({ space: 12 }) {
          Column() {
            Text('🎬').fontSize(26)
          }
          .width(54).height(54).borderRadius(27).backgroundColor(COLORS.goldD)
          .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)

          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Text('晚风剧场').fontSize(16).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
              Text('追剧 VIP').fontSize(8).fontColor(COLORS.goldD)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .backgroundColor(COLORS.gold).borderRadius(7)
            }
            Text('剧场 ID:jujuchang_1118 · 已连续追剧 156 天')
              .fontSize(9).fontColor(COLORS.title).opacity(0.72)
              .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Start)
        }
        .width('100%')

        // 三格观影数据
        Row({ space: 10 }) {
          Column({ space: 3 }) {
            Text('8').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            Text('在追剧集').fontSize(8).fontColor(COLORS.title).opacity(0.7)
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)

          Column({ space: 3 }) {
            Text('128').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            Text('看过部数').fontSize(8).fontColor(COLORS.title).opacity(0.7)
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)

          Column({ space: 3 }) {
            Text('1.2万').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            Text('观影分钟').fontSize(8).fontColor(COLORS.title).opacity(0.7)
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)
        }
        .width('100%').margin({ top: 2 })
      }
      .width('100%').padding(16).borderRadius(14)
      .linearGradient({ angle: 135, colors: [[COLORS.goldD, 0], [COLORS.gold, 1]] })

      // 2. 功能清单行(UserStat)
      ForEach(this.statList, (item: UserStat) => {
        Row({ space: 10 }) {
          Text(item.icon).fontSize(16)
          Text(item.label).fontSize(11).fontColor(COLORS.title).layoutWeight(1)
            .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
          Text(item.value).fontSize(10).fontColor(COLORS.sub)
            .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
          if (item.arrow) {
            Text('›').fontSize(14).fontColor(COLORS.text3)
          }
        }
        .width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(10)
      }, (item: UserStat) => item.label)
    }
    .width('100%')
  }

我的 Tab 的布局分为两部分。第一部分是用户 VIP 渐变大卡,使用与头部 Banner 相同的 135 度鎏金黄渐变。卡片内部分为上下两层:上层是用户信息行,左侧是 54x54 的圆形头像容器(暗金底色 + 电影 emoji),右侧是用户昵称"晚风剧场"和"追剧 VIP"标签胶囊,下方是剧场 ID 和连续追剧天数文案;下层是三格观影数据统计——在追剧集数 8、看过部数 128、观影分钟 1.2 万,三格等宽分布使用 layoutWeight(1),数值用白色粗体,标签用半透明白色。

第二部分是功能清单行,通过 ForEach 遍历 statList 渲染 8 条功能入口。每行采用 Row 水平布局:左侧 emoji 图标(16px),中间功能名称(11px 白色,layoutWeight(1) 撑开),右侧状态文案(10px 灰色),最右侧根据 item.arrow 条件渲染箭头指示符。所有文本都设置了 maxLines(1)Ellipsis 溢出处理,确保长文本不会破坏行布局。整个功能清单使用统一的卡片底色和 10px 圆角,视觉简洁一致。

8.7 chartCard 图表卡

  /** 图表卡:月度观影时长柱状图(6 个月,柱高随呼吸 ±5% 波动) */
  @Builder
  chartCard() {
    Column({ space: 10 }) {
      Row() {
        Text('📊 月度观影时长').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text('单位:小时').fontSize(9).fontColor(COLORS.text3)
      }
      .width('100%')

      // 渐变柱状图(Column + ForEach 实现)
      Row({ space: 8 }) {
        ForEach(MONTH_IDX, (i: number) => {
          Column({ space: 5 }) {
            Text(MONTH_HOURS[i].toString()).fontSize(8)
              .fontColor(this.breath ? COLORS.gold : COLORS.sub)
            Column().width(18)
              .height(Math.max(20, MONTH_HOURS[i] / MONTH_MAX * 110 * (this.breath ? 1.05 : 0.95)))
              .borderRadius(5)
              .linearGradient({ angle: 180, colors: [[COLORS.gold, 0], [COLORS.goldD, 1]] })
            Text(MONTH_LABELS[i]).fontSize(8).fontColor(COLORS.text3)
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)
        }, (i: number) => 'm' + i.toString())
      }
      .width('100%').alignItems(VerticalAlign.Bottom).height(150)

      // 汇总行
      Row() {
        Text('近 6 月累计 374 小时').fontSize(8).fontColor(COLORS.sub)
        Column().layoutWeight(1)
        Text('环比 +10.3%').fontSize(8).fontColor(COLORS.green)
      }
      .width('100%')
    }
    .width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)
  }

chartCard 是月度观影时长柱状图的 Builder 函数。柱状图完全使用 ArkUI 基础组件 ColumnRow 构建,无需引入任何图表库。标题行右侧标注"单位:小时",图表区使用 RowVerticalAlign.Bottom 底部对齐让所有柱子从底部对齐生长。每个柱子是一个 Column 容器:顶部是数值文本(颜色受 breath 在金色和灰色间切换),中间是宽度 18px 的渐变色块(高度通过 Math.max(20, MONTH_HOURS[i] / MONTH_MAX * 110 * (this.breath ? 1.05 : 0.95)) 计算得到,breathtrue 时柱高放大 5%、为 false 时缩小 5%,形成呼吸动效),底部是月份标签。色块使用 180 度垂直渐变(从鎏金黄到暗金)模拟金属质感。图表底部汇总行显示累计时长和环比增长率(绿色表示正增长)。

8.8 tabBar 底部导航

  /** 底部导航:4 Tab 单排(选中鎏金黄高亮 + 图标放大) */
  @Builder
  tabBar() {
    Row() {
      ForEach(TAB_LIST, (t: TabMeta, idx: number) => {
        Column({ space: 3 }) {
          Text(t.icon).fontSize(this.currentTab === idx ? 20 : 17)
            .opacity(this.currentTab === idx ? 1 : 0.65)
          Text(t.label).fontSize(9)
            .fontColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
            .fontWeight(this.currentTab === idx ? FontWeight.Bold : FontWeight.Normal)
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Center)
        .padding({ top: 7, bottom: 7 })
        .onClick(() => {
          this.currentTab = idx;
        })
      }, (t: TabMeta) => t.label)
    }
    .width('100%')
    .backgroundColor(COLORS.card)
    .border({ width: { top: 1 }, color: COLORS.line })
  }

tabBar 是底部导航栏的 Builder 函数。通过 ForEach 遍历 TAB_LIST 渲染 4 个 Tab 项,每个 Tab 使用 layoutWeight(1) 等宽分布。选中态通过三层视觉差异表现:图标 fontSize 从 17px 放大到 20px,图标 opacity 从 0.65 提升到 1.0,标签 fontColor 从三级灰色变为鎏金黄、fontWeight 从 Normal 变为 Bold。这种"大小+透明度+颜色+字重"四维差异让选中态具有强烈的视觉焦点感。导航栏使用卡片底色,顶部有 1px 线条色分割线,与内容区形成清晰的视觉分隔。点击 Tab 项设置 currentTab 索引,驱动 build() 中的条件渲染切换内容区。

8.9 弹窗系统

  /** 弹窗全屏遮罩(点击遮罩关闭弹窗) */
  @Builder
  modalOverlay(onClose: () => void) {
    Stack() {
      Column().width('100%').height('100%').backgroundColor(COLORS.mask)
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Center)
    .onClick(() => onClose())
  }

modalOverlay 是弹窗全屏遮罩的 Builder 函数,接收一个 onClose 回调参数。遮罩使用 COLORS.maskrgba(6,6,10,0.72))半透明背景色覆盖整个屏幕,onClick 事件调用 onClose() 回调,实现"点击遮罩区域关闭弹窗"的交互行为。alignContent(Alignment.Center) 确保弹窗内容在遮罩中居中显示。这个 Builder 被 panelAddpanelEditpanelDel 三个弹窗面板调用,作为它们的底层遮罩层。

  /** 新建片单弹窗面板(名称 + 类型输入) */
  @Builder
  panelAdd(onClose: () => void) {
    Stack() {
      this.modalOverlay(onClose)
      Column({ space: 12 }) {
        Text('新建片单').fontSize(15).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

        Column({ space: 6 }) {
          Text('片单名称').fontSize(9).fontColor(COLORS.sub)
          TextInput({ text: this.formName, placeholder: '如:深夜追剧 · 一口气刷完' })
            .fontSize(11).fontColor(COLORS.title)
            .backgroundColor(COLORS.chip).borderRadius(8)
            .onChange((value: string) => {
              this.formName = value;
            })
        }
        .width('100%').alignItems(HorizontalAlign.Start)

        Column({ space: 6 }) {
          Text('片单类型').fontSize(9).fontColor(COLORS.sub)
          TextInput({ text: this.formTag, placeholder: '如:悬疑 / 古装 / 甜宠' })
            .fontSize(11).fontColor(COLORS.title)
            .backgroundColor(COLORS.chip).borderRadius(8)
            .onChange((value: string) => {
              this.formTag = value;
            })
        }
        .width('100%').alignItems(HorizontalAlign.Start)

        Row({ space: 10 }) {
          Text('取消').fontSize(12).fontColor(COLORS.sub)
            .layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 9, bottom: 9 }).backgroundColor(COLORS.chip).borderRadius(9)
            .onClick(() => onClose())
          Text('创建').fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            .layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 9, bottom: 9 }).backgroundColor(COLORS.gold).borderRadius(9)
            .onClick(() => {
              this.saveDrama();
            })
        }
        .width('100%')
      }
      .width('78%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Center)
  }

panelAdd 是新建片单弹窗面板,采用 Stack 层叠结构——底层是 modalOverlay 遮罩,上层是 78% 宽度的弹窗内容卡片。弹窗内容包含标题"新建片单"、两个 TextInput 输入框(片单名称和片单类型,onChange 回调将输入值同步到 formNameformTag 状态)和底部操作按钮行。按钮行包含"取消"(调用 onClose 关闭弹窗)和"创建"(调用 saveDrama() 保存新建片单)。panelEditpanelDel 的结构与 panelAdd 类似,分别处理编辑和删除确认场景,通过不同的回调方法(updateDrama()delDrama())执行业务逻辑。

三个弹窗面板共同构成了影库片单的模态交互层,与主内容层通过 Stack 层叠组合,通过 @State 布尔开关按需挂载和卸载。这种"条件渲染 + Stack 层叠"的弹窗架构是 ArkUI 中实现模态交互的标准模式,相比系统级 Dialog 组件具有更高的样式定制自由度。


九、技术特性对比

技术维度 传统字幕方案 本应用 AI 字幕方案(Speech Kit 6.1.1)
字幕来源 预置 SRT/ASS 字幕文件 AI 实时语音识别 + 翻译生成
语言配置 固定单一语言 sourceLanguage(zh/en)+ targetLanguage(zh/en/zh-en)双向可配
字号调整 需修改字幕文件或播放器设置 fontSize 四档枚举实时切换(SMALL/NORMAL/BIG/LARGE)
颜色定制 需修改字幕样式文件 fontColor 五预设色一键切换(ResourceColor)
无字幕资源 无法观看 生肉资源 AI 自动补字幕
双语字幕 需特殊字幕文件支持 targetLanguage=‘zh-en’ 原生支持中英对照
音频输入 不涉及 AICaptionController.writeAudio() 持续写入 PCM 流
运行时感知 onPrepared/onError 回调实时状态感知
主题适配 与应用主题割裂 影院黑+鎏金黄深色主题深度融合
布局多样性 单一播放器界面 4 Tab 四种完全不同布局(横滑/进度条/五区块/大卡)
动效体系 无或依赖第三方库 setInterval 呼吸动画全局联动(封面/柱图/进度条/指示灯)
数据管理 无追剧管理 @Observed 模型 + CRUD 弹窗系统(新建/编辑/删除确认)

十、总结


通过对这个影视追剧平台完整源码的逐段深入分析,我们可以清晰地看到 HarmonyOS ArkUI 框架在构建复杂业务场景应用时的强大能力和优雅范式。从技术架构层面来看,整个应用采用了"接口约束 → 常量定义 → 辅助函数 → 数据模型 → 组件主体 → Builder 函数群"的六层分层架构,每一层职责明确、边界清晰。ColorPaletteTabMetaGenreEntryLangOptionSizeOption 等接口构成了类型契约层,在编译期就锁定了数据结构的合法性;COLORS 常量和各类 Mock 数据数组构成了数据规范层,为主题修改和数据替换提供了集中管理入口;genreColordayColorsizeNamelangNamecolorName 等辅助函数构成了工具函数层,将语义映射逻辑从 UI 代码中解耦;DramaItemFollowItemCaptionSceneUserStat 四个 @Observed 类构成了响应式数据模型层,为 UI 提供了深度观察的数据源。

在 AI 字幕技术层面,本应用完整展示了 HarmonyOS 6.1.1 Speech Kit 的 AICaptionComponent 组件及其四大新增字段的实战用法。sourceLanguage 字段通过源语言二选一(中文/英文)的交互设计,让用户能够指定 AI 语音识别引擎的语种假设;targetLanguage 字段通过源语言联动目标语言的智能逻辑,在中文源时自动锁定中文、在英文源时提供中文/英文/中英双语三选项,避免了无效组合;fontSize 字段通过 AICaptionFontSize 四档枚举(小号/标准/大号/超大),满足不同视力和观看距离下的可读性需求;fontColor 字段通过五个预设色彩选择(经典白/暖阳黄/薄荷绿/云朵蓝/樱花粉),让字幕颜色也能成为观影氛围的一部分。AICaptionControllerwriteAudio() 方法通过持续写入 PCM 音频流驱动 AI 识别引擎工作,onPreparedonError 回调为应用提供了完整的运行时状态感知能力。整个 AI 字幕 Tab 通过预览、语言、外观、代码、场景五个区块,将这一技术能力以可视化的方式完整呈现。

在 UI 设计层面,"影院黑+鎏金黄"深色主题的选择并非偶然——影院黑 #101014 模拟了电影院熄灯后的沉浸环境,鎏金黄 #F0B429 呼应了影院灯光和 VIP 金卡的经典意象,两者结合营造出"专业影视平台"的视觉气质。四个 Tab 页面采用了完全不同的布局风格:影库 Tab 的横滑海报大卡墙 + 三列片型宫格、追剧 Tab 的进度条清单 + 月度柱状图、AI 字幕 Tab 的五区块特性页、我的 Tab 的渐变大卡 + 功能清单行,实现了"四 Tab 四布局"的视觉差异化设计目标。setInterval 驱动的呼吸动画(breath 状态每秒翻转)让海报透明度、柱状图柱高、进度条颜色、状态指示灯等多个元素产生有节奏的微动效,为静态界面注入了生命力。三层弹窗系统(modalOverlay 遮罩 + panelAdd/panelEdit/panelDel 面板)通过 @State 布尔开关按需挂载,配合条件渲染和 Stack 层叠实现了完整的模态交互能力。

从工程实践角度来看,这份代码体现了多个值得借鉴的最佳实践:@Observed 装饰器确保数据模型属性变化能被框架深度感知;@State 状态变量建立了 UI 与数据的响应式绑定;@Builder 函数将复杂 UI 拆分为可复用的构建单元;ForEach 的键值生成函数确保列表差分渲染的正确性;aboutToAppearaboutToDisappear 生命周期回调实现了定时器的创建-销毁成对管理;try-catch 块处理音频写入可能发生的运行时异常;slice() 创建新数组引用触发 @State 变化检测的响应式刷新技巧。这些实践共同构成了一个健壮、可维护、可扩展的 HarmonyOS 应用工程范式,为开发者构建自己的复杂业务应用提供了极具参考价值的技术蓝本。

附录:DevEco Studio 创建新项目与查看 SDK 版本

本章节演示如何使用 DevEco Studio 创建一个 HarmonyOS 新项目,并查看当前 IDE 已安装的 SDK 版本,适合作为其他技术博文的补充操作指南。


一、创建新项目

1.1 进入欢迎界面

启动 DevEco Studio 后,首先看到的是欢迎界面。左侧导航栏默认选中 “项目”,右侧提供三个主要入口:

  • 新建项目:从头创建新项目
  • 打开项目:打开本地已有项目
  • 克隆仓库:从 Git 等版本控制拉取代码

点击 “新建项目” 按钮,进入项目创建向导。

在这里插入图片描述

1.2 选择项目模板

在弹出的"新建项目"对话框中,左侧分类标签提供了两种项目类型:

类型 说明
应用(Application) 开发标准的 HarmonyOS 应用,具备完整的 Ability 生命周期
元服务(Atomic Service) 开发轻量级的原子化服务,无需安装即可使用

选择 “应用” 标签后,右侧展示多种模板。对于大多数场景,推荐选择 “Empty Ability” —— 这是一个最基础的入门模板,仅包含 Hello World 功能,适合从零开始构建应用。

在这里插入图片描述

1.3 配置项目信息

点击 “下一步” 后,进入项目配置界面,需要填写以下核心参数:

配置项 示例值 说明
项目名称(Project name) rollboat 应用的项目名称,建议使用英文命名
包名(Bundle name) com.rollboat.myapplication 应用唯一标识,采用反向域名格式
保存路径(Save location) D:\CodeFactory\rollboat 项目本地存储路径,避免使用中文和空格
兼容 SDK(Compatible SDK) 6.1.1(24) 目标 HarmonyOS API 版本,点击"查看参考"可了解各版本差异
模块名称(Module name) entry 主模块名称,默认 entry 为应用入口模块
设备类型(Device types) ☑ Phone 勾选目标设备:Phone / Tablet / 2in1 / Car / Wearable / TV

右侧预览区会实时展示当前模板的默认效果 —— 一个居中显示的 “Hello World” 文本。

在这里插入图片描述

1.4 完成创建

确认配置无误后,点击右下角 “完成” 按钮,IDE 将自动执行以下操作:

  1. 生成项目骨架(Stage 模型目录结构)
  2. 执行 ohpm install 安装依赖
  3. 运行 Hvigor 构建初始化(Build Init

构建日志中显示 “退出代码为 0” 表示项目初始化成功。

在这里插入图片描述

1.5 项目结构概览

创建完成后,左侧项目面板展示的是标准的 Stage 模型 目录结构:

rollboat/
├── .hvigor/                   # Hvigor 构建工具缓存
├── .idea/                     # IDE 配置文件
├── AppScope/                  # 应用级全局配置
│   └── app.json5
├── entry/                     # 主模块(入口模块)
│   ├── src/main/ets/
│   │   ├── entryability/      # Ability 生命周期管理
│   │   │   └── EntryAbility.ets
│   │   └── pages/             # UI 页面
│   │       └── Index.ets      # 首页(默认 Hello World)
│   ├── src/main/resources/    # 资源文件
│   ├── module.json5           # 模块配置
│   └── build-profile.json5    # 构建配置
├── oh_modules/                # OHPM 依赖包
├── build-profile.json5        # 工程构建配置
├── hvigorfile.ts              # Hvigor 构建脚本
└── oh-package.json5           # 包管理配置

核心文件 Index.ets 的默认代码如下,采用 ArkTS 声明式 UI 语法:

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    RelativeContainer() {
      Text(this.message)
        .id('HelloWorld')
        .fontSize($r('app.float.page_text_font_size'))
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          this.message = 'Welcome';
        })
    }
    .height('100%')
    .width('100%')
  }
}
关键语法 作用
@Entry 标记为页面入口,可用于路由跳转
@Component 声明为自定义组件
@State 状态变量,数据变更时自动触发 UI 刷新
RelativeContainer 相对布局容器,替代传统线性布局
.onClick() 点击事件,此处点击后文本变为 “Welcome”

打开右侧 Previewer(预览器),选择 Phone 设备,即可实时预览 Hello World 效果,无需连接真机或启动模拟器。

在这里插入图片描述


二、查看 SDK 版本

2.1 查看 HarmonyOS SDK

DevEco Studio 安装时已内置 HarmonyOS SDK,无需单独下载。通过以下路径查看:

文件 → 设置 → HarmonyOS SDK(或快捷键 Ctrl + Alt + S 搜索 “HarmonyOS SDK”)

在设置面板中,可以看到当前已安装的 SDK 版本信息:

名称 阶段 状态
HarmonyOS 6.1.1 Release ✅ 已安装

界面顶部提示:“HarmonyOS SDK 已经包含在 IDE,无需单独安装”,省去了手动配置 SDK 的繁琐步骤。

在这里插入图片描述

2.2 查看 ArkUI-X SDK(跨平台扩展)

如果项目需要将 ArkUI 框架扩展到多个 OS 平台(Android / iOS / OpenHarmony),还需要配置 ArkUI-X SDK。路径如下:

文件 → 设置 → 语言和框架 → ArkUI-X

在这里可以查看已安装和可选的 ArkUI-X SDK 版本:

版本 SDK 版本号 阶段 状态
API Version 24 6.1.1.100 Release ✅ 已安装
API Version 23 6.1.0.28 Beta1 未安装
API Version 22 6.0.2.112 Release 未安装

安装路径示例:D:\DevTools\ArkUI-X\sdk

说明:ArkUI-X 允许开发者使用一套 ArkTS 主代码,同时构建多平台应用。如果仅开发 HarmonyOS 原生应用,无需额外安装 ArkUI-X SDK。

在这里插入图片描述


三、小结

步骤 操作 关键点
创建项目 欢迎页 → 新建项目 → 选择 Empty Ability 模板 → 配置项目信息 → 完成 使用 Stage 模型 + ArkTS 语言
查看 SDK 设置 → HarmonyOS SDK SDK 已内置,无需手动安装
跨平台扩展 设置 → ArkUI-X 根据需要安装对应 API 版本

至此,DevEco Studio 的项目创建与 SDK 环境确认全部完成,可以开始 HarmonyOS 应用的功能开发。


本文基于 DevEco Studio 6.1.1 Release 版本编写,不同版本界面可能存在细微差异。

Logo

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

更多推荐