深色极夜美学遇上 AI 实时字幕:HarmonyOS ArkUI 音乐流媒体应用的全栈解析,从 Speech Kit 四大新特性到四 Tab 差异化布局的工程实践
一、技术前言:当音乐流媒体遇见 HarmonyOS AI 语音能力

HarmonyOS 的 ArkUI 框架是华为为全场景分布式设备打造的声明式 UI 开发范式。它以 ArkTS 语言为根基,在 TypeScript 的类型安全之上引入了 @Entry、@Component、@State、@Builder、@Observed 等装饰器体系,让开发者可以用接近自然语言的方式描述界面的结构与状态。与传统的命令式 UI 编程不同,ArkUI 采用数据驱动渲染,当状态变量发生变更时,框架会自动精确地刷新与该状态绑定的组件节点,无需手动调用 findViewById 或 setText。这种范式极大地简化了复杂界面的状态管理,特别适合需要频繁局部刷新的音乐流媒体类应用。

Speech Kit 是 HarmonyOS 提供的场景化语音服务套件,涵盖了语音识别、语音合成、声纹识别以及 AI 字幕等多维能力。其中,AICaptionComponent 是 Speech Kit 在 HarmonyOS 6.1.1 版本中重点强化的组件,它为开发者提供了一个开箱即用的实时字幕渲染界面。开发者只需将音频流通过 AICaptionController 的 writeAudio 方法推入组件,系统即可自动完成语音识别、文本转写、语言翻译及字幕渲染的全流程。这使得音乐流媒体应用可以在不引入额外第三方 SDK 的前提下,为用户提供外语歌词的实时翻译、播客内容的双语对照等高价值功能。

HarmonyOS 6.1.1 为 AICaptionOptions 带来了四大重磅新增字段,这也是本应用重点演示的核心特性。第一是 sourceLanguage(源语言),取值为 'zh'(中文)或 'en'(英文),用于告知字幕引擎用户音频的原始语种。第二是 targetLanguage(目标语言),取值为 'zh'、'en' 或 'zh-en'(中英双语),用于指定字幕翻译的方向,当源语言为英文时可选择翻译为中文、保留英文或双语对照显示,而中文源时由于不存在有意义的翻译方向,目标语言会锁定为 'zh'。第三是 fontSize(字体大小),采用 AICaptionFontSize 枚举类型,提供 SMALL(小号)、NORMAL(标准)、BIG(大号)、LARGE(超大)四档可选值,让用户根据自身视力和场景灵活调节。第四是 fontColor(字体颜色),类型为 ResourceColor,支持任意合法的颜色资源值,开发者可以预设多种配色方案供用户选择。这四个字段的加入使得 AI 字幕从"能看"进化到"好看、好用、可定制"。

音乐流媒体是当下移动互联网最具代表性的内容消费场景之一。用户在听歌时往往面临一个痛点:外语歌曲的歌词难以理解,尤其是欧美流行、日语动漫歌曲等非母语内容。传统做法是依赖人工翻译的静态歌词文件,但这存在更新滞后、翻译质量参差不齐、缺乏实时性等问题。将 Speech Kit 的 AI 字幕能力嵌入音乐流媒体应用后,用户可以在播放外语歌曲或播客时获得实时的双语字幕,甚至可以边听边看译文来辅助语言学习。这为应用赋予了差异化的竞争壁垒。

