一、技术前言

在这里插入图片描述

在移动端内容消费形态持续演进的当下,新闻资讯音频化已经成为一个不可逆转的行业趋势。从早期的图文资讯门户,到短视频新闻流,再到如今的"听新闻"模式——新闻消费的介质正在从"看"向"听"迁移。这一迁移背后的驱动力是多方面的:通勤场景下双手被占用但耳朵空闲、老年用户群体对大字号音频内容的天然偏好、多任务并行场景下音频的伴随性优势,以及智能耳机和车载音频终端的普及。在这一背景下,"快闻早报·新闻资讯音频平台"应运而生——它将传统的新闻图文资讯转化为音频节目形态,配合 AI 字幕能力,构建了一个"听+看"双通道的新闻消费体验。本文将从代码的第一行开始,逐段深入剖析这个基于 HarmonyOS ArkUI 框架构建的新闻资讯音频应用的完整技术实现。

在这里插入图片描述
HarmonyOS 的 ArkUI 框架是华为自研的声明式 UI 开发范式,其设计理念与 SwiftUI、Jetpack Compose、Flutter 等现代声明式框架一脉相承。开发者通过 @Entry@Component@State@Builder@Observed 等装饰器以接近自然描述的方式声明界面结构和状态依赖关系,由框架负责高效的差分渲染和响应式更新。在 ArkTS 语言体系中,类型安全被提升到了前所未有的高度——所有变量、参数、返回值都需要明确的类型标注,接口(interface)被广泛用于定义数据结构契约,这使得编译期就能捕获大量潜在的类型错误。在本应用中,ArkUI 的这些特性得到了充分发挥:4 个 Tab 页面通过 @Builder 函数独立封装且每个布局风格截然不同,20 余个状态变量通过 @State 管理并驱动响应式更新,4 个数据模型通过 @Observed 实现深度观察,3 个弹窗面板通过条件渲染按需挂载在 Stack 层叠之上。

在这里插入图片描述
Speech Kit 是 HarmonyOS 提供的语音能力套件,其中 AICaptionComponent 是 HarmonyOS 6.1.1 版本重点增强的 AI 字幕组件。在 6.1.1 版本中,AICaptionOptions 接口新增了四大关键字段,使得 AI 字幕能力从"有"迈向了"精":sourceLanguage(源语言,取值 'zh''en')用于指定输入音频的原始语言;targetLanguage(目标语言,取值 'zh''en''zh-en')用于指定字幕翻译的目标语言,其中 'zh-en' 表示中英双语对照显示;fontSize(字体大小,类型为 AICaptionFontSize 枚举,包含 SMALLNORMALBIGLARGE 四档)用于控制字幕文字的显示尺寸;fontColor(字体颜色,类型为 ResourceColor)用于自定义字幕文字的显示色彩。这四个字段的引入,使得开发者能够根据不同的使用场景——如国际新闻精听、双语晨间简报、通勤静音模式——灵活配置字幕的语言方向和视觉外观,极大提升了 AI 字幕在真实业务场景中的可用性和体验质量。

在这里插入图片描述
在技术架构层面,AICaptionComponent 的工作机制是一个"音频流入→AI 识别→字幕渲染"的完整链路。开发者通过 AICaptionControllerwriteAudio 方法向字幕服务写入 PCM 音频数据块(如 16kHz/16bit/单声道的 PCM 块),AI 引擎在服务端进行语音识别和翻译,识别结果通过 isShown 双向绑定的 @Link 状态控制字幕组件的显示与隐藏,onPrepared 回调在字幕服务就绪时触发,onError 回调在出现异常时返回 BusinessError 错误对象。这种"控制器写入 + 组件渲染 + 回调通知"的三段式架构,既保证了音频数据写入的灵活性,又确保了字幕渲染的声明式一致性,是 HarmonyOS 系统能力组件化设计的典型范例。

在这里插入图片描述
从业务场景来看,"快闻早报"定位为新闻资讯音频行业应用,其核心价值在于让用户能够以"听"的方式高效获取每日新闻资讯。应用设计了 4 个功能维度:头条 Tab 以"虚线早报摘要大卡 + 左色条新闻列表"呈现当日要闻与早报内容;电台 Tab 以"正在播出渐变大卡 + 时长大卡节目单 + 月度收听柱状图"展示电台节目与收听数据;AI 字幕 Tab 作为 Speech Kit 特性页,将 AICaptionComponent 的四大新字段通过"预览/语言/外观/代码/场景"五个区块完整呈现;我的 Tab 以"会员渐变大卡 + 功能清单行"展示用户信息与功能入口。整体设计采用浅色主题——新闻白(#F7F8FA)作为背景底色、权威藏蓝(#2B5CAD)作为主操作色、快讯红(#D94A3D)作为热度与警示色,营造出新闻媒体特有的权威感与时效感并存的视觉氛围。

在这里插入图片描述
从设计理念来看,浅色主题的选择并非随意。新闻资讯类应用的核心信息载体是文字——新闻标题、来源媒体、发布时间、热度数值——这些文字信息在浅色背景上具有天然的高对比度和高可读性优势。权威藏蓝(#2B5CAD)传达了新闻媒体的严肃与可信气质,快讯红(#D94A3D)则在热度标记、删除按钮等需要视觉警示的位置形成醒目的视觉锚点。整个色彩体系通过 ColorPalette 接口集中管理 15 个颜色字段,涵盖背景色、卡片色、标签色、三级文字层次(title/sub/text3)、主色及其深色变体、功能色(红/绿/金)、分割线和遮罩等,每一组色系都服务于特定的信息表达需求。渐变设计(linearGradient)在头部 Banner、电台"正在播出"大卡、"我的"页会员卡等位置反复运用,在浅色主题中创造了视觉层次感和焦点引导效果。

在工程化层面,本应用同样体现了良好的实践。数据模型使用 @Observed 装饰器标注,为响应式数据更新做好准备;状态管理通过 @State 统一管理,涵盖 Tab 索引、呼吸动画、频道选中、弹窗开关、字幕状态等 20 余个状态变量;生命周期函数 aboutToAppear/aboutToDisappear 负责呼吸动画定时器的创建与清理;弹窗系统通过 Stack 层叠包裹,用三个独立的面板构建函数管理订阅频道、编辑快讯、删除确认三种交互场景;辅助函数(catColorsizeNamelangNamecolorName)将业务逻辑与视图渲染解耦,使得 Builder 函数内部的代码更加简洁清晰。下面,我们将从代码的第一行开始,逐段、逐块地深入分析这个新闻资讯音频平台的完整技术实现。


二、整体架构流程图

为了更好地理解"快闻早报"应用的整体架构和各模块间的数据流向,我们使用以下 Mermaid 流程图来展示组件之间的层次关系与调用链路:

弹窗系统

底部导航

内容区域 Scroll

核心业务方法

数据层

AI字幕状态层 6.1.1

状态管理层 @State

入口组件

主页面组件
@Entry @Component

currentTab: number
当前激活 Tab 索引

breath: boolean
呼吸动画开关(每秒翻转)

timer: number
呼吸动画定时器句柄

cateIdx: number
频道 chips 选中索引

addModal / editModal / delModal
三个弹窗开关

editIdx / delIdx
当前编辑/删除索引

newsList / radioList
sceneList / statList
四组业务列表数据

formName / formNote
订阅表单字段

editTitle / editSrc
编辑表单字段

captionController
AICaptionController 控制器

captionShown: boolean
字幕显示状态 @Link

srcLang: string
sourceLanguage 源语言

tgtLang: string
targetLanguage 目标语言

captionSize: AICaptionFontSize
fontSize 字体大小

captionColor: string
fontColor 字体颜色

captionReady / captionErrMsg
captionFed
就绪/错误/写入计数

ColorPalette 色彩体系
15 个颜色常量

NewsItem / RadioItem
CaptionScene / UserStat
4 个 @Observed 数据模型

NEWS_LIST / RADIO_LIST
SCENE_LIST / STAT_LIST
4 组 Mock 数据

SRC_LANGS / TGT_LANGS_EN
SIZE_OPTIONS / CAPTION_FONT_COLORS
字幕语言/字号/颜色常量

MORNING_BRIEFS / CATE_TAGS
MONTH_* 图表常量

catColor / sizeName
langName / colorName
4 个辅助函数

buildCaptionOptions
组装 AICaptionOptions

switchSourceLang
源语言联动目标语言

feedDemoAudio
写入演示 PCM 音频流

openEditNews / saveNews
updateNews / delNews
新闻增删改

headerMain
渐变 Banner + 搜索条 + chips

tabNews
Tab0 虚线摘要卡 + 左色条新闻列表

tabRadio
Tab1 正在播出卡 + 节目单 + 柱状图

tabCaption
Tab2 AI字幕特性五区块

tabMine
Tab3 会员卡 + 功能清单

chartCard
月度收听柱状图

tabBar
4 Tab 单排

modalOverlay
全屏遮罩

panelAdd
订阅频道弹窗

panelEdit
编辑快讯弹窗

panelDel
删除确认弹窗

从架构图中可以清晰地看到,整个应用以主页面组件为入口,向下分为五大层次:状态管理层(含通用状态与 AI 字幕专用状态)、数据层(色彩体系 + 数据模型 + Mock 数据 + 字幕常量 + 辅助函数)、核心业务方法层(字幕选项构建 + 语言联动 + 音频写入 + 增删改)、内容区域层(头部 + 四个 Tab + 柱状图 + 底部导航)、弹窗系统层(遮罩 + 三个面板)。各层之间通过状态变量和 Builder 函数建立依赖关系,形成了一个层次清晰、职责分明的组件化架构。


三、文件头部注释与设计意图

/**
 * =====================================================================
 * 1119 快闻早报 · 新闻资讯音频平台(现代行业:新闻资讯音频)浅色主题
 * 头部样式:顶部渐变 Banner(日期+天气问候+今日要闻数)+ 搜索条 + 横滑资讯频道 chips
 * 布局风格:4 个 Tab 每个布局完全不同
 *   头条=左色条新闻列表+虚线早报摘要大卡 / 电台=时长大卡节目单+月度收听柱状图
 *   AI字幕=Speech Kit 特性页 / 我的=用户会员渐变大卡+功能清单行
 * Speech Kit 特性(HarmonyOS 6.1.1 新特性):AI字幕支持设置源语言、目标语言
 *   以及对应语言下字体颜色和字体大小(AICaptionOptions 四大新增字段)
 *   sourceLanguage('zh'|'en')/ targetLanguage('zh'|'en'|'zh-en')
 *   fontSize(AICaptionFontSize 四档枚举)/ fontColor(ResourceColor 字幕色)
 * 弹窗系统:modalOverlay 全屏遮罩 + panelAdd 订阅频道 / panelEdit 编辑快讯 / panelDel 删除确认
 * 底部 4 Tab 单排,主题:新闻白(#F7F8FA) + 权威藏蓝(#2B5CAD) + 快讯红(#D94A3D)
 * =====================================================================
 */

文件头部注释是整个应用的"技术蓝图",它以结构化的方式概述了应用的所有设计决策。这段注释包含了六个关键维度的信息:

第一,应用定位——"快闻早报 · 新闻资讯音频平台"明确了这是一个面向新闻资讯音频行业的应用,"现代行业"标签表明它不是一个通用 Demo,而是针对特定行业场景设计的产品级实现。

第二,头部样式——顶部渐变 Banner 承载日期、天气问候和今日要闻数三类信息,搜索条提供内容检索入口,横滑资讯频道 chips 提供分类导航。这三层头部结构是新闻类应用的标准布局模式。

第三,布局风格——4 个 Tab 的布局风格"完全不同",这是一个大胆的设计决策。头条采用"左色条新闻列表 + 虚线早报摘要大卡",电台采用"时长大卡节目单 + 月度收听柱状图",AI字幕作为 Speech Kit 特性页,我的采用"会员渐变大卡 + 功能清单行"。这种"一页多态"的设计哲学本质上是对"不同业务场景需要不同的信息表达方式"这一设计原则的深度践行。

第四,Speech Kit 特性——这是整个应用的技术核心。HarmonyOS 6.1.1 为 AICaptionOptions 新增了四大字段:sourceLanguage(源语言,'zh''en')、targetLanguage(目标语言,'zh''en''zh-en' 中英双语)、fontSizeAICaptionFontSize 四档枚举)、fontColorResourceColor 字幕色)。这四个字段的组合配置使得 AI 字幕能够适应国际新闻精听、双语晨间简报、通勤静音模式等多种新闻消费场景。

