一、技术前言:HarmonyOS ArkUI 与 Speech Kit 的技术背景

HarmonyOS 作为华为推出的面向万物互联时代的分布式操作系统,其应用开发框架 ArkUI 承载着构建多端一致体验的核心使命。ArkUI 采用声明式开发范式,开发者通过 ArkTS 语言(TypeScript 的超集,专为 HarmonyOS 设备侧应用开发定制)描述界面结构与状态绑定关系,框架底层负责高效的差分渲染与状态驱动更新。在 ArkUI 的声明式模型中,@Entry 标注入口组件,@Component 声明自定义组件,@State 管理组件内部可变状态,@Builder 封装可复用的 UI 构建函数,@Observed 配合 @ObjectLink 实现跨组件的对象级观察。这套状态管理---声明式构建---差分渲染的三层体系,是理解 HarmonyOS 原生应用开发的基础范式。

ArkUI 的布局能力覆盖了 Flex 弹性布局、Stack 堆叠布局、Column 线性纵向、Row 线性横向、Scroll 滚动容器等容器组件,以及 Text、Image、TextInput、Circle 等基础内容组件。其中 linearGradient 属性支持线性渐变背景,borderRadius 实现圆角裁切,opacity 控制透明度------这些视觉属性与 ArkUI 的状态绑定机制结合后,可以轻松实现呼吸动画、选中高亮、渐变主题等动效。本案例大量运用了渐变背景配合定时器驱动的布尔状态翻转,实现了柱状图波动、头像呼吸、就绪状态闪烁等轻量动画效果,充分展示了声明式 UI 在动效表达上的简洁与高效。

HarmonyOS 的 Speech Kit(语音服务套件)是系统级 AI 能力对外开放的重要窗口,其中 AICaptionComponent(AI 字幕组件)是 Speech Kit 在 HarmonyOS 6.1.1 版本中重点增强的核心组件。该组件能够接收外部音频流,通过端侧或云侧语音识别引擎将语音转化为字幕文本,并支持实时翻译显示。在 K歌社交、直播连麦、会议纪要等场景中,AI 字幕组件为听障用户、外语用户、嘈杂环境用户提供了关键的无障碍信息获取通道。组件以 isShown(显示状态,支持 @Link 双向绑定)、controller(控制器实例,调用 writeAudio 写入音频)、options(配置选项对象)三大入参驱动,形成"音频输入---识别翻译---字幕渲染"的完整闭环。

HarmonyOS 6.1.1 版本为 AICaptionComponent 的 AICaptionOptions 配置对象新增了四个关键字段,显著提升了字幕的可定制性与多语言适配能力。第一,sourceLanguage(源语言):取值为 'zh'(中文)或 'en'(英文),指定音频流的源语言类型,引擎据此选择对应语种的声学模型与语言模型;第二,targetLanguage(目标语言):取值为 'zh'、'en' 或 'zh-en'(中英双语),指定字幕输出的目标语言,当源语言为中文时目标语言锁定为 'zh'(无翻译方向),当源语言为英文时可选择翻译为中文、保持英文或输出中英双语字幕;第三,fontSize(字体大小):类型为 AICaptionFontSize 枚举,提供 SMALL(小号)、NORMAL(标准)、BIG(大号)、LARGE(超大)四档可选,满足不同视力需求与屏幕距离场景;第四,fontColor(字体颜色):类型为 ResourceColor,允许开发者传入任意颜色值自定义字幕文字颜色。这四个字段的加入,使 AI 字幕从"能用"迈向"好用",开发者可以针对 K歌跟唱、说唱速记、粤语辅助、直播连麦等细分场景做出精细化的语言与外观配置。

K歌社交是移动互联网娱乐赛道中兼具实时性与社交性的垂直品类。用户在虚拟歌房中排麦、连麦、合唱,通过实时语音互动建立社交连接。这一场景对字幕有独特需求:外语歌词跟唱时需要原文与中文译文对照显示(中英双语模式),说唱歌曲语速极快需要更大字号确保可读性,粤语音频因 sourceLanguage 仅支持 zh/en 而归入中文源处理,连麦对话场景则需要双语字幕辅助跨语言交流。这些细分需求恰恰对应了 6.1.1 新增四字段的配置能力,使 AI 字幕真正融入业务流程而非仅作辅助展示。