本应用名为"云韵音乐",采用极夜黑(#0F0A1E)+ 流光紫(#9B6BFF)+ 霓虹青(#4FE3D0)的深色主题方案。深色主题不仅是审美偏好,更在 OLED 屏幕上具有显著的省电效果,同时降低了暗光环境下的视觉刺激。应用设计了四个功能完全独立的 Tab 页面:乐库(每日推荐 + 歌单墙管理)、排行(热歌榜 + 月度柱状图)、AI 字幕(Speech Kit 特性演示页)、我的(用户信息 + 功能清单)。每个 Tab 的布局结构都经过差异化设计,避免了千篇一律的列表式布局。同时应用内置了完整的弹窗系统,支持歌单的新建、编辑、删除全流程操作,是一个功能完备的工程级示例。

从架构设计角度看,本应用将代码分为六个清晰的层次:颜色系统、常量定义、辅助函数、数据模型、组件主体、Builder 函数群。每一层各司其职,颜色集中管理便于主题切换,常量与 Mock 数据分离便于后续接入真实接口,辅助函数封装可复用的映射逻辑,数据模型使用 @Observed 装饰器实现可观察的数据响应,组件主体集中管理状态与业务方法,Builder 函数群则负责将界面拆分为高内聚的渲染单元。这种分层设计在保持单文件可读性的同时,也为未来拆分多文件模块化奠定了基础。

二、整体架构流程图
三、模块导入与 Speech Kit 依赖引入
3.1 导入语句
import { AICaptionComponent, AudioData, AICaptionOptions, AICaptionController, AICaptionFontSize } from '@kit.SpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';
这两行导入语句是整个 AI 字幕功能的基石。第一行从 @kit.SpeechKit 中批量导入了五个核心符号。AICaptionComponent 是 AI 字幕的 UI 组件,它继承自 ArkUI 的组件体系,可以直接在 build() 方法中作为布局元素使用。AudioData 是音频数据的封装接口,包含一个 Uint8Array 类型的 data 字段,用于承载 PCM 格式的原始音频字节流。AICaptionOptions 是字幕配置选项接口,除了 HarmonyOS 6.1.1 新增的四大字段外,还包含 initialOpacity(初始透明度)、onPrepared(就绪回调)和 onError(错误回调)等基础字段。AICaptionController 是字幕控制器类,提供了 writeAudio 方法用于向组件推入音频数据,是驱动字幕识别运转的核心控制器。AICaptionFontSize 是字号枚举类型,定义了 SMALL/NORMAL/BIG/LARGE 四档可选值。
第二行从 @kit.BasicServicesKit 中导入了 BusinessError 类型。这是 HarmonyOS 统一的错误处理接口,包含 code(错误码)和 message(错误描述)两个字段。在 AICaptionOptions 的 onError 回调中,参数类型即为 BusinessError,通过它开发者可以精确捕获字幕服务运行过程中的各类异常,例如网络中断、模型加载失败、音频格式不支持等,进而向用户展示友好的错误提示信息。
四、颜色系统设计
4.1 颜色接口定义
interface ColorPalette {
bg: string;
card: string;
chip: string;
title: string;
sub: string;
text3: string;
purple: string;
purpleD: string;
cyan: string;
red: string;
green: string;
gold: string;
line: string;
tabOn: string;
mask: string;
}
ColorPalette 接口将页面用到的所有颜色字段集中声明为一个类型契约。这种设计的好处在于:当需要切换主题(例如从深色主题切换到浅色主题)时,只需创建一个新的 ColorPalette 实现对象替换 COLORS 常量即可,无需在代码中逐一搜索替换散落的颜色值。接口中的字段涵盖了界面所需的全部色彩角色:bg 是页面背景色,card 是卡片背景色,chip 是标签芯片背景色,title 是主标题文字色,sub 是次要文字色,text3 是三级辅助文字色,purple 和 purpleD 是品牌主色及其深色变体,cyan 是霓虹青强调色,red/green/gold 分别用于警示/正向/高亮场景,line 是分割线颜色,tabOn 是底部 Tab 选中色,mask 是弹窗遮罩色。
4.2 深色主题色板常量
const COLORS: ColorPalette = {
bg: '#0F0A1E',
card: '#1C1430',
chip: '#261B42',
title: '#F2EDFF',
sub: '#B9A8E0',
text3: '#7E6FA8',
purple: '#9B6BFF',
purpleD: '#6E3FE0',
cyan: '#4FE3D0',
red: '#FF6B81',
green: '#6ED491',
gold: '#FFD36E',
line: '#2E2350',
tabOn: '#9B6BFF',
mask: 'rgba(8,4,20,0.66)'
};
这段代码将 ColorPalette 接口实例化为一个具体的深色主题常量 COLORS。极夜黑 #0F0A1E 作为背景色,它不是纯黑而是带有极微弱紫调的深色,在保持暗光环境舒适度的同时避免了纯黑带来的生硬感。卡片背景 #1C1430 比背景略亮一些,形成微妙的层次感。流光紫 #9B6BFF 是品牌主色,饱和度适中、亮度较高,在深色背景上具有出色的辨识度。霓虹青 #4FE3D0 作为强调色用于选中状态、数据高亮等场景,其冷色调与紫色的暖色调形成互补色对比,视觉张力十足。
值得注意的是 mask 字段使用了 rgba(8,4,20,0.66) 的半透明写法,透明度为 66%,既能有效遮挡底层内容又不至于完全阻断视觉联系。这种半透明遮罩是移动端弹窗的常见做法,在保持上下文感知的同时突出弹窗主体。整个色板的设计遵循了 60-30-10 的配色比例原则:极夜黑占据约 60% 的面积(背景),流光紫占据约 30%(卡片、按钮、渐变),霓虹青及其他强调色占据约 10%(选中态、图标高亮)。
五、常量定义体系
5.1 底部导航 Tab 常量
interface TabMeta {
icon: string;
label: string;
}
const TAB_LIST: TabMeta[] = [
{ icon: '🎵', label: '乐库' },
{ icon: '🏆', label: '排行' },
{ icon: '🗣', label: 'AI字幕' },
{ icon: '👤', label: '我的' }
];
TabMeta 接口定义了底部导航项的元数据结构,包含 icon(图标 emoji)和 label(标签文字)两个字段。TAB_LIST 常量数组定义了四个 Tab 项的数据。使用 emoji 作为图标是一种轻量化的设计方案,无需引入图标字体或 SVG 资源文件,在跨设备渲染时也能保持一致的视觉效果。四个 Tab 分别对应乐库、排行、AI 字幕、我的四个功能模块,覆盖了音乐流媒体应用的核心场景。
5.2 横滑分类标签
const CATE_TAGS: string[] = ['推荐', '华语', '欧美', '日语', '电子', '说唱', '民谣', '古典'];
CATE_TAGS 数组定义了头部横滑分类条的音乐流派标签。这八个标签涵盖了中文用户最常见的音乐分类需求,从大而全的"推荐"到具体语种的"华语"“欧美”“日语”,再到曲风的"电子"“说唱”“民谣”“古典”。在实际应用中,点击这些标签会触发后端的分类筛选请求,本示例中通过 cateIdx 状态变量记录选中索引,以高亮方式反馈用户选择。
5.3 每日推荐歌曲数据
interface SongItem {
cover: string;
name: string;
artist: string;
meta: string;
}
const DAILY_SONGS: SongItem[] = [
{ cover: '🌠', name: '流光隧道', artist: '云韵少年团', meta: '因你常听电子流行而推荐' },
{ cover: '🌙', name: '夜航星', artist: '林月声', meta: '昨日单曲循环 12 次' },
{ cover: '🚇', name: '雾中地铁站', artist: '白噪计划', meta: '深夜歌单高频曲目' }
];
SongItem 接口定义了每日推荐歌曲的数据结构,包含封面 emoji、歌曲名、歌手名和推荐理由四个字段。DAILY_SONGS 数组提供了三条 Mock 数据。每条数据的 meta 字段模拟了推荐算法的解释性文案——“因你常听电子流行而推荐”“昨日单曲循环 12 次”“深夜歌单高频曲目”,这些文案让推荐结果具有可解释性,增强用户对推荐系统的信任感。在真实应用中,这些数据应由推荐服务接口动态返回。
5.4 AI 字幕语言常量
interface LangOption {
code: string;
name: string;
}
const SRC_LANGS: LangOption[] = [
{ code: 'zh', name: '中文' },
{ code: 'en', name: '英文' }
];
const TGT_LANGS_EN: LangOption[] = [
{ code: 'zh', name: '中文' },
{ code: 'en', name: '英文' },
{ code: 'zh-en', name: '中英双语' }
];
LangOption 接口将语言码与展示名配对封装,便于在 UI 中通过 ForEach 渲染语言选择按钮。SRC_LANGS 定义了源语言的两个选项——中文和英文,对应 Speech Kit 的 'zh' 和 'en' 取值。TGT_LANGS_EN 定义了当源语言为英文时可选的目标语言——中文、英文和中英双语三种。注意这里没有定义中文源时的目标语言选项,因为中文源时目标语言被锁定为 'zh',不存在翻译方向选择。这种设计逻辑直接体现在 UI 上:当用户选择中文源语言时,目标语言区域会显示一个锁定提示而非可选项列表。
5.5 字幕字号与颜色常量
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: '超大' }
];
const CAPTION_FONT_COLORS: string[] = ['#FFFFFF', '#FFE9B0', '#9CE8B5', '#9CD0FF', '#FFB3C1'];
SizeOption 接口将 AICaptionFontSize 枚举值与中文展示名配对。SIZE_OPTIONS 数组完整覆盖了 SMALL/NORMAL/BIG/LARGE 四档字号。这四档字号从"小号"到"超大"依次递增,满足不同用户的视力需求和不同使用场景(如车内远距离观看需要超大字号)。CAPTION_FONT_COLORS 数组预设了五种字幕颜色:经典白(#FFFFFF,最通用的字幕色)、暖阳黄(#FFE9B0,暖色调适合夜间阅读)、薄荷绿(#9CE8B5,清新护眼)、云朵蓝(#9CD0FF,冷色调科技感)、樱花粉(#FFB3C1,柔和浪漫)。这五种颜色经过精心调配,在深色字幕区域上都具有足够的对比度。
5.6 月度柱状图常量
const MONTH_IDX: number[] = [0, 1, 2, 3, 4, 5];
const MONTH_LABELS: string[] = ['3月', '4月', '5月', '6月', '7月', '8月'];
const MONTH_HOURS: number[] = [86, 94, 78, 102, 118, 126];
const MONTH_MAX: number = 140;
这组常量用于排行 Tab 底部的月度收听时长柱状图。MONTH_IDX 是月份索引数组,用于 ForEach 遍历。MONTH_LABELS 是横轴标签。MONTH_HOURS 是每月收听小时数,数据呈现逐月递增的趋势(从 3 月的 86 小时增长到 8 月的 126 小时),暗示用户使用频率持续上升。MONTH_MAX 是柱状图的最大刻度值,设为 140 小时,比最大数据值 126 略大,确保最高柱不会顶满图表区域,留出视觉呼吸空间。
六、辅助函数设计
6.1 趋势颜色与图标映射
function trendColor(t: string): string {
if (t === 'up') { return COLORS.red; }
if (t === 'down') { return COLORS.green; }
return COLORS.text3;
}
function trendIcon(t: string): string {
if (t === 'up') { return '↑ 上升'; }
if (t === 'down') { return '↓ 下降'; }
return '— 持平';
}
trendColor 和 trendIcon 两个函数将榜单趋势状态码映射为颜色值和图标文案。注意这里有一个有趣的设计:上升趋势用红色(COLORS.red)表示,下降趋势用绿色(COLORS.green)表示。这与中国股市的"红涨绿跌"惯例一致,但在欧美市场惯例中恰好相反。由于这是一个面向中文用户的音乐应用,采用红涨绿跌是合理的选择。持平状态使用三级文字色 text3 进行弱化处理,视觉上不与上升和下降抢夺注意力。
6.2 字号与语言码转名函数
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';
}
function langName(code: string): string {
if (code === 'zh') { return '中文'; }
if (code === 'en') { return '英文'; }
return '中英双语';
}
sizeName 函数将 AICaptionFontSize 枚举值转换为对应的英文枚举名字符串,这个函数主要用于 AI 字幕 Tab 中的实时代码预览区域——在模拟显示 AICaptionOptions 对象的 fontSize 字段值时,需要展示枚举的原始英文名而非中文展示名,以让开发者直观理解配置对象的实际结构。langName 函数将语言码转换为中文名,用于语言组合摘要行的显示。两个函数都采用 if-else 链式判断,由于取值范围有限,这种写法清晰直观,无需引入额外的映射表。
6.3 颜色预设转中文名
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 '樱花粉';
}
colorName 函数将颜色十六进制值转换为富有诗意的中文颜色名。通过下标引用 CAPTION_FONT_COLORS 数组的方式确保了颜色值与名称的一致性——即使将来修改颜色值,也无需同步修改此函数。五个名称"经典白"“暖阳黄”“薄荷绿”“云朵蓝”"樱花粉"都与音乐应用的文艺气质相符,避免了"颜色 1""颜色 2"这类冰冷的命名。
七、数据模型设计
7.1 歌单条目模型
@Observed export class PlayItem {
name: string;
cover: string;
tag: string;
count: string;
tracks: string;
constructor(name: string, cover: string, tag: string, count: string, tracks: string) {
this.name = name;
this.cover = cover;
this.tag = tag;
this.count = count;
this.tracks = tracks;
}
}
PlayItem 类使用 @Observed 装饰器声明为可观察数据模型。@Observed 是 ArkUI 响应式系统的关键装饰器,它使类的实例在被 @State 管理时,其属性变更能够触发 UI 局部刷新。这对于歌单编辑功能至关重要——当用户修改歌单名称或标签后,对应的歌单卡片需要自动更新显示。类定义了五个字段:歌单名、emoji 封面、流派标签、播放量文本和歌曲数文本。使用 export 关键字导出,便于在多文件项目中复用。构造函数采用全参数设计,确保对象创建时所有字段都被初始化。
const PLAY_LIST: Array<PlayItem> = [
new PlayItem('深夜电台 · 晚安白噪音', '🌙', '轻音乐', '328万', '36 首'),
new PlayItem('街角唱片行 · 华语流行', '🎤', '华语流行', '512万', '45 首'),
new PlayItem('欧美新歌速递 8 月刊', '🌍', '欧美流行', '268万', '30 首'),
new PlayItem('自习室专注白噪音', '📚', '学习专注', '196万', '24 首'),
new PlayItem('晚风汽水 · 日系治愈', '🎐', '日语流行', '154万', '28 首'),
new PlayItem('说唱新声代 Vol.9', '🎧', '说唱', '221万', '32 首'),
new PlayItem('落日琴房 · 古典钢琴', '🎹', '古典', '98万', '20 首'),
new PlayItem('演唱会现场 LIVE 合集', '🎪', '现场Live', '176万', '26 首')
];
PLAY_LIST 数组提供了八条精选歌单的 Mock 数据,覆盖了轻音乐、华语流行、欧美流行、学习专注、日语流行、说唱、古典、现场 Live 等多种音乐类型。每条歌单名都经过精心命名——“深夜电台 · 晚安白噪音”“街角唱片行 · 华语流行”"晚风汽水 · 日系治愈"等名称富有画面感和情绪色彩,符合音乐应用的文艺调性。播放量和歌曲数以字符串形式存储(如 '328万'、'36 首'),这样可以直接用于 UI 显示而无需二次格式化。
7.2 榜单条目模型
@Observed export class RankItem {
rank: string;
title: string;
artist: string;
heat: string;
trend: string;
constructor(rank: string, title: string, artist: string, heat: string, trend: string) {
this.rank = rank;
this.title = title;
this.artist = artist;
this.heat = heat;
this.trend = trend;
}
}
const RANK_LIST: Array<RankItem> = [
new RankItem('1', '流光隧道', '云韵少年团', '98.2万', 'up'),
new RankItem('2', '夜航星', '林月声', '86.5万', 'up'),
new RankItem('3', '极夜来信', '星尘合唱团', '79.8万', 'flat'),
new RankItem('4', '霓虹慢跑', 'DJ Cyan', '72.4万', 'down'),
new RankItem('5', '雾中地铁站', '白噪计划', '65.1万', 'up'),
new RankItem('6', '夏夜告白', '苏晚晴', '58.9万', 'down'),
new RankItem('7', '月光造句法', '陈屿', '51.6万', 'up'),
new RankItem('8', '晚风手写信', '叶声声', '47.3万', 'flat')
];
RankItem 类同样使用 @Observed 装饰器,定义了榜单条目的数据结构,包含名次、歌名、歌手、热度文本和趋势状态五个字段。RANK_LIST 提供了八条热歌榜数据,趋势字段 trend 的取值为 'up'(上升)、'down'(下降)和 'flat'(持平),这些值会被前文介绍的 trendColor 和 trendIcon 函数消费。注意歌单和榜单中有部分歌曲重名(如"流光隧道"“夜航星”“雾中地铁站”),这模拟了推荐歌曲同时上榜的真实场景,增强了数据的真实性。
7.3 字幕场景模型
@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;
}
}
const SCENE_LIST: Array<CaptionScene> = [
new CaptionScene('外语歌词翻译', '欧美新歌同步中文歌词,边听边理解', 'en', 'zh-en'),
new CaptionScene('英文播客学习', '锻炼听力,双语对照不漏细节', 'en', 'zh'),
new CaptionScene('华语歌曲跟唱', '中文歌词原样展示,跟唱更轻松', 'zh', 'zh'),
new CaptionScene('日推外语歌速览', '快速了解歌曲内容再决定收藏', 'en', 'zh'),
new CaptionScene('演唱会现场字幕', '现场版音频实时转文字', 'zh', 'zh')
];
CaptionScene 类是 AI 字幕 Tab 中场景推荐列表的数据模型,包含场景名、场景说明、推荐源语言和推荐目标语言四个字段。SCENE_LIST 提供了五个与音乐行业相关的字幕使用场景,每个场景都预配置了合理的语言组合。例如"外语歌词翻译"场景推荐英文源+中英双语目标,"英文播客学习"场景推荐英文源+中文目标,"华语歌曲跟唱"场景推荐中文源+中文目标。用户点击场景条目时,应用会自动套用该场景的语言组合到 AICaptionOptions 中,实现一键配置的便捷体验。
7.4 用户功能清单模型
@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;
}
}
const STAT_LIST: Array<UserStat> = [
new UserStat('🎵', '本地音乐', '128 首', true),
new UserStat('⬇', '最近下载', '36 首 · 臻品音质', true),
new UserStat('🕘', '最近播放', '89 首', true),
new UserStat('❤', '我喜欢的音乐', '486 首', true),
new UserStat('📻', '我的电台', '每晚 22:00 更新', true),
new UserStat('🗣', 'AI 字幕偏好', '源 zh · 目标 zh', true),
new UserStat('🎁', '黑胶会员特权', '2026-12-08 到期', true),
new UserStat('⚙', '播放与音质设置', '无损音质 2.0', true)
];
UserStat 类定义了"我的" Tab 中功能清单条目的数据结构,包含图标、功能名、状态值和是否显示右箭头四个字段。arrow 字段的设计体现了数据驱动的 UI 思想——通过布尔值控制箭头图标的显示与否,而非在布局中硬编码条件判断。STAT_LIST 提供了八条功能项数据,从本地音乐管理到最近播放历史,从收藏列表到 AI 字幕偏好,从会员信息到音质设置,覆盖了音乐应用个人中心的典型功能矩阵。其中"AI 字幕偏好"条目的值 '源 zh · 目标 zh' 直接反映了当前字幕语言设置,将 AI 字幕状态与用户中心打通。
八、组件主体:状态管理
8.1 页面组件声明与基础状态
@Entry
@Component
struct Page1111 {
@State currentTab: number = 0;
@State breath: boolean = false;
@State timer: number = -1;
@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 playList: Array<PlayItem> = PLAY_LIST;
@State rankList: Array<RankItem> = RANK_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 = '';
@Entry 装饰器标记此组件为页面入口组件,@Component 声明它是一个 ArkUI 自定义组件。Page1111 结构体内部首先声明了一组 @State 状态变量。currentTab 记录当前选中的 Tab 索引,初始为 0(乐库),它驱动整个页面内容的切换。breath 是呼吸动画的布尔翻转开关,配合 timer 定时器句柄实现每秒翻转的效果,这个动画状态被多个组件消费——头部 Banner 的音符图标透明度、乐库每日推荐的播放图标、排行 Tab 的柱状图柱高波动等,营造出整个页面"活"的呼吸感。
cateIdx 记录头部分类 chips 的选中索引。addModal、editModal、delModal 三个布尔值分别控制新建、编辑、删除三个弹窗的显隐。editIdx 和 delIdx 记录当前正在编辑或删除的歌单索引,用于在弹窗操作中精确定位目标歌单。playList、rankList、sceneList、statList 四个数组状态分别绑定四个 Tab 的列表数据,虽然初始值来自常量数组,但声明为 @State 后,数组的修改(push、splice、slice)能够触发 ForEach 的重新渲染。formName、formTag、editName、editTag 是弹窗表单的输入字段,与 TextInput 组件双向绑定。
8.2 AI 字幕状态字段
private captionController: AICaptionController = new AICaptionController();
@State captionShown: boolean = false;
@State srcLang: string = 'zh';
@State tgtLang: string = 'zh';
@State captionSize: AICaptionFontSize = AICaptionFontSize.NORMAL;
@State captionColor: string = CAPTION_FONT_COLORS[0];
@State captionReady: boolean = false;
@State captionErrMsg: string = '';
@State captionFed: number = 0;
这段代码声明了 AI 字幕功能的核心状态。captionController 是 AICaptionController 实例,注意它使用 private 修饰而非 @State——因为控制器本身是一个引用对象,不需要响应式追踪,它的方法调用(如 writeAudio)是命令式的。captionShown 控制字幕组件的显示与隐藏,通过 @Link 双向绑定到 AICaptionComponent 的 isShown 属性。
srcLang 和 tgtLang 分别记录当前选中的源语言和目标语言,初始值均为 'zh'(中文源+中文目标),这符合中文用户最常用的场景。captionSize 记录字号设置,初始为 AICaptionFontSize.NORMAL(标准)。captionColor 记录字体颜色,初始取 CAPTION_FONT_COLORS[0](经典白)。captionReady 记录字幕服务是否就绪,由 onPrepared 回调触发。captionErrMsg 记录错误信息,由 onError 回调触发。captionFed 记录已写入的音频块数量,用于在 UI 上展示"写入演示音频 ×N"的计数反馈。
九、组件主体:业务方法
9.1 构建字幕配置选项
buildCaptionOptions(): AICaptionOptions {
const opts: AICaptionOptions = {
initialOpacity: 1,
sourceLanguage: this.srcLang,
targetLanguage: this.tgtLang,
fontSize: this.captionSize,
fontColor: this.captionColor,
onPrepared: () => {
this.captionReady = true;
this.captionErrMsg = '';
},
onError: (error: BusinessError) => {
this.captionErrMsg = '字幕服务异常 ' + error.code + ':' + error.message;
}
};
return opts;
}
buildCaptionOptions 方法是 AI 字幕功能的核心方法,它将四个状态变量(srcLang、tgtLang、captionSize、captionColor)组装为 AICaptionOptions 对象。这个方法在 AICaptionComponent 的 options 属性中被调用,由于 ArkUI 的响应式机制,每当任一状态变量变更时,buildCaptionOptions() 会被重新调用生成新的 options 对象,AICaptionComponent 随之刷新配置。
initialOpacity 设为 1 表示字幕初始完全不透明。onPrepared 回调在字幕服务初始化完成时触发,将 captionReady 置为 true 并清空错误信息,UI 上的状态标签会从"初始化中"变为"已就绪"。onError 回调在字幕服务发生异常时触发,将错误码和错误描述拼接为友好文案存入 captionErrMsg,UI 上会显示一个红色警示条。注意这两个回调使用了箭头函数,确保 this 指向组件实例——如果使用普通函数,this 会丢失指向导致状态更新失败。
9.2 源语言切换联动逻辑
switchSourceLang(code: string) {
this.srcLang = code;
if (code === 'zh') {
this.tgtLang = 'zh';
} else {
this.tgtLang = 'zh-en';
}
}
switchSourceLang 方法处理源语言切换时的联动逻辑,这是 AI 字幕语言设置的关键业务规则。当用户将源语言切换为中文时,目标语言自动锁定为 'zh'——因为中文源到中文目标不存在翻译方向,Speech Kit 不支持中文到英文的字幕翻译。当用户将源语言切换为英文时,目标语言默认设为 'zh-en'(中英双语),这是最实用的默认选项,用户可以随后手动切换为纯中文或纯英文。
这种联动设计体现了对 Speech Kit 能力边界的准确理解。在 UI 层面,当源语言为中文时,目标语言选择区域会显示一个带锁图标的锁定提示行,告知用户"中文源锁定中文:无翻译方向,targetLanguage 固定为 zh";当源语言为英文时,目标语言区域才会显示三个可选按钮。这种视觉反馈与业务逻辑的一致性确保了用户不会对系统行为产生困惑。
9.3 演示音频写入
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 方法生成一段演示音频数据并推入字幕控制器。代码首先创建一个 640 字节的 Uint8Array,这个大小对应 16kHz 采样率、16 位位深、单声道格式下约 20 毫秒的 PCM 音频数据。循环体内通过正弦函数生成 440Hz(标准音 A4)的音频波形,振幅为 6000(16 位有符号整数的范围是 -32768 到 32767,6000 约为满幅的 18%,是一个适中的音量)。
每两个字节表示一个采样点(小端序),低字节 block[i] 存储低 8 位,高字节 block[i + 1] 存储高 8 位。位运算 v & 0xFF 取低 8 位,(v >> 8) & 0xFF 取高 8 位。生成完毕后,将字节数组封装为 AudioData 对象,调用 captionController.writeAudio() 推入字幕组件。成功后 captionFed 递增,UI 上的"写入演示音频 ×N"计数器同步刷新。整个调用包裹在 try-catch 中,防止音频写入失败导致应用崩溃。
在实际应用中,这里应该替换为真实的音频流——可以是麦克风录音的实时数据,也可以是音乐播放器的音频解码输出。本示例使用合成正弦波仅用于演示 writeAudio 接口的调用方式。
9.4 歌单增删改方法
openEditPlay(idx: number) {
this.editIdx = idx;
this.editName = this.playList[idx].name;
this.editTag = this.playList[idx].tag;
this.editModal = true;
}
savePlay() {
const name = this.formName === '' ? '未命名歌单' : this.formName;
const tag = this.formTag === '' ? '自建' : this.formTag;
this.playList.push(new PlayItem(name, '🎼', tag, '0', '0 首'));
this.formName = '';
this.formTag = '';
this.addModal = false;
}
updatePlay() {
if (this.editIdx >= 0 && this.editIdx < this.playList.length) {
if (this.editName !== '') {
this.playList[this.editIdx].name = this.editName;
}
if (this.editTag !== '') {
this.playList[this.editIdx].tag = this.editTag;
}
this.playList = this.playList.slice();
}
this.editModal = false;
}
delPlay() {
if (this.delIdx >= 0 && this.delIdx < this.playList.length) {
this.playList.splice(this.delIdx, 1);
}
this.delModal = false;
}
这四个方法共同构成了歌单的增删改(CRUD)业务逻辑。openEditPlay 打开编辑弹窗时回填当前歌单的名称和标签到表单字段,这是编辑功能的标准做法——用户看到的输入框应该预填当前值而非空白。savePlay 处理新建歌单,对空字段进行默认值兜底(空名称默认"未命名歌单",空标签默认"自建"),然后通过 push 向数组末尾添加新条目,最后清空表单并关闭弹窗。
updatePlay 处理编辑保存,首先做索引边界检查防止越界,然后仅在字段非空时更新(允许用户只修改名称不修改标签),最后通过 this.playList = this.playList.slice() 创建数组的新引用。这一步非常关键——ArkUI 的 ForEach 在检测到数组引用不变时可能不会重新渲染列表项,通过 slice() 创建浅拷贝可以确保数组引用变更,触发列表的完整刷新。delPlay 处理删除,同样做边界检查后通过 splice 移除指定索引的元素。
9.5 生命周期方法
aboutToAppear() {
this.timer = setInterval(() => {
this.breath = !this.breath;
}, 1000);
}
aboutToDisappear() {
clearInterval(this.timer);
}
aboutToAppear 是组件生命周期的回调方法,在组件创建后、build 执行前被调用。这里启动一个每 1000 毫秒翻转一次 breath 布尔值的定时器,将定时器句柄存入 this.timer 状态变量。这个呼吸动画是整个应用的"心跳",它驱动多处视觉效果:头部 Banner 的音符图标透明度在 0.6 到 1.0 之间脉动,每日推荐卡的播放图标透明度在 0.65 到 1.0 之间变化,字幕就绪标签的透明度在 0.55 到 1.0 之间闪烁,月度柱状图的柱高在基准值的 ±5% 范围内波动。
aboutToDisappear 在组件销毁前被调用,这里通过 clearInterval 清理定时器。这是防止内存泄漏的关键操作——如果不清理定时器,组件销毁后定时器仍在运行,持续修改已不存在的状态变量会导致错误。这种成对的生命周期管理是 ArkUI 开发的基本规范。
十、组件主体:页面构建
10.1 主构建方法
build() {
Stack() {
Column() {
this.headerMain()
Divider().strokeWidth(1).color(COLORS.line)
Scroll() {
Column() {
if (this.currentTab === 0) {
this.tabLibrary()
} else if (this.currentTab === 1) {
this.tabRank()
} 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)
}
build 方法是 ArkUI 组件的渲染入口。最外层使用 Stack 堆叠布局容器,将主内容与弹窗层叠在一起——Stack 的后入元素会覆盖在先入元素之上,弹窗自然地浮在主内容之上。Stack 内部分为两部分:主内容 Column 和三个条件渲染的弹窗。
主内容 Column 的结构从上到下依次是:headerMain() 头部区域、一条分割线 Divider、可滚动的 Tab 内容区 Scroll、底部导航 tabBar()。Scroll 设置了 layoutWeight(1) 使其占据 Column 中除头部和底部导航之外的剩余空间,scrollBar(BarState.Off) 隐藏滚动条保持界面简洁。Tab 内容区内部通过 if-else 条件分支根据 currentTab 渲染对应的 Builder 函数,这种方式比 Tabs 组件更灵活,因为每个 Tab 的内容完全独立,不会有预加载的性能开销。
三个弹窗(panelAdd、panelEdit、panelDel)通过条件渲染控制显隐——只有对应的状态变量为 true 时才渲染弹窗组件。每个弹窗接收一个 onClose 回调函数,用于在点击遮罩或取消按钮时关闭弹窗。这种回调式弹窗设计保持了弹窗组件的通用性,关闭逻辑由调用方决定。
十一、Builder 函数群:头部区域
11.1 头部渐变 Banner
@Builder
headerMain() {
Column({ space: 12 }) {
Column({ space: 10 }) {
Row() {
Column({ space: 5 }) {
Text('晚上好,听歌的人').fontSize(16).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text('今日推荐 · 流光电台为你备好 12 首晚安曲').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.purpleD)
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
}
.width('100%')
Row({ space: 8 }) {
Text('🎧 在听 328 万人').fontSize(9).fontColor(COLORS.title).opacity(0.9)
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).backgroundColor(COLORS.purpleD).borderRadius(8)
Text('🆕 新歌上线 86 首').fontSize(9).fontColor(COLORS.title).opacity(0.9)
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).backgroundColor(COLORS.purpleD).borderRadius(8)
Text('⭐ 黑胶专属曲库').fontSize(9).fontColor(COLORS.purpleD)
.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.purpleD, 0], [COLORS.purple, 1]] })
headerMain 是头部区域的 Builder 函数,使用 @Builder 装饰器声明为可复用的构建函数。整体结构是一个 Column 包含三部分:渐变 Banner、搜索条、横滑分类 chips。
渐变 Banner 是头部的视觉焦点。它使用 linearGradient 实现从 purpleD(深紫 #6E3FE0)到 purple(亮紫 #9B6BFF)的 135 度对角渐变,角度 135 表示从左上角到右下角。Banner 内部分为上下两行:上行左侧是问候语和推荐语,右侧是一个 44x44 的圆形音乐图标容器,图标透明度随 breath 状态在 0.6 到 1.0 之间脉动;下行是三个状态标签——“在听 328 万人”“新歌上线 86 首”“黑胶专属曲库”,前两个使用深紫背景白字,第三个使用金色背景深紫字以突出会员特权。
文字溢出处理值得注意:推荐语文本设置了 maxLines(1) 和 textOverflow({ overflow: TextOverflow.Ellipsis }),当文字过长时自动截断并显示省略号。这是移动端文本展示的标准做法,防止长文本撑破布局。
11.2 搜索条与分类标签
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)
Scroll() {
Row({ space: 8 }) {
ForEach(CATE_TAGS, (tg: string, idx: number) => {
Text(tg).fontSize(11)
.fontColor(this.cateIdx === idx ? COLORS.cyan : 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]] })
}
搜索条是一个 Row 布局,包含搜索图标、占位文案和语音入口。语音入口的麦克风图标设置了 onClick 事件——点击后直接跳转到 AI 字幕 Tab(currentTab = 2),这种跨功能入口设计让用户从搜索场景自然过渡到语音字幕场景。搜索条使用 borderRadius(20) 实现药丸形圆角,配合 COLORS.card 卡片背景色,在头部渐变背景上形成清晰的层次。
横滑分类 chips 使用 Scroll 包裹 Row 实现,设置 scrollable(ScrollDirection.Horizontal) 允许水平滚动,scrollBar(BarState.Off) 隐藏滚动条。八个标签通过 ForEach 遍历 CATE_TAGS 数组渲染,选中态通过 cateIdx 状态控制:选中时文字为霓虹青、背景为芯片色,未选中时文字为次要色、背景为卡片色。ForEach 的第三个参数是键值生成函数 (tg: string) => tg,使用标签文案本身作为唯一键,确保列表项的高效 diff 更新。
头部 Column 本身也应用了一个从上到下的渐变背景(COLORS.chip 到 COLORS.bg),使头部到内容区的过渡自然平滑,避免了硬边界。
十二、Builder 函数群:乐库 Tab
12.1 每日推荐渐变大卡
@Builder
tabLibrary() {
Column({ space: 12 }) {
Column({ space: 10 }) {
Row() {
Column({ space: 4 }) {
Text('每日推荐').fontSize(17).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text('根据口味生成 · 每天 06:00 更新 · 共 30 首').fontSize(9)
.fontColor(COLORS.title).opacity(0.72)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text('🎧').fontSize(24).opacity(this.breath ? 1 : 0.65)
}
.width(44).height(44).borderRadius(22).backgroundColor(COLORS.purpleD)
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
}
.width('100%')
ForEach(DAILY_SONGS, (s: SongItem, idx: number) => {
Row({ space: 10 }) {
Text((idx + 1).toString()).fontSize(11).fontColor(COLORS.title).opacity(0.6)
Text(s.cover).fontSize(18)
Column({ space: 2 }) {
Text(s.name).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Text(s.artist + ' · ' + s.meta).fontSize(8).fontColor(COLORS.title).opacity(0.68)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('▶').fontSize(12).fontColor(COLORS.title).opacity(0.85)
}
.width('100%').padding(10).backgroundColor(COLORS.purpleD).borderRadius(10)
}, (s: SongItem) => s.name)
}
.width('100%').padding(16).borderRadius(14)
.linearGradient({ angle: 135, colors: [[COLORS.purpleD, 0], [COLORS.purple, 1]] })
tabLibrary 是乐库 Tab 的 Builder 函数。第一个区块是每日推荐渐变大卡,它采用与头部 Banner 相同的 135 度紫色渐变,形成视觉一致性。卡片标题"每日推荐"使用 17 号粗体字,副标题说明推荐机制"根据口味生成 · 每天 06:00 更新 · 共 30 首",增强了用户对推荐系统的信任感。
推荐歌曲列表通过 ForEach 遍历 DAILY_SONGS 渲染三首歌曲。每首歌曲是一个 Row 布局,从左到右依次是:序号(1/2/3)、封面 emoji、歌曲信息列(歌名+歌手+推荐理由)、播放按钮。歌曲信息列的副标题将歌手名与推荐理由用 ' · ' 连接,信息密度高但不显杂乱——这得益于 8 号小字号和 0.68 透明度的弱化处理。每行使用 COLORS.purpleD 深紫背景和 10 号圆角,在渐变大卡内部形成子卡片的层次效果。
12.2 歌单墙标题行与新建入口
Row() {
Text('🎵 精选歌单墙').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text(this.playList.length.toString() + ' 个歌单').fontSize(9).fontColor(COLORS.text3)
Text('+ 新建').fontSize(9).fontColor(COLORS.cyan)
.padding({ left: 9, right: 9, top: 4, bottom: 4 })
.backgroundColor(COLORS.chip).borderRadius(8)
.onClick(() => {
this.addModal = true;
})
}
.width('100%')
歌单墙标题行是一个典型的三段式布局:左侧标题、中间弹性占位 Column().layoutWeight(1)、右侧操作区。右侧显示歌单总数(通过 playList.length 动态计算)和"新建"按钮。点击新建按钮将 addModal 设为 true 触发弹窗。使用 Column().layoutWeight(1) 作为弹性占位是 ArkUI 中实现两端对齐的常用技巧,它占据所有剩余空间将两侧元素推向两端。
12.3 双列卡片歌单墙
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(this.playList, (item: PlayItem, idx: number) => {
Column({ space: 8 }) {
Column() {
Text(item.cover).fontSize(30)
Column().layoutWeight(1)
Row() {
Text(item.tag).fontSize(8).fontColor(COLORS.cyan)
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.backgroundColor(COLORS.purpleD).borderRadius(5)
Column().layoutWeight(1)
Text(item.tracks).fontSize(8).fontColor(COLORS.title).opacity(0.7)
}
.width('100%').margin({ bottom: 8 })
}
.width('100%').height(88).borderRadius(10)
.linearGradient({ angle: 145, colors: [[COLORS.purpleD, 0], [COLORS.purple, 1]] })
Text(item.name).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Text('▶ ' + item.count + ' 次播放').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.openEditPlay(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('48%').padding(10).backgroundColor(COLORS.card).borderRadius(12)
}, (item: PlayItem) => item.name + item.tag)
}
.width('100%')
}
.width('100%')
}
双列卡片歌单墙使用 Flex 布局容器,设置 wrap: FlexWrap.Wrap 允许换行,justifyContent: FlexAlign.SpaceBetween 使每行的两张卡片均匀分布在两侧。每个卡片宽度设为 48%,留出 4% 的间隙。
每张卡片内部包含四个部分:封面渐变块、歌单名、播放量、编辑删除操作行。封面块高度 88 像素,内部从上到下依次是 30 号大字 emoji 封面、弹性占位、底部标签行(流派标签+歌曲数)。封面块同样使用紫色渐变,角度 145 度略大于常规的 135 度,为每张卡片带来微妙的角度差异。
编辑和删除按钮各占 layoutWeight(1) 的等宽空间,通过 textAlign(TextAlign.Center) 居中文字。编辑按钮点击调用 openEditPlay(idx) 打开编辑弹窗,删除按钮点击设置 delIdx 并打开删除确认弹窗。ForEach 的键值生成函数使用 item.name + item.tag(歌单名+标签)作为联合键,确保歌单名称或标签变更时列表项能正确识别和更新。
十三、Builder 函数群:排行 Tab
13.1 榜单标题与热歌榜
@Builder
tabRank() {
Column({ space: 12 }) {
Row() {
Text('🏆 云韵热歌榜').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('实时 · 每小时刷新').fontSize(9).fontColor(COLORS.text3)
}
.width('100%')
ForEach(this.rankList, (item: RankItem, idx: number) => {
Row({ space: 10 }) {
Text(item.rank).fontSize(idx < 3 ? 24 : 17)
.fontColor(idx === 0 ? COLORS.gold : (idx === 1 ? COLORS.purple : (idx === 2 ? COLORS.cyan : COLORS.text3)))
.fontWeight(FontWeight.Bold)
.width(34).textAlign(TextAlign.Center)
Column({ space: 4 }) {
Text(item.title).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.artist).fontSize(9).fontColor(COLORS.sub)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column({ space: 3 }) {
Text(item.heat).fontSize(10).fontColor(COLORS.red).fontWeight(FontWeight.Bold)
Text(trendIcon(item.trend)).fontSize(9).fontColor(trendColor(item.trend))
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(12).borderRadius(12)
.backgroundColor(idx < 3 ? COLORS.chip : COLORS.card)
}, (item: RankItem) => item.rank + item.title)
this.chartCard()
}
.width('100%')
}
排行 Tab 的设计亮点在于前三名的高亮处理。ForEach 遍历 rankList 时,通过 idx 索引判断排名位置:前三名(idx 0/1/2)的名次字号放大到 24 号,分别使用金色(COLORS.gold)、紫色(COLORS.purple)、青色(COLORS.cyan)作为名次颜色,背景使用芯片色(COLORS.chip)进行高亮;第四名及以后名次字号缩小到 17 号,颜色为三级文字色,背景为普通卡片色。这种"金银铜"的视觉等级制度是榜单 UI 的经典设计语言。
每行从左到右依次是:名次编号(固定宽 34 像素居中)、歌曲信息列(歌名+歌手,弹性占满剩余空间)、热度与趋势列(右对齐)。热度值使用红色粗体显示,趋势文案通过 trendIcon 函数生成(如"↑ 上升"),趋势颜色通过 trendColor 函数映射(上升红、下降绿、持平灰)。这种将业务逻辑委托给辅助函数的做法保持了 Builder 函数的简洁性。
13.2 月度柱状图
@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%')
Row({ space: 8 }) {
ForEach(MONTH_IDX, (i: number) => {
Column({ space: 5 }) {
Text(MONTH_HOURS[i].toString()).fontSize(8)
.fontColor(this.breath ? COLORS.cyan : 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.purple, 0], [COLORS.purpleD, 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 月累计 604 小时').fontSize(8).fontColor(COLORS.sub)
Column().layoutWeight(1)
Text('环比 +6.8%').fontSize(8).fontColor(COLORS.green)
}
.width('100%')
}
.width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)
}
chartCard 是一个纯 ArkUI 组件实现的柱状图,没有使用任何图表库。柱状图由一个 Row 容器和 ForEach 生成的六根柱子组成,每根柱子是一个 Column,从上到下包含:数值文本、柱体(一个设置了高度和渐变背景的空 Column)、月份标签。
柱高的计算公式为 Math.max(20, MONTH_HOURS[i] / MONTH_MAX * 110 * (this.breath ? 1.05 : 0.95))。这个公式将实际小时数映射为像素高度:先除以最大值 140 得到比例,再乘以 110 像素的基础高度,最后根据 breath 状态乘以 1.05 或 0.95 实现 ±5% 的呼吸波动。Math.max(20, ...) 确保最小柱高不低于 20 像素,防止数值过小时柱子消失。柱体使用 180 度垂直渐变(从 purple 到 purpleD),呈现从亮到暗的立体效果。
整个 Row 设置 alignItems(VerticalAlign.Bottom) 使所有柱子底部对齐,height(150) 固定图表区域高度。数值标签的颜色也随 breath 状态在霓虹青和次要色之间切换,配合柱体的波动形成统一的呼吸动画。底部汇总行显示"近 6 月累计 604 小时"和"环比 +6.8%",为用户提供数据解读上下文。
十四、Builder 函数群:AI 字幕 Tab(核心特性页)
14.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.purpleD, 0], [COLORS.purple, 1]] })
AI 字幕 Tab 是整个应用的核心特性展示页。顶部特性简介条使用紫色渐变背景,突出展示 HarmonyOS 6.1.1 的四大新增能力。简介文案"AI字幕支持源语言 / 目标语言 / 字体颜色 / 字体大小"直接点明了本页要演示的四个配置维度,让用户在进入页面时就明确知道将要体验什么功能。
14.2 区块一:组件实时预览卡
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%')
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.purpleD : COLORS.purple)
.borderRadius(10)
.onClick(() => {
this.captionShown = !this.captionShown;
})
Row({ space: 5 }) {
Text('写入演示音频').fontSize(12).fontColor(COLORS.cyan)
Text('×' + this.captionFed.toString()).fontSize(9).fontColor(COLORS.cyan)
}
.layoutWeight(1).justifyContent(FlexAlign.Center)
.padding({ top: 9, bottom: 9 })
.backgroundColor(COLORS.chip).borderRadius(10)
.onClick(() => {
this.feedDemoAudio();
})
}
.width('100%')
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)
这是 AI 字幕 Tab 的第一个区块——组件实时预览卡。卡片顶部标题行右侧有一个状态标签,显示"已就绪"或"初始化中",颜色分别对应绿色和金色。当处于初始化状态时,标签的透明度随 breath 状态在 0.55 到 1.0 之间闪烁,给用户一种"正在加载"的动态反馈。
核心组件 AICaptionComponent 通过三个参数进行配置:isShown 通过 @Link 双向绑定到 captionShown 状态,控制字幕的显示与隐藏;controller 传入 captionController 实例,用于调用 writeAudio 推入音频;options 调用 buildCaptionOptions() 方法动态生成配置对象。组件高度固定为 110 像素,使用 1 像素的线条色边框划定字幕渲染区域。
控制按钮行包含两个等宽按钮:左侧的"开启/隐藏字幕"按钮通过翻转 captionShown 状态控制字幕显隐,按钮文案和背景色根据当前状态动态切换(开启时亮紫背景,隐藏时深紫背景);右侧的"写入演示音频"按钮调用 feedDemoAudio() 方法,并实时显示已写入的次数 captionFed。错误信息行通过条件渲染 if (this.captionErrMsg !== '') 控制——仅在发生错误时显示红色警示条,正常状态下不占据布局空间。
14.3 区块二:语言设置卡
Column({ space: 10 }) {
Text('🌐 语言设置').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
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.purple : COLORS.chip)
.borderRadius(10)
.onClick(() => {
this.switchSourceLang(l.code);
})
}, (l: LangOption) => l.code)
}
.width('100%')
语言设置卡的源语言部分。标题行同时展示了字段名 sourceLanguage 和取值范围 'zh' | 'en',这种将 API 字段名直接暴露给开发者的设计使得本页不仅是用户功能页,也是技术参考页。两个源语言选项通过 ForEach 渲染为等宽按钮,选中态使用亮紫背景+白字粗体,未选中态使用芯片背景+次要色文字。点击时调用 switchSourceLang 方法,该方法会根据语言联动设置目标语言。
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.purple : 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.cyan)
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' 时,渲染三个可选目标语言按钮(中文、英文、中英双语),点击直接设置 tgtLang 状态。
底部摘要行使用一个 6 像素的青色小圆点作为视觉标记,后接"当前组合:源 X → 目标 Y"的文案,通过 langName 函数将语言码转为中文名。这行为用户提供当前配置的完整摘要,防止在多次切换后对当前状态产生困惑。
14.4 区块三:外观设置卡
Column({ space: 10 }) {
Text('🎨 外观设置').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
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.purple : COLORS.chip)
.borderRadius(10)
.onClick(() => {
this.captionSize = s.size;
})
}, (s: SizeOption) => s.name)
}
.width('100%')
外观设置卡的字体大小部分。四个字号选项通过 ForEach 渲染为等宽按钮,选中态与语言选择按钮保持一致的视觉风格。标题行右侧标注了枚举类型名 AICaptionFontSize,为开发者提供类型参考。点击按钮直接设置 captionSize 状态,AICaptionComponent 的 options 会随之重建,字幕的字号即时生效。
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.cyan : 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)
字体颜色部分使用五个 26 像素的圆形色块作为选择器,选中态通过 2 像素的霓虹青描边标识,未选中态使用线条色描边。五个色块通过 justifyContent(FlexAlign.SpaceBetween) 均匀分布在整行宽度上。底部的当前选中颜色说明行展示了一个小圆点预览、颜色中文名(通过 colorName 函数转换)和十六进制值,右侧注释"作用于字幕原文与译文"说明该颜色设置的影响范围。
14.5 区块四:实时代码预览卡
Column({ space: 10 }) {
Row() {
Text('💻 AICaptionOptions 实时代码').fontSize(13)
.fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('随设置联动').fontSize(8).fontColor(COLORS.cyan)
}
.width('100%')
Column({ space: 5 }) {
Text('AICaptionOptions = {').fontSize(9).fontColor(COLORS.text3).fontFamily('monospace')
Row({ space: 4 }) {
Text('● sourceLanguage:').fontSize(9).fontColor(COLORS.purple).fontFamily('monospace')
Text("'" + this.srcLang + "'").fontSize(9).fontColor(COLORS.cyan).fontFamily('monospace')
}
.width('100%')
Row({ space: 4 }) {
Text('● targetLanguage:').fontSize(9).fontColor(COLORS.purple).fontFamily('monospace')
Text("'" + this.tgtLang + "'").fontSize(9).fontColor(COLORS.cyan).fontFamily('monospace')
}
.width('100%')
Row({ space: 4 }) {
Text('● fontSize:').fontSize(9).fontColor(COLORS.purple).fontFamily('monospace')
Text(sizeName(this.captionSize)).fontSize(9).fontColor(COLORS.gold).fontFamily('monospace')
}
.width('100%')
Row({ space: 4 }) {
Text('● fontColor:').fontSize(9).fontColor(COLORS.purple).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)
实时代码预览卡是 AI 字幕 Tab 的一个独特设计。它在卡片内部模拟了一个代码编辑器的外观,使用 fontFamily('monospace') 等宽字体和深色背景(COLORS.bg),展示当前 AICaptionOptions 对象的实时结构。
每一行代码由键名和值两部分组成,键名使用紫色(COLORS.purple)高亮,值使用不同颜色:语言值用青色(COLORS.cyan),字号枚举用金色(COLORS.gold),颜色值直接使用当前选中的字幕颜色渲染(.fontColor(this.captionColor))。这种"代码即界面"的设计让开发者直观地看到每次设置变更对应的配置对象变化,是非常巧妙的技术演示手法。
底部注释行"6.1.1 新增字段:sourceLanguage / targetLanguage / fontSize / fontColor"明确标注了这四个字段的版本来源,帮助开发者理解 HarmonyOS 版本演进带来的能力扩展。
14.6 区块五:字幕场景推荐列表
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.purple : (idx % 3 === 1 ? COLORS.cyan : COLORS.gold))
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.cyan)
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Text('套用 ›').fontSize(9).fontColor(COLORS.purple)
}
.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%')
}
场景推荐列表是 AI 字幕 Tab 的第五个也是最后一个区块。五个场景条目通过 ForEach 遍历 sceneList 渲染,每行左侧有一个 4 像素宽的彩色竖条作为视觉标识,颜色按 idx % 3 在紫色、青色、金色之间轮换,为列表增添节奏感。
中间区域是场景信息的三行文本:场景名(粗体)、场景说明(次要色)、语言组合(青色),信息层次清晰。右侧的"套用 ›"文字提示用户可以点击套用。整行设置 onClick 事件——点击后调用 switchSourceLang(item.src) 切换源语言(自动联动目标语言),然后直接设置 this.tgtLang = item.tgt 覆盖为目标场景的推荐值。这种一键套用设计让用户无需手动逐项设置语言组合,极大提升了配置效率。
十五、Builder 函数群:我的 Tab
15.1 用户信息与会员大卡
@Builder
tabMine() {
Column({ space: 12 }) {
Column({ space: 12 }) {
Row({ space: 12 }) {
Column() {
Text('🎧').fontSize(26)
}
.width(54).height(54).borderRadius(27).backgroundColor(COLORS.purpleD)
.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.purpleD)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(COLORS.gold).borderRadius(7)
}
Text('云韵 ID:yunyun_0825 · 已连续听歌 128 天')
.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('486').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('24').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.purpleD, 0], [COLORS.purple, 1]] })
我的 Tab 的用户信息大卡采用与头部 Banner 相同的紫色渐变设计,保持了视觉风格的一致性。卡片上半部分是用户头像(54x54 圆形深紫容器内放耳机 emoji)、用户名"月栖云端"(搭配金色"黑胶 VIP"标签)和用户摘要信息(云韵 ID + 连续听歌天数)。下半部分是三格统计数据——收藏歌曲 486 首、创建歌单 24 个、听歌 1.2 万小时,三格等宽居中对齐,数据用粗体突出,标签用小号弱化文字。
15.2 功能清单行
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%')
}
功能清单行通过 ForEach 遍历 statList 渲染八条功能项。每行从左到右依次是:功能图标 emoji、功能名(弹性占满)、状态值、右箭头(条件渲染)。右箭头通过 if (item.arrow) 条件判断控制显示,虽然当前所有条目的 arrow 都为 true,但这种数据驱动的设计预留了未来某些功能项不显示箭头的灵活性。
每行使用卡片背景色和 10 号圆角,行与行之间通过外层 Column 的 space: 12 间距分隔。这种"卡片列表"布局是个人中心页面的标准设计模式,简洁清晰、信息密度适中。
十六、Builder 函数群:底部导航
16.1 四 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 })
}
底部导航使用 Row 单排布局,四个 Tab 通过 ForEach 渲染,每个 Tab 占据 layoutWeight(1) 等宽空间。选中态通过多维度视觉差异体现:图标字号从 17 放大到 20、透明度从 0.65 提升到 1.0、标签文字从三级色变为紫色(COLORS.tabOn)、字重从常规变为粗体。这种多维度的状态反馈让用户对当前所在页面有明确的感知。
顶部使用 border({ width: { top: 1 }, color: COLORS.line }) 仅设置上边框线,将导航栏与内容区视觉分隔。点击事件直接设置 currentTab 状态,触发主内容区的条件分支切换到对应 Tab 的 Builder 函数。这种简洁的 Tab 切换机制无需 Tabs 组件的复杂配置,适合内容区不需要滑动切换的场景。
十七、Builder 函数群:弹窗系统
17.1 全屏遮罩
@Builder
modalOverlay(onClose: () => void) {
Stack() {
Column().width('100%').height('100%').backgroundColor(COLORS.mask)
}
.width('100%')
.height('100%')
.alignContent(Alignment.Center)
.onClick(() => onClose())
}
modalOverlay 是弹窗系统的通用遮罩组件。它接收一个 onClose 回调函数作为参数,点击遮罩区域时调用该回调关闭弹窗。遮罩使用 COLORS.mask(rgba(8,4,20,0.66))半透明背景色,覆盖全屏。外层 Stack 设置 alignContent(Alignment.Center) 使后续叠加的弹窗面板自动居中显示。
这个设计体现了 Builder 函数的参数化能力——通过传入不同的回调函数,同一个遮罩组件可以服务于多个弹窗场景。这种"遮罩+面板"的组合模式是移动端弹窗的经典实现,遮罩负责点击外部关闭,面板负责具体业务交互。
17.2 新建歌单弹窗
@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.purple).borderRadius(9)
.onClick(() => {
this.savePlay();
})
}
.width('100%')
}
.width('78%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
}
.width('100%')
.height('100%')
.alignContent(Alignment.Center)
}
新建歌单弹窗在 modalOverlay 之上叠加一个 78% 宽度的面板。面板包含标题、两个输入字段(歌单名称和歌单标签)和两个按钮(取消和创建)。TextInput 组件通过 text 参数绑定状态变量、通过 onChange 回调更新状态,实现了双向数据绑定。占位符文案提供了格式示例(“如:深夜电台 · 晚安白噪音”),引导用户输入规范的内容。
取消按钮调用 onClose 回调关闭弹窗,创建按钮调用 savePlay 方法保存数据。两个按钮等宽(layoutWeight(1)),取消使用芯片背景+次要色文字,创建使用紫色背景+白色粗体文字,通过颜色对比突出主操作。
17.3 编辑歌单弹窗
@Builder
panelEdit(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.editName, placeholder: '歌单名称' })
.fontSize(11).fontColor(COLORS.title)
.backgroundColor(COLORS.chip).borderRadius(8)
.onChange((value: string) => {
this.editName = value;
})
}
.width('100%').alignItems(HorizontalAlign.Start)
Column({ space: 6 }) {
Text('歌单标签').fontSize(9).fontColor(COLORS.sub)
TextInput({ text: this.editTag, placeholder: '歌单标签' })
.fontSize(11).fontColor(COLORS.title)
.backgroundColor(COLORS.chip).borderRadius(8)
.onChange((value: string) => {
this.editTag = 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.cyan).borderRadius(9)
.onClick(() => {
this.updatePlay();
})
}
.width('100%')
}
.width('78%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
}
.width('100%')
.height('100%')
.alignContent(Alignment.Center)
}
编辑歌单弹窗的结构与新建弹窗几乎相同,区别在于:绑定的是 editName 和 editTag 状态变量(而非 formName 和 formTag),这些变量在 openEditPlay 方法中被预填充了当前歌单的值;保存按钮文案为"保存修改",背景色使用霓虹青(COLORS.cyan)而非紫色,通过颜色差异帮助用户区分新建和编辑两种操作场景;点击保存调用 updatePlay 方法而非 savePlay。
17.4 删除确认弹窗
@Builder
panelDel(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column({ space: 12 }) {
Text('删除歌单').fontSize(15).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text('确认删除该歌单吗?删除后歌单内的缓存歌曲将一并清理,且不可恢复。')
.fontSize(10).fontColor(COLORS.sub)
.maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
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.red).borderRadius(9)
.onClick(() => {
this.delPlay();
})
}
.width('100%')
}
.width('78%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
}
.width('100%')
.height('100%')
.alignContent(Alignment.Center)
}
}
删除确认弹窗比新建和编辑弹窗更简洁,因为它不需要输入框。面板包含标题、风险提示文案和两个按钮。提示文案"确认删除该歌单吗?删除后歌单内的缓存歌曲将一并清理,且不可恢复。"明确告知用户删除操作的后果和不可逆性,这是破坏性操作的标准设计规范——必须让用户充分知情后再做决定。
确认删除按钮使用红色(COLORS.red)背景,与取消按钮的芯片色形成强烈对比,红色在 UI 设计中代表警示和危险,提醒用户这是一个不可逆的破坏性操作。点击确认调用 delPlay 方法执行删除。
十八、技术特性对比
| 特性维度 | 本应用实现方案 | 传统实现方案 | 优势说明 |
|---|---|---|---|
| AI 字幕能力 | Speech Kit AICaptionComponent 原生组件 | 第三方语音识别 SDK + 自研字幕渲染层 | 无需引入额外依赖,系统级优化功耗更低,渲染效果与系统原生一致 |
| 源语言配置 | sourceLanguage 字段,支持 zh/en 双语种 | 固定语种或需自行封装语言切换逻辑 | 原生字段直接配置,响应式刷新无需手动调用接口 |
| 目标语言配置 | targetLanguage 字段,支持 zh/en/zh-en 三选项 | 仅支持单一翻译方向 | 中英双语模式是独有优势,满足语言学习场景的对照需求 |
| 字体大小调节 | fontSize 字段,AICaptionFontSize 四档枚举 | 需自行实现字号映射和渲染逻辑 | 系统级字号控制,适配不同屏幕密度的自动缩放 |
| 字体颜色定制 | fontColor 字段,ResourceColor 类型 | 需自行处理字幕文本着色 | 原生支持任意合法颜色值,无需操作渲染层 |
| 主题方案 | 极夜黑+流光紫+霓虹青深色三色系 | Material Design 标准色板或自定义浅色 | OLED 省电、暗光护眼、品牌辨识度高 |
| 布局策略 | 四 Tab 完全差异化布局 | 统一列表式布局 | 视觉丰富度高,避免审美疲劳,功能区分明确 |
| 数据响应 | @Observed + @State 装饰器 | 手动调用 notifyDataSetChanged | 精准局部刷新,性能开销低,代码简洁 |
| 弹窗系统 | Stack 叠层 + 条件渲染 + 回调式关闭 | Dialog 组件或第三方弹窗库 | 无外部依赖,关闭逻辑灵活可控,遮罩与面板解耦 |
| 动画效果 | setInterval 呼吸动画驱动多组件 | 属性动画或 Lottie | 轻量级实现,一处状态驱动多处效果,资源占用低 |
| 歌单管理 | @Observed 类 + push/splice/slice | 直接操作数组或引入状态管理库 | slice 创建新引用确保 ForEach 刷新,无需额外库 |
| 图表实现 | 纯 ArkUI 组件(Column + 渐变) | MPChart 等第三方图表库 | 零依赖,可定制性高,支持呼吸动画无缝集成 |
十九、总结
本应用完整展示了 HarmonyOS ArkUI 框架在音乐流媒体场景下的工程实践能力。从整体架构来看,代码通过颜色系统、常量定义、辅助函数、数据模型、组件主体、Builder 函数群六个层次的清晰划分,实现了高内聚低耦合的代码组织。颜色系统集中管理 15 个主题色字段,为未来主题切换预留了接口;常量定义将所有 Mock 数据和配置选项抽离为独立常量,便于后续替换为真实接口数据;辅助函数封装了趋势映射、枚举转名等可复用逻辑;数据模型使用 @Observed 装饰器实现响应式数据追踪。
AI 字幕功能是本应用的技术核心。通过 AICaptionComponent 组件,应用实现了从音频推流到字幕渲染的完整链路。HarmonyOS 6.1.1 新增的 sourceLanguage、targetLanguage、fontSize、fontColor 四大字段为字幕赋予了前所未有的可定制性——用户可以自由选择源语言和目标语言的组合(包括中英双语模式),调节四档字号适应不同视力需求,从五种预设颜色中选择最舒适的字幕色。这些配置通过 buildCaptionOptions 方法组装为 AICaptionOptions 对象,由于 ArkUI 的响应式机制,任何配置变更都会即时生效,无需手动触发刷新。语言切换时的联动逻辑(中文源锁定中文目标)体现了对 Speech Kit 能力边界的准确理解。
四 Tab 差异化布局设计是本应用在 UI 层面的亮点。乐库 Tab 采用渐变大卡 + 双列 Flex 换行歌单墙的组合,通过每日推荐大卡吸引用户注意力,歌单墙提供丰富的选择;排行 Tab 采用大编号热歌榜 + 纯组件柱状图的组合,前三名金银铜高亮和数据可视化增强了信息密度;AI 字幕 Tab 采用五区块纵向排列,从实时预览到语言设置、外观设置、代码预览、场景推荐,层层递进地展示了 Speech Kit 的全部能力;我的 Tab 采用会员渐变大卡 + 功能清单行的组合,简洁地组织个人信息和功能入口。每个 Tab 的布局都经过独立设计,避免了千篇一律的列表式界面。
深色主题方案的实现也值得称道。极夜黑 #0F0A1E 作为背景色不是简单的纯黑,而是带有微弱紫调的深色,在保持暗光环境舒适度的同时避免了生硬感。流光紫 #9B6BFF 和霓虹青 #4FE3D0 的互补色搭配创造了强烈的视觉张力。多处使用 linearGradient 渐变背景(135 度对角渐变用于卡片,180 度垂直渐变用于柱状图柱体)为界面增添了层次感和立体感。呼吸动画通过一个 setInterval 定时器每秒翻转 breath 布尔值,驱动头部图标透明度、柱状图柱高、字幕就绪标签等多处视觉效果,实现了"一处状态控制多处动画"的高效设计。
弹窗系统采用了 Stack 叠层 + 条件渲染 + 回调式关闭的架构。modalOverlay 提供通用的半透明遮罩,panelAdd/panelEdit/panelDel 三个面板分别处理新建、编辑、删除场景。每个面板接收 onClose 回调函数,实现了关闭逻辑的灵活委托。删除确认弹窗使用红色背景的确认按钮和"不可恢复"的风险提示文案,遵循了破坏性操作的设计规范。歌单编辑保存时通过 this.playList = this.playList.slice() 创建数组新引用确保 ForEach 列表刷新,这是 ArkUI 响应式系统的一个关键技巧。
从工程实践的角度看,本应用虽然是一个功能演示级别的单文件应用,但其代码组织方式、状态管理策略、UI 设计理念都体现了成熟的前端工程思维。@Observed + @State 的响应式数据流、Builder 函数的参数化复用、条件渲染驱动的弹窗系统、数据驱动的 UI 适配,这些模式都可以直接迁移到更大规模的模块化项目中。对于希望学习 HarmonyOS ArkUI 开发和 Speech Kit AI 字幕能力的开发者来说,本应用是一个结构完整、注释详尽、可直接运行参考的优质学习材料。
附录: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 将自动执行以下操作:
- 生成项目骨架(Stage 模型目录结构)
- 执行
ohpm install安装依赖 - 运行 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 版本编写,不同版本界面可能存在细微差异。
更多推荐

所有评论(0)