第五,弹窗系统——modalOverlay 全屏遮罩配合三个面板(panelAdd 订阅频道、panelEdit 编辑快讯、panelDel 删除确认),构成了一个完整的增删改交互闭环。

第六,主题色彩——新闻白(#F7F8FA)作为浅色背景底色,权威藏蓝(#2B5CAD)作为主操作色和品牌色,快讯红(#D94A3D)作为热度标记和警示色,三色组合营造出新闻媒体特有的权威感与时效感。


四、模块导入与颜色系统设计

4.1 Speech Kit 模块导入

import { AICaptionComponent, AudioData, AICaptionOptions, AICaptionController, AICaptionFontSize } from '@kit.SpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';

这两行导入语句是整个应用技术能力的根基。第一行从 @kit.SpeechKit 模块中导入了五个关键类型:AICaptionComponent 是 AI 字幕的 UI 组件,负责字幕的声明式渲染;AudioData 是音频数据接口,用于封装写入字幕服务的 PCM 数据块;AICaptionOptions 是字幕配置选项接口,承载 sourceLanguage/targetLanguage/fontSize/fontColor 四大新字段以及 onPrepared/onError 回调;AICaptionController 是字幕控制器,提供 writeAudio 方法向字幕服务写入音频流;AICaptionFontSize 是字体大小枚举,包含 SMALL/NORMAL/BIG/LARGE 四档。

第二行从 @kit.BasicServicesKit 导入 BusinessError 类型,它是 HarmonyOS 系统能力调用的标准错误对象,包含 code(错误码)和 message(错误描述)两个字段。在 AICaptionOptionsonError 回调中,错误信息正是通过 BusinessError 对象传递的。

4.2 ColorPalette 色彩接口

/** 主题色板接口:集中声明页面所有颜色字段(新闻白+权威藏蓝+快讯红浅色系) */
interface ColorPalette {
  bg: string;
  card: string;
  chip: string;
  title: string;
  sub: string;
  text3: string;
  navy: string;
  navyD: string;
  red: string;
  green: string;
  gold: string;
  line: string;
  tabOn: string;
  white: string;
  mask: string;
}

ColorPalette 接口是整个应用的色彩契约。通过接口集中声明所有颜色字段,而非散落在代码各处使用魔法字符串,这种设计带来了三个显著优势:第一,类型安全——任何使用 ColorPalette 类型标注的变量都会在编译期进行字段检查,拼写错误会立即被捕获;第二,可维护性——如果需要切换主题(如增加深色模式),只需提供另一个实现 ColorPalette 接口的常量对象即可,所有引用处无需修改;第三,可读性——COLORS.navy'#2B5CAD' 更能表达设计意图。

接口中定义了 15 个颜色字段,可以分为五组:背景与容器色bg 页面背景、card 卡片白底、chip 浅灰标签底);文字三级层次title 主标题深色、sub 副文本中灰、text3 辅助文本浅灰);品牌主色navy 权威藏蓝、navyD 深藏蓝变体);功能色red 快讯红、green 成功绿、gold 高亮金);辅助色line 分割线、tabOn Tab 选中色、white 纯白、mask 遮罩半透明黑)。三级文字层次的设计是浅色主题中的关键——它通过颜色深浅创造了信息优先级,引导用户视线从标题到副文本再到辅助信息逐层递减。

4.3 COLORS 色彩常量

/** 浅色主题色板常量(快闻早报 · 新闻白 + 权威藏蓝 + 快讯红) */
const COLORS: ColorPalette = {
  bg: '#F7F8FA',
  card: '#FFFFFF',
  chip: '#EDF0F5',
  title: '#222831',
  sub: '#5F6B7A',
  text3: '#9AA5B4',
  navy: '#2B5CAD',
  navyD: '#1E4485',
  red: '#D94A3D',
  green: '#4EA860',
  gold: '#C9A45C',
  line: '#E3E7EE',
  tabOn: '#2B5CAD',
  white: '#FFFFFF',
  mask: 'rgba(34,40,49,0.5)'
};

COLORS 常量是 ColorPalette 接口的浅色主题实现。每个色值都经过精心选择,服务于特定的视觉表达需求:#F7F8FA 是一种带有微弱冷调的灰白色,比纯白更柔和、更适合长时间阅读新闻内容;#222831 作为标题色是一种接近黑色的深蓝灰,在浅色背景上对比度极高且不刺眼;#2B5CAD 权威藏蓝是整个应用的品牌色,它出现在 Tab 选中、按钮主操作、左色条、渐变起点等所有需要强调品牌身份的位置;#D94A3D 快讯红则用于热度数值和删除操作,传达紧迫感和警示感。

值得注意的是 mask 字段使用了 rgba() 格式而非十六进制色值,rgba(34,40,49,0.5) 表示基于标题色 #222831 的半透明遮罩,透明度 0.5 既保证了背景内容的可见性,又确保了弹窗面板的视觉焦点突出。tabOnnavy 使用相同的色值,这种冗余设计是有意为之——tabOn 语义上表示"Tab 选中色",如果未来需要将 Tab 选中色改为其他颜色(如金色),只需修改 tabOn 而不影响 navy 的其他引用。


五、常量定义与数据配置

5.1 底部导航 Tab 常量

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

/** 底部导航 Tab 常量列表(4 Tab 单排) */
const TAB_LIST: TabMeta[] = [
  { icon: '📰', label: '头条' },
  { icon: '🎙', label: '电台' },
  { icon: '🗣', label: 'AI字幕' },
  { icon: '👤', label: '我的' }
];

TabMeta 接口定义了底部导航每个 Tab 的数据结构,包含 icon(emoji 图标)和 label(文本标签)两个字段。TAB_LIST 常量数组定义了 4 个 Tab 的完整配置——头条(📰)、电台(🎙)、AI字幕(🗣)、我的(👤)。使用 emoji 作为图标是一种在 Demo 应用中简化资源管理的实用策略——它避免了引入图片资源文件的复杂性,同时在大多数设备上都能正确渲染。在生产环境中,可以将 emoji 替换为 Image 组件加载的 SVG 或 PNG 图标资源。

4 个 Tab 的顺序设计也值得关注:头条作为核心内容入口排在首位,电台作为音频节目的载体排在第二,AI字幕作为技术特性展示页排在第三,我的作为个人中心排在末尾——这符合"内容优先 → 功能次之 → 个人最后"的信息架构原则。

5.2 资讯频道与早报要点常量

/** 头部横滑资讯频道 chips 文案 */
const CATE_TAGS: string[] = ['要闻', '国际', '财经', '科技', '体育', '民生', '评论', '视频'];

/** 今日早报要点条目(虚线大卡内展示) */
interface BriefItem {
  icon: string;   // 要点 emoji
  text: string;   // 要点文案
}

/** 今日早报要点 Mock 数据(3 条) */
const MORNING_BRIEFS: BriefItem[] = [
  { icon: '🌍', text: '全球气候峰会闭幕:多国达成新一轮减排共识' },
  { icon: '📉', text: '央行宣布降准 0.5 个百分点,释放长期资金' },
  { icon: '🤖', text: '新一代国产大模型发布:推理成本再降四成' }
];

CATE_TAGS 定义了 8 个资讯频道分类标签,从"要闻"到"视频"覆盖了新闻资讯的主要分类维度。这些标签将渲染为头部横滑的 chips 组件,用户点击后通过 cateIdx 状态变量记录选中索引。BriefItem 接口定义了早报要点的数据结构,包含 icon(emoji 图标)和 text(要点文案),MORNING_BRIEFS 数组提供了 3 条 Mock 要点数据,内容涵盖国际、财经、科技三大领域,体现了早报内容的多元覆盖性。

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

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

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

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

这组常量是 Speech Kit 6.1.1 AI 字幕语言配置的数据基础。LangOption 接口将语言码(如 'zh')与展示名(如"中文")配对,使得 UI 渲染时可以同时显示人类可读的名称和传递给 API 的语言码。SRC_LANGS 定义了源语言的两个选项——中文和英文,对应 sourceLanguage 字段的取值范围 'zh'|'en'TGT_LANGS_EN 定义了当源语言为英文时,目标语言的三个选项——中文、英文、中英双语,对应 targetLanguage 字段的取值范围 'zh'|'en'|'zh-en'

这里有一个重要的业务逻辑设计:当源语言为中文时,目标语言锁定为 'zh'(因为中文源没有翻译方向),因此 TGT_LANGS_EN 只在英文源时使用。这种"源语言联动目标语言"的约束逻辑在后续的 switchSourceLang 方法中实现。

/** 字号选项接口(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'];

SizeOption 接口将 AICaptionFontSize 枚举值与展示名配对。SIZE_OPTIONS 数组定义了四档字号选项——小号(SMALL)、标准(NORMAL)、大号(BIG)、超大(LARGE),完整覆盖了 AICaptionFontSize 枚举的所有取值。四档字号的设计满足了不同用户群体的可读性需求:老年用户可能需要"超大"字号,年轻用户可能偏好"标准"字号。

CAPTION_FONT_COLORS 数组定义了 5 个字幕字体颜色预设——经典白(#FFFFFF)、暖阳黄(#FFE9B0)、薄荷绿(#9CE8B5)、云朵蓝(#9CD0FF)、樱花粉(#FFB3C1)。这些颜色都是高饱和度的浅色系,确保在字幕组件的深色背景上具有足够的对比度。fontColor 字段的类型为 ResourceColor,它可以接受十六进制色值字符串、Color 枚举值或 Resource 资源引用,本应用使用十六进制字符串这一最直接的形式。

5.4 月度柱状图常量

/** 月度收听时长柱状图月份索引 */
const MONTH_IDX: number[] = [0, 1, 2, 3, 4, 5];
/** 月度柱状图月份名称 */
const MONTH_LABELS: string[] = ['3月', '4月', '5月', '6月', '7月', '8月'];
/** 月度收听时长数值(小时) */
const MONTH_HOURS: number[] = [36, 42, 45, 51, 58, 66];
/** 月度柱状图最大值(小时) */
const MONTH_MAX: number = 80;

这组常量为电台 Tab 的月度收听柱状图提供数据支撑。MONTH_IDX 是索引数组,用于 ForEach 遍历;MONTH_LABELS 提供横轴标签;MONTH_HOURS 提供柱状高度数据,呈现从 3 月的 36 小时到 8 月的 66 小时的持续增长趋势;MONTH_MAX 设为 80 小时作为 Y 轴最大值,确保最高的 66 小时柱不会顶到图表上限,留出适当的视觉余量。四个数组通过相同的数组下标建立索引关联,这是在缺乏结构体的情况下一种简洁的多维数据组织方式。


六、辅助函数设计

/** 新闻频道分类色映射:国际藏蓝 / 财经金 / 科技绿 / 体育红 / 民生深蓝 */
function catColor(cat: string): string {
  if (cat === '国际') { return COLORS.navy; }
  if (cat === '财经') { return COLORS.gold; }
  if (cat === '科技') { return COLORS.green; }
  if (cat === '体育') { return COLORS.red; }
  if (cat === '民生') { return COLORS.navyD; }
  return COLORS.sub;
}

catColor 函数是新闻列表左色条和分类标签的颜色映射器。它将新闻分类文本映射为主题色板中的对应颜色——国际新闻用权威藏蓝、财经新闻用高亮金、科技新闻用成功绿、体育新闻用快讯红、民生新闻用深藏蓝。这种"分类即色彩"的设计使得用户在浏览新闻列表时,通过左色条的颜色就能快速识别新闻分类,无需仔细阅读分类标签文字,提升了信息扫描效率。默认返回 COLORS.sub(副文本灰)作为兜底色,确保未匹配的分类也能正常显示。