本案例"麦霸欢唱·K歌社交应用"正是基于上述技术栈与业务理解构建的完整示例应用。应用采用舞台黑(#120A1A)作为全局底色,叠加霓虹紫(#A855F7)与欢唱粉(#F472B6)形成深色舞台主题,营造沉浸式 K 歌氛围。应用设置四个底部 Tab:歌房(分类宫格入口+热门麦霸横滑大卡)、点唱(大编号热歌点唱榜+月度欢唱柱状图)、AI字幕(Speech Kit 特性展示页,含预览/语言设置/外观设置/代码预览/场景推荐五个区块)、我的(会员渐变大卡+功能清单行)。同时配套创建/编辑/解散歌房的三套弹窗系统,通过全屏遮罩+居中面板的模式实现模态交互。整体设计将 Speech Kit 的 AI 能力深度嵌入 K 歌社交流程,是 HarmonyOS 原生应用中 AI 能力与垂直业务场景融合的典型范例。

二、整体架构流程图
以下流程图展示了本应用的整体架构层次,从顶层组件到底层数据模型与系统服务的依赖关系:
#mermaid-svg-C7yTo4haOTkOnyBO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-C7yTo4haOTkOnyBO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-C7yTo4haOTkOnyBO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-C7yTo4haOTkOnyBO .error-icon{fill:#552222;}#mermaid-svg-C7yTo4haOTkOnyBO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-C7yTo4haOTkOnyBO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-C7yTo4haOTkOnyBO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-C7yTo4haOTkOnyBO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-C7yTo4haOTkOnyBO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-C7yTo4haOTkOnyBO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-C7yTo4haOTkOnyBO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-C7yTo4haOTkOnyBO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-C7yTo4haOTkOnyBO .marker.cross{stroke:#333333;}#mermaid-svg-C7yTo4haOTkOnyBO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-C7yTo4haOTkOnyBO p{margin:0;}#mermaid-svg-C7yTo4haOTkOnyBO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-C7yTo4haOTkOnyBO .cluster-label text{fill:#333;}#mermaid-svg-C7yTo4haOTkOnyBO .cluster-label span{color:#333;}#mermaid-svg-C7yTo4haOTkOnyBO .cluster-label span p{background-color:transparent;}#mermaid-svg-C7yTo4haOTkOnyBO .label text,#mermaid-svg-C7yTo4haOTkOnyBO span{fill:#333;color:#333;}#mermaid-svg-C7yTo4haOTkOnyBO .node rect,#mermaid-svg-C7yTo4haOTkOnyBO .node circle,#mermaid-svg-C7yTo4haOTkOnyBO .node ellipse,#mermaid-svg-C7yTo4haOTkOnyBO .node polygon,#mermaid-svg-C7yTo4haOTkOnyBO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-C7yTo4haOTkOnyBO .rough-node .label text,#mermaid-svg-C7yTo4haOTkOnyBO .node .label text,#mermaid-svg-C7yTo4haOTkOnyBO .image-shape .label,#mermaid-svg-C7yTo4haOTkOnyBO .icon-shape .label{text-anchor:middle;}#mermaid-svg-C7yTo4haOTkOnyBO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-C7yTo4haOTkOnyBO .rough-node .label,#mermaid-svg-C7yTo4haOTkOnyBO .node .label,#mermaid-svg-C7yTo4haOTkOnyBO .image-shape .label,#mermaid-svg-C7yTo4haOTkOnyBO .icon-shape .label{text-align:center;}#mermaid-svg-C7yTo4haOTkOnyBO .node.clickable{cursor:pointer;}#mermaid-svg-C7yTo4haOTkOnyBO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-C7yTo4haOTkOnyBO .arrowheadPath{fill:#333333;}#mermaid-svg-C7yTo4haOTkOnyBO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-C7yTo4haOTkOnyBO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-C7yTo4haOTkOnyBO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C7yTo4haOTkOnyBO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-C7yTo4haOTkOnyBO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C7yTo4haOTkOnyBO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-C7yTo4haOTkOnyBO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-C7yTo4haOTkOnyBO .cluster text{fill:#333;}#mermaid-svg-C7yTo4haOTkOnyBO .cluster span{color:#333;}#mermaid-svg-C7yTo4haOTkOnyBO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-C7yTo4haOTkOnyBO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-C7yTo4haOTkOnyBO rect.text{fill:none;stroke-width:0;}#mermaid-svg-C7yTo4haOTkOnyBO .icon-shape,#mermaid-svg-C7yTo4haOTkOnyBO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C7yTo4haOTkOnyBO .icon-shape p,#mermaid-svg-C7yTo4haOTkOnyBO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-C7yTo4haOTkOnyBO .icon-shape .label rect,#mermaid-svg-C7yTo4haOTkOnyBO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C7yTo4haOTkOnyBO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-C7yTo4haOTkOnyBO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-C7yTo4haOTkOnyBO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Speech Kit 系统服务
Builder构建层
状态管理层
入口层
0
1
2
3
数据模型层
RoomItem 歌房
SingerItem 麦霸
SongItem 点唱榜
CaptionScene 字幕场景
UserStat 功能清单
Page1120 主页面
@Entry @Component
currentTab 当前Tab索引
breath 呼吸动画开关
addModal / editModal / delModal
三套弹窗开关
AI字幕状态组
srcLang / tgtLang /
captionSize / captionColor
业务数据组
roomList / singerList /
songList / sceneList / statList
headerMain 头部
渐变Banner+搜索+chips
tabRoom 歌房Tab
宫格+横滑大卡
tabOrder 点唱Tab
热歌榜+柱状图
tabCaption AI字幕Tab
五区块特性页
tabMine 我的Tab
会员卡+功能清单
chartCard 柱状图
tabBar 底部导航
panelAdd / panelEdit / panelDel
三套弹窗面板
AICaptionComponent
AI字幕组件
AICaptionController
控制器 writeAudio
AICaptionOptions
sourceLanguage/targetLanguage
fontSize/fontColor
AudioData
音频数据块
流程图清晰地呈现了应用的五层架构:入口层的 Page1120 作为根组件驱动整个页面;状态管理层维护 Tab 切换、呼吸动画、弹窗开关、AI 字幕配置与五大业务数据列表;Builder 构建层将界面拆分为头部、四个 Tab 内容、图表卡、底部导航与三套弹窗面板,每个 Builder 职责单一、可独立维护;数据模型层以 @Observed 类承载歌房、麦霸、点唱榜、字幕场景、功能清单等业务实体;Speech Kit 系统服务层则将 AICaptionComponent、AICaptionController、AICaptionOptions、AudioData 四个核心 API 有机串联,形成 AI 字幕能力闭环。
三、颜色系统与常量定义
3.1 导入声明与主题色板接口
应用首先从 Speech Kit 与基础服务 Kit 中导入所需的类型,随后定义颜色系统接口与常量。
typescript
import { AICaptionComponent, AudioData, AICaptionOptions, AICaptionController, AICaptionFontSize } from '@kit.SpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';
// ============ ② 颜色系统 ============
/** 主题色板接口:集中声明页面所有颜色字段(舞台黑+霓虹紫+欢唱粉深色系) */
interface ColorPalette {
bg: string;
card: string;
chip: string;
title: string;
sub: string;
text3: string;
purple: string;
purpleD: string;
pink: string;
cyan: string;
green: string;
gold: string;
line: string;
tabOn: string;
mask: string;
}
导入语句从 @kit.SpeechKit 中引入了五个核心标识符:AICaptionComponent 是 AI 字幕的 UI 组件,直接在 build() 中渲染;AudioData 是音频数据接口,封装 PCM 字节流供控制器消费;AICaptionOptions 是配置选项接口,承载 6.1.1 新增的四大字段;AICaptionController 是控制器类,通过其实例方法 writeAudio() 向引擎推送音频;AICaptionFontSize 是字号枚举,提供四档字幕大小。从 @kit.BasicServicesKit 引入的 BusinessError 则用于 onError 回调中的错误信息结构化处理。
ColorPalette 接口是整个应用视觉风格的核心契约。它集中声明了十五个颜色字段,覆盖背景底色(bg)、卡片底色(card)、标签底色(chip)、三级文字色(title/sub/text3)、主题渐变双色(purple/purpleD)、辅助强调色(pink/cyan/green/gold)、分割线色(line)、导航选中色(tabOn)与弹窗遮罩色(mask)。通过接口约束颜色字段,确保所有颜色引用有类型保障,避免拼写错误导致的运行时颜色缺失。这种"接口先行、常量后补"的设计模式,使得主题切换成为可能------只需提供另一个实现 ColorPalette 接口的对象即可整体替换配色方案。
3.2 深色主题色板常量
typescript
/** 深色主题色板常量(麦霸欢唱 · 舞台黑 + 霓虹紫 + 欢唱粉) */
const COLORS: ColorPalette = {
bg: '#120A1A',
card: '#1E1230',
chip: '#2A1A44',
title: '#F6EEFF',
sub: '#C0A8E8',
text3: '#8270A8',
purple: '#A855F7',
purpleD: '#7C3AED',
pink: '#F472B6',
cyan: '#4FE3D0',
green: '#6ED491',
gold: '#FFD36E',
line: '#38245C',
tabOn: '#A855F7',
mask: 'rgba(10,4,20,0.7)'
};
COLORS 常量是应用唯一的颜色事实来源。舞台黑 #120A1A 作为全局底色,带有微妙的紫色调,比纯黑更有层次感与温度感;卡片底色 #1E1230 比底色略亮,通过明度差异构建层次纵深;标签底色 #2A1A44 再亮一档,用于 chips、代码块、按钮等交互元素的背景。三级文字色形成清晰的明度阶梯:title(#F6EEFF)偏暖白用于主标题与重要数值,sub(#C0A8E8)淡紫用于辅助说明,text3(#8270A8)灰紫用于最弱的元信息文字。主题渐变双色 purple(#A855F7)与 purpleD(#7C3AED)构成 135 度线性渐变,从深紫到亮紫营造舞台聚光灯般的视觉效果。欢唱粉 #F472B6 作为强调色点缀在 LIVE 角标、解散按钮等高情绪点上;霓虹青 #4FE3D0 用于选中态描边与提示文字;薄荷绿 #6ED491 用于下降趋势与环比正值;金黄 #FFD36E 用于排行榜冠军与 VIP 标识。
3.3 Tab导航与房间类型常量
typescript
/** Tab 元数据接口:底部导航图标 + 标签 */
interface TabMeta {
icon: string;
label: string;
}
/** 底部导航 Tab 常量列表(4 Tab 单排) */
const TAB_LIST: TabMeta[] = [
{ icon: '🎤', label: '房间' },
{ icon: '🎼', label: '点唱' },
{ icon: '🗣', label: 'AI字幕' },
{ icon: '👤', label: '我的' }
];
/** 头部横滑房间类型 chips 文案 */
const CATE_TAGS: string[] = ['推荐', '情歌房', '粤语房', '说唱房', '摇滚房', '民谣房', '戏腔房', '合唱房'];
TabMeta 接口将导航图标与标签文本绑定为结构化数据,TAB_LIST 以数组形式存储四个 Tab 的元信息。使用 emoji 作为图标既避免了图片资源的加载开销,又自带跨平台一致性显示。四个 Tab 覆盖了 K 歌社交的核心功能域:房间(发现与创建歌房)、点唱(热歌榜单与数据统计)、AI字幕(语音服务能力展示)、我的(个人中心与设置)。CATE_TAGS 定义了八个房间分类标签,横滑展示在头部,涵盖了情歌、粤语、说唱、摇滚、民谣、戏腔、合唱等主流 K 歌曲风分类,用户点击可切换分类筛选歌房列表。
3.4 字幕语言与样式常量(Speech Kit 6.1.1 核心配置)
typescript
// ============ 字幕语言/样式常量(Speech Kit 6.1.1) ============
/** 源语言选项接口(取值范围:['zh','en']) */
interface LangOption {
code: string; // 语言码
name: string; // 展示名
}
/** 源语言选项列表(sourceLanguage 仅支持中文 / 英文两种) */
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: '中英双语' }
];
/** 字号选项接口(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'];
这段常量定义是整个应用与 Speech Kit 6.1.1 新特性对接的核心区域,值得逐项深入分析。LangOption 接口将语言码与展示名配对,使 UI 渲染与 API 调用使用同一数据源,避免了展示名与码值不一致的风险。SRC_LANGS 数组明确定义了 sourceLanguage 字段仅支持中文与英文两种取值------这是 6.1.1 版本的能力边界,粤语音频因无独立语言码而归入 'zh' 处理。TGT_LANGS_EN 定义了英文源时的三个目标语言选项:'zh'(翻译为中文)、'en'(保持英文)、'zh-en'(中英双语对照显示),其中双语模式是 K 歌跟唱场景的高频选择,用户可以同时看到英文原文与中文译文。
SizeOption 接口将 AICaptionFontSize 枚举值与中文展示名绑定。SIZE_OPTIONS 完整列出了四档字号:SMALL 适合信息密集场景,NORMAL 是默认标准大小,BIG 适合中等距离观看,LARGE 则面向远距离或视力辅助需求。在 K 歌场景中,用户手持设备距离通常在 30-50 厘米,BIG 与 LARGE 档位是高频选择,尤其说唱类歌曲语速快,大字号能确保用户在跟唱时快速捕捉每句歌词。CAPTION_FONT_COLORS 提供了五种预设颜色:经典白(#FFFFFF)是最高对比度选择,麦光黄(#FFE9B0)带有暖色调适合舞台氛围,薄荷绿(#9CE8B5)与云朵蓝(#9CD0FF)提供柔和的彩色选项,樱花粉(#FFB3C1)则与应用的欢唱粉主题呼应。这些颜色作为 fontColor 字段的候选值传入 AICaptionOptions,实现字幕文字的个性化定制。
3.5 月度柱状图常量
typescript
/** 月度欢唱时长柱状图月份索引 */
const MONTH_IDX: number[] = [0, 1, 2, 3, 4, 5];
/** 月度柱状图月份名称 */
const MONTH_LABELS: string[] = ['3月', '4月', '5月', '6月', '7月', '8月'];
/** 月度欢唱时长数值(小时) */
const MONTH_HOURS: number[] = [62, 74, 58, 96, 108, 122];
/** 月度柱状图最大值(小时) */
const MONTH_MAX: number = 140;
这组常量为点唱 Tab 底部的月度欢唱时长柱状图提供数据支撑。MONTH_IDX 是月份索引数组用于 ForEach 遍历,MONTH_LABELS 与 MONTH_HOURS 平行数组分别存储月份名称与欢唱小时数,MONTH_MAX 作为柱高归一化的分母(140 小时),确保最高柱(8 月 122 小时)约占柱形容器 87% 的高度,留出顶部空间放置数值标签。数据呈现逐月递增趋势(从 3 月 62 小时到 8 月 122 小时),呼应"环比 +13.0%"的汇总文案,展示用户欢唱活跃度的增长曲线。
四、辅助函数与数据模型
4.1 辅助函数群
typescript
// ============ ④ 辅助函数 ============
/** 榜单趋势颜色映射:上升欢唱粉 / 下降薄荷绿 / 持平弱化 */
function trendColor(t: string): string {
if (t === 'up') { return COLORS.pink; }
if (t === 'down') { return COLORS.green; }
return COLORS.text3;
}
/** 榜单趋势图标文案:上升 / 下降 / 持平 */
function trendIcon(t: string): string {
if (t === 'up') { return '↑ 上升'; }
if (t === 'down') { return '↓ 下降'; }
return '--- 持平';
}
/** 字号枚举转展示名(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 '樱花粉';
}
辅助函数群承担了数据到展示的映射职责,是 UI 渲染与业务数据之间的适配层。trendColor 与 trendIcon 两个函数服务于点唱榜的趋势展示:上升趋势用欢唱粉(COLORS.pink)传递正向情绪,下降趋势用薄荷绿(COLORS.green)做柔和提示------值得注意的是,这里没有使用红色表示下降,而是用绿色,因为欢唱数据的下降不构成负面警告,绿色在深色主题中视觉舒适度更高。sizeName 函数将 AICaptionFontSize 枚举值转为大写英文名,专门服务于 AI 字幕 Tab 中的代码预览区块,使代码块中显示的字段值与实际枚举名一致。langName 函数处理三种语言码到中文名的转换,colorName 函数将五种颜色十六进制值映射为诗意化的中文名(麦光黄、薄荷绿、云朵蓝、樱花粉),增强应用的情感化表达。
4.2 歌房与麦霸数据模型
typescript
// ============ ⑤ 数据模型 ============
/** 歌房条目(房间 Tab 分类宫格入口) */
@Observed export class RoomItem {
icon: string; // 房间图标 emoji
name: string; // 房间名(如"深夜情歌房")
online: string; // 在线人数文本(如"1.2万在线")
tag: string; // 房间标签(如"掌麦")
constructor(icon: string, name: string, online: string, tag: string) {
this.icon = icon;
this.name = name;
this.online = online;
this.tag = tag;
}
}
/** 歌房广场 Mock 数据(8 条) */
const ROOM_LIST: Array<RoomItem> = [
new RoomItem('🌙', '深夜情歌房', '1.2万在线', '掌麦'),
new RoomItem('🎸', '摇滚嗨唱房', '8963在线', '连麦'),
new RoomItem('🎤', '麦王争霸房', '7531在线', '排麦'),
new RoomItem('🎼', '粤语金曲房', '6420在线', '掌麦'),
new RoomItem('🎧', '说唱快嘴房', '5184在线', '连麦'),
new RoomItem('🏮', '戏腔国风房', '4309在线', '排麦'),
new RoomItem('🍃', '民谣小馆房', '3625在线', '掌麦'),
new RoomItem('🎉', '生日派对房', '2876在线', '连麦')
];
RoomItem 类使用 @Observed 装饰器标注,这是 ArkUI 的可观察对象机制------当该类的实例属性发生变化时,引用了该实例的 @ObjectLink 或 @State 组件会自动触发重新渲染。类的四个属性分别承载房间的视觉图标、名称、在线人数与互动模式标签。标签字段(tag)的取值为"掌麦"、"排麦"、"连麦"三种 K 歌互动模式:掌麦指单人独唱模式,排麦指排队轮流上麦模式,连麦指双人或多人实时合唱模式。ROOM_LIST 提供了八条 Mock 数据,覆盖深夜情歌、摇滚嗨唱、麦王争霸、粤语金曲、说唱快嘴、戏腔国风、民谣小馆、生日派对等多元曲风场景,在线人数从 2876 到 1.2 万递减,形成自然的推荐排序。
4.3 麦霸数据模型
typescript
/** 麦霸条目(房间 Tab 热门麦霸横滑大卡) */
@Observed export class SingerItem {
name: string; // 麦友昵称
avatar: string; // emoji 头像
level: string; // 等级文本(如"麦王 Lv.8")
fans: string; // 粉丝文本
hit: string; // 成名曲文本
constructor(name: string, avatar: string, level: string, fans: string, hit: string) {
this.name = name;
this.avatar = avatar;
this.level = level;
this.fans = fans;
this.hit = hit;
}
}
/** 热门麦霸 Mock 数据(6 条) */
const SINGER_LIST: Array<SingerItem> = [
new SingerItem('檀夜笙', '🦊', '麦王 Lv.8', '128万', '《夜色七点半》'),
new SingerItem('老陆的烟嗓', '🐺', '歌神 Lv.9', '156万', '《江边十二楼》'),
new SingerItem('苏打菠萝', '🍍', '麦霸 Lv.7', '96万', '《汽水气泡音》'),
new SingerItem('柠檬汽水Girl', '🍋', '麦霸 Lv.6', '82万', '《夏日三分甜》'),
new SingerItem('高音小钢炮', '🚀', '麦王 Lv.8', '110万', '《海豚音练习生》'),
new SingerItem('半糖芝士', '🧸', '麦霸 Lv.5', '64万', '《奶茶慢半拍》')
];
SingerItem 同样标注 @Observed,其属性涵盖麦友的身份信息(昵称、头像)、社交资产(等级、粉丝数)与作品标识(成名曲)。等级体系分为三档:麦霸(Lv.5-7)、麦王(Lv.8)、歌神(Lv.9),粉丝数从 64 万到 156 万递增。六条 Mock 数据的昵称设计极具社交平台的个性化特征------"檀夜笙"、"老陆的烟嗓"、"苏打菠萝"等名称带有强烈的情感色彩与人设暗示,符合 K 歌社交应用的用户画像。成名曲名称与应用内点唱榜的歌曲名称形成交叉引用,构建了"麦霸---歌曲---歌房"的内容关联网络。
4.4 点唱榜与字幕场景数据模型
typescript
/** 点唱榜条目(点唱 Tab 大编号热歌点唱榜) */
@Observed export class SongItem {
rank: string; // 名次字符串
title: string; // 歌名
artist: string; // 歌手
heat: string; // 点唱热度文本
trend: string; // 趋势:'up' / 'down' / 'flat'
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;
}
}
/** 热歌点唱榜 Mock 数据(8 条) */
const SONG_LIST: Array<SongItem> = [
new SongItem('1', '夜色七点半', '檀夜笙', '98.6万', 'up'),
new SongItem('2', '汽水气泡音', '苏打菠萝', '87.2万', 'up'),
new SongItem('3', '江边十二楼', '老陆的烟嗓', '80.4万', 'flat'),
new SongItem('4', '夏日三分甜', '柠檬汽水Girl', '73.9万', 'down'),
new SongItem('5', '奶茶慢半拍', '半糖芝士', '66.8万', 'up'),
new SongItem('6', '海豚音练习生', '高音小钢炮', '59.5万', 'down'),
new SongItem('7', '晚风合唱团', '麦田合唱社', '52.1万', 'up'),
new SongItem('8', '深夜出租车', '白噪计划', '48.7万', 'flat')
];
SongItem 模型承载点唱榜单数据。rank 字段特意设计为字符串类型而非数字,因为前端显示时名次可能带有特殊符号(如"No.1")或前缀补零(如"01")。trend 字段使用 'up'/'down'/'flat' 三种字符串值表示趋势方向,配合前面定义的 trendColor 与 trendIcon 函数即可完成趋势的视觉渲染。八条榜单数据从 98.6 万到 48.7 万热度递减,前三名分别标注上升、上升、持平,第四名下降暗示榜单的动态变化。歌曲与歌手名称与热门麦霸的成名曲交叉呼应,体现了应用内部数据的关联性设计。
4.5 字幕场景与用户功能清单数据模型
typescript
/** 字幕场景条目(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;
}
}
/** 字幕场景 Mock 数据(5 条:K歌场景相关) */
const SCENE_LIST: Array<CaptionScene> = [
new CaptionScene('外语歌词跟唱', '英文歌歌词实时翻译跟唱', 'en', 'zh-en'),
new CaptionScene('说唱歌词速记', '语速快也能看清每句词', 'en', 'en'),
new CaptionScene('中文歌曲合唱', '大字幕合唱不跑调', 'zh', 'zh'),
new CaptionScene('粤语歌辅助', '粤语发音看字幕跟唱(sourceLanguage 仅 zh/en,粤语归中文源)', 'zh', 'zh'),
new CaptionScene('直播连麦互动', '连麦对话双语字幕', 'en', 'zh')
];
CaptionScene 模型是 AI 字幕 Tab 场景推荐区块的数据支撑,每条数据预置了场景名、说明文字与推荐的源/目标语言组合。五条场景数据精准对应了 K 歌社交中的典型字幕需求:外语跟唱使用英文源+中英双语目标(en→zh-en),说唱速记使用英文源+英文目标(en→en,纯识别无翻译以降低延迟),中文合唱使用中文源+中文目标(zh→zh),粤语辅助因 sourceLanguage 仅支持 zh/en 而归入中文源处理,直播连麦则使用英文源+中文目标(en→zh)实现跨语言交流。这些场景预设让用户一键套用语言组合,降低了配置门槛。
typescript
/** 我的页功能清单条目 */
@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;
}
}
/** 我的页功能清单 Mock 数据(8 条) */
const STAT_LIST: Array<UserStat> = [
new UserStat('🎙', '我的录音棚', '36 首作品', true),
new UserStat('🏆', '排位赛段位', '钻石麦王 III', true),
new UserStat('🎁', '黄金麦霸会员', '2026-11-21 到期', true),
new UserStat('👥', '关注的麦友', '128 人', true),
new UserStat('❤', '我喜欢的合唱', '56 场', true),
new UserStat('🗣', 'AI 字幕偏好', '源 zh · 目标 zh', true),
new UserStat('📊', '欢唱数据报告', '本周欢唱 6.5 小时', true),
new UserStat('⚙', '音效与伴奏设置', '录音棚级混响', true)
];
UserStat 模型用于我的 Tab 的功能清单行,arrow 布尔字段控制是否显示右侧导航箭头------虽然 Mock 数据中八条全部为 true,但该字段保留了未来某些纯信息展示行不显示箭头的扩展能力。功能清单覆盖了作品管理(录音棚)、竞技排名(排位赛)、付费权益(会员)、社交关系(关注/合唱)、AI设置(字幕偏好)、数据统计(欢唱报告)与音频设置(音效伴奏)七大功能域,其中"AI 字幕偏好"条目直接展示当前用户的字幕语言配置(源 zh·目标 zh),与 AI 字幕 Tab 形成数据联动。
五、组件主体:状态声明与核心方法
5.1 状态变量声明
typescript
// ============ ⑥ 组件主体 ============
/** 1120 麦霸欢唱 · K歌社交主页面 */
@Entry
@Component
struct Page1120 {
/** 当前选中 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 roomList: Array<RoomItem> = ROOM_LIST;
/** 热门麦霸横滑数据 */
@State singerList: Array<SingerItem> = SINGER_LIST;
/** 热歌点唱榜数据 */
@State songList: Array<SongItem> = SONG_LIST;
/** 字幕场景列表数据 */
@State sceneList: Array<CaptionScene> = SCENE_LIST;
/** 我的页功能清单数据 */
@State statList: Array<UserStat> = STAT_LIST;
/** 创建表单:歌房名称 */
@State formName: string = '';
/** 创建表单:歌房类型(掌麦/排麦/连麦) */
@State formType: string = '';
/** 编辑表单:歌房名称 */
@State editName: string = '';
/** 编辑表单:歌房类型 */
@State editType: string = '';
组件主体 Page1120 以 @Entry 与 @Component 双装饰器声明,是应用的根入口组件。状态变量声明可分为四组:导航与动画状态(currentTab/breath/timer/cateIdx)、弹窗状态(addModal/editModal/delModal/editIdx/delIdx)、业务数据列表(roomList/singerList/songList/sceneList/statList)与表单数据(formName/formType/editName/editType)。
breath 是一个精妙的动画驱动设计------它是一个每秒翻转一次的布尔值,通过 setInterval 定时器在 true 与 false 之间切换。虽然只是一个简单的布尔翻转,但通过在 UI 绑定中使用三元表达式(如 this.breath ? 1 : 0.6、this.breath ? COLORS.cyan : COLORS.sub、this.breath ? 1.05 : 0.95),驱动了头像透明度呼吸、柱状图颜色交替、柱状图高度 ±5% 波动、就绪状态闪烁等多处动效。这种"单状态驱动多动效"的设计模式极大简化了动画管理,是声明式 UI 框架下轻量动画的经典实践。
typescript
// --- 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 字幕状态组是组件与 Speech Kit 交互的核心状态区。captionController 声明为 private 成员而非 @State,因为控制器实例本身不需要触发 UI 重新渲染------它的 writeAudio 方法被调用后,字幕组件内部的渲染由 AICaptionComponent 自行管理。captionShown 标注 @State,因为它通过 @Link 双向绑定到 AICaptionComponent 的 isShown 参数,控制字幕的显示与隐藏,UI 需要响应其变化。
四个核心字段------srcLang(源语言,默认中文)、tgtLang(目标语言,默认中文)、captionSize(字号,默认 NORMAL 标准)、captionColor(颜色,默认经典白)------直接映射 6.1.1 新增的 sourceLanguage/targetLanguage/fontSize/fontColor 四大配置项。当用户在 AI 字幕 Tab 中点击语言或外观选项时,这些状态值随之变化,进而触发 buildCaptionOptions() 重建配置对象,AICaptionComponent 接收新的 options 参数并热更新字幕的识别与显示策略。captionReady 与 captionErrMsg 分别服务于 onPrepared 与 onError 两个回调,前者在字幕服务就绪时置 true 驱动 UI 显示"已就绪"状态,后者在出错时存储错误信息驱动 UI 显示错误提示条。captionFed 计数器记录已写入的音频块数量,为演示场景提供可视化的数据流指标。
5.2 AICaptionOptions 组装方法
typescript
/** 组装 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() 方法是连接组件状态与 Speech Kit 配置的桥梁。每次调用时,它读取当前的 srcLang、tgtLang、captionSize、captionColor 四个状态值,组装为一个全新的 AICaptionOptions 对象返回。这个方法在 build() 中被 AICaptionComponent 的 options 参数调用,意味着每次 ArkUI 框架因状态变化触发重新渲染时,都会重建配置对象并传递给字幕组件,实现配置的实时热更新。
initialOpacity 设为 1 表示字幕组件初始完全可见(透明度为 1.0)。四个标注星号(★)的字段正是 6.1.1 版本的新增能力。onPrepared 回调在字幕服务初始化完成、可以接收音频流时被框架调用,此时将 captionReady 置为 true 并清空错误信息,UI 上"初始化中"的提示随即切换为"已就绪"绿色标签。onError 回调接收 BusinessError 类型参数,包含 code(错误码)与 message(错误描述),将其拼接为用户可读的错误文本存入 captionErrMsg,UI 上以粉色提示条展示。这两个回调将 Speech Kit 的异步生命周期事件转换为组件状态,体现了声明式编程中"事件---状态---渲染"的标准响应链路。
5.3 源语言联动方法
typescript
/** 切换源语言时联动目标语言(中文源时目标语言仅支持 'zh') */
switchSourceLang(code: string) {
this.srcLang = code;
if (code === 'zh') {
this.tgtLang = 'zh'; // 中文源:无翻译方向,锁定中文
} else {
this.tgtLang = 'zh-en'; // 英文源:默认双语,可再选 zh/en
}
}
switchSourceLang 方法封装了源语言切换时的联动逻辑,是 6.1.1 新特性业务规则的代码实现。当 sourceLanguage 设为 'zh'(中文)时,引擎无需翻译------中文源识别为中文字幕,因此 targetLanguage 被锁定为 'zh';当 sourceLanguage 设为 'en'(英文)时,引擎支持翻译,默认设为 'zh-en'(中英双语),用户仍可在三个选项(zh/en/zh-en)中切换。这一联动逻辑确保了配置组合的合法性,避免了"中文源+英文目标"等无意义组合。在 AI 字幕 Tab 的 UI 层,中文源时目标语言区域显示锁定提示行(🔒图标+"中文源锁定中文"文案),英文源时显示三选项选择器,实现了联动逻辑的视觉表达。
5.4 演示音频写入方法
typescript
/** 演示写入音频流:生成 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 音频数据并写入字幕控制器,是 AI 字幕功能端到端演示的关键一环。方法首先创建一个 640 字节的 Uint8Array 缓冲区。按照 16kHz 采样率、16bit 位深(每采样 2 字节)、单声道的 PCM 格式计算,640 字节恰好对应 320 个采样点,即 20 毫秒的音频时长------这是语音识别引擎推荐的写入粒度。
循环体中以步长 2 遍历缓冲区(每两字节为一个 16bit 采样值),通过正弦函数 Math.sin(2 * Math.PI * 440 * t) 生成 440Hz 的标准音 A4 的波形数据,振幅设为 6000(16bit 有符号范围 -32768~32767 的约 18%),确保音频不会削波失真。生成的整数值通过位运算 v & 0xFF(低 8 位)和 (v >> 8) & 0xFF(高 8 位)拆分为两个字节写入缓冲区,实现小端序的 16bit PCM 编码。随后将缓冲区包装为 AudioData 对象({ data: block }),调用 captionController.writeAudio(audioData) 推送给 Speech Kit 引擎。写入成功后 captionFed 自增,UI 上的"×N"计数器同步更新。try-catch 块捕获写入过程中可能的异常(如服务未就绪、缓冲区溢出等),将错误信息存入 captionErrMsg 供 UI 展示。在实际 K 歌应用中,这段音频写入逻辑的输入源应替换为麦克风采集的实时 PCM 流,但演示场景使用合成正弦波已足以验证数据通路的完整性。
5.5 歌房增删改方法
typescript
/** 打开编辑歌房弹窗(回填当前歌房名称与类型) */
openEditRoom(idx: number) {
this.editIdx = idx;
this.editName = this.roomList[idx].name;
this.editType = this.roomList[idx].tag;
this.editModal = true;
}
/** 保存新建歌房(空字段用默认值兜底) */
saveRoom() {
const name = this.formName === '' ? '未命名歌房' : this.formName;
const type = this.formType === '' ? '掌麦' : this.formType;
this.roomList.push(new RoomItem('🎶', name, '0在线', type));
this.formName = '';
this.formType = '';
this.addModal = false;
}
/** 保存编辑歌房(整体刷新数组引用以刷新歌房宫格) */
updateRoom() {
if (this.editIdx >= 0 && this.editIdx < this.roomList.length) {
if (this.editName !== '') {
this.roomList[this.editIdx].name = this.editName;
}
if (this.editType !== '') {
this.roomList[this.editIdx].tag = this.editType;
}
this.roomList = this.roomList.slice();
}
this.editModal = false;
}
/** 解散歌房(确认弹窗回调) */
delRoom() {
if (this.delIdx >= 0 && this.delIdx < this.roomList.length) {
this.roomList.splice(this.delIdx, 1);
}
this.delModal = false;
}
歌房的增删改(CRUD)操作通过四个方法实现。openEditRoom 方法接收歌房索引,将当前歌房的名称与类型回填到编辑表单状态(editName/editType),记录编辑索引(editIdx)并打开编辑弹窗------这一"先回填后弹出"的模式确保了用户看到的编辑表单已预填了当前数据,是典型的编辑交互流程。
saveRoom 方法处理创建歌房逻辑,对空字段做默认值兜底:名称为空时使用"未命名歌房",类型为空时使用"掌麦"。新创建的 RoomItem 以 emoji 🎶 作为图标、'0在线' 作为初始在线人数,被 push 到 roomList 数组末尾。由于 roomList 标注了 @State,数组引用变化(push 会修改原数组,ArkUI 的 @State 对数组方法如 push 会触发更新检测)后 UI 自动追加渲染新的歌房卡片。最后清空表单状态并关闭弹窗。
updateRoom 方法在保存编辑时面临一个 ArkUI 的状态更新机制细节:直接修改数组元素的属性(this.roomList[this.editIdx].name = ...)可能不会触发 @State 数组级别的重新渲染检测。因此方法在修改属性后调用 this.roomList = this.roomList.slice(),通过 slice() 创建数组的浅拷贝并重新赋值,强制刷新数组引用以触发 UI 更新。这是 ArkUI 声明式状态管理中处理数组元素属性修改的常见技巧。
delRoom 方法使用 splice 从数组中移除指定索引的歌房条目,splice 会修改原数组并返回被删除的元素,ArkUI 能够检测到这种数组长度变化并触发列表重新渲染。
5.6 生命周期方法
typescript
/** 生命周期:启动呼吸动画定时器(每 1000ms 翻转 breath) */
aboutToAppear() {
this.timer = setInterval(() => {
this.breath = !this.breath;
}, 1000);
}
/** 生命周期:销毁时清理定时器 */
aboutToDisappear() {
clearInterval(this.timer);
}
ArkUI 组件的生命周期回调 aboutToAppear 在组件创建后、build() 执行前被调用,是初始化定时器、订阅事件等副作用的理想时机。这里通过 setInterval 注册了一个每 1000 毫秒执行一次的回调函数,回调内部将 breath 状态翻转------由于 breath 是 @State 变量,每次翻转都会触发依赖它的 UI 组件重新渲染,从而驱动呼吸动画。aboutToDisappear 在组件销毁前被调用,通过 clearInterval 清理定时器,避免组件销毁后定时器仍在运行导致的内存泄漏与无效状态更新------这是 HarmonyOS 应用开发中资源管理的标准实践。
六、页面主构建与头部布局
6.1 页面主构建
typescript
/** 页面主构建:Stack 包裹主内容与三层弹窗 */
build() {
Stack() {
Column() {
this.headerMain()
Divider().strokeWidth(1).color(COLORS.line)
Scroll() {
Column() {
if (this.currentTab === 0) {
this.tabRoom()
} else if (this.currentTab === 1) {
this.tabOrder()
} 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() 方法定义了整个页面的结构与层级关系。最外层是 Stack 堆叠容器------这是实现弹窗系统的关键布局策略。Stack 的子元素按声明顺序从底到高层叠,后声明的子元素覆盖在前者之上。因此,主内容 Column(包含头部、滚动区、底部导航)作为第一个子元素位于底层,三个弹窗面板根据各自的状态开关条件渲染,覆盖在主内容之上。
主内容 Column 的结构从上到下依次为:headerMain()(头部渐变 Banner+搜索条+chips)、Divider(分割线)、Scroll(可滚动内容区)与 tabBar()(底部导航)。Scroll 设置 layoutWeight(1) 占据头部与底部导航之间的全部剩余空间,scrollBar(BarState.Off) 隐藏滚动条保持界面简洁。Scroll 内部的 Column 通过 if-else 条件语句根据 currentTab 的值渲染对应的 Tab 内容------这是 ArkUI 声明式条件渲染的典型用法,只有匹配条件的 Builder 会被执行并渲染,其他 Tab 的 UI 不参与构建,保证了渲染效率。
三个弹窗面板(panelAdd/panelEdit/panelDel)各自由对应的布尔状态控制渲染:当 addModal 为 true 时创建歌房弹窗渲染在 Stack 顶层,依次类推。每个弹窗 Builder 接收一个 onClose 回调函数作为参数(这里传入的是将对应状态置 false 的箭头函数),弹窗内部点击遮罩或取消按钮时调用该回调关闭自身。这种"状态驱动渲染+回调关闭"的模式简洁而高效,是 ArkUI 中实现模态弹窗的推荐实践。
6.2 头部渐变Banner与搜索条
typescript
/** 头部:顶部渐变 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('今夜 3.2 万间歌房正在欢唱 · 你的麦位已预留')
.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('🎧 在线麦友 26 万').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.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 构建了页面的头部区域,由渐变 Banner、搜索条和横滑 chips 三部分组成。渐变 Banner 是头部的视觉焦点,通过 linearGradient 属性设置 135 度对角渐变,从深紫 purpleD(#7C3AED)到亮紫 purple(#A855F7),模拟舞台聚光灯的照射效果。Banner 内部分为两行:上行是欢迎语与副标题的左右布局,左侧 Column 通过 layoutWeight(1) 占据剩余空间,右侧的圆形 emoji 容器通过 this.breath ? 1 : 0.6 三元表达式驱动透明度呼吸动画,使 🎤 图标每秒闪烁一次。
欢迎语"晚上好,开嗓的人"使用了 K 歌场景特有的拟人化称呼,副标题"今夜 3.2 万间歌房正在欢唱·你的麦位已预留"传递实时活跃度信息与参与邀请。三个标签 chip 分别展示在线麦友数、排位赛状态与会员特权,其中前两个使用深紫底白字(purpleD 底 + title 文字色),第三个"黄金麦霸特权"反转配色为金底紫字(gold 底 + purpleD 文字色),形成视觉层级差异------金色标签更醒目,暗示付费特权的稀缺性。标签通过 maxLines(1) 与 textOverflow({ overflow: TextOverflow.Ellipsis }) 确保文字溢出时以省略号截断,防止内容溢出破坏布局。
typescript
// 搜索条(右侧语音入口跳转 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)
搜索条以 Row 容器实现横向布局,左侧 🔍 搜索图标、中间占位提示文字(text3 灰紫色,弱化视觉权重)、右侧 🎙 麦克风图标。提示文字使用 layoutWeight(1) 填充剩余空间并设置单行省略。右侧麦克风图标绑定了 onClick 点击事件,点击后将 currentTab 设为 2(AI 字幕 Tab 索引),实现从搜索栏到 AI 字幕功能的快捷跳转------这一设计巧妙地将语音输入入口与 AI 字幕功能关联,用户想用语音搜索时自然被引导到字幕功能页,形成功能间的有机串联。搜索条整体使用 card 底色与 borderRadius(20) 大圆角,呈现出胶囊状的搜索输入外观。
6.3 横滑房间类型chips
typescript
// 横滑房间类型 chips
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]] })
}
横滑 chips 区域使用水平滚动的 Scroll 容器包裹 Row,通过 scrollable(ScrollDirection.Horizontal) 启用横向滚动。ForEach 遍历 CATE_TAGS 数组的八个分类标签,每个标签的 fontColor 与 backgroundColor 通过三元表达式根据 cateIdx === idx 判断是否选中:选中态使用霓虹青文字(cyan)+ 标签底色(chip),未选中态使用辅助文字色(sub)+ 卡片底色(card),形成清晰但不刺眼的选中对比。第三个参数 (tg: string) => tg 是 ForEach 的键值生成函数,使用标签文本本身作为唯一键,确保列表渲染时的元素正确复用。
整个头部区域的外层 Column 也使用了 linearGradient 设置 180 度纵向渐变,从 chip(#2A1A44)渐变到 bg(#120A1A),使头部与下方内容区的过渡更加自然。这种"双重渐变"设计------头部区域纵向渐变+内部 Banner 对角渐变------在深色主题中构建了丰富的层次感。
七、四大Tab页面详解
7.1 歌房Tab:分类宫格与麦霸横滑
typescript
/** 房间 Tab:歌房分类宫格入口 + 热门麦霸横滑大卡 */
@Builder
tabRoom() {
Column({ space: 12 }) {
// 1. 歌房广场标题行 + 创建入口
Row() {
Text('🎤 歌房广场').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text(this.roomList.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%')
歌房 Tab 的第一个区块是标题行,左侧"🎤 歌房广场"标题使用粗体白色文字,右侧依次显示歌房总数与"+创建"按钮。总数通过 this.roomList.length.toString() 动态计算,当用户创建或解散歌房时数字实时更新。"+创建"按钮绑定 onClick 事件将 addModal 置 true,触发创建歌房弹窗的条件渲染。标题行使用 Column().layoutWeight(1) 在左右内容之间插入弹性占位,实现两端对齐布局。
typescript
// 2. 歌房分类宫格(Flex 换行双列宫格入口)
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(this.roomList, (item: RoomItem, idx: number) => {
Column({ space: 8 }) {
// 房间封面渐变块(emoji 图标 + LIVE 角标 + 标签)
Column() {
Text(item.icon).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('LIVE').fontSize(8).fontColor(COLORS.pink).fontWeight(FontWeight.Bold)
}
.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.online).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.openEditRoom(idx);
})
Text('解散').fontSize(8).fontColor(COLORS.pink)
.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: RoomItem) => item.name + item.tag)
}
.width('100%')
歌房分类宫格使用 Flex 布局容器并设置 wrap: FlexWrap.Wrap(允许换行)与 justifyContent: FlexAlign.SpaceBetween(两端对齐),实现每行两列的宫格布局。每个歌房卡片宽度设为 48%(而非 50%),SpaceBetween 对齐会在两个卡片之间自动分配剩余的 4% 空间作为间距。卡片内部从上到下分为四个区块:封面渐变块、房间名、在线人数、操作按钮行。
封面渐变块是卡片的视觉核心,高度 88、使用 145 度渐变(比标准 135 度略偏),内含大号 emoji 图标(fontSize(30))、底部的标签 chip(掌麦/排麦/连麦)与 "LIVE" 角标。标签使用青色文字+深紫底,LIVE 使用粉色粗体------在深色背景上粉色比青色更醒目,暗示直播进行中的活跃状态。封面块内的 Column().layoutWeight(1) 在 emoji 与底部标签行之间填充弹性空间,将 emoji 推到顶部、标签行沉到底部。
操作按钮行的"编辑"与"解散"分别绑定不同的点击事件:编辑调用 openEditRoom(idx) 回填数据并打开编辑弹窗,解散先将 delIdx 赋值为当前索引再打开解散确认弹窗------解散操作需要二次确认以防止误操作,这是涉及不可逆操作时的标准交互模式。ForEach 的键值生成函数使用 item.name + item.tag(房间名+标签的组合)作为唯一键,确保每个卡片的身份可追踪。
typescript
// 3. 热门麦霸标题行
Row() {
Text('🔥 热门麦霸').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('左右横滑围观').fontSize(9).fontColor(COLORS.text3)
}
.width('100%')
// 4. 热门麦霸横滑大卡(横向 Scroll + 固定宽人物大卡)
Scroll() {
Row({ space: 10 }) {
ForEach(this.singerList, (s: SingerItem) => {
Column({ space: 8 }) {
// 渐变头像圆块(呼吸动画)
Column() {
Text(s.avatar).fontSize(26).opacity(this.breath ? 1 : 0.75)
}
.width(52).height(52).borderRadius(26)
.linearGradient({ angle: 135, colors: [[COLORS.purpleD, 0], [COLORS.pink, 1]] })
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
// 昵称 + 等级
Text(s.name).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Text(s.level).fontSize(9).fontColor(COLORS.gold)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
// 粉丝 + 成名曲
Column({ space: 3 }) {
Text('粉丝 ' + s.fans).fontSize(8).fontColor(COLORS.sub)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Text('成名曲 ' + s.hit).fontSize(8).fontColor(COLORS.text3)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
}
.alignItems(HorizontalAlign.Center)
// 围观按钮
Text('围观').fontSize(9).fontColor(COLORS.title)
.padding({ left: 16, right: 16, top: 5, bottom: 5 })
.backgroundColor(COLORS.purple).borderRadius(10)
}
.width(132).padding(12).backgroundColor(COLORS.card).borderRadius(12)
.alignItems(HorizontalAlign.Center)
}, (s: SingerItem) => s.name)
}
.padding({ left: 2, right: 2 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
}
.width('100%')
}
热门麦霸区块使用横向 Scroll 容器实现横滑大卡列表,每张卡片固定宽度 132 像素。卡片内部从上到下依次为:渐变头像圆块、昵称、等级、粉丝与成名曲信息、"围观"按钮。头像圆块使用 135 度渐变从深紫到欢唱粉,emoji 头像的透明度受 breath 状态驱动在 1.0 与 0.75 之间波动,形成呼吸闪烁效果------这与头部 Banner 中的 🎤 图标呼吸动画共享同一个 breath 状态,实现了全局联动的动效节奏。
等级文本使用金色(COLORS.gold),在深色卡片背景上形成与紫色渐变头像的互补色对比。粉丝与成名曲使用更小的字号(8)与更弱的文字色(sub 与 text3),形成"主名---等级---辅助信息"的三级视觉层次。"围观"按钮使用紫色背景+白色文字,是卡片中唯一的行动召唤元素,宽度通过 padding 的左右 16 撑开为胶囊形按钮。卡片整体使用 alignItems(HorizontalAlign.Center) 使所有子元素水平居中对齐,保持了横滑列表的整齐视觉节奏。
7.2 点唱Tab:热歌榜单与柱状图
typescript
/** 点唱 Tab:大编号热歌点唱榜 + 月度欢唱时长柱状图 */
@Builder
tabOrder() {
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.songList, (item: SongItem, 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.pink : 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.pink).fontWeight(FontWeight.Bold)
Text(trendIcon(item.trend)).fontSize(9).fontColor(trendColor(item.trend))
}
.alignItems(HorizontalAlign.End)
// 点唱按钮
Text('点唱').fontSize(9).fontColor(COLORS.title)
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
.backgroundColor(COLORS.purple).borderRadius(8)
}
.width('100%').padding(12).borderRadius(12)
.backgroundColor(idx < 3 ? COLORS.chip : COLORS.card)
}, (item: SongItem) => item.rank + item.title)
// 月度欢唱时长柱状图(呼吸 ±5% 波动)
this.chartCard()
}
.width('100%')
}
点唱 Tab 的核心是热歌点唱榜列表,通过 ForEach 遍历 songList 渲染八条榜单数据。每行采用 Row 横向布局,从左到右依次为:大编号、歌曲信息列、热度趋势列、点唱按钮。编号的 fontSize 与 fontColor 通过嵌套三元表达式实现前三名差异化高亮------第一名金色 24 号字、第二名紫色 24 号字、第三名粉色 24 号字、第四名及以后灰紫色 17 号字。这种前三名放大高亮的设计是排行榜类 UI 的经典模式,通过视觉权重差异引导用户关注头部歌曲。
前三名的行背景使用 chip 色(比普通行的 card 色更亮),进一步强化头部排名的视觉突出。歌曲信息列使用 layoutWeight(1) 填充编号与热度之间的空间,内含歌名(粗体白色)与歌手名(辅助紫色)。热度趋势列右对齐显示,热度数值用粉色粗体,趋势文案通过 trendIcon 与 trendColor 两个辅助函数生成对应的图标与颜色。点唱按钮使用紫色背景+白色文字,固定宽度通过 padding 控制,是榜单行中的行动入口。
榜单下方通过 this.chartCard() 调用月度欢唱时长柱状图 Builder,将数据可视化与榜单信息组合在同一 Tab 中,形成"榜单+统计"的内容闭环。
7.3 AI字幕Tab:Speech Kit特性页
AI字幕 Tab 是整个应用的技术核心区块,它将 Speech Kit 6.1.1 的四大新特性以五个功能区块的形式完整展示给用户。这个 Tab 不仅是一个功能页面,更是一份可交互的特性文档。
7.3.1 特性简介条与实时预览卡
typescript
/** 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.purpleD, 0], [COLORS.purple, 1]] })
特性简介条是 AI 字幕 Tab 的顶部标识区域,使用与应用头部 Banner 相同的 135 度渐变背景,形成视觉一致性。简介条左侧是大号 🗣 图标,右侧两行文字分别展示主标题"Speech Kit·场景化语音服务"与副标题"HarmonyOS 6.1.1:AI字幕支持源语言/目标语言/字体颜色/字体大小"------副标题直接点明了四大新增字段的名称,作为用户进入该 Tab 后的首要信息告知。
typescript
// ===== 区块 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.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%')
// 错误信息行(onError 回调触发显示)
if (this.captionErrMsg !== '') {
Text('⚠ ' + this.captionErrMsg).fontSize(9).fontColor(COLORS.pink)
.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 的核心交互区域。预览卡顶部的状态标签通过 captionReady 状态驱动显示"已就绪"(绿色)或"初始化中"(金色)。当未就绪时,标签的 opacity 受 breath 状态驱动在 1.0 与 0.55 之间闪烁,形成"正在加载"的动效暗示------这是将 AI 字幕组件的异步初始化生命周期可视化的巧妙设计。
AICaptionComponent 组件本身是这一区块的灵魂,它接收三个参数:isShown(通过 @Link 双向绑定到 captionShown 状态,控制字幕显示/隐藏)、controller(传入 captionController 实例,用于调用 writeAudio 写入音频)、options(调用 buildCaptionOptions() 方法实时构建配置对象)。组件宽度 100%、高度 110、圆角 10,使用线条色边框勾勒出字幕显示区域的边界。当用户修改语言或外观设置时,buildCaptionOptions() 返回的配置对象随之变化,ArkUI 框架检测到 options 参数变化后触发字幕组件的热更新------字幕的识别语言、翻译方向、字体大小与颜色即时生效,无需重新初始化组件。
控制按钮行提供两个操作入口:左侧"开启字幕/隐藏字幕"按钮根据 captionShown 状态切换文案与背景色(开启态用亮紫 purple,隐藏态用深紫 purpleD),点击翻转 captionShown 值;右侧"写入演示音频"按钮调用 feedDemoAudio() 方法向控制器推送合成的 PCM 音频块,并显示已写入次数(×N)。错误信息行通过条件渲染仅在 captionErrMsg 非空时显示,以粉色文字+芯片色背景的警示样式呈现 Speech Kit 的 onError 回调信息。
7.3.2 语言设置卡
typescript
// ===== 区块 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.purple : COLORS.chip)
.borderRadius(10)
.onClick(() => {
this.switchSourceLang(l.code);
})
}, (l: LangOption) => l.code)
}
.width('100%')
语言设置卡是 6.1.1 新增的 sourceLanguage 与 targetLanguage 两个配置字段的 UI 操作入口。源语言部分通过 ForEach 渲染 SRC_LANGS 数组的两个选项(中文/英文),每个选项的 fontColor、fontWeight 与 backgroundColor 通过 this.srcLang === l.code 判断是否选中:选中态为白色粗体+紫色背景,未选中态为辅助色常规+芯片色背景。点击选项调用 switchSourceLang(l.code) 方法,该方法内部会根据语言码联动设置目标语言的默认值(中文源锁定 zh,英文源默认 zh-en)。
源语言标题行右侧标注"取值 'zh'|'en'",直接向开发者展示该字段的可选值范围------这种将 API 文档信息嵌入 UI 的设计使该 Tab 兼具用户功能页与技术文档的双重角色。
typescript
// 目标语言标题行(中文源锁定 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.purple : COLORS.chip)
.borderRadius(10)
.onClick(() => {
this.tgtLang = l.code;
})
}, (l: LangOption) => l.code)
}
.width('100%')
}
目标语言区域使用 if-else 条件渲染实现中文源与英文源的差异化展示。当 srcLang === 'zh'(中文源)时,渲染一个带 🔒 锁图标的锁定提示行,文字说明"中文源锁定中文:无翻译方向,targetLanguage 固定为 zh",向用户解释为何该选项不可选------这种透明的规则解释提升了用户体验,避免了"为什么不能选"的困惑。当 srcLang === 'en'(英文源)时,渲染三个目标语言选项(中文/英文/中英双语),选项的选中态样式与源语言一致。点击选项直接将 tgtLang 赋值为对应语言码,由于这些状态都是 @State,赋值后自动触发 buildCaptionOptions() 重建与字幕组件热更新。
typescript
// 当前语言组合摘要
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)
语言设置卡底部以一个青色小圆点+文字摘要的形式展示当前语言组合,通过 langName 辅助函数将语言码转为中文名,形成"当前组合:源 中文 → 目标 中英双语"这样的可读文本。这一摘要行让用户在调整设置后能够即时确认当前配置,是配置反馈的标准设计。
7.3.3 外观设置卡
typescript
// ===== 区块 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.purple : COLORS.chip)
.borderRadius(10)
.onClick(() => {
this.captionSize = s.size;
})
}, (s: SizeOption) => s.name)
}
.width('100%')
外观设置卡对应 6.1.1 新增的 fontSize 与 fontColor 两个字段。字体大小部分通过 ForEach 渲染 SIZE_OPTIONS 数组的四档选项(小号/标准/大号/超大),选中态样式与语言选项一致。标题行右侧标注"AICaptionFontSize"类型名,向开发者展示该字段的类型为枚举而非随意数字。点击选项将 captionSize 赋值为对应的 AICaptionFontSize 枚举值,触发字幕组件字号热更新。
typescript
// 字体颜色标题行(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.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)
字体颜色部分使用 Circle 圆形组件渲染五个颜色预设色块,每个色块直径 26 像素,填充对应颜色。选中态通过 border 的 color 属性切换描边颜色实现:选中时使用霓虹青(COLORS.cyan)描边宽度 2,未选中时使用线条色(COLORS.line)描边------在深色背景上,霓虹青描边形成了发光般的选中提示,与色块本身形成冷暖色对比。点击色块将 captionColor 赋值为对应颜色值。
底部说明行展示当前选中颜色的中文名(通过 colorName 函数转换)与十六进制值,并标注"作用于歌词原文与译文",向用户说明字体颜色同时影响字幕原文与翻译文本。fontColor 字段标题右侧标注"ResourceColor"类型名,表明该字段接受 ResourceColor 类型的值(包括十六进制色值字符串、Color 枚举等),具有灵活的颜色指定能力。
7.3.4 options实时代码预览卡
typescript
// ===== 区块 4:options 实时代码预览卡 =====
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%')
// 深色代码块(monospace,键名紫色高亮)
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)
区块四是 AICaptionOptions 的实时代码预览卡,这是一个极具特色的设计------它将当前的字幕配置以代码块的形式可视化展示在页面上,使用户(尤其是开发者用户)能够直观地看到配置对象的完整结构。代码块使用 fontFamily('monospace') 等宽字体,背景为最深的 bg 色(#120A1A),模拟代码编辑器的深色主题外观。
代码块内部的每一行由 Row 容器组合两个 Text 组件构成:左侧键名(如 sourceLanguage:)使用紫色(COLORS.purple),右侧值(如 'zh')使用青色(COLORS.cyan)或金色(COLORS.gold),实现了语法高亮的视觉效果。四个字段的值直接绑定到组件状态:srcLang、tgtLang、captionSize(通过 sizeName 转为枚举名字符串)、captionColor。当用户在语言设置卡或外观设置卡中修改配置时,这些状态值变化,代码块中的对应值实时同步更新------这种"所见即所得"的代码联动展示,使该 Tab 成为了一个可交互的 Speech Kit 特性文档。
特别值得注意的是 fontColor 行的值 Text 组件使用了 .fontColor(this.captionColor),即值的文字颜色就是用户选中的字幕颜色本身------这样当用户选择"樱花粉"时,代码块中的 '#FFB3C1' 值文字也会变成粉色,形成配置与展示的一致性反馈。底部标注"★ 6.1.1 新增字段"明确标识了这四个字段是版本新增能力。
7.3.5 字幕场景推荐列表
typescript
// ===== 区块 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.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%')
}
区块五是字幕场景推荐列表,将五种 K 歌场景及其推荐的语言组合以列表形式展示。每条场景行左侧有一条 4 像素宽、42 像素高的彩色竖条,颜色按索引模 3 轮换使用紫色、青色与金色,为列表项增加视觉区分度。中间区域展示场景名(粗体白色)、场景说明(辅助紫色)与推荐语言组合(青色"源 en → 目标 zh-en"格式)。右侧"套用 ›"文字暗示该行可点击。
点击事件调用 switchSourceLang(item.src) 设置源语言(该方法内部会联动目标语言默认值),随后直接将 tgtLang 覆盖为场景推荐的目标语言值------两步操作确保了语言组合的准确套用。这一设计将抽象的配置参数转化为具体的业务场景一键预设,大幅降低了用户理解与配置成本,是"场景驱动配置"设计思想的典型实现。
7.4 我的Tab:会员大卡与功能清单
typescript
/** 我的 Tab:用户信息+会员渐变大卡 + 功能清单行 */
@Builder
tabMine() {
Column({ space: 12 }) {
// 1. 用户信息 + 黄金麦霸会员渐变大卡(霓虹紫 → 欢唱粉)
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:mb1120 · 已连续开嗓欢唱 89 天')
.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('36').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('2.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('520').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 的顶部是用户信息与会员权益的渐变大卡,使用与应用头部相同的 135 度渐变背景。大卡分为上下两部分:上部是头像(圆形深紫底+大号 🎤)与用户信息(昵称"阿澈爱高音"+金色 VIP 标签+欢唱 ID 与连续天数),下部是三格欢唱数据(欢唱作品 36 首作品、获赞总数 2.8 万、欢唱小时 520 小时)。三格数据通过 Row 与 layoutWeight(1) 实现均等三等分布局,数值使用粗体白色,标签使用 70% 透明度的白色(opacity(0.7)),形成"数值醒目---标签弱化"的层级。
typescript
// 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%')
}
功能清单通过 ForEach 遍历 statList 渲染八条功能行,每行采用 Row 横向布局:左侧 emoji 图标、中间功能名(layoutWeight(1) 填充空间)、右侧状态/数值文本、最右侧条件渲染的导航箭头(›)。arrow 布尔字段控制箭头是否显示,虽然当前 Mock 数据全部为 true,但保留了纯信息行不显示箭头的扩展能力。每行使用卡片底色与 10 像素圆角,形成紧凑而整齐的设置列表外观。
八、图表卡与底部导航
8.1 月度欢唱柱状图
typescript
/** 图表卡:月度欢唱时长柱状图(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.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.pink, 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 月累计 520 小时').fontSize(8).fontColor(COLORS.sub)
Column().layoutWeight(1)
Text('环比 +13.0%').fontSize(8).fontColor(COLORS.green)
}
.width('100%')
}
.width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)
}
chartCard Builder 实现了一个纯 ArkUI 声明式的柱状图组件,无需引入任何图表库。柱状图使用 Row 容器横向排列六个月份的数据列,每列通过 Column 纵向堆叠数值标签(顶部)、柱体(中间)与月份标签(底部)。Row 设置 alignItems(VerticalAlign.Bottom) 使所有列底部对齐,高度 150 像素。
柱体高度通过数学公式计算:Math.max(20, MONTH_HOURS[i] / MONTH_MAX * 110 * (this.breath ? 1.05 : 0.95))。MONTH_HOURS[i] / MONTH_MAX 将月度数值归一化为 0-1 的比例,乘以 110(柱体最大高度基数),再乘以呼吸系数(breath 为 true 时 1.05,为 false 时 0.95),实现 ±5% 的柱高波动。Math.max(20, ...) 确保柱体最小高度 20 像素,防止低数值月份的柱体过矮不可见。柱体使用 180 度纵向渐变从紫色到粉色,borderRadius(5) 使柱顶圆角,视觉上柔和而不尖锐。数值标签的颜色也受 breath 驱动在青色与辅助紫色之间切换,与柱高波动形成双重动效联动。
ForEach 的键值生成函数使用 'm' + i.toString()(如 m0、m1)作为每列的唯一键,确保列表渲染时元素正确复用。汇总行展示"近 6 月累计 520 小时"与"环比 +13.0%"(绿色正值),为柱状图提供数据总览。
8.2 底部导航栏
typescript
/** 底部导航: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 })
}
底部导航栏使用 Row 容器等分排列四个 Tab,每个 Tab 通过 layoutWeight(1) 均分宽度。选中态的视觉差异通过三个维度体现:图标 fontSize 从 17 放大到 20(放大暗示重要性)、图标 opacity 从 0.65 提升到 1.0(不透明度增强)、标签 fontColor 从灰紫 text3 切换到霓虹紫 tabOn 并加粗。这种多维度的选中态差异确保了用户能够清晰识别当前所在 Tab。
ForEach 的键值生成函数使用 t.label(Tab 标签名)作为唯一键。点击 Tab 将 currentTab 赋值为对应索引,触发 build() 中的条件渲染切换显示对应的 Tab 内容。底部导航栏使用 card 底色与仅顶部的 1 像素线条边框(border({ width: { top: 1 }, color: COLORS.line })),与上方内容区形成视觉分隔。
九、弹窗系统
9.1 全屏遮罩
typescript
/** 弹窗全屏遮罩(点击遮罩关闭弹窗) */
@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 回调函数作为参数。遮罩使用 rgba(10,4,20,0.7) 半透明背景色(在 COLORS.mask 中定义),覆盖全屏。Stack 容器设置 alignContent(Alignment.Center) 使后续弹窗面板内容居中显示。遮罩的 onClick 事件绑定 onClose 回调,用户点击遮罩区域(弹窗面板外的区域)即可关闭弹窗------这是模态弹窗的标准交互模式,提供了除关闭按钮外的快捷退出方式。
9.2 创建歌房弹窗
typescript
/** 创建歌房弹窗面板(名称 + 类型输入) */
@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.formType, placeholder: '如:掌麦 / 排麦 / 连麦' })
.fontSize(11).fontColor(COLORS.title)
.backgroundColor(COLORS.chip).borderRadius(8)
.onChange((value: string) => {
this.formType = 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.saveRoom();
})
}
.width('100%')
}
.width('78%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
}
.width('100%')
.height('100%')
.alignContent(Alignment.Center)
}
创建歌房弹窗在 Stack 中先渲染遮罩层(modalOverlay),再渲染居中的 Column 面板。面板宽度 78%,使用卡片底色与 14 像素圆角。面板内部从上到下为:标题"创建歌房"、歌房名称输入框(TextInput 绑定 formName 状态)、房间类型输入框(TextInput 绑定 formType 状态,placeholder 提示三种类型)、取消与创建按钮行。
TextInput 的 text 参数绑定到对应的状态变量,onChange 回调在输入变化时更新状态------这是 ArkUI 表单双向绑定的标准模式。两个按钮使用 layoutWeight(1) 等分宽度,取消按钮使用辅助色文字+芯片色底(弱视觉权重),创建按钮使用白色粗体文字+紫色底(强视觉权重),形成主次分明的行动引导。创建按钮点击调用 saveRoom() 方法保存新建歌房,该方法内部会关闭弹窗并清空表单。
9.3 编辑歌房弹窗
typescript
/** 编辑歌房弹窗面板(回填名称 + 类型) */
@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.editType, placeholder: '房间类型' })
.fontSize(11).fontColor(COLORS.title)
.backgroundColor(COLORS.chip).borderRadius(8)
.onChange((value: string) => {
this.editType = 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.updateRoom();
})
}
.width('100%')
}
.width('78%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
}
.width('100%')
.height('100%')
.alignContent(Alignment.Center)
}
编辑歌房弹窗与创建弹窗结构高度一致,区别在于:标题为"编辑歌房"、TextInput 绑定 editName 与 editType 状态(这两个状态在 openEditRoom 方法中已被回填了当前歌房的数据)、确认按钮文案为"保存修改"且使用青色背景(COLORS.cyan,区别于创建弹窗的紫色按钮)、点击调用 updateRoom() 方法保存修改。编辑弹窗的 TextInput 在 openEditRoom 被调用时就已经通过状态赋值完成了数据回填,用户打开弹窗即可看到预填的当前歌房名称与类型,这是编辑流程的标准交互。
9.4 解散歌房确认弹窗
typescript
/** 解散歌房确认弹窗面板 */
@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.pink).borderRadius(9)
.onClick(() => {
this.delRoom();
})
}
.width('100%')
}
.width('78%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
}
.width('100%')
.height('100%')
.alignContent(Alignment.Center)
}
}
解散歌房确认弹窗是一个纯确认型弹窗,不含输入框,只有标题、风险提示文字与取消/确认按钮。提示文字"确认解散该歌房吗?解散后房内麦友与合唱记录将一并清理,且不可恢复"明确告知用户操作的不可逆性与影响范围------这是涉及数据销毁操作时的必要告知。"确认解散"按钮使用欢唱粉背景(COLORS.pink),与创建弹窗的紫色按钮和编辑弹窗的青色按钮形成颜色区分,暗示该操作具有更高的风险性。点击确认调用 delRoom() 方法执行删除,点击取消调用 onClose 关闭弹窗。
三套弹窗(创建/编辑/解散)的确认按钮分别使用紫色、青色、粉色三种不同颜色,不仅形成视觉区分,更隐含了操作的风险层级:创建(紫色,主题色,中性操作)、编辑(青色,辅助色,安全操作)、解散(粉色,警示色,危险操作)。这种通过颜色语义编码操作风险等级的设计,是深色主题应用中无障碍交互的细节体现。
十、技术特性对比表格
| 技术维度 | 传统字幕方案 | 本案例 AI 字幕方案(Speech Kit 6.1.1) | 优势说明 |
|---|---|---|---|
| 字幕生成方式 | 预录入文本或第三方 ASR 接口 | 端侧 AICaptionComponent 组件实时识别 | 端侧低延迟,无需自建 ASR 服务,系统集成度高 |
| 源语言支持 | 固定或需后端配置 | sourceLanguage 字段 'zh'|'en' 前端动态切换 | 用户实时切换源语言,引擎即时适配对应语种模型 |
| 目标语言/翻译方向 | 无翻译或固定单方向 | targetLanguage 字段 'zh'|'en'|'zh-en' 三选 | 支持中英双语对照显示,适配跟唱与跨语言场景 |
| 字体大小 | 固定或需 CSS 覆盖 | fontSize 枚举 SMALL/NORMAL/BIG/LARGE 四档 | 枚举类型保障取值合法性,四档满足不同视距需求 |
| 字体颜色 | 固定或需样式覆盖 | fontColor 字段 ResourceColor 任意色值 | 五预设色一键切换,用户个性化字幕外观 |
| 配置热更新 | 需重建组件或刷新页面 | options 实时重建,组件热更新无感切换 | 修改语言/外观后字幕即时生效,无中断体验 |
| 音频数据输入 | 文件上传或流推送 | AICaptionController.writeAudio 逐块推送 | 支持 20ms 粒度 PCM 实时写入,适配实时麦克风采集 |
| 生命周期管理 | 手动管理初始化与销毁 | onPrepared/onError 回调驱动状态机 | 就绪与异常状态自动回调,UI 无缝响应生命周期 |
| 动画方案 | CSS 或第三方动画库 | 单 breath 布尔状态驱动多动效 | 极简的定时器翻转,驱动呼吸/波动/闪烁等动效 |
| 主题系统 | 各处硬编码颜色 | ColorPalette 接口 + COLORS 常量集中管理 | 接口约束字段,主题切换只需替换常量对象 |
| 布局策略 | 固定或响应式框架 | Flex 换行宫格+横向 Scroll+条件渲染 | 声明式 Flex/Scroll/ForEach 灵活组合无第三方依赖 |
| 弹窗系统 | 第三方 Modal 组件 | Stack 堆叠+状态驱动+遮罩回调 | 原生 Stack 层叠实现模态,无额外组件依赖 |
| 数据模型 | interface 或 plain object | @Observed class + @State 数组 | 可观察对象自动驱动 UI 更新,数组引用刷新技巧 |
十一、总结
本文完整剖析了一个基于 HarmonyOS ArkUI 框架与 Speech Kit 6.1.1 新特性构建的 K 歌社交应用。从技术架构层面看,该应用展示了 ArkUI 声明式开发范式的完整实践:@Entry/@Component/@State/@Builder/@Observed 等装饰器协同工作,构建了从入口组件到数据模型的全链路状态管理---声明式渲染体系。Stack 堆叠容器作为弹窗系统的底层支撑,Flex 弹性布局实现换行宫格,Scroll 横向滚动实现横滑列表,ForEach 实现数据驱动渲染------这些原生容器组件的组合使用证明了 ArkUI 在不依赖第三方 UI 库的情况下,足以构建功能完整、交互丰富、视觉精致的应用界面。
从 Speech Kit 6.1.1 新特性的实践层面看,该应用将 AICaptionComponent 的 sourceLanguage、targetLanguage、fontSize、fontColor 四大新增字段全部纳入前端可配置状态管理。通过 buildCaptionOptions() 方法在每次渲染时实时组装配置对象,实现了语言切换、字号调整、颜色更换的配置热更新------用户修改设置后字幕组件即时响应,无需重新初始化。AICaptionController.writeAudio() 方法配合合成的 PCM 音频数据,完整演示了从音频输入到字幕输出的端到端数据通路。onPrepared 与 onError 两个回调将异步生命周期事件转换为组件状态,驱动 UI 上的就绪提示与错误展示。五个功能区块(实时预览/语言设置/外观设置/代码预览/场景推荐)的有机组合,使该 Tab 兼具用户功能页与技术文档的双重角色,是 AI 能力在业务场景中深度融合的典范。
从设计理念层面看,应用采用舞台黑(#120A1A)+霓虹紫(#A855F7)+欢唱粉(#F472B6)的深色主题,营造了 K 歌夜场的沉浸式视觉氛围。ColorPalette 接口与 COLORS 常量构成的集中式颜色管理系统,确保了视觉一致性与主题可扩展性。单 breath 布尔状态驱动的多动效联动(头像呼吸、柱状图波动、就绪闪烁、数值变色)是声明式 UI 下轻量动画的经典实践------用一个定时器翻转一个布尔值,即可驱动全局多处动效,体现了"最小状态---最大表达"的设计哲学。三套弹窗的确认按钮分别使用紫色、青色、粉色三色编码,隐含了创建(中性)、编辑(安全)、解散(危险)的操作风险层级,是无障碍交互的细节体现。
从业务场景层面看,五个字幕场景预设(外语跟唱、说唱速记、中文合唱、粤语辅助、连麦互动)精准覆盖了 K 歌社交中的典型字幕需求。粤语音频因 sourceLanguage 仅支持 zh/en 而归入中文源处理的设计,客观反映了 6.1.1 版本的能力边界与应用层面的适配策略。场景推荐列表的"点击套用语言组合"交互,将抽象的 API 参数配置转化为具体的业务场景一键预设,是"场景驱动配置"设计思想的落地实践。歌房数据模型中"掌麦/排麦/连麦"三种互动模式的分类,以及歌房分类标签覆盖八大曲风(情歌/粤语/说唱/摇滚/民谣/戏腔/合唱),体现了对 K 歌社交业务域的深入理解。
总体而言,该应用是 HarmonyOS 原生生态中 AI 能力与垂直业务场景融合的典型范例,对 Speech Kit 6.1.1 新特性的实践具有直接的参考价值。它不仅展示了"怎么用"(四大字段的配置方法与热更新机制),更展示了"为什么用"(每个字段对应的 K 歌业务场景需求)和"用得怎样"(配置变更后的实时预览与场景化推荐),为 HarmonyOS 开发者理解与运用 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 语法:
typescript
@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 版本编写,不同版本界面可能存在细微差异。