/** 字号枚举转展示名(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';
}

sizeName 函数将 AICaptionFontSize 枚举值转换为对应的字符串名称,用于 AI 字幕 Tab 的"AICaptionOptions 实时代码预览"区块。在代码预览卡中,开发者需要看到当前配置的 fontSize 字段对应的枚举名(如 SMALLNORMAL),而非中文名(如"小号"、“标准”),这样才能体现"代码级"的预览效果。这个函数本质上是一个枚举值到代码字符串的序列化器。

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

langName 函数将语言码(如 'zh''en''zh-en')转换为人类可读的展示名(如"中文"、“英文”、“中英双语”)。它在 AI 字幕 Tab 的"当前语言组合摘要"行中使用,让用户能够直观理解当前选择的语言方向。'zh-en' 作为双语模式的语言码,其展示名"中英双语"比原始码更清晰地表达了"同时显示中文和英文字幕"的含义。

/** 字幕颜色预设转中文名(按 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 '樱花粉';
}

colorName 函数将 CAPTION_FONT_COLORS 数组中的色值转换为诗意的中文名——经典白、暖阳黄、薄荷绿、云朵蓝、樱花粉。这些名称不仅描述了颜色的视觉特征,还赋予了情感色彩,使得用户在选择字幕颜色时获得更好的体验。函数通过数组下标顺序匹配,最后的 return '樱花粉' 作为兜底返回值对应数组第五个元素。

这四个辅助函数的共同设计模式是"将业务逻辑或映射关系从视图代码中提取为独立函数",使得 Builder 函数内部的代码更加简洁,映射关系更加集中和可维护。如果未来需要新增分类色或字幕颜色预设,只需修改对应的函数和常量数组即可。


七、数据模型设计

7.1 NewsItem 新闻条目模型

/** 新闻条目(头条 Tab 左色条新闻列表) */
@Observed export class NewsItem {
  title: string;   // 新闻标题
  cat: string;     // 频道分类(如"国际")
  source: string;  // 来源媒体名
  time: string;    // 发布时间文本
  hot: string;     // 热度文本
  audio: string;   // 音频时长(如"3 分 24 秒")

  constructor(title: string, cat: string, source: string, time: string, hot: string, audio: string) {
    this.title = title;
    this.cat = cat;
    this.source = source;
    this.time = time;
    this.hot = hot;
    this.audio = audio;
  }
}

NewsItem 是头条 Tab 新闻列表的数据模型,使用 @Observed 装饰器标注。@Observed 的作用是使得该类的实例属性在被修改时能够触发 UI 的响应式更新——当 newsList 数组中某个 NewsItemtitlesource 被编辑修改后,对应的列表项 UI 会自动刷新。模型包含 6 个字段:title(新闻标题)、cat(频道分类,如"国际"、“财经”)、source(来源媒体名,如"环球电讯")、time(发布时间文本,如"08:12")、hot(热度文本,如"128.6万")、audio(音频时长,如"3 分 24 秒")。

export 关键字使得该类可以被其他文件导入使用,体现了模块化设计。构造函数接受 6 个参数并依次赋值给实例属性,这种"全参数构造"的模式适合 Mock 数据初始化场景。在真实项目中,可能需要添加可选参数和默认值来适应更多场景。

7.2 RadioItem 电台节目模型

/** 电台节目条目(电台 Tab 时长大卡节目单) */
@Observed export class RadioItem {
  title: string;   // 节目名(如"早间新闻联播")
  host: string;    // 主播名
  dur: string;     // 时长(如"15 分钟")
  plays: string;   // 收听量文本
  cat: string;     // 栏目分类

  constructor(title: string, host: string, dur: string, plays: string, cat: string) {
    this.title = title;
    this.host = host;
    this.dur = dur;
    this.plays = plays;
    this.cat = cat;
  }
}

RadioItem 是电台 Tab 节目单的数据模型,包含 5 个字段:title(节目名)、host(主播名)、dur(时长文本)、plays(收听量文本)、cat(栏目分类)。与 NewsItem 不同,RadioItem 没有 timehot 字段,而是有 host(主播)和 dur(时长)——这反映了电台节目与新闻快讯在信息结构上的差异:电台节目更强调"谁在播"和"播多久",新闻快讯更强调"何时发"和"多热门"。

7.3 CaptionScene 字幕场景模型

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

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

CaptionScene 是 AI 字幕 Tab 场景推荐列表的数据模型,包含 4 个字段:scene(场景名,如"国际新闻精听")、desc(场景说明)、src(推荐源语言码)、tgt(推荐目标语言码)。这个模型的核心设计是将"使用场景"与"语言配置"关联起来——用户点击某个场景后,应用会自动套用该场景推荐的源语言和目标语言组合。这种"场景化配置"的设计降低了用户理解语言参数的门槛,用户无需理解 sourceLanguagetargetLanguage 的技术含义,只需选择"我想在什么场景下使用字幕"即可。

7.4 UserStat 用户功能清单模型

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

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

UserStat 是"我的"页功能清单行的数据模型,包含 4 个字段:icon(功能图标 emoji)、label(功能名,如"订阅频道")、value(状态或数值文本,如"12 个")、arrow(是否显示右箭头,指示可点击进入下一级页面)。arrow 字段使用 boolean 类型而非 string 或可选渲染,是一种语义清晰的设计——它直接表达了"这一行是否可跳转"的意图,而非"这一行末尾显示什么文字"。


八、Mock 数据定义

8.1 新闻列表 Mock 数据

/** 今日要闻 Mock 数据(8 条) */
const NEWS_LIST: Array<NewsItem> = [
  new NewsItem('全球气候峰会闭幕:多国达成新一轮减排共识', '国际', '环球电讯', '08:12', '128.6万', '3 分 24 秒'),
  new NewsItem('央行宣布降准 0.5 个百分点,释放长期资金约万亿', '财经', '财经前线', '07:55', '96.3万', '2 分 48 秒'),
  new NewsItem('新一代国产大模型发布:推理成本再降四成', '科技', '科技观察', '07:30', '88.1万', '4 分 05 秒'),
  new NewsItem('寒潮蓝色预警继续:全国多地气温骤降 8 至 12 度', '民生', '都市生活报', '07:02', '75.4万', '2 分 36 秒'),
  new NewsItem('城市马拉松本周日鸣枪,三条主干道将临时管制', '体育', '体育周报', '06:47', '63.9万', '3 分 12 秒'),
  new NewsItem('欧盟通过新规:2030 年起全面禁用一次性塑料制品', '国际', '环球电讯', '06:20', '54.2万', '3 分 40 秒'),
  new NewsItem('三季度数据发布:消费市场持续回暖,线上增速领跑', '财经', '财经前线', '06:02', '47.8万', '5 分 10 秒'),
  new NewsItem('多地试行"地铁早读计划",通勤路上听新闻成新风尚', '民生', '都市生活报', '05:45', '35.6万', '2 分 15 秒')
];

NEWS_LIST 提供了 8 条新闻 Mock 数据,覆盖国际、财经、科技、民生、体育五大分类。数据设计的特点是:时间从 08:12 递减到 05:45,模拟早报时段的新闻发布时序;热度从 128.6 万递减到 35.6 万,形成热度排行;音频时长在 2~5 分钟之间,符合"听新闻"场景的碎片化消费特征。使用 Array<NewsItem> 类型标注确保了类型安全,数组中的每个元素都是 NewsItem 实例。

8.2 电台节目单与字幕场景 Mock 数据

/** 今日节目单 Mock 数据(8 条) */
const RADIO_LIST: Array<RadioItem> = [
  new RadioItem('早间新闻联播', '沈亦然', '15 分钟', '826万', '新闻'),
  new RadioItem('今日财经观察', '林知远', '12 分钟', '512万', '财经'),
  new RadioItem('环球时事速递', '顾未央', '18 分钟', '468万', '国际'),
  new RadioItem('科技早知道', '程一诺', '10 分钟', '352万', '科技'),
  new RadioItem('深夜读报人', '苏晚行', '25 分钟', '289万', '读报'),
  new RadioItem('体育快讯场', '陆嘉禾', '8 分钟', '236万', '体育'),
  new RadioItem('城市民生热线', '叶声声', '20 分钟', '198万', '民生'),
  new RadioItem('周三时事圆桌', '快闻评论组', '30 分钟', '175万', '评论')
];

/** 字幕场景 Mock 数据(5 条:新闻资讯音频行业相关) */
const SCENE_LIST: Array<CaptionScene> = [
  new CaptionScene('国际新闻精听', '英语新闻实时汉化理解', 'en', 'zh'),
  new CaptionScene('双语晨间简报', '头条原文译文对照浏览', 'en', 'zh-en'),
  new CaptionScene('中文快评速记', '评论员观点实时转文字', 'zh', 'zh'),
  new CaptionScene('英语听力特训', '纯英文字幕听国际新闻', 'en', 'en'),
  new CaptionScene('通勤静音模式', '地铁上也能"看"新闻', 'zh', 'zh')
];

RADIO_LIST 提供了 8 条电台节目 Mock 数据,每个节目都有主播名(如"沈亦然"、“林知远”)、时长(8~30 分钟)和收听量(175 万~826 万)。主播名采用富有文学感的中文姓名,增添了电台节目的亲切感和人文气质。

SCENE_LIST 提供了 5 条字幕场景 Mock 数据,每个场景都关联了特定的语言组合:国际新闻精听(英文源→中文目标)、双语晨间简报(英文源→中英双语目标)、中文快评速记(中文源→中文目标)、英语听力特训(英文源→英文目标)、通勤静音模式(中文源→中文目标)。这五个场景完整覆盖了 sourceLanguagetargetLanguage 的所有合法组合,是 AI 字幕能力在新闻资讯音频场景中的典型应用映射。

8.3 用户功能清单 Mock 数据

/** 我的页功能清单 Mock 数据(8 条) */
const STAT_LIST: Array<UserStat> = [
  new UserStat('📻', '订阅频道', '12 个', true),
  new UserStat('🕘', '收听历史', '268 条', true),
  new UserStat('⬇', '离线早报', '18 期', true),
  new UserStat('❤', '我的收藏', '96 条', true),
  new UserStat('🗣', 'AI 字幕偏好', '源 zh · 目标 zh', true),
  new UserStat('⏰', '早报闹钟', '每天 07:00', true),
  new UserStat('🏅', '快闻会员', '2026-11-20 到期', true),
  new UserStat('⚙', '播放与流量设置', '智能音质 · 仅 Wi-Fi 下载', true)
];

STAT_LIST 提供了 8 条用户功能清单 Mock 数据。其中"AI 字幕偏好"条目的 value 为"源 zh · 目标 zh",巧妙地将字幕语言配置状态融入了用户中心页面,让用户能够从"我的"页快速查看当前的字幕语言偏好。所有条目的 arrow 均为 true,表示这些功能项都可以点击进入下一级设置页面。


九、组件主体与状态管理

9.1 组件声明与通用状态

/** 1119 快闻早报 · 新闻资讯音频主页面 */
@Entry
@Component
struct Page1119 {
  /** 当前选中 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;

@Entry 装饰器标记 Page1119 为页面入口组件,可以被路由系统直接加载。@Component 声明它是一个自定义组件。这两个装饰器的组合是 ArkUI 页面组件的标准声明模式。

通用状态变量分为四组:导航状态currentTab 当前 Tab 索引、cateIdx 频道选中索引);动画状态breath 呼吸开关、timer 定时器句柄);弹窗状态addModal/editModal/delModal 三个弹窗开关、editIdx/delIdx 操作索引)。@State 装饰器确保这些变量的变更会自动触发依赖它们的 UI 组件的响应式刷新。

breath 变量是整个应用的"心跳"——它每 1000 毫秒在 truefalse 之间翻转,驱动多个视觉元素的呼吸动画效果:头部 Banner 的 📰 图标透明度、早报摘要卡的 📻 图标透明度、电台"正在播出"的 🎙 图标透明度、字幕就绪状态的透明度、柱状图柱高的 ±5% 波动和柱顶数值的颜色切换。通过一个状态变量联动多个视觉元素,是 ArkUI 响应式编程的典型实践。

9.2 业务列表数据状态

  /** 今日要闻列表数据 */
  @State newsList: Array<NewsItem> = NEWS_LIST;
  /** 电台节目单数据 */
  @State radioList: Array<RadioItem> = RADIO_LIST;
  /** 字幕场景列表数据 */
  @State sceneList: Array<CaptionScene> = SCENE_LIST;
  /** 我的页功能清单数据 */
  @State statList: Array<UserStat> = STAT_LIST;
  /** 订阅表单:频道名称 */
  @State formName: string = '';
  /** 订阅表单:订阅备注 */
  @State formNote: string = '';
  /** 编辑表单:新闻标题 */
  @State editTitle: string = '';
  /** 编辑表单:来源媒体 */
  @State editSrc: string = '';

四组业务列表数据通过 @State 管理并初始化为对应的 Mock 常量。将 Mock 数据赋值给 @State 变量而非直接在 build 中引用常量,是为了支持后续的增删改操作——当用户订阅新频道时,newsList 会通过 unshift 插入新条目;当用户编辑快讯时,newsList 中的条目属性会被修改并通过 slice() 刷新数组引用;当用户删除快讯时,newsList 会通过 splice 移除条目。这些操作都依赖于 @State 的响应式机制来触发 UI 更新。

表单字段(formName/formNote/editTitle/editSrc)用于弹窗中的 TextInput 双向绑定。当用户在输入框中键入内容时,onChange 回调将值同步到 @State 变量;当弹窗打开时,编辑表单的初始值从目标数据条目回填。

9.3 AI 字幕状态(6.1.1 特性)

  // --- 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;

这是整个应用技术含量最高的状态区块。captionController 使用 private 修饰符声明(非 @State),因为控制器实例本身不需要触发 UI 更新——它是一个行为对象,通过 writeAudio 方法向字幕服务写入数据。captionShown 使用 @State 并双向绑定到 AICaptionComponentisShown 参数,控制字幕组件的显示与隐藏。

四大新字段各有独立的 @State 变量:srcLang(源语言,默认 'zh')、tgtLang(目标语言,默认 'zh')、captionSize(字体大小,默认 NORMAL)、captionColor(字体颜色,默认经典白)。当用户在语言设置卡或外观设置卡中切换选项时,对应的 @State 变量变更,触发 buildCaptionOptions() 方法重新组装 AICaptionOptions 对象,AICaptionComponentoptions 参数随之更新,字幕组件实时响应新的语言和外观配置。这种"状态变更 → 选项重建 → 组件更新"的响应式链路,是 ArkUI 声明式编程与 Speech Kit 组件化能力的深度结合。

captionReadycaptionErrMsg 分别对应 onPreparedonError 回调的状态映射——当字幕服务就绪时 captionReady 置为 true,当发生错误时 captionErrMsg 填入错误信息。captionFed 记录已写入的音频块计数,为用户提供"已写入 N 块"的可视化反馈。


十、AI 字幕选项构建与语言联动

10.1 buildCaptionOptions 方法

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

buildCaptionOptions 方法是 AI 字幕四大新字段的汇聚点。它将 srcLangtgtLangcaptionSizecaptionColor 四个 @State 变量的当前值组装为一个完整的 AICaptionOptions 对象。每次这些状态变量发生变化时,AICaptionComponentoptions 参数会重新调用此方法获取最新的配置对象,字幕组件随之实时更新语言方向和外观样式。

除了四大新字段,AICaptionOptions 还包含 initialOpacity(初始透明度,设为 1 表示完全不透明)和两个回调函数:onPrepared 在字幕服务初始化完成时触发,将 captionReady 置为 true 并清空错误信息;onError 在字幕服务发生异常时触发,将 BusinessErrorcodemessage 拼接为错误信息字符串存入 captionErrMsg。这两个回调将异步的系统服务状态映射为同步的 UI 状态变量,是 ArkUI 响应式编程处理异步事件的典型模式。

10.2 switchSourceLang 语言联动方法

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

switchSourceLang 方法实现了源语言与目标语言之间的联动约束逻辑。当用户切换源语言为中文('zh')时,目标语言自动锁定为 'zh'——因为中文源没有翻译方向,字幕就是中文原文,targetLanguage 字段在这种情况下只能取 'zh'。当用户切换源语言为英文('en')时,目标语言默认设为 'zh-en'(中英双语),用户可以在中文、英文、中英双语三个选项中进一步选择。

这种联动逻辑的设计体现了对 sourceLanguagetargetLanguage 字段语义的深度理解。如果缺少这个联动逻辑,用户可能在中文源下选择英文目标,导致字幕服务收到无效的语言组合配置而产生错误。通过在 UI 层面约束合法的语言组合,避免了无效配置传递到系统服务,是一种防御性编程的实践。


十一、演示音频写入与字幕驱动

  /** 演示写入音频流:生成 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 方法是 AI 字幕"音频流入→字幕渲染"链路的起点。它通过纯代码生成一个 640 字节的 PCM 音频数据块,模拟真实的音频流入过程。

音频数据生成的技术细节:640 字节的 Uint8Array 在 16kHz 采样率、16bit 位深(每采样 2 字节)、单声道的 PCM 格式下,恰好对应 320 个采样点,时长约 20 毫秒(320 / 16000 = 0.02 秒)。循环以步长 2 遍历数组,每次处理一个 16bit 采样点:先计算时间戳 t = (i / 2) / 16000(将采样点索引转换为秒),再计算 440Hz 正弦波的振幅 v = Math.round(Math.sin(2 * Math.PI * 440 * t) * 6000)(440Hz 是标准音 A4 的频率,振幅 6000 在 16bit 范围内为中等音量),最后将振幅值的小端字节序写入 block[i](低字节)和 block[i+1](高字节)。

写入字幕服务的流程:将生成的 PCM 块封装为 AudioData 对象({ data: block }),调用 captionController.writeAudio(audioData) 写入字幕服务。如果写入成功,captionFed 计数器递增,UI 上显示"×N"的写入次数。如果写入过程中发生异常(如字幕服务未就绪),catch 块将错误信息设为"音频写入失败"。try-catch 的使用确保了音频写入失败不会导致应用崩溃,是健壮性设计的体现。

在真实场景中,writeAudio 接收的应该是从麦克风实时采集的音频流或从音频文件解码得到的 PCM 数据。本方法使用正弦波生成的演示音频仅为展示 writeAudio 的调用方式——在真实新闻资讯音频场景中,用户播放新闻音频时,应用的音频解码模块会将解码后的 PCM 数据块持续写入 captionController,AI 字幕服务实时识别并渲染字幕。


十二、新闻增删改操作

12.1 打开编辑弹窗

  /** 打开编辑快讯弹窗(回填当前新闻标题与来源媒体) */
  openEditNews(idx: number) {
    this.editIdx = idx;
    this.editTitle = this.newsList[idx].title;
    this.editSrc = this.newsList[idx].source;
    this.editModal = true;
  }

openEditNews 方法处理编辑快讯弹窗的打开逻辑。它接收目标新闻的数组索引 idx,将索引存入 editIdx,然后从 newsList 中读取该条新闻的 titlesource 分别回填到 editTitleeditSrc 表单字段。这种"数据回填"的设计确保了用户在弹窗中看到的初始值就是当前新闻的内容,而非空白输入框,提升了编辑体验。

12.2 保存订阅频道

  /** 保存订阅频道(空字段用默认值兜底,新频道快讯插入列表顶部) */
  saveNews() {
    const name = this.formName === '' ? '新订阅频道' : this.formName;
    const note = this.formNote === '' ? '自订' : this.formNote;
    this.newsList.unshift(new NewsItem('「' + name + '」频道已订阅', note, '快闻订阅', '刚刚', '新上线', '待推送'));
    this.formName = '';
    this.formNote = '';
    this.addModal = false;
  }

saveNews 方法处理订阅频道弹窗的保存逻辑。它采用了"空值兜底"策略——当用户未填写频道名称时使用"新订阅频道"作为默认值,未填写备注时使用"自订"作为默认值。这种设计避免了空值导致的 UI 显示异常,同时允许用户快速保存而无需填写所有字段。

新订阅的频道通过 unshift 方法插入 newsList 数组头部,使其出现在新闻列表的最顶部——这符合"新内容优先展示"的用户预期。新条目的 time 设为"刚刚",hot 设为"新上线",audio 设为"待推送",这些特殊文案标识了这是一条订阅通知而非新闻快讯。保存完成后清空表单字段并关闭弹窗,为下次打开提供干净的初始状态。

12.3 更新与删除快讯

  /** 保存编辑快讯(整体刷新数组引用以刷新新闻列表) */
  updateNews() {
    if (this.editIdx >= 0 && this.editIdx < this.newsList.length) {
      if (this.editTitle !== '') {
        this.newsList[this.editIdx].title = this.editTitle;
      }
      if (this.editSrc !== '') {
        this.newsList[this.editIdx].source = this.editSrc;
      }
      this.newsList = this.newsList.slice();
    }
    this.editModal = false;
  }

  /** 删除快讯(确认弹窗回调) */
  delNews() {
    if (this.delIdx >= 0 && this.delIdx < this.newsList.length) {
      this.newsList.splice(this.delIdx, 1);
    }
    this.delModal = false;
  }

updateNews 方法处理编辑快讯的保存。它先进行索引边界检查(editIdx >= 0 && editIdx < newsList.length),然后分别检查 editTitleeditSrc 是否非空——只有非空的字段才会更新到目标条目,这允许用户只修改部分字段而保留其他字段不变。更新完成后,关键的一行是 this.newsList = this.newsList.slice()——slice() 方法返回数组的浅拷贝,创建一个新的数组引用。这一步是必要的,因为直接修改数组元素的属性(this.newsList[idx].title = ...)不会触发 ArkUI 的响应式更新,只有数组引用本身发生变化时,@State 才会感知到变化并触发 ForEach 重新渲染。

delNews 方法处理快讯删除。同样先进行边界检查,然后通过 splice(delIdx, 1) 从数组中移除目标条目。splice 方法会直接修改原数组并改变数组长度,这种修改方式能够被 @State 感知到并触发 UI 更新。删除完成后关闭确认弹窗。


十三、生命周期管理

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

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

aboutToAppearaboutToDisappear 是 ArkUI 组件的生命周期回调函数,分别在组件创建后和销毁前触发。

aboutToAppear 中通过 setInterval 创建了一个每 1000 毫秒执行一次的定时器,将 breath 状态在 truefalse 之间翻转。定时器句柄存入 this.timer 供后续清理使用。这个定时器是整个应用呼吸动画的驱动源——breath 的每次翻转都会触发所有依赖它的 UI 组件更新,包括头部图标透明度、早报图标透明度、电台图标透明度、字幕就绪状态透明度、柱状图柱高波动和数值颜色切换。

aboutToDisappear 中通过 clearInterval(this.timer) 清理定时器。这一步至关重要——如果不清理,当组件被销毁后定时器仍在运行,持续修改已不存在的组件的状态变量,会导致内存泄漏和潜在的错误。aboutToAppearaboutToDisappear 的成对使用是 ArkUI 资源管理的标准范式:在 appear 中获取资源(定时器、事件监听、网络连接等),在 disappear 中释放资源,确保组件生命周期的资源平衡。


十四、页面主构建 build()

  /** 页面主构建:Stack 包裹主内容与三层弹窗 */
  build() {
    Stack() {
      Column() {
        this.headerMain()
        Divider().strokeWidth(1).color(COLORS.line)
        Scroll() {
          Column() {
            if (this.currentTab === 0) {
              this.tabNews()
            } else if (this.currentTab === 1) {
              this.tabRadio()
            } 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 包裹的主内容区(头部 + 分割线 + 可滚动内容区 + 底部导航),上层是三个条件渲染的弹窗面板。

主内容区的结构是垂直排列的:headerMain() 构建头部区域,Divider 作为分割线,Scroll 包裹的可滚动内容区通过 if-else 条件分支根据 currentTab 渲染对应的 Tab 内容,tabBar() 构建底部导航。ScrolllayoutWeight(1) 使其占据头部和底部导航之间的所有剩余空间,scrollBar(BarState.Off) 隐藏滚动条保持界面简洁。

弹窗层通过三个 if 条件渲染——当 addModal/editModal/delModaltrue 时,对应的弹窗面板挂载到 Stack 上层。每个弹窗面板接收一个 onClose 回调函数,用于在遮罩点击或取消按钮点击时将对应的弹窗开关置为 false,从而触发条件渲染移除弹窗。这种"条件渲染 + Stack 层叠"的弹窗管理模式简洁高效,不需要额外的弹窗管理框架即可实现多弹窗的按需显示。

整个 Stack 设置了 backgroundColor(COLORS.bg) 作为页面背景色,确保所有区域(包括弹窗遮罩半透明区域透出的底色)都呈现统一的新闻白底色。


十五、头部构建 headerMain

  /** 头部:顶部渐变 Banner(日期+天气问候+今日要闻数)+ 搜索条 + 横滑资讯频道 chips */
  @Builder
  headerMain() {
    Column({ space: 12 }) {
      // 渐变 Banner:日期 + 天气问候 + 今日要闻数
      Column({ space: 10 }) {
        Row() {
          Column({ space: 5 }) {
            Text('早上好,快闻早报已更新').fontSize(16).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
            Text('8 月 25 日 周二 · 晴 26℃ · 空气优').fontSize(9).fontColor(COLORS.white).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.navyD)
          .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
        }
        .width('100%')

        Row({ space: 8 }) {
          Text('📰 今日要闻 128 条').fontSize(9).fontColor(COLORS.white).opacity(0.92)
            .padding({ left: 8, right: 8, top: 3, bottom: 3 }).backgroundColor(COLORS.navyD).borderRadius(8)
          Text('⚡ 快讯 36 条').fontSize(9).fontColor(COLORS.white).opacity(0.92)
            .padding({ left: 8, right: 8, top: 3, bottom: 3 }).backgroundColor(COLORS.navyD).borderRadius(8)
          Text('⏱ 早报 12 分钟').fontSize(9).fontColor(COLORS.navyD)
            .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.navyD, 0], [COLORS.navy, 1]] })

头部区域的渐变 Banner 是整个应用的视觉焦点之一。它采用 135 度角的线性渐变,从深藏蓝(#1E4485)渐变到权威藏蓝(#2B5CAD),营造出沉稳而权威的新闻媒体氛围。Banner 内部分为两行:第一行是问候语与天气信息(“早上好,快闻早报已更新"和"8 月 25 日 周二 · 晴 26℃ · 空气优”),右侧是一个 44x44 的圆形 📰 图标容器,其透明度随 breath 状态在 1 和 0.6 之间交替变化,形成呼吸效果。第二行是三个数据胶囊——今日要闻 128 条、快讯 36 条、早报 12 分钟——前两个使用深藏蓝底白字,第三个使用金色底深藏蓝字以形成视觉区分。

天气信息行使用 maxLines(1)textOverflow({ overflow: TextOverflow.Ellipsis }) 确保文本超出宽度时以省略号截断而非换行,这在窄屏设备上是必要的防御性布局。

      // 搜索条(右侧语音入口跳转 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)

搜索条是一个视觉上的搜索入口(当前为 Mock),右侧的 🎙 语音图标具有实际功能——点击后通过 this.currentTab = 2 跳转到 AI 字幕 Tab,巧妙地将搜索入口与 AI 字幕功能关联起来。搜索条使用 borderRadius(20) 形成胶囊形外观,卡片白底在浅灰页面背景上形成微弱的层次感。

      // 横滑资讯频道 chips
      Scroll() {
        Row({ space: 8 }) {
          ForEach(CATE_TAGS, (tg: string, idx: number) => {
            Text(tg).fontSize(11)
              .fontColor(this.cateIdx === idx ? COLORS.white : COLORS.sub)
              .padding({ left: 13, right: 13, top: 6, bottom: 6 })
              .backgroundColor(this.cateIdx === idx ? COLORS.navy : 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,通过 ForEach 遍历 CATE_TAGS 数组渲染 8 个频道标签。每个 chip 的颜色根据 cateIdx === idx 条件判断——选中时白字藏蓝底,未选中时灰字白底。点击 chip 时更新 cateIdx 状态,触发 chips 的响应式样式更新。整个头部区域使用 180 度线性渐变(从 chip 浅灰到 bg 新闻白),使头部到内容区的过渡更加柔和自然。


十六、头条 Tab 构建 tabNews

16.1 虚线早报摘要大卡

  /** 头条 Tab:虚线早报摘要大卡 + 左色条新闻列表 */
  @Builder
  tabNews() {
    Column({ space: 12 }) {
      // 1. 虚线大卡(今日早报摘要卡:藏蓝虚线边框 + 早报要点 + 播放/订阅入口)
      Column({ space: 10 }) {
        Row() {
          Column({ space: 4 }) {
            Text('快闻早报 · 8 月 25 日刊').fontSize(16).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            Text('每天 07:00 · 12 分钟听懂昨夜今晨的世界').fontSize(9)
              .fontColor(COLORS.sub)
              .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)

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

头条 Tab 的第一个视觉元素是虚线早报摘要大卡。它使用 border({ width: 1.5, color: COLORS.navy, style: BorderStyle.Dashed }) 创建藏蓝虚线边框——虚线边框在视觉上传达了"报纸/刊物"的隐喻,与"快闻早报"的品牌定位高度契合。卡片内部包含:标题行(“快闻早报 · 8 月 25 日刊"和"每天 07:00 · 12 分钟听懂昨夜今晨的世界”)与右侧的圆形 📻 图标容器,图标透明度随 breath 呼吸变化。

        // 早报要点列表(BriefItem)
        ForEach(MORNING_BRIEFS, (b: BriefItem) => {
          Row({ space: 8 }) {
            Text(b.icon).fontSize(12)
            Text(b.text).fontSize(10).fontColor(COLORS.sub).layoutWeight(1)
              .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
            Text('▶').fontSize(9).fontColor(COLORS.navy)
          }
          .width('100%').padding(8).backgroundColor(COLORS.chip).borderRadius(8)
        }, (b: BriefItem) => b.text)

早报要点列表通过 ForEach 遍历 MORNING_BRIEFS 数组,每条要点渲染为一行 Row——左侧 emoji 图标、中间要点文案(使用 layoutWeight(1) 占据剩余空间并以省略号截断)、右侧播放符号 ▶。每条要点使用浅灰底圆角容器,在白色卡片背景上形成微弱的层次感。ForEach 的第三个参数 (b: BriefItem) => b.text 是键值生成器,使用要点文案作为唯一标识,确保列表更新的高效差分。

        // 按钮行:播放今日早报 + 订阅频道
        Row({ space: 10 }) {
          Text('▶ 播放今日早报').fontSize(12)
            .fontColor(COLORS.white).fontWeight(FontWeight.Bold)
            .layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 9, bottom: 9 })
            .backgroundColor(COLORS.navy).borderRadius(10)

          Text('+ 订阅频道').fontSize(12).fontColor(COLORS.navy)
            .layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 9, bottom: 9 })
            .backgroundColor(COLORS.chip).borderRadius(10)
            .onClick(() => {
              this.addModal = true;
            })
        }
        .width('100%')
      }
      .width('100%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
      .border({ width: 1.5, color: COLORS.navy, style: BorderStyle.Dashed })

按钮行包含两个等宽按钮:“播放今日早报”(藏蓝底白字,主操作)和"+ 订阅频道"(浅灰底藏蓝字,次操作)。点击"订阅频道"按钮将 addModal 置为 true,触发订阅频道弹窗的显示。两个按钮都使用 layoutWeight(1) 平分宽度,textAlign(TextAlign.Center) 居中文字,形成对称的视觉布局。

16.2 左色条新闻列表

      // 2. 新闻列表标题行 + 订阅入口
      Row() {
        Text('📰 今日要闻').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text(this.newsList.length.toString() + ' 条快讯').fontSize(9).fontColor(COLORS.text3)
        Text('+ 订阅').fontSize(9).fontColor(COLORS.navy)
          .padding({ left: 9, right: 9, top: 4, bottom: 4 })
          .backgroundColor(COLORS.chip).borderRadius(8)
          .onClick(() => {
            this.addModal = true;
          })
      }
      .width('100%')

      // 3. 左色条新闻列表(NewsItem:频道分类色条 + 标题 + 元信息 + 播放块)
      ForEach(this.newsList, (item: NewsItem, idx: number) => {
        Column({ space: 8 }) {
          Row({ space: 10 }) {
            // 左色条(频道分类色)
            Column().width(4).height(46).borderRadius(2).backgroundColor(catColor(item.cat))

            Column({ space: 4 }) {
              Text(item.title).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
                .maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
              Row({ space: 6 }) {
                Text(item.cat).fontSize(8).fontColor(catColor(item.cat)).fontWeight(FontWeight.Bold)
                Text(item.source).fontSize(8).fontColor(COLORS.sub)
                Text(item.time).fontSize(8).fontColor(COLORS.text3)
              }
              .width('100%')
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Start)

            // 播放块(音频时长)
            Column({ space: 2 }) {
              Text('▶').fontSize(11).fontColor(COLORS.navy)
              Text(item.audio).fontSize(8).fontColor(COLORS.text3)
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%').alignItems(VerticalAlign.Center)

新闻列表的标题行展示"今日要闻"标题、快讯数量(动态读取 this.newsList.length)和订阅入口。新闻列表本身通过 ForEach 遍历 this.newsList 渲染,每条新闻渲染为一张卡片。卡片内部采用"左色条 + 中间内容 + 右侧播放块"的三栏结构:

左色条是一个 4px 宽、46px 高的圆角 Column,其背景色通过 catColor(item.cat) 根据新闻分类动态映射——国际新闻为藏蓝、财经新闻为金色、科技新闻为绿色、体育新闻为红色、民生新闻为深藏蓝。这种"左色条分类标识"的设计是新闻列表的经典布局模式,它使得用户在快速滑动浏览时能够通过颜色瞬间识别新闻分类。

中间内容区包含标题(最多 2 行,超出省略号截断)和元信息行(分类标签、来源媒体、发布时间),元信息行的分类标签颜色同样使用 catColor 映射,与左色条形成色彩呼应。右侧播放块显示播放符号 ▶ 和音频时长,引导用户点击收听。

          // 热度 + 编辑/删除迷你操作行
          Row({ space: 8 }) {
            Text('🔥 ' + item.hot).fontSize(8).fontColor(COLORS.red)
            Column().layoutWeight(1)
            Text('编辑').fontSize(8).fontColor(COLORS.sub)
              .padding({ left: 8, right: 8, top: 3, bottom: 3 })
              .backgroundColor(COLORS.chip).borderRadius(6)
              .onClick(() => {
                this.openEditNews(idx);
              })
            Text('删除').fontSize(8).fontColor(COLORS.red)
              .padding({ left: 8, right: 8, top: 3, bottom: 3 })
              .backgroundColor(COLORS.chip).borderRadius(6)
              .onClick(() => {
                this.delIdx = idx;
                this.delModal = true;
              })
          }
          .width('100%')
        }
        .width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(12)
      }, (item: NewsItem) => item.title)
    }
    .width('100%')
  }

每张新闻卡片的底部是操作行——左侧显示热度数值(🔥 图标 + 热度文本,快讯红色),右侧是"编辑"和"删除"两个迷你操作按钮。点击"编辑"调用 openEditNews(idx) 打开编辑弹窗,点击"删除"先将索引存入 delIdx 再将 delModal 置为 true 打开删除确认弹窗。删除操作需要二次确认是良好的 UX 实践——防止用户误触删除重要新闻。ForEach 的键值生成器使用 item.title 作为唯一标识,确保列表增删改时的高效差分渲染。


十七、电台 Tab 构建 tabRadio

17.1 正在播出渐变大卡

  /** 电台 Tab:正在播出渐变大卡 + 时长大卡节目单 + 月度收听柱状图 */
  @Builder
  tabRadio() {
    Column({ space: 12 }) {
      // 1. 正在播出渐变大卡(直播徽章 + 节目信息 + 播放进度条)
      Column({ space: 10 }) {
        Row() {
          Column({ space: 5 }) {
            Row({ space: 6 }) {
              Text('● 正在播出').fontSize(8).fontColor(COLORS.white)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .backgroundColor(COLORS.red).borderRadius(6)
              Text('第 237 期').fontSize(8).fontColor(COLORS.white).opacity(0.75)
            }
            Text('早间新闻联播').fontSize(15).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
            Text('主播 沈亦然 · 已播 06:12 / 15:00').fontSize(9).fontColor(COLORS.white).opacity(0.78)
              .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.navyD)
          .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
        }
        .width('100%')

        // 播放进度条(已播白色 / 未播深藏蓝)
        Row({ space: 0 }) {
          Column().height(4).width('41%').backgroundColor(COLORS.white).borderRadius(2)
          Column().height(4).layoutWeight(1).backgroundColor(COLORS.navyD).borderRadius(2)
        }
        .width('100%')
      }
      .width('100%').padding(16).borderRadius(14)
      .linearGradient({ angle: 135, colors: [[COLORS.navyD, 0], [COLORS.navy, 1]] })

电台 Tab 的第一个视觉元素是"正在播出"渐变大卡。它与头部 Banner 使用相同的 135 度深藏蓝渐变,形成视觉一致性。卡片内部包含:直播徽章(“● 正在播出”,快讯红底白字圆角胶囊)和期数(“第 237 期”),节目名(“早间新闻联播”),主播和播放进度信息(“主播 沈亦然 · 已播 06:12 / 15:00”),以及右侧的圆形 🎙 图标(透明度随 breath 呼吸变化)。

播放进度条是一个两段式 Row——左侧 41% 宽度白色柱表示已播放进度(06:12 / 15:00 ≈ 41%),右侧剩余宽度深藏蓝柱表示未播放部分。两个 Column 通过 borderRadius(2) 形成圆角,space: 0 确保两段无缝衔接。这种纯布局方式实现的进度条简洁高效,无需 Canvas 绘制。

17.2 时长大卡节目单

      // 2. 节目单标题行
      Row() {
        Text('🎙 今日节目单').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text('按时段更新 · 点击收听').fontSize(9).fontColor(COLORS.text3)
      }
      .width('100%')

      // 3. 时长大卡节目单(RadioItem:左侧时长渐变块 + 节目信息 + 收听量)
      ForEach(this.radioList, (item: RadioItem, idx: number) => {
        Row({ space: 12 }) {
          // 时长渐变块(视觉主体)
          Column({ space: 3 }) {
            Text('时长').fontSize(8).fontColor(COLORS.white).opacity(0.75)
            Text(item.dur).fontSize(12).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
          }
          .width(66).padding({ top: 10, bottom: 10 })
          .justifyContent(FlexAlign.Center)
          .linearGradient({ angle: 160, colors: [[COLORS.navyD, 0], [COLORS.navy, 1]] })
          .borderRadius(10)

          // 节目信息(节目名 + 主播/栏目 + 收听量)
          Column({ space: 4 }) {
            Text(item.title).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
              .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
            Row({ space: 6 }) {
              Text('主播 ' + item.host).fontSize(9).fontColor(COLORS.sub)
                .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
              Text('· ' + item.cat + '栏目').fontSize(9).fontColor(catColor(item.cat))
                .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
            }
            .width('100%')
            Text('🔊 ' + item.plays + ' 人收听').fontSize(8).fontColor(COLORS.text3)
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Start)

          // 播放按钮
          Column() {
            Text('▶').fontSize(13).fontColor(COLORS.navy)
          }
          .width(30).height(30).borderRadius(15).backgroundColor(COLORS.chip)
          .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
        }
        .width('100%').padding(10).backgroundColor(COLORS.card).borderRadius(12)
        .alignItems(VerticalAlign.Center)
      }, (item: RadioItem) => item.title)

节目单通过 ForEach 遍历 this.radioList 渲染,每个节目渲染为一张"左侧时长渐变块 + 中间节目信息 + 右侧播放按钮"的三栏卡片。左侧时长块是一个 66px 宽的渐变 Column(160 度深藏蓝渐变),内部显示"时长"标签和具体时长值(如"15 分钟"),作为视觉主体吸引用户注意。中间信息区包含节目名、主播/栏目行(栏目颜色同样使用 catColor 映射)、收听量。右侧播放按钮是一个 30x30 的圆形浅灰底藏蓝箭头。

这种"时长块作为视觉主体"的设计巧妙地将电台节目的核心信息(时长)提升为视觉焦点——用户在浏览节目单时最关心的就是"这个节目多长",将这一信息从文字元信息提升为渐变色块,增强了信息获取效率。

17.3 月度收听柱状图

      // 4. 月度收听时长柱状图(呼吸 ±5% 波动)
      this.chartCard()
    }
    .width('100%')
  }

电台 Tab 的底部调用 this.chartCard() 渲染月度收听时长柱状图。柱状图的详细实现将在后续章节分析。


十八、AI 字幕 Tab 构建 tabCaption(Speech Kit 6.1.1 特性页)

AI 字幕 Tab 是整个应用的技术核心,它将 AICaptionComponent 的四大新字段通过五个区块完整呈现:组件实时预览、语言设置、外观设置、options 代码预览、字幕场景推荐。

18.1 特性简介条

  /** 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.white).fontWeight(FontWeight.Bold)
          Text('HarmonyOS 6.1.1:AI字幕支持源语言 / 目标语言 / 字体颜色 / 字体大小')
            .fontSize(8).fontColor(COLORS.white).opacity(0.8)
            .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
      }
      .width('100%').padding(10).borderRadius(10)
      .linearGradient({ angle: 135, colors: [[COLORS.navyD, 0], [COLORS.navy, 1]] })

特性简介条是 AI 字幕 Tab 的顶部标识区,使用深藏蓝渐变背景,内部展示"Speech Kit · 场景化语音服务"标题和"HarmonyOS 6.1.1:AI字幕支持源语言 / 目标语言 / 字体颜色 / 字体大小"特性描述。这段描述精确地概括了四大新字段的核心能力,让用户在进入 Tab 的第一眼就了解该页面的技术特性。

18.2 区块 1:组件实时预览卡

      // ===== 区块 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.white).fontWeight(FontWeight.Bold)
            .layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 9, bottom: 9 })
            .backgroundColor(this.captionShown ? COLORS.navyD : COLORS.navy)
            .borderRadius(10)
            .onClick(() => {
              this.captionShown = !this.captionShown;
            })

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

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

组件实时预览卡是 AI 字幕 Tab 的核心区块。它包含三个关键元素:

字幕就绪状态徽章——根据 captionReady 显示"已就绪"(绿色)或"初始化中"(金色,透明度随 breath 呼吸变化),让用户直观感知字幕服务的就绪状态。这种状态可视化是异步系统能力调用的 UX 最佳实践——用户需要知道系统是否已经准备好。

AICaptionComponent 组件实例——这是整个应用技术核心的体现。组件接收三个参数:isShown(双向绑定到 captionShown,控制字幕显示/隐藏)、controller(传入 captionController 实例,用于 writeAudio 写入音频流)、options(调用 buildCaptionOptions() 方法实时构建配置对象,包含四大新字段和两个回调)。组件高度设为 110px,圆角 10px,带 1px 分割线色边框。每当 srcLangtgtLangcaptionSizecaptionColor 任一状态变更时,buildCaptionOptions() 返回新的配置对象,AICaptionComponentoptions 参数更新,字幕组件实时响应新的语言和外观配置——这种"状态变更 → 选项重建 → 组件更新"的响应式链路是 ArkUI 声明式编程与 Speech Kit 组件化能力的深度结合。

控制按钮行——"开启/隐藏字幕"按钮通过 this.captionShown = !this.captionShown 切换字幕显示状态,"写入演示音频"按钮调用 feedDemoAudio() 向字幕服务写入 PCM 音频块并显示已写入计数。按钮文案和颜色根据 captionShown 状态动态变化——显示字幕时为深藏蓝底"隐藏字幕"文案,隐藏字幕时为藏蓝底"开启字幕"文案。

错误信息行——当 captionErrMsg 非空时显示错误信息(快讯红色文字,⚠ 前缀),最多 2 行省略号截断。这是 onError 回调的 UI 出口,让用户在字幕服务异常时能够看到错误提示。

18.3 区块 2:语言设置卡

      // ===== 区块 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.white : 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.navy : COLORS.chip)
              .borderRadius(10)
              .onClick(() => {
                this.switchSourceLang(l.code);
              })
          }, (l: LangOption) => l.code)
        }
        .width('100%')

语言设置卡是 sourceLanguagetargetLanguage 两大新字段的配置入口。源语言部分通过 ForEach 遍历 SRC_LANGS 渲染两个等宽选项按钮(中文、英文),选中项为白字藏蓝底加粗,未选中项为灰字浅灰底。点击选项时调用 switchSourceLang(l.code) 切换源语言并联动目标语言——这是前文分析过的语言联动逻辑的 UI 触发点。标题行右侧标注"取值 ‘zh’ | ‘en’",以代码注释风格展示字段取值范围,帮助开发者理解技术约束。

        // 目标语言标题行(中文源锁定 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.white : 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.navy : COLORS.chip)
                .borderRadius(10)
                .onClick(() => {
                  this.tgtLang = l.code;
                })
            }, (l: LangOption) => l.code)
          }
          .width('100%')
        }

目标语言部分根据源语言动态切换 UI:当源语言为中文时,显示一个 🔒 锁定提示行——“中文源锁定中文:无翻译方向,targetLanguage 固定为 zh”,不提供选择按钮;当源语言为英文时,通过 ForEach 遍历 TGT_LANGS_EN 渲染三个选项(中文、英文、中英双语),选中项样式与源语言选项一致。这种条件渲染的方式直观地表达了语言联动约束——用户在中文源下看到锁定提示而非可选按钮,避免了选择无效语言组合的可能。

        // 当前语言组合摘要
        Row({ space: 6 }) {
          Circle().width(6).height(6).fill(COLORS.navy)
          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 函数将语言码转换为展示名。这一行让用户在配置完语言后能够一眼确认当前的语言方向,是配置反馈的最佳实践。

18.4 区块 3:外观设置卡

      // ===== 区块 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.white : 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.navy : COLORS.chip)
              .borderRadius(10)
              .onClick(() => {
                this.captionSize = s.size;
              })
          }, (s: SizeOption) => s.name)
        }
        .width('100%')

外观设置卡是 fontSizefontColor 两大新字段的配置入口。字体大小部分通过 ForEach 遍历 SIZE_OPTIONS 渲染四个等宽选项按钮(小号、标准、大号、超大),对应 AICaptionFontSize 枚举的 SMALL/NORMAL/BIG/LARGE 四档。选中项样式与前述语言选项一致。点击选项时将 this.captionSize 设为对应的枚举值,触发 buildCaptionOptions() 重建配置对象。

        // 字体颜色标题行(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.navy : COLORS.line })
              .onClick(() => {
                this.captionColor = c;
              })
          }, (c: string) => c)
        }
        .width('100%').justifyContent(FlexAlign.SpaceBetween)

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

字体颜色部分通过 ForEach 遍历 CAPTION_FONT_COLORS 渲染五个圆形色块(26x26),选中色块使用藏蓝描边(2px),未选中色块使用分割线色描边。点击色块时将 this.captionColor 设为对应的色值。色块行使用 justifyContent(FlexAlign.SpaceBetween) 使五个色块均匀分布。

底部颜色说明行显示当前选中色块的小圆点(10x10)、颜色中文名(通过 colorName 函数转换)和色值,右侧标注"作用于字幕原文与译文"——说明 fontColor 字段同时影响字幕原文和译文的文字颜色。

18.5 区块 4:options 实时代码预览卡

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

        // 深色代码块(monospace,键名金色高亮)
        Column({ space: 5 }) {
          Text('AICaptionOptions = {').fontSize(9).fontColor(COLORS.text3).fontFamily('monospace')
          Row({ space: 4 }) {
            Text('● sourceLanguage:').fontSize(9).fontColor(COLORS.gold).fontFamily('monospace')
            Text("'" + this.srcLang + "'").fontSize(9).fontColor(COLORS.white).fontFamily('monospace')
          }
          .width('100%')
          Row({ space: 4 }) {
            Text('● targetLanguage:').fontSize(9).fontColor(COLORS.gold).fontFamily('monospace')
            Text("'" + this.tgtLang + "'").fontSize(9).fontColor(COLORS.white).fontFamily('monospace')
          }
          .width('100%')
          Row({ space: 4 }) {
            Text('● fontSize:').fontSize(9).fontColor(COLORS.gold).fontFamily('monospace')
            Text(sizeName(this.captionSize)).fontSize(9).fontColor(COLORS.gold).fontFamily('monospace')
          }
          .width('100%')
          Row({ space: 4 }) {
            Text('● fontColor:').fontSize(9).fontColor(COLORS.gold).fontFamily('monospace')
            Text("'" + this.captionColor + "'").fontSize(9)
              .fontColor(this.captionColor).fontFamily('monospace')
          }
          .width('100%')
          Text('}').fontSize(9).fontColor(COLORS.text3).fontFamily('monospace')
        }
        .width('100%').padding(12).backgroundColor(COLORS.navyD).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 的配置以代码格式实时渲染——深藏蓝背景(COLORS.navyD)、monospace 等宽字体、键名金色高亮、值白色或当前选中颜色显示。四个字段的值随用户的设置选择实时更新:sourceLanguagetargetLanguage 显示为 'zh''en''zh-en' 字符串,fontSize 通过 sizeName 函数显示为 SMALL/NORMAL/BIG/LARGE 枚举名,fontColor 显示为色值字符串且字体颜色就是该色值本身——即用户选中"暖阳黄"时,fontColor 行的文字就是暖阳黄色的。

这种"代码实时预览"的设计让开发者能够直观看到 buildCaptionOptions() 方法生成的配置对象的确切结构,是技术 Demo 应用的优秀实践。底部标注"★ 6.1.1 新增字段:sourceLanguage / targetLanguage / fontSize / fontColor"明确标识了四大新字段的来源版本。

18.6 区块 5:字幕场景推荐列表

      // ===== 区块 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.navy : (idx % 3 === 1 ? COLORS.red : 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.navy)
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Start)

            Text('套用 ›').fontSize(9).fontColor(COLORS.navy)
          }
          .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 遍历 this.sceneList 渲染 5 条场景推荐,每条场景渲染为"左色条 + 中间场景信息 + 右侧套用按钮"的行卡片。左色条颜色按 idx % 3 在藏蓝、快讯红、金色之间轮换,形成视觉节奏。中间信息包含场景名、场景说明和语言组合摘要(如"源 en → 目标 zh")。

点击场景行时,调用 this.switchSourceLang(item.src) 切换源语言并联动目标语言,然后直接设置 this.tgtLang = item.tgt 覆盖联动结果为目标语言值——这是因为场景推荐的语言组合是预设的固定组合,需要确保目标语言与推荐一致。这种"一键套用场景语言组合"的设计极大降低了用户配置字幕语言参数的门槛——用户无需理解 sourceLanguagetargetLanguage 的技术含义,只需选择"我在什么场景下使用字幕",应用就会自动套用最优的语言配置。


十九、我的 Tab 构建 tabMine

19.1 用户信息与会员渐变大卡

  /** 我的 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.navyD)
          .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)

          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Text('闻声行者').fontSize(16).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
              Text('快闻 VIP').fontSize(8).fontColor(COLORS.navyD)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .backgroundColor(COLORS.gold).borderRadius(7)
            }
            Text('快闻 ID:kuaiwen_0825 · 已连续收听早报 236 天')
              .fontSize(9).fontColor(COLORS.white).opacity(0.75)
              .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Start)
        }
        .width('100%')

我的 Tab 的第一个视觉元素是用户信息与会员渐变大卡,使用与头部 Banner 相同的 135 度深藏蓝渐变。卡片内部分为两部分:用户信息行(54x54 圆形 📰 头像、用户名"闻声行者"、金色"快闻 VIP"徽章、快闻 ID 与连续收听天数)和三格收听数据(订阅频道 12、收听早报 236、收听小时 298)。三格数据使用 layoutWeight(1) 等分宽度居中排列,白色加粗数字 + 半透明白色标签,在藏蓝渐变背景上形成清晰的数据展示。

        // 三格收听数据
        Row({ space: 10 }) {
          Column({ space: 3 }) {
            Text('12').fontSize(13).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
            Text('订阅频道').fontSize(8).fontColor(COLORS.white).opacity(0.75)
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)

          Column({ space: 3 }) {
            Text('236').fontSize(13).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
            Text('收听早报').fontSize(8).fontColor(COLORS.white).opacity(0.75)
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)

          Column({ space: 3 }) {
            Text('298').fontSize(13).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
            Text('收听小时').fontSize(8).fontColor(COLORS.white).opacity(0.75)
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)
        }
        .width('100%').margin({ top: 2 })
      }
      .width('100%').padding(16).borderRadius(14)
      .linearGradient({ angle: 135, colors: [[COLORS.navyD, 0], [COLORS.navy, 1]] })

19.2 功能清单行

      // 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 遍历 this.statList 渲染 8 条功能项,每条渲染为"图标 + 功能名 + 状态值 + 右箭头"的行卡片。功能名使用 layoutWeight(1) 占据主要空间,状态值和右箭头靠右排列。右箭头通过 if (item.arrow) 条件渲染,只有可跳转的功能项才显示。这种"功能清单行"的布局模式是个人中心页面的标准设计,简洁清晰且信息密度适中。


二十、月度收听柱状图 chartCard

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

      // 渐变柱状图(Column + ForEach 实现)
      Row({ space: 8 }) {
        ForEach(MONTH_IDX, (i: number) => {
          Column({ space: 5 }) {
            Text(MONTH_HOURS[i].toString()).fontSize(8)
              .fontColor(this.breath ? COLORS.navy : 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.navy, 0], [COLORS.navyD, 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 月累计 298 小时').fontSize(8).fontColor(COLORS.sub)
        Column().layoutWeight(1)
        Text('环比 +13.8%').fontSize(8).fontColor(COLORS.green)
      }
      .width('100%')
    }
    .width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)
  }

chartCard 是纯 ArkUI 声明式布局实现的柱状图,无需 Canvas 绘制。通过 ForEach 遍历 MONTH_IDX(6 个月份索引),每个月份渲染为一个垂直 Column:顶部数值标签、中间渐变柱体、底部月份标签。柱体宽度固定 18px,高度通过 Math.max(20, MONTH_HOURS[i] / MONTH_MAX * 110 * (this.breath ? 1.05 : 0.95)) 动态计算——基础高度为 月度数值 / 最大值 * 110,呼吸动画开启时乘以 1.05(+5%),关闭时乘以 0.95(-5%),Math.max(20, ...) 确保最小高度不低于 20px。柱体使用 180 度藏蓝渐变(从 navynavyD),圆角 5px。

柱状图区域整体高度 150px,alignItems(VerticalAlign.Bottom) 确保所有柱体底部对齐。数值标签颜色随 breath 在藏蓝和副文本灰之间切换,形成与柱体呼吸同步的色彩波动。底部汇总行显示"近 6 月累计 298 小时"和"环比 +13.8%"(绿色增长率),为柱状图提供数据解读。

这种"纯布局柱状图"的优势在于代码简洁、响应式天然支持(柱高随状态变化自动更新)、无需 Canvas 上下文管理;劣势是灵活性低于 Canvas 绘制(无法绘制折线、面积填充等复杂图形)。在简单柱状图场景中,这种方案是首选。


二十一、底部导航栏 tabBar

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

底部导航栏通过 ForEach 遍历 TAB_LIST 渲染 4 个 Tab,每个 Tab 使用 layoutWeight(1) 等分宽度。选中 Tab 的图标字号为 20(未选中 17)形成放大效果,透明度为 1(未选中 0.65),标签颜色为藏蓝(未选中浅灰)并加粗。这种"图标放大 + 透明度提升 + 颜色高亮 + 字重加粗"的四重选中态反馈,确保了当前 Tab 的视觉辨识度。底部导航使用白色卡片背景和顶部 1px 分割线,与内容区形成清晰边界。


二十二、弹窗系统

22.1 全屏遮罩 modalOverlay

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

modalOverlay 是所有弹窗面板的共用遮罩层。它是一个全屏 Stack,内部填充一个半透明黑色 ColumnCOLORS.maskrgba(34,40,49,0.5))。alignContent(Alignment.Center) 确保弹窗面板内容居中显示。点击遮罩区域时调用 onClose 回调关闭弹窗——这是移动端弹窗的标准交互模式,用户可以通过点击遮罩快速关闭弹窗而不需要寻找关闭按钮。

modalOverlay 接收一个 onClose: () => void 类型的回调函数参数,这种"回调注入"的设计使得三个弹窗面板可以共用同一个遮罩组件,只需传入各自的关闭逻辑即可。@Builder 函数接收回调参数是 ArkUI 中实现组件复用的重要模式。

22.2 订阅频道弹窗 panelAdd

  /** 订阅频道弹窗面板(名称 + 备注输入) */
  @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.formNote, placeholder: '如:只推音频快讯 / 每日 3 条' })
            .fontSize(11).fontColor(COLORS.title)
            .backgroundColor(COLORS.chip).borderRadius(8)
            .onChange((value: string) => {
              this.formNote = 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.white).fontWeight(FontWeight.Bold)
            .layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 9, bottom: 9 }).backgroundColor(COLORS.navy).borderRadius(9)
            .onClick(() => {
              this.saveNews();
            })
        }
        .width('100%')
      }
      .width('78%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Center)
  }

panelAdd 是订阅频道弹窗面板,采用 Stack 层叠结构——底层是 modalOverlay(onClose) 遮罩,上层是 78% 宽度的白色圆角面板。面板内部包含标题"订阅频道"、频道名称输入框(TextInput 绑定 this.formName)、订阅备注输入框(TextInput 绑定 this.formNote)和按钮行(取消 + 立即订阅)。

TextInputtext 参数绑定 @State 变量实现双向绑定——用户输入时 onChange 回调将值同步到状态变量,状态变量变更时 TextInput 的显示内容也随之更新。取消按钮调用 onClose() 关闭弹窗,立即订阅按钮调用 saveNews() 保存数据并关闭弹窗。

22.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.editTitle, placeholder: '新闻标题' })
            .fontSize(11).fontColor(COLORS.title)
            .backgroundColor(COLORS.chip).borderRadius(8)
            .onChange((value: string) => {
              this.editTitle = value;
            })
        }
        .width('100%').alignItems(HorizontalAlign.Start)

        Column({ space: 6 }) {
          Text('来源媒体').fontSize(9).fontColor(COLORS.sub)
          TextInput({ text: this.editSrc, placeholder: '来源媒体' })
            .fontSize(11).fontColor(COLORS.title)
            .backgroundColor(COLORS.chip).borderRadius(8)
            .onChange((value: string) => {
              this.editSrc = 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.white).fontWeight(FontWeight.Bold)
            .layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 9, bottom: 9 }).backgroundColor(COLORS.green).borderRadius(9)
            .onClick(() => {
              this.updateNews();
            })
        }
        .width('100%')
      }
      .width('78%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Center)
  }

panelEdit 是编辑快讯弹窗面板,结构与 panelAdd 完全对称——标题、两个输入框(新闻标题 + 来源媒体)、按钮行。区别在于:输入框绑定的是 editTitleeditSrc 变量(在 openEditNews 中已从目标新闻条目回填),保存按钮使用绿色背景(COLORS.green)区分于订阅弹窗的藏蓝主操作色,调用 updateNews() 保存修改。

  /** 删除快讯确认弹窗面板 */
  @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.white).fontWeight(FontWeight.Bold)
            .layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 9, bottom: 9 }).backgroundColor(COLORS.red).borderRadius(9)
            .onClick(() => {
              this.delNews();
            })
        }
        .width('100%')
      }
      .width('78%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Center)
  }
}

panelDel 是删除确认弹窗面板,比编辑弹窗更简洁——只有标题、确认文案和按钮行。确认删除按钮使用快讯红色背景(COLORS.red),与编辑弹窗的绿色保存和订阅弹窗的藏蓝主操作形成三色区分——绿色表示安全操作(保存修改),藏蓝表示常规操作(立即订阅),红色表示危险操作(确认删除)。这种通过操作按钮颜色区分操作风险等级的设计,是 UX 设计中的色彩语义最佳实践。确认删除按钮调用 delNews() 执行删除。

三个弹窗面板的代码结构高度一致——都使用 Stack 层叠 modalOverlay 和白色面板,都接收 onClose 回调,都在底部排列取消和确认按钮。这种结构一致性使得代码可读性强、维护成本低。如果未来需要新增更多弹窗(如"分享弹窗"、“设置弹窗”),只需复制现有面板代码并修改内容即可。


二十三、技术特性对比表

技术维度 头条 Tab 电台 Tab AI字幕 Tab 我的 Tab
布局风格 左色条列表 + 虚线摘要卡 时长大卡节目单 + 柱状图 五区块特性展示页 渐变大卡 + 功能清单
核心视觉元素 虚线边框 + 左色条 + 热度标识 渐变时长块 + 进度条 + 柱状图 AICaptionComponent + 圆形色块 深藏蓝渐变会员卡 + 三格数据
数据模型 NewsItem(6 字段) RadioItem(5 字段) CaptionScene(4 字段) UserStat(4 字段)
数据量 8 条新闻 + 3 条早报要点 8 条节目 + 6 月柱状数据 5 条场景 8 条功能项
动画效果 📻 图标呼吸 🎙 图标呼吸 + 进度条静态 就绪状态呼吸 + 色块描边 渐变大卡静态
交互操作 播放早报/订阅/编辑/删除 播放节目/查看柱状图 切换语言/字号/颜色/写入音频/套用场景 查看功能项
颜色策略 catColor 分类色映射 时长块渐变 + catColor 栏目色 藏蓝选中 + 金色代码高亮 + 五色色块 藏蓝渐变 + 金色 VIP 徽章
信息密度 高(摘要卡 + 列表 + 操作行) 中高(播出卡 + 节目单 + 图表) 高(预览 + 语言 + 外观 + 代码 + 场景) 中(渐变卡 + 清单)
技术亮点 虚线边框 + 左色条分类色 纯布局柱状图 + 呼吸波动 Speech Kit 6.1.1 四大新字段 线性渐变 + 条件箭头渲染
能力维度 实现方式 关键代码/机制
AI 字幕源语言 AICaptionOptions.sourceLanguage srcLang: string 取值 ‘zh’ | ‘en’
AI 字幕目标语言 AICaptionOptions.targetLanguage tgtLang: string 取值 ‘zh’ | ‘en’ | ‘zh-en’
AI 字幕字体大小 AICaptionOptions.fontSize captionSize: AICaptionFontSize 四档枚举
AI 字幕字体颜色 AICaptionOptions.fontColor captionColor: string 五预设色值
语言联动约束 switchSourceLang 方法 中文源锁定 zh / 英文源默认 zh-en
音频流写入 captionController.writeAudio 640 字节 PCM 块(16kHz/16bit/单声道)
字幕显示控制 isShown @State 双向绑定 captionShown: boolean 切换
字幕就绪/错误 onPrepared / onError 回调 captionReady / captionErrMsg 状态映射
场景化配置 CaptionScene 模型 + 套用逻辑 点击场景行自动设置语言组合
代码实时预览 monospace + 状态联动渲染 buildCaptionOptions 返回值实时展示
状态管理 @State + @Observed 响应式 20+ 状态变量 + 4 个 @Observed 类
组件化拆分 @Builder 构建函数 12 个 Builder 函数
弹窗系统 Stack 层叠 + 条件渲染 + 遮罩 modalOverlay + panelAdd/Edit/Del
浅色主题 ColorPalette 接口 + 15 色常量 COLORS 常量对象
呼吸动画 setInterval 定时器 + breath 状态 aboutToAppear 启动 + aboutToDisappear 清理
纯布局柱状图 Column 高度动态计算 Math.max + linearGradient 渐变柱

二十四、总结

"快闻早报·新闻资讯音频平台"作为一款基于 HarmonyOS ArkUI 框架开发的新闻资讯音频行业 Demo 应用,在 Speech Kit AICaptionComponent 四大新字段的工程化落地、浅色新闻主题色彩工程、多 Tab 差异化布局、纯布局柱状图、弹窗系统等方面展现了丰富的技术实践和工程化思考,是鸿蒙原生应用开发中一份极具参考价值的实现方案。

Speech Kit AI 字幕四大新字段方面,这是本应用最具技术深度的核心特性。应用完整呈现了 HarmonyOS 6.1.1 为 AICaptionOptions 新增的四大字段的配置能力——sourceLanguage(源语言,'zh''en')通过 SRC_LANGS 常量和 switchSourceLang 方法实现了源语言切换与目标语言联动约束;targetLanguage(目标语言,'zh''en''zh-en')通过 TGT_LANGS_EN 常量和条件渲染实现了中文源锁定/英文源三选的动态 UI;fontSizeAICaptionFontSize 四档枚举)通过 SIZE_OPTIONS 常量实现了小号/标准/大号/超大的字号选择;fontColorResourceColor 字幕色)通过 CAPTION_FONT_COLORS 五预设色值和圆形色块选择器实现了经典白/暖阳黄/薄荷绿/云朵蓝/樱花粉的颜色配置。这四大字段通过 buildCaptionOptions 方法汇聚为完整的 AICaptionOptions 对象,传递给 AICaptionComponentoptions 参数,实现了"状态变更 → 选项重建 → 组件实时更新"的响应式配置链路。在真实新闻资讯音频场景中,用户可以在"国际新闻精听"时配置英文源中文目标,在"双语晨间简报"时配置英文源中英双语目标,在"通勤静音模式"时配置中文源中文目标,配合字号和颜色偏好,获得个性化的字幕体验。

音频流入与字幕驱动方面,应用通过 feedDemoAudio 方法展示了 AICaptionController.writeAudio 的调用方式——生成 640 字节的 16kHz/16bit/单声道 PCM 正弦波数据块,封装为 AudioData 对象后写入字幕服务。onPrepared 回调在字幕服务就绪时将 captionReady 置为 trueonError 回调在异常时将 BusinessError 的错误码和描述存入 captionErrMsgisShown 通过 @State 双向绑定控制字幕显示/隐藏。这条"音频流入 → AI 识别 → 字幕渲染 → 状态回调"的完整链路,是 HarmonyOS Speech Kit 系统能力组件化调用的典型范例。

浅色新闻主题色彩工程方面,应用通过 ColorPalette 接口集中管理 15 个颜色字段,采用"新闻白 + 权威藏蓝 + 快讯红"的三色组合方案。新闻白(#F7F8FA)作为背景底色提供了柔和而不刺眼的阅读环境,比纯白更适合长时间阅读新闻内容;权威藏蓝(#2B5CAD)作为品牌主色出现在 Tab 选中、主操作按钮、左色条、渐变起点等所有需要强调品牌身份的位置,传达了新闻媒体的严肃与可信气质;快讯红(#D94A3D)在热度标记和删除操作中传达紧迫感和警示感。三级文字层次(title 深蓝灰 #222831 / sub 中灰 #5F6B7A / text3 浅灰 #9AA5B4)在浅色背景上创造了清晰的信息优先级,引导用户视线从标题到副文本再到辅助信息逐层递减。linearGradient 渐变在头部 Banner、电台"正在播出"大卡、"我的"页会员卡等位置反复运用,在浅色主题中创造了视觉层次感和焦点引导效果。catColor 辅助函数将新闻分类映射为色彩——国际藏蓝/财经金/科技绿/体育红/民生深蓝——使得左色条和分类标签的色彩形成呼应,用户通过颜色就能快速识别新闻分类。

多 Tab 差异化布局方面,4 个 Tab 采用左色条列表式、时长大卡节目单式、五区块特性展示式、渐变大卡清单式四种截然不同的布局风格。头条 Tab 的"虚线早报摘要大卡 + 左色条新闻列表"组合是布局设计中的亮点——虚线边框在视觉上传达了"报纸/刊物"的隐喻,与"快闻早报"的品牌定位高度契合;左色条通过 catColor 函数实现分类色映射,使得用户在快速滑动浏览时能够通过颜色瞬间识别新闻分类。电台 Tab 的"时长渐变块"设计巧妙地将电台节目的核心信息(时长)提升为视觉焦点,用户在浏览节目单时最关心的"这个节目多长"以渐变色块形式直接呈现。AI 字幕 Tab 的五区块结构(预览/语言/外观/代码/场景)将 AICaptionComponent 四大新字段的配置能力层层展开,从实时预览到参数设置到代码可视化到场景推荐,形成了一个完整的特性展示流程。我的 Tab 的渐变会员大卡 + 三格收听数据 + 功能清单行的组合,是个人中心页面的标准设计模式。

工程化实践方面,应用展示了多个值得借鉴的工程化细节:@Observed 数据模型为响应式更新做好准备,列表数据变化自动反映到 UI;aboutToAppear/aboutToDisappear 生命周期函数负责呼吸动画定时器的创建与清理,避免内存泄漏;弹窗系统通过 Stack 层叠 + 条件渲染 + 统一遮罩 + 回调注入实现三种交互场景的集中管理,三个面板结构高度一致且各自独立;switchSourceLang 方法的语言联动约束逻辑在 UI 层面约束了合法的语言组合,避免了无效配置传递到系统服务;feedDemoAudio 方法通过纯代码生成 PCM 正弦波数据块展示了 writeAudio 的调用方式,try-catch 块确保了音频写入失败不会导致应用崩溃;buildCaptionOptions 方法将四大新字段的状态值汇聚为配置对象,每次状态变更都会重建配置并驱动 AICaptionComponent 实时更新;辅助函数(catColor/sizeName/langName/colorName)将业务映射逻辑从视图代码中提取为独立函数,使得 Builder 函数内部代码更加简洁。这些工程化细节虽然不如视觉设计那样直观,但正是它们支撑了应用在功能完整性、交互流畅度和代码可维护性上的平衡。

呼吸动画联动设计方面,应用通过一个 breath 布尔状态变量和 setInterval 定时器,实现了多个视觉元素的同步呼吸效果。每 1000 毫秒 breathtruefalse 之间翻转,驱动头部 📰 图标透明度(1/0.6)、早报摘要 📻 图标透明度(1/0.65)、电台 🎙 图标透明度(1/0.6)、字幕就绪状态透明度(1/0.55)、柱状图柱高波动(±5%)和柱顶数值颜色切换(藏蓝/副文本灰)。通过一个状态变量联动多个视觉元素,是 ArkUI 响应式编程的典型实践——只需维护一个状态源,所有依赖该状态的 UI 组件自动同步更新,无需手动管理多个动画实例。

改进方向方面,虽然 Demo 应用已经展现了很高的技术完成度,但在实际生产环境中仍有以下拓展空间:使用 LazyForEach 替代 ForEach 以优化长列表(如大量新闻条目)的渲染性能;引入路由管理处理新闻详情、电台播放、字幕偏好设置等深层页面跳转;将数据模型和 Mock 数据抽取到独立模块文件中,实现关注点分离;增加网络请求层实现真实新闻数据加载和电台音频流播放;为 feedDemoAudio 替换为从音频文件解码或麦克风实时采集的真实 PCM 数据流;添加错误处理和加载状态(如字幕服务初始化中的 Toast 提示);使用 @StorageLink@Provide/@Consume 实现跨组件状态共享(如字幕语言偏好的全局同步);将柱状图封装为可复用的自定义组件,支持动态数据源和配置参数。这些改进方向既是 Demo 走向生产化的必经之路,也是进一步探索 ArkUI 框架能力的方向。

总的来说,"快闻早报"通过 HarmonyOS 6.1.1 Speech Kit AICaptionComponent 四大新字段(sourceLanguage/targetLanguage/fontSize/fontColor)的完整工程化落地、浅色新闻白+权威藏蓝+快讯红主题的色彩工程、4 种差异化 Tab 布局的设计表达、纯声明式柱状图的呼吸联动动画、以及 Stack 层叠弹窗系统的交互管理,成功地构建了一个功能完整、技术深厚、视觉优雅的新闻资讯音频平台 Demo。它不仅展示了 ArkUI 框架在复杂行业应用中的技术承载力,更为 AI 字幕场景化配置、新闻资讯音频化、浅色主题色彩工程等 HarmonyOS 特色能力的工程化实践提供了有价值的参考范例。

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

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


一、创建新项目

1.1 进入欢迎界面

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

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

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

在这里插入图片描述

1.2 选择项目模板

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

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

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

在这里插入图片描述

1.3 配置项目信息

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

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

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

在这里插入图片描述

1.4 完成创建

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

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

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

在这里插入图片描述

1.5 项目结构概览

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

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

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

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

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

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

在这里插入图片描述


二、查看 SDK 版本

2.1 查看 HarmonyOS SDK

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

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

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

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

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

在这里插入图片描述

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

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

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

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

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

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

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

在这里插入图片描述


三、小结

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

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


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

Logo

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

更多推荐