一、技术前言

在这里插入图片描述

HarmonyOS ArkUI 框架作为华为鸿蒙生态的核心 UI 声明式开发范式,自诞生之日起便承载着"一次开发、多端部署"的战略使命。它采用基于 TypeScript 扩展的 ArkTS 语言,通过 @Component@Entry@State@Builder 等装饰器构建起一套响应式的状态驱动 UI 体系。与传统命令式 UI 开发不同,ArkUI 的开发者只需声明 UI 的结构与状态之间的映射关系,框架便会自动追踪状态变更并精准触发对应组件的重渲染,从而极大降低了复杂界面的维护成本。在直播电商这类信息密度极高、交互频次极繁的场景中,ArkUI 的声明式范式尤为契合——直播间墙的品类筛选、订单状态的实时刷新、AI 字幕参数的动态联动,每一处状态变化都能被框架精确捕获并高效渲染。

在这里插入图片描述
HarmonyOS 6.1.1 版本对 Speech Kit(语音服务套件)进行了重大升级,其中最引人注目的变化集中在 AICaptionComponent(AI 字幕组件)上。该组件为开发者提供了一条开箱即用的 AI 语音转字幕通道,底层集成了华为强大的语音识别引擎和机器翻译引擎,能够在不依赖云端长连接的前提下,将实时音频流转换为文字字幕并可选地进行多语言翻译。组件通过 AICaptionController 控制器接收开发者写入的 AudioData 音频数据块,内部完成语音识别、文本对齐、翻译转换等全链路处理,最终在组件区域内渲染出带有时间轴对齐的字幕文本,极大降低了实时语音转文字的集成门槛。

在这里插入图片描述
此次 6.1.1 版本为 AICaptionOptions 配置接口新增了四个关键字段,使 AI 字幕的定制能力迈上了一个新的台阶。其一是 sourceLanguage(源语言),取值范围为 'zh'(中文)和 'en'(英文),用于告知识别引擎主播说的是哪种语言,从而选择对应的声学模型和语言模型,提升识别准确率。其二是 targetLanguage(目标语言),取值范围为 'zh''en' 以及 'zh-en'(中英双语),当目标语言与源语言不同时,组件会自动在识别结果之上叠加一层翻译处理,'zh-en' 则会同时输出原文与译文形成双语对照字幕。其三是 fontSize(字体大小),类型为 AICaptionFontSize 枚举,包含 SMALLNORMALBIGLARGE 四档,让用户可以根据直播间画面大小和个人视力偏好灵活调整字幕尺寸。其四是 fontColor(字体颜色),类型为 ResourceColor,支持任意合法的颜色资源值,使字幕颜色能够与直播画面的背景形成足够的对比度,保障可读性。这四个字段的加入,使得 AI 字幕从"能看"进化到"好看且可定制",为跨境直播、双语带货、静音逛播等细分场景提供了精细化的语言与视觉控制能力。

在这里插入图片描述
直播电商作为近年来增长最为迅猛的数字商业模式之一,其核心逻辑是将传统电视购物的"导购-互动-下单"链路搬上移动互联网,并通过实时互动、限时秒杀、主播人设等手段极大缩短消费者的决策路径。一个成熟的直播电商平台通常需要同时承载直播间浏览、商品详情、即时互动、订单管理、物流追踪、会员体系等多个功能域,对前端的页面架构能力和状态管理能力提出了相当高的要求。在多语言、跨境直播逐渐成为行业趋势的当下,AI 字幕的引入更是解决了"听不清主播说什么"和"听不懂外语主播"两大核心痛点,使直播购物从"必须听"进化为"可以看",在静音场景、嘈杂环境、跨境购物等场景下显著提升了信息获取效率。

在这里插入图片描述
本文将要剖析的这款"播购购·直播带货平台"应用,正是在上述技术背景和业务背景下诞生的一份 ArkUI 实战范例。它以浅色活力白(#F8F7F5)搭配热销红橙(#E8442E + #F0821E)作为视觉主调,营造出明亮、温暖、富有促销氛围的购物体验。应用整体由四个 Tab 页面构成——直播间页采用双列卡片墙布局展示正在热播的直播间,订单页以两段式票券订单卡搭配月度成交额柱状图呈现消费数据,AI 字幕页作为 Speech Kit 特性的集中展示区,我的页则以渐变会员大卡和功能清单行构成个人中心。整套设计在视觉一致性、交互流畅度和功能完整度三个维度上都做了精心打磨,尤其在 AI 字幕模块,将 sourceLanguagetargetLanguagefontSizefontColor 四大新增字段以可视化的方式完整呈现给用户,堪称 HarmonyOS Speech Kit 新特性落地实践的优秀参考。

在这里插入图片描述
从设计理念上看,这款应用追求的是"促销感但不廉价、信息密但不杂乱"的平衡感。热销红橙渐变用于头部 Banner、直播间卡片封面、会员大卡等需要营造紧迫感和促销氛围的区域;活力白和米色芯片色(#F0EBE4)则作为大面积背景和卡片底色,保证内容区的清爽与可读性;绿色、蓝色、金色作为语义辅助色,分别用于已签收、运输中、会员专享等状态标识。这种色彩分层策略使得用户在快速浏览时能够凭借颜色直觉快速定位关键信息,符合直播电商"秒级决策"的交互节奏。

二、整体架构流程图

数据层

四个 Tab 页面

播购购·直播带货平台

主题色板

活力白 #F8F7F5

热销红 #E8442E

热销橙 #F0821E

深红 #C22F1D

弹窗系统

新增地址弹窗

编辑地址弹窗

删除确认弹窗

全屏遮罩

AI 字幕核心能力

writeAudio 写入音频

sourceLanguage/targetLanguage
fontSize/fontColor

AICaptionController
控制器

AICaptionComponent
渲染组件

AICaptionOptions
配置项

头部区域
渐变 Banner + 搜索条 + 品类 Chips

滚动内容区
根据 Tab 索引切换

底部导航栏
4 Tab 单排

直播间 Tab
双列卡片墙 + 品类筛选

订单 Tab
票券订单卡 + 月度柱状图

AI字幕 Tab
Speech Kit 特性页

我的 Tab
会员大卡 + 地址管理

直播间数据
LiveItem 8 条

订单数据
OrderItem 8 条

字幕场景
CaptionScene 5 条

功能清单
UserStat 8 条

收货地址
UserStat 3 条

上图展示了应用的整体架构。从顶层来看,应用由头部区域、滚动内容区和底部导航栏三段式构成,内容区根据 currentTab 状态索引在四个 Tab 之间切换。AI 字幕 Tab 是 Speech Kit 特性的核心承载区,通过 AICaptionController 写入音频流、通过 AICaptionOptions 配置四大新增字段、最终由 AICaptionComponent 完成字幕渲染。我的 Tab 的收货地址管理模块则配备了完整的弹窗系统,涵盖新增、编辑、删除三种操作面板,均叠加在全屏遮罩之上。整个数据层以 Mock 常量数组形式集中管理,通过 @State 绑定到组件树实现响应式刷新。

三、依赖引入与模块导入

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

应用的第一行代码便揭示了其技术栈的核心——从 @kit.SpeechKit 语音服务套件中批量导入了五个关键类型。AICaptionComponent 是 AI 字幕的 UI 渲染组件,它负责在界面上绘制字幕文本区域,开发者只需将它放置在页面布局中,便拥有了一个具备实时语音转字幕能力的可视化控件。AudioData 是音频数据的封装结构,开发者需要将原始 PCM 音频字节填充到其 data 字段中,再通过控制器写入组件。AICaptionOptions 是字幕配置接口,本次 6.1.1 版本新增的四个字段全部挂载在这个接口上。AICaptionController 是字幕控制器实例,提供了 writeAudio 方法用于持续喂入音频块,是驱动字幕识别流程的引擎。AICaptionFontSize 是字号枚举类型,定义了 SMALLNORMALBIGLARGE 四档可选值。

第二行从 @kit.BasicServicesKit 基础服务套件中导入了 BusinessError 类型。这是鸿蒙系统中用于统一描述业务错误的标准化类型,包含 code(错误码)和 message(错误描述)两个字段。在 AI 字幕的 onError 回调中,组件会将运行时异常封装为 BusinessError 实例传回给开发者,后者可以据此判断是网络异常、权限不足、音频格式不支持等具体问题,并向用户展示友好的错误提示。

这种按需导入的设计体现了鸿蒙 Kit 的模块化理念——开发者只引入真正使用的类型,不会因为引入整个 Kit 而增加编译负担。同时,@kit.SpeechKit 作为系统级 Kit,其底层能力由鸿蒙系统直接提供,不依赖额外的三方库,确保了在各类鸿蒙设备上的可用性和性能表现。

四、颜色系统与主题色板

4.1 颜色接口定义

interface ColorPalette {
  bg: string;
  card: string;
  chip: string;
  title: string;
  sub: string;
  text3: string;
  red: string;
  redD: string;
  orange: string;
  green: string;
  blue: string;
  gold: string;
  line: string;
  tabOn: string;
  mask: string;
  white: string;
}

应用首先定义了一个名为 ColorPalette 的接口,将页面中可能用到的所有颜色字段集中声明在一个类型约束内。这种做法的好处是多方面的:首先,它起到了颜色字典的作用,开发者在编写 UI 时只需查阅接口定义即可知道有哪些颜色可用,避免了在代码各处散落魔法字符串;其次,它为主题切换提供了结构化的基础——未来若要支持深色模式,只需创建另一个实现了 ColorPalette 接口的常量对象即可完成全局换肤;最后,TypeScript 的类型检查会在编译期校验所有颜色字段是否齐全,若遗漏某个字段会直接报错,从源头上杜绝了"某处颜色未设置导致默认黑色"这类难以排查的样式问题。

接口中包含了 16 个颜色字段,涵盖了背景色(bg)、卡片色(card)、芯片底色(chip)、三级文字色(title/sub/text3)、主辅强调色(red/redD/orange)、语义辅助色(green/blue/gold)、分隔线色(line)、Tab 选中色(tabOn)、遮罩色(mask)和白色(white)。其中三级文字色的设计尤其值得注意:title 用于主标题、sub 用于副文本、text3 用于辅助说明文字,通过同色系不同明度的层次递进,在浅色背景上构建出清晰的视觉层级。

4.2 浅色主题色板常量

const COLORS: ColorPalette = {
  bg: '#F8F7F5',
  card: '#FFFFFF',
  chip: '#F0EBE4',
  title: '#332A24',
  sub: '#8C7E6F',
  text3: '#B3A696',
  red: '#E8442E',
  redD: '#C22F1D',
  orange: '#F0821E',
  green: '#4EA860',
  blue: '#5B8FD9',
  gold: '#D9A441',
  line: '#EAE3DA',
  tabOn: '#E8442E',
  mask: 'rgba(51,42,36,0.5)',
  white: '#FFFFFF'
};

COLORS 常量是整个应用的视觉基石,它实现了 ColorPalette 接口并填入了具体的色值。主背景色 bg 采用 #F8F7F5,这是一种带有极轻微暖调的接近纯白的颜色,比纯白 #FFFFFF 多了一份柔和感,长时间观看不易产生视觉疲劳,同时又能与纯白卡片底色形成微妙的层次区分。卡片色 card 使用纯白 #FFFFFF,使卡片内容区在暖白背景上"浮"起来,形成自然的卡片悬浮效果。

热销红橙色系是整个主题的灵魂。主红色 red#E8442E,这是一种偏向暖调的正红,饱和度高但不刺眼,传递出促销、热销、紧迫的视觉信号。深红色 redD#C22F1D,明度更低,用于渐变的起始色和需要更强的视觉重量感的区域。热销橙 orange#F0821E,从红色过渡到橙色,形成红橙渐变——这是直播电商最常见的促销配色组合,头部 Banner、直播间封面、会员大卡等核心视觉区域均采用这套渐变方案。

文字三级色采用了一套暖灰系:title#332A24(近乎黑色的深棕灰,用于最重要标题)、sub#8C7E6F(中灰棕,用于副文本)、text3#B3A696(浅灰棕,用于辅助说明)。这套暖灰文字色与暖白背景在色温上保持一致,避免了"冷调文字配暖调背景"的违和感。辅助语义色则分别用于不同状态:green#4EA860,已签收)、blue#5B8FD9,运输中、可点击链接)、gold#D9A441,会员专享、金标)。遮罩色 mask 使用 rgba(51,42,36,0.5),即主文字色加上 50% 透明度,形成半透明的深棕灰遮罩,在弹窗出现时压暗背景内容。

五、常量定义与元数据

5.1 底部导航 Tab 元数据

interface TabMeta {
  icon: string;
  label: string;
}

const TAB_LIST: TabMeta[] = [
  { icon: '🔴', label: '直播间' },
  { icon: '🧾', label: '订单' },
  { icon: '🗣', label: 'AI字幕' },
  { icon: '👤', label: '我的' }
];

底部导航栏的配置采用了"数据驱动 UI"的设计思路。TabMeta 接口定义了每个 Tab 项的两个字段:icon(图标,使用 emoji 字符)和 label(标签文本)。TAB_LIST 常量数组按照 Tab 的排列顺序依次定义了直播间、订单、AI 字幕、我的四个入口。这种将导航配置抽离为独立常量的做法,使得 Tab 的增删改只需修改一处数据源,底部导航的 ForEach 渲染逻辑无需任何改动即可自动适配,充分体现了声明式 UI "数据即视图"的核心理念。

四个 Tab 的选择也体现了直播电商的业务全景:直播间是用户进入购物场景的入口,订单是购物后的履约跟踪,AI 字幕是解决多语言/静音场景下的信息获取问题,我的则是个人中心和地址管理等基础功能。emoji 图标的使用则兼顾了开发效率和视觉表现力——无需引入图标字体或图片资源,纯文本即可渲染出彩色图标,同时在不同设备上的显示效果也基本一致。

5.2 商品分类与直播间品类筛选

const CATE_TAGS: string[] = ['美妆', '服饰', '零食', '数码', '家电', '家居', '珠宝', '宠物'];

const LIVE_CATS: string[] = ['全部', '美妆', '服饰', '食品', '数码', '家居', '母婴', '珠宝'];

应用定义了两套品类常量。CATE_TAGS 是头部横滑商品分类 chips 的文案列表,包含 8 个品类标签,覆盖了直播电商的主流商品品类。LIVE_CATS 是直播间 Tab 内的品类筛选 chips,额外包含了一个"全部"选项,用于显示所有品类的直播间。两套品类的命名略有差异——CATE_TAGS 中是"零食",LIVE_CATS 中是"食品";LIVE_CATS 包含"母婴"而 CATE_TAGS 包含"家电"和"宠物"——这反映了不同页面维度对品类分类的差异化诉求:头部分类面向全站商品搜索,品类更细更全;直播间筛选面向直播内容,品类按直播间的品类分布做了精简和适配。

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

interface LangOption {
  code: string;
  name: string;
}

const SRC_LANGS: LangOption[] = [
  { code: 'zh', name: '中文' },
  { code: 'en', name: '英文' }
];

const TGT_LANGS_EN: LangOption[] = [
  { code: 'zh', name: '中文' },
  { code: 'en', name: '英文' },
  { code: 'zh-en', name: '中英双语' }
];

这一段是 Speech Kit 6.1.1 新特性的语言配置数据。LangOption 接口定义了语言选项的结构:code 是传给 sourceLanguage/targetLanguage 字段的语言码,name 是展示给用户看的语言中文名。SRC_LANGS 定义了源语言的两个选项——中文和英文,对应 sourceLanguage 字段的 'zh''en' 取值。TGT_LANGS_EN 定义了当源语言为英文时可选的三种目标语言——中文、英文和中英双语,分别对应 targetLanguage 字段的 'zh''en''zh-en' 三个取值。

值得注意的是,这里没有为中文源定义目标语言选项列表,因为根据组件的语义约束,当源语言为中文时,目标语言被锁定为 'zh'(无翻译方向,只做中文语音转中文字幕)。这一约束在后文的 switchSourceLang 方法中被编码实现,体现了数据定义与方法逻辑的一致性。'zh-en' 这个特殊取值是双语模式的标识,组件会在字幕区域同时展示原文和译文,非常适合双语带货学习场景。

5.4 字号与字幕颜色预设

interface SizeOption {
  size: AICaptionFontSize;
  name: string;
}

const SIZE_OPTIONS: SizeOption[] = [
  { size: AICaptionFontSize.SMALL, name: '小号' },
  { size: AICaptionFontSize.NORMAL, name: '标准' },
  { size: AICaptionFontSize.BIG, name: '大号' },
  { size: AICaptionFontSize.LARGE, name: '超大' }
];

const CAPTION_FONT_COLORS: string[] = ['#FFFFFF', '#FFE9B0', '#9CE8B5', '#9CD0FF', '#FFB3C1'];

SizeOption 接口将 AICaptionFontSize 枚举值与用户可读的中文名配对,SIZE_OPTIONS 数组按从小到大顺序定义了四档字号选项。AICaptionFontSize 是 Speech Kit 提供的枚举类型,SMALL 对应小号、NORMAL 对应标准、BIG 对应大号、LARGE 对应超大。这四档字号覆盖了从紧凑显示到大字号易读的全谱系需求,用户可以根据直播间画面尺寸、观看距离、个人视力等因素自由选择。

CAPTION_FONT_COLORS 定义了五档字幕字体颜色预设,每一档都有明确的语义指向:#FFFFFF(经典白,适配深色直播画面背景)、#FFE9B0(暖阳黄,在浅色背景上仍有良好对比度且不刺眼)、#9CE8B5(薄荷绿,清新风格选择)、#9CD0FF(云朵蓝,科技感色调)、#FFB3C1(樱花粉,偏女性化审美选择)。这五个颜色均采用了较高的明度和中等饱和度,确保在各类直播画面背景上都能保持足够的可读性,同时通过色彩多样性满足用户的个性化偏好。fontColor 字段的类型是 ResourceColor,这意味着它不仅支持十六进制字符串,还支持 Resource 资源引用和 Color 数值类型,具备极强的扩展性。

5.5 月度成交额柱状图数据

const MONTH_IDX: number[] = [0, 1, 2, 3, 4, 5];
const MONTH_LABELS: string[] = ['3月', '4月', '5月', '6月', '7月', '8月'];
const MONTH_AMOUNTS: number[] = [326, 388, 352, 465, 592, 738];
const MONTH_MAX: number = 800;

这组常量服务于订单 Tab 底部的月度成交额柱状图。MONTH_IDX 是月份索引数组,用于 ForEach 遍历时提供稳定的 key 生成依据。MONTH_LABELS 是横轴月份标签。MONTH_AMOUNTS 是各月成交额数值(单位:元),呈现出从 3 月到 8 月逐月攀升的增长趋势,最后一个月 8 月达到 738 元,是最高点。MONTH_MAX 是柱状图的满量程值 800 元,用于计算每根柱子的相对高度比例——例如 3 月的柱高为 326 / 800 * 基准高度。将满量程设为 800 而非 738(实际最大值),是为了给最高柱留出顶部空间,避免柱顶贴到图表上边界,视觉效果更舒展。

六、辅助函数群

6.1 订单状态颜色映射

function statusColor(s: string): string {
  if (s === '待发货') { return COLORS.orange; }
  if (s === '运输中') { return COLORS.blue; }
  if (s === '已签收') { return COLORS.green; }
  return COLORS.red;
}

statusColor 函数将订单状态文本映射为对应的语义色。待发货映射为热销橙(COLORS.orange),传达"等待中、即将发出"的提示感;运输中映射为蓝色(COLORS.blue),传递"在途、可追踪"的理性信息;已签收映射为绿色(COLORS.green),表示"完成、满意"的正向状态;其余状态(即退款中)映射为红色(COLORS.red),表示"异常、需关注"的警示状态。这种状态-颜色的映射是电商类应用中极为常见的设计模式,通过颜色直觉帮助用户在众多订单中快速识别需要关注的条目。

函数采用了 if-return 的早返回写法,而非 switch-case 或对象查表,在分支数量较少(4 条)的情况下可读性最佳。最后一行 return COLORS.red 作为兜底返回,确保函数在任何输入下都有返回值,避免了 undefined 的可能性。

6.2 品类颜色映射

function catColor(c: string): string {
  if (c === '美妆') { return COLORS.red; }
  if (c === '服饰') { return COLORS.orange; }
  if (c === '食品') { return COLORS.gold; }
  if (c === '数码') { return COLORS.blue; }
  if (c === '家居') { return COLORS.green; }
  if (c === '母婴') { return COLORS.orange; }
  return COLORS.redD;
}

catColor 函数为直播间品类标签提供颜色映射。与订单状态映射类似,它为每个品类分配了一个语义色:美妆用红色(女性化、热情)、服饰用橙色(时尚、活力)、食品用金色(温暖、食欲感)、数码用蓝色(科技、理性)、家居用绿色(自然、舒适)、母婴用橙色(温暖、关爱),其余品类(如珠宝)用深红色兜底。这种品类-颜色的固定映射使用户在浏览直播间墙时能够通过颜色快速定位自己感兴趣的品类,形成视觉记忆和色彩导航的双重效果。

6.3 枚举与语言码转展示名

function sizeName(size: AICaptionFontSize): string {
  if (size === AICaptionFontSize.SMALL) { return 'SMALL'; }
  if (size === AICaptionFontSize.BIG) { return 'BIG'; }
  if (size === AICaptionFontSize.LARGE) { return 'LARGE'; }
  return 'NORMAL';
}

function langName(code: string): string {
  if (code === 'zh') { return '中文'; }
  if (code === 'en') { return '英文'; }
  return '中英双语';
}

function colorName(c: string): string {
  if (c === CAPTION_FONT_COLORS[0]) { return '经典白'; }
  if (c === CAPTION_FONT_COLORS[1]) { return '暖阳黄'; }
  if (c === CAPTION_FONT_COLORS[2]) { return '薄荷绿'; }
  if (c === CAPTION_FONT_COLORS[3]) { return '云朵蓝'; }
  return '樱花粉';
}

这三个辅助函数服务于 AI 字幕 Tab 的代码预览和状态展示区域。sizeNameAICaptionFontSize 枚举值转换为对应的英文枚举名字符串,用于代码预览块中以高亮展示当前选中的字号值。langName 将语言码('zh''en''zh-en')转换为中文名,用于语言组合的当前状态展示行。colorName 将颜色十六进制值转换为诗意的中文色名(经典白、暖阳黄、薄荷绿、云朵蓝、樱花粉),用于字体颜色选中状态的文字说明。

三个函数都采用了相同的早返回模式,最后一个 return 作为默认兜底。colorName 函数特别值得注意——它通过比较输入色值与 CAPTION_FONT_COLORS 数组的下标元素来确定色名,这意味着如果传入了一个不在预设数组中的颜色值,函数会返回"樱花粉"作为兜底。这种设计虽然简单,但隐含了一个约束:colorName 只应接收来自 CAPTION_FONT_COLORS 的颜色值,不应接收任意颜色。

七、数据模型与 Mock 数据

7.1 直播间数据模型

@Observed export class LiveItem {
  title: string;
  host: string;
  cover: string;
  heat: string;
  cat: string;
  tag: string;

  constructor(title: string, host: string, cover: string, heat: string, cat: string, tag: string) {
    this.title = title;
    this.host = host;
    this.cover = cover;
    this.heat = heat;
    this.cat = cat;
    this.tag = tag;
  }
}

LiveItem 是直播间卡片的数据模型类,使用了 @Observed 装饰器修饰。@Observed 是 ArkUI 提供的观察装饰器,它使得被修饰的类的实例属性变为可观测的——当属性值发生变化时,绑定了该实例的 UI 组件会自动触发重渲染。这在直播间列表场景中尤为重要:当某个直播间的热度数据更新时,只有依赖该属性的卡片会刷新,其余卡片不受影响,实现了精准的局部更新。

类中定义了六个字段:title(直播间名,如"美妆小课堂·夏日持妆")、host(主播名)、cover(封面,使用 emoji 字符简化展示)、heat(热度文本,如"1.2万人在看")、cat(品类标签)、tag(促销标签,如"秒杀"、“清仓”)。构造函数采用全参数列表的形式,强制使用者在创建实例时提供所有字段值,避免了"创建后忘记设置某字段导致 UI 显示 undefined"的问题。

const LIVE_LIST: Array<LiveItem> = [
  new LiveItem('美妆小课堂 · 夏日持妆', '主播糖糖', '💄', '1.2万人在看', '美妆', '秒杀'),
  new LiveItem('羽绒服清仓专场', '北方服饰严选', '🧥', '8632人在看', '服饰', '清仓'),
  new LiveItem('零食大礼包开箱夜', '吃货小分队', '🍫', '9876人在看', '食品', '爆款'),
  new LiveItem('旗舰手机首发测评', '极客老王', '📱', '5412人在看', '数码', '新品'),
  new LiveItem('懒人收纳家居节', '收纳师小雨', '🏠', '7320人在看', '家居', '补贴'),
  new LiveItem('婴童用品大牌特卖', '宝妈莉莉', '🍼', '6158人在看', '母婴', '秒杀'),
  new LiveItem('黄金珠宝捡漏夜', '珠宝鉴定老周', '💍', '1.5万人在看', '珠宝', '捡漏'),
  new LiveItem('球鞋服饰运动大促', '球鞋阿凯', '👟', '8066人在看', '服饰', '大促')
];

LIVE_LIST 是直播间的 Mock 数据常量,共 8 条记录,覆盖了美妆、服饰、食品、数码、家居、母婴、珠宝七大品类(其中服饰有两条)。每条数据都经过了精心编排:直播间名包含了品类关键词和促销话术(如"夏日持妆"、“清仓专场”、“首发测评”),主播名带有身份暗示(“主播糖糖”、“极客老王”、“收纳师小雨”),热度文本使用了真实感强的数字(“1.2万人在看”、“8632人在看”),促销标签涵盖了秒杀、清仓、爆款、新品、补贴、捡漏、大促等多种电商话术。这些 Mock 数据虽然不是真实接口返回的数据,但在视觉呈现和信息密度上高度还原了真实直播电商平台的体验。

7.2 订单数据模型

@Observed export class OrderItem {
  goods: string;
  price: string;
  status: string;
  logistics: string;
  time: string;

  constructor(goods: string, price: string, status: string, logistics: string, time: string) {
    this.goods = goods;
    this.price = price;
    this.status = status;
    this.logistics = logistics;
    this.time = time;
  }
}

OrderItem 是订单卡的数据模型,同样使用 @Observed 修饰。字段设计贴近真实订单信息:goods(商品名)、price(价格文本,已包含货币符号)、status(订单状态,限定为"待发货"、“运输中”、“已签收”、“退款中"四种取值之一)、logistics(物流信息文本,已格式化为可读语句)、time(下单时间,格式为"MM-DD HH:mm”)。status 字段虽然定义为 string 类型而非联合类型,但配合 statusColor 函数和订单 UI 的渲染逻辑,已经形成了隐式的枚举约束。

const ORDER_LIST: Array<OrderItem> = [
  new OrderItem('玻尿酸精华面膜 30 片装', '¥59.9', '运输中', '顺丰速运 · 已从杭州转运中心发出', '08-22 21:36'),
  new OrderItem('新疆长绒棉四件套', '¥199.0', '待发货', '商家备货中 · 预计 48 小时内发出', '08-24 19:02'),
  new OrderItem('每日坚果 30 包家庭装', '¥89.9', '已签收', '已由丰巢快递柜代收 · 签收人本人', '08-18 12:40'),
  new OrderItem('骁龙旗舰手机 512G', '¥4299.0', '运输中', '京东物流 · 派送员张师傅 138****6672', '08-23 10:15'),
  new OrderItem('儿童安全座椅 0-12 岁', '¥1580.0', '退款中', '退款申请处理中 · 预计 1 个工作日到账', '08-20 15:28'),
  new OrderItem('足金小克重转运珠', '¥799.0', '已签收', '顺丰保价 · 本人当面签收验货完成', '08-16 09:47'),
  new OrderItem('轻量跑步鞋 男女同款', '¥329.0', '待发货', '仓库拣货中 · 大促订单激增顺延发出', '08-25 08:11'),
  new OrderItem('沉浸式护眼台灯 Pro', '¥159.0', '运输中', '中通快递 · 已到达武汉光谷网点', '08-24 22:05')
];

ORDER_LIST 包含 8 条订单 Mock 数据,四种状态齐全(运输中 3 条、待发货 2 条、已签收 2 条、退款中 1 条)。商品名带有详细的规格描述(“30 片装”、“512G”、“0-12 岁”),价格从 ¥59.9 到 ¥4299.0 覆盖了低价到高价区间,物流信息模拟了真实快递公司的推送文案(“顺丰速运”、“京东物流”、“中通快递”),下单时间分布在 08-16 到 08-25 之间,模拟了近 10 天内的订单。这些数据组合在一起,使订单 Tab 在视觉呈现上具备了一个成熟电商应用应有的信息丰富度和真实感。

7.3 字幕场景数据模型

@Observed export class CaptionScene {
  scene: string;
  desc: string;
  src: string;
  tgt: string;

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

const SCENE_LIST: Array<CaptionScene> = [
  new CaptionScene('跨境直播购物', '海外主播直播实时汉化', 'en', 'zh'),
  new CaptionScene('双语带货学习', '主播话术原文译文对照', 'en', 'zh-en'),
  new CaptionScene('中文直播速记', '没声音也能看懂讲解', 'zh', 'zh'),
  new CaptionScene('英语叫卖特训', '只开英文字幕练听力', 'en', 'en'),
  new CaptionScene('静音逛播模式', '办公室静音看直播', 'zh', 'zh')
];

CaptionScene 是 AI 字幕场景推荐列表的数据模型,每条记录包含场景名、场景描述、推荐的源语言码和目标语言码。SCENE_LIST 定义了 5 个直播字幕场景,覆盖了跨境购物、双语学习、中文速记、英语特训、静音逛播五种典型使用场景。每个场景都精心搭配了语言组合:跨境直播购物(en→zh)将英文主播的话术翻译成中文,双语带货学习(en→zh-en)同时展示英文原文和中文译文便于学习,中文直播速记和静音逛播(zh→zh)只做语音转文字不做翻译,英语叫卖特训(en→en)保留英文原文用于听力训练。

这些场景的设计精妙之处在于它们不仅是功能介绍,更是用户教育——通过具体的场景描述引导用户理解"源语言"和"目标语言"这两个概念的实际含义,降低了用户配置 AI 字幕参数的认知门槛。当用户点击某个场景条目时,应用会自动套用该场景的语言组合,实现了"一键切换语言模式"的便捷操作。

7.4 用户功能清单与收货地址数据

@Observed export class UserStat {
  icon: string;
  label: string;
  value: string;
  arrow: boolean;

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

UserStat 是一个复用度很高的数据模型,同时用于我的 Tab 的功能清单行和收货地址行。字段设计兼顾了两种用途:icon 是 emoji 图标,label 在功能清单中是功能名、在地址列表中是收件人姓名加手机号,value 在功能清单中是状态/数值文本、在地址列表中是详细地址,arrow 控制是否显示右侧箭头(功能清单行显示箭头表示可点击进入,地址行不显示箭头)。

const STAT_LIST: Array<UserStat> = [
  new UserStat('📦', '我的订单', '12 笔待收货', true),
  new UserStat('🎟', '优惠券', '8 张可用', true),
  new UserStat('❤', '我的收藏', '86 件宝贝', true),
  new UserStat('🪙', '播币余额', '2,580', true),
  new UserStat('🏆', '签到有礼', '已连签 15 天', true),
  new UserStat('🗣', 'AI 字幕偏好', '源 zh · 目标 zh', true),
  new UserStat('👑', '购物会员', '白金会员 2026-12-31 到期', true),
  new UserStat('⚙', '直播与画质设置', '超清 1080P', true)
];

STAT_LIST 是我的页功能清单的 Mock 数据,8 条记录覆盖了订单、优惠券、收藏、播币、签到、AI 字幕偏好、会员、画质设置等核心功能入口。每条数据都带有具体的状态值(“12 笔待收货”、“8 张可用”、“已连签 15 天”),使得功能清单不只是干瘪的入口列表,而是用户状态的实时摘要。其中"AI 字幕偏好"一条特别值得关注——它将用户在 AI 字幕 Tab 中设置的语言偏好(源 zh·目标 zh)同步到了个人中心,体现了应用内跨 Tab 的数据联动意识。

const ADDR_LIST: Array<UserStat> = [
  new UserStat('🏠', '林晚晚 137****2288', '浙江省杭州市西湖区文三路 168 号 3 幢 2 单元 501', false),
  new UserStat('🏢', '林晚晚 137****2288', '上海市徐汇区宜州路 180 号 B 座 12F 前台代收', false),
  new UserStat('🏠', '陈默 159****7364', '广东省深圳市南山区深南大道 9988 号 6 栋 802', false)
];

ADDR_LIST 是收货地址的 Mock 数据,3 条记录模拟了一个用户在杭州、上海、深圳三个城市的收货地址。手机号已做脱敏处理(137****2288),地址描述精确到门牌号和楼层,arrow 字段统一设为 false(地址行不显示右侧箭头,而是通过下方的编辑/删除按钮提供操作入口)。

八、组件主体与状态声明

8.1 组件声明与基础状态

@Entry
@Component
struct Page1116 {
  @State currentTab: number = 0;
  @State breath: boolean = false;
  @State timer: number = -1;
  @State cateIdx: number = 0;
  @State liveCatIdx: number = 0;
  @State addModal: boolean = false;
  @State editModal: boolean = false;
  @State delModal: boolean = false;
  @State editIdx: number = 0;
  @State delIdx: number = 0;

组件主体以 @Entry@Component 双装饰器声明。@Entry 标识该组件为页面入口组件,会被鸿蒙系统的路由系统注册为一个可导航的页面。@Component 声明这是一个自定义组件,其内部可以使用 @State@Builder@Prop@Link 等装饰器。组件名为 Page1116

前三个状态是应用级的基础状态。currentTab 记录当前选中的 Tab 索引(0-3),初始值为 0 表示默认展示直播间 Tab,它的变化驱动着内容区的页面切换。breath 是一个布尔型"呼吸"开关,初始为 false,它会被一个每秒翻转的定时器持续翻转,从而驱动柱状图柱高波动、直播角标闪烁、字幕就绪状态呼吸等多处动效。timer 是定时器句柄,初始为 -1(表示未启动),用于在组件销毁时精准清理。

接下来是两个品类筛选索引:cateIdx 控制头部商品分类 chips 的选中态,liveCatIdx 控制直播间 Tab 内品类筛选 chips 的选中态。然后是三个弹窗开关:addModaleditModaldelModal 分别控制新增地址、编辑地址、删除确认三个弹窗的显示与隐藏。最后是两个操作索引:editIdx 记录当前正在编辑的地址索引,delIdx 记录当前正在删除的地址索引。

8.2 数据列表状态

  @State liveList: Array<LiveItem> = LIVE_LIST;
  @State orderList: Array<OrderItem> = ORDER_LIST;
  @State sceneList: Array<CaptionScene> = SCENE_LIST;
  @State statList: Array<UserStat> = STAT_LIST;
  @State addrList: Array<UserStat> = ADDR_LIST;
  @State formName: string = '';
  @State formAddr: string = '';
  @State editName: string = '';
  @State editAddr: string = '';

这一组状态变量将 Mock 数据数组绑定到组件状态上。liveListorderListsceneListstatListaddrList 分别是直播间、订单、字幕场景、功能清单、收货地址的数据源。将常量赋值给 @State 变量而非直接在 build 中使用常量,是为了保留数据变更的能力——虽然 Mock 数据在当前版本是只读的,但通过 @State 包装后,未来若接入真实接口只需修改这些变量的赋值来源即可,UI 渲染逻辑无需改动。

formNameformAddr 是新增地址弹窗的表单输入值,初始为空字符串。editNameeditAddr 是编辑地址弹窗的表单输入值,会在打开编辑弹窗时回填当前地址的收件人和详细地址。这种"每个弹窗独立维护表单状态"的设计避免了多个弹窗共享表单状态时的数据串扰问题。

8.3 AI 字幕核心状态

  private captionController: AICaptionController = new AICaptionController();
  @State captionShown: boolean = false;
  @State srcLang: string = 'zh';
  @State tgtLang: string = 'zh';
  @State captionSize: AICaptionFontSize = AICaptionFontSize.NORMAL;
  @State captionColor: string = CAPTION_FONT_COLORS[0];
  @State captionReady: boolean = false;
  @State captionErrMsg: string = '';
  @State captionFed: number = 0;

这一段是 AI 字幕功能的核心状态声明,也是 Speech Kit 6.1.1 新特性在应用层的具体落地。captionControllerAICaptionController 的实例,声明为 private 而非 @State——因为控制器实例本身不需要触发 UI 重渲染,它只是一个工具对象,通过调用其 writeAudio 方法向组件推送音频数据。

接下来的五个状态直接对应 AICaptionOptions 的五个可配置字段:captionShown 对应 isShown(字幕显示开关,初始为 false 即默认隐藏)、srcLang 对应 sourceLanguage(初始为 'zh' 即中文源)、tgtLang 对应 targetLanguage(初始为 'zh' 即中文目标)、captionSize 对应 fontSize(初始为 AICaptionFontSize.NORMAL 即标准字号)、captionColor 对应 fontColor(初始为 CAPTION_FONT_COLORS[0] 即经典白)。这五个状态全部使用 @State 修饰,意味着用户在界面上修改任何一个设置时,对应的 UI 会立即刷新,同时 AICaptionComponentoptions 参数也会实时重建,使字幕组件即时应用新的配置。

captionReady 是字幕服务就绪标志,由 onPrepared 回调置 true,用于在 UI 上展示"已就绪"状态。captionErrMsg 存储 onError 回调返回的错误信息,非空时在界面上展示错误提示。captionFed 是已写入音频块的计数器,每次成功调用 writeAudio 后递增,用于在 UI 上展示"已写入 N 块"的调试信息。

九、AI 字幕方法群

9.1 buildCaptionOptions:组装配置对象

  buildCaptionOptions(): AICaptionOptions {
    const opts: AICaptionOptions = {
      initialOpacity: 1,
      sourceLanguage: this.srcLang,
      targetLanguage: this.tgtLang,
      fontSize: this.captionSize,
      fontColor: this.captionColor,
      onPrepared: () => {
        this.captionReady = true;
        this.captionErrMsg = '';
      },
      onError: (error: BusinessError) => {
        this.captionErrMsg = '字幕服务异常 ' + error.code + ':' + error.message;
      }
    };
    return opts;
  }

buildCaptionOptions 方法是 AI 字幕配置的组装工厂。它在每次被调用时,读取当前的 srcLangtgtLangcaptionSizecaptionColor 四个状态值,组装为一个全新的 AICaptionOptions 对象返回。这个方法在 AICaptionComponentoptions 参数处被调用,由于四个状态均为 @State,当任意一个状态变化时,组件的 options 参数会获得一个新的配置对象,从而触发组件内部重新应用配置——这就是"用户切换语言/字号/颜色后字幕立即生效"的底层机制。

配置对象中除了四大新增字段外,还包含了 initialOpacity(初始不透明度,设为 1 即完全不透明)、onPrepared 回调(字幕服务初始化完成时触发,置就绪标志并清空错误信息)和 onError 回调(字幕服务发生异常时触发,将 BusinessErrorcodemessage 拼接为可读的错误字符串存入状态)。onPreparedonError 两个回调为开发者提供了对字幕服务生命周期的感知能力,使得 UI 能够根据服务的实际状态展示"初始化中"、“已就绪”、"异常"等不同状态,而非简单地假设服务永远可用。

9.2 switchSourceLang:源语言切换联动

  switchSourceLang(code: string) {
    this.srcLang = code;
    if (code === 'zh') {
      this.tgtLang = 'zh';
    } else {
      this.tgtLang = 'zh-en';
    }
  }

switchSourceLang 方法处理源语言切换时的目标语言联动逻辑。当用户将源语言切换为中文时,目标语言被锁定为 'zh'——因为中文源时没有翻译方向,字幕只做中文语音转中文字幕,不支持翻译为英文。当用户将源语言切换为英文时,目标语言默认设为 'zh-en'(中英双语),用户后续仍可在中文、英文、中英双语三者之间自由切换。

这种联动设计体现了对 Speech Kit 语义约束的准确理解:sourceLanguage = 'zh'targetLanguage 只能为 'zh',如果强行设为 'en''zh-en' 会导致组件行为未定义或报错。通过在方法层主动约束目标语言的取值范围,避免了用户配置出无效的语言组合,提升了交互的健壮性。

9.3 feedDemoAudio:演示音频写入

  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 方法演示了如何向 AICaptionController 写入音频数据。由于是演示场景而非真实直播,方法通过代码生成了一段 640 字节的 PCM 音频数据——640 字节 / 2(16bit 采样)= 320 个采样点,采样率 16000Hz,时长约 20 毫秒。音频内容是一个 440Hz 的正弦波(标准音 A4),通过 Math.sin(2 * Math.PI * 440 * t) 计算每个采样点的振幅值,乘以 6000 作为振幅缩放,然后通过 Math.round 取整为 16 位有符号整数。

16 位 PCM 的字节序采用小端序(Little Endian),因此低字节存放在 block[i]v & 0xFF 取低 8 位),高字节存放在 block[i + 1](v >> 8) & 0xFF 右移 8 位再取低 8 位)。组装好的 Uint8Array 被封装进 AudioData 结构的 data 字段,然后调用 this.captionController.writeAudio(audioData) 将音频块推入字幕组件。写入成功后 captionFed 计数器递增,UI 上展示"×N"的已写入次数;若写入抛出异常,则将错误信息存入 captionErrMsg 在界面上展示。

这段代码虽然使用的是合成正弦波而非真实语音,但它完整展示了"构造 PCM 数据→封装 AudioData→调用 writeAudio"的全链路,开发者只需将 block 的数据来源替换为真实的麦克风采集或直播流解码,即可在真实场景中使用。

十、地址管理与业务方法

10.1 品类筛选与地址编辑

  filteredLives(): Array<LiveItem> {
    const cat: string = LIVE_CATS[this.liveCatIdx];
    if (cat === '全部') {
      return this.liveList;
    }
    return this.liveList.filter((l: LiveItem) => l.cat === cat);
  }

  openEditAddr(idx: number) {
    this.editIdx = idx;
    this.editName = this.addrList[idx].label;
    this.editAddr = this.addrList[idx].value;
    this.editModal = true;
  }

filteredLives 方法根据当前选中的品类筛选索引 liveCatIdx,返回过滤后的直播间列表。当选中"全部"时直接返回全量列表,否则使用 Array.filter 方法按品类字段过滤。这个方法在直播间 Tab 的 ForEach 中被调用,当用户点击品类筛选 chips 改变 liveCatIdx 时,filteredLives 返回值变化,ForEach 据此重新渲染直播间墙。

openEditAddr 方法在用户点击地址行的"编辑"按钮时被调用。它首先记录当前编辑的地址索引到 editIdx,然后从 addrList 中取出对应条目的 label(收件人)和 value(详细地址)回填到 editNameeditAddr 表单状态中,最后打开编辑弹窗(editModal = true)。这种"先回填再打开"的模式确保了用户在弹窗中看到的是当前地址的原有内容,可以直接在原内容基础上修改。

10.2 地址保存与删除

  saveAddr() {
    const name = this.formName === '' ? '新收件人 138****0000' : this.formName;
    const addr = this.formAddr === '' ? '默认地址待补充' : this.formAddr;
    this.addrList.push(new UserStat('🏠', name, addr, false));
    this.formName = '';
    this.formAddr = '';
    this.addModal = false;
  }

  updateAddr() {
    if (this.editIdx >= 0 && this.editIdx < this.addrList.length) {
      if (this.editName !== '') {
        this.addrList[this.editIdx].label = this.editName;
      }
      if (this.editAddr !== '') {
        this.addrList[this.editIdx].value = this.editAddr;
      }
      this.addrList = this.addrList.slice();
    }
    this.editModal = false;
  }

  delAddr() {
    if (this.delIdx >= 0 && this.delIdx < this.addrList.length) {
      this.addrList.splice(this.delIdx, 1);
    }
    this.delModal = false;
  }

这三个方法分别处理地址的新增、编辑保存和删除。saveAddr 在新增弹窗的"保存"按钮点击时调用,对空的表单字段做默认值兜底(收件人为空时填"新收件人 138****0000",地址为空时填"默认地址待补充"),然后将新的 UserStat 实例 push 到 addrList 数组末尾,清空表单状态并关闭弹窗。

updateAddr 在编辑弹窗的"保存修改"按钮点击时调用。它首先做边界检查(editIdx 在有效范围内),然后分别检查 editNameeditAddr 是否非空(非空才更新,空则保留原值),最后通过 this.addrList = this.addrList.slice() 创建数组的浅拷贝赋值给自身。这行看似冗余的代码至关重要——ArkUI 的 @State 数组监听是基于引用变化的,直接修改数组元素的属性不会触发引用变化,slice() 创建新数组引用赋值给状态变量,才能让框架感知到数组内容已变更并触发重渲染。

delAddr 在删除确认弹窗的"确认删除"按钮点击时调用,使用 Array.splice 方法按索引删除一条地址,然后关闭弹窗。splice 方法会直接修改原数组(增删元素),在 ArkUI 中对 @State 数组调用 splice 是被框架支持的可观测变更操作。

十一、生命周期与页面主构建

11.1 生命周期方法

  aboutToAppear() {
    this.timer = setInterval(() => {
      this.breath = !this.breath;
    }, 1000);
  }

  aboutToDisappear() {
    clearInterval(this.timer);
  }

aboutToAppearaboutToDisappear 是 ArkUI 组件的两个核心生命周期回调。aboutToAppear 在组件创建后、build 执行前被调用,适合做初始化操作。这里在 aboutToAppear 中启动了一个每 1000 毫秒(1 秒)执行一次的 setInterval 定时器,定时器回调中翻转 breath 布尔值。这个每秒翻转的 breath 状态是整个应用动效的驱动源——柱状图的柱高会随之 ±5% 波动、直播角标会随之明暗闪烁、字幕就绪状态会随之呼吸明灭,形成一种"应用有生命"的活力感。

aboutToDisappear 在组件销毁前被调用,这里通过 clearInterval(this.timer) 清理定时器。这是极为重要的一步——如果不在组件销毁时清理定时器,定时器回调会继续在后台执行,不仅浪费系统资源,还可能引用已销毁的组件状态导致异常。这种"创建-清理"的对称写法是前端开发的基本素养。

11.2 页面主构建方法

  build() {
    Stack() {
      Column() {
        this.headerMain()
        Divider().strokeWidth(1).color(COLORS.line)
        Scroll() {
          Column() {
            if (this.currentTab === 0) {
              this.tabLive()
            } 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 的后序位置,因此会覆盖在主内容之上,形成弹窗浮于页面的视觉效果。

Stack 内部的第一个子元素是一个 Column 纵向容器,自上而下依次排列:头部区域(this.headerMain())、一条分隔线(Divider)、可滚动的内容区(Scroll)、底部导航栏(this.tabBar())。内容区的 Scroll 组件设置了 layoutWeight(1),使其占据头部和底部导航之间的全部剩余空间;scrollBar(BarState.Off) 隐藏了滚动条,保持界面整洁。Scroll 内部的 Column 通过 if-else if-else 条件分支根据 currentTab 的值调用对应的 Tab Builder 方法,实现页面切换——这种"条件渲染"的方式比使用 Tabs 组件更灵活,因为每个 Tab 的布局结构完全独立,不受 TabContent 的统一约束。

Stack 内部的后续三个子元素是三个弹窗面板,每个弹窗前都有对应的布尔状态做条件渲染(if (this.addModal) 等),只有当状态为 true 时弹窗才被构建并渲染。每个弹窗 Builder 接收一个 onClose 回调函数,在弹窗内部点击遮罩或取消按钮时调用该回调将对应状态置 false,从而关闭弹窗。这种"状态驱动弹窗显隐+回调关闭"的模式是 ArkUI 中实现弹窗的常见做法。

十二、头部区域构建

12.1 渐变 Banner

  @Builder
  headerMain() {
    Column({ space: 12 }) {
      Column({ space: 10 }) {
        Row() {
          Column({ space: 5 }) {
            Text('晚上好,逛播的人').fontSize(16).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
            Text('826 直播狂欢节 · 全场 5 折起 · 整点秒杀不停').fontSize(9).fontColor(COLORS.white).opacity(0.88)
              .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.redD)
          .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
        }
        .width('100%')

        Row({ space: 8 }) {
          Text('🔥 1286 个直播间热播').fontSize(9).fontColor(COLORS.white).opacity(0.95)
            .padding({ left: 8, right: 8, top: 3, bottom: 3 }).backgroundColor(COLORS.redD).borderRadius(8)
          Text('⚡ 整点秒杀 12:00 开抢').fontSize(9).fontColor(COLORS.white).opacity(0.95)
            .padding({ left: 8, right: 8, top: 3, bottom: 3 }).backgroundColor(COLORS.redD).borderRadius(8)
          Text('👑 会员专享价').fontSize(9).fontColor(COLORS.redD)
            .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.redD, 0], [COLORS.red, 0.55], [COLORS.orange, 1]] })

头部区域的第一块是渐变 Banner。它是一个 Column 容器,内部从上到下分为两部分:第一部分是一行 Row,左侧是问候语和大促 slogan 文本(“晚上好,逛播的人"和"826 直播狂欢节·全场 5 折起·整点秒杀不停”),右侧是一个 44x44 的圆形图标容器,内部放置了一个 📺 emoji,其不透明度随 breath 状态在 1 和 0.6 之间切换,形成"呼吸闪烁"的视觉效果。第二部分是一行标签 chips,分别展示"1286 个直播间热播"(红底白字)、“整点秒杀 12:00 开抢”(红底白字)、“会员专享价”(金底红字)。

整个 Banner 通过 linearGradient 设置了 135 度的线性渐变,从左上角的 COLORS.redD(深红 #C22F1D)经过 55% 处的 COLORS.red(热销红 #E8442E)过渡到右下角的 COLORS.orange(热销橙 #F0821E)。135 度角意味着渐变方向从左上到右下,与人类视觉"从左到右、从上到下"的扫描习惯一致,增强了动感和层次感。深红到橙的渐变色带传达出"热销、促销、紧迫"的视觉信号,是直播电商 Banner 的经典配色。

12.2 搜索条与品类 Chips

      Row({ space: 8 }) {
        Text('🔍').fontSize(14)
        Text('搜索宝贝 / 直播间 / 主播').fontSize(11).fontColor(COLORS.text3).layoutWeight(1)
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        Text('🎙').fontSize(14).onClick(() => {
          this.currentTab = 2;
        })
      }
      .width('100%').padding({ left: 14, right: 14, top: 10, bottom: 10 })
      .backgroundColor(COLORS.card).borderRadius(20)

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

搜索条是一个 Row 行容器,从左到右依次是放大镜 emoji(🔍)、占位提示文本(“搜索宝贝 / 直播间 / 主播”)和麦克风 emoji(🎙)。占位文本使用 COLORS.text3 浅灰色并设置 layoutWeight(1) 占据中间空间,maxLines(1)textOverflow(Ellipsis) 确保文本过长时单行省略而非折行。最右侧的麦克风 emoji 绑定了点击事件——点击后切换到 AI 字幕 Tab(this.currentTab = 2),巧妙地将"语音"图标与"AI 字幕"功能关联起来,形成跨 Tab 的导航快捷入口。

品类 chips 是一个横向滚动的 Scroll 容器,内部 Row 通过 ForEach 遍历 CATE_TAGS 数组渲染 8 个品类标签。每个标签的字体颜色和背景色根据 cateIdx === idx 判断:选中时白字红底,未选中时灰字白底。点击标签时更新 cateIdx 状态。横向滚动(scrollable(ScrollDirection.Horizontal))确保在品类数量超出屏幕宽度时用户可以左右滑动浏览,scrollBar(BarState.Off) 隐藏滚动条保持视觉简洁。整个头部区域的最外层 Column 也设置了一个 180 度的纵向渐变背景,从顶部的芯片色(COLORS.chip)渐变到底部的背景色(COLORS.bg),使头部与内容区之间有一个柔和的色彩过渡。

十三、直播间 Tab 构建

  @Builder
  tabLive() {
    Column({ space: 12 }) {
      Scroll() {
        Row({ space: 8 }) {
          ForEach(LIVE_CATS, (c: string, idx: number) => {
            Text(c).fontSize(11)
              .fontColor(this.liveCatIdx === idx ? COLORS.white : COLORS.sub)
              .padding({ left: 13, right: 13, top: 6, bottom: 6 })
              .backgroundColor(this.liveCatIdx === idx ? COLORS.red : COLORS.card)
              .borderRadius(13)
              .onClick(() => {
                this.liveCatIdx = idx;
              })
          }, (c: string) => c)
        }
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')

      Row() {
        Text('🔴 热播直播间').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text(this.filteredLives().length.toString() + ' 个直播间').fontSize(9).fontColor(COLORS.text3)
      }
      .width('100%')

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.filteredLives(), (item: LiveItem) => {
          Column({ space: 8 }) {
            Column() {
              Text(item.cover).fontSize(30)
              Column().layoutWeight(1)
              Row() {
                Text('🔴 ' + item.heat).fontSize(8).fontColor(COLORS.white)
                  .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
                  .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                  .backgroundColor(COLORS.redD).borderRadius(5)
                Column().layoutWeight(1)
                Text(item.tag).fontSize(8).fontColor(COLORS.white)
                  .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                  .backgroundColor(COLORS.orange).borderRadius(5)
              }
              .width('100%').margin({ bottom: 8 })
            }
            .width('100%').height(96).borderRadius(10)
            .linearGradient({ angle: 145, colors: [[COLORS.redD, 0], [COLORS.orange, 1]] })

            Text(item.title).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
              .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })

            Row({ space: 6 }) {
              Text('👤 ' + item.host).fontSize(9).fontColor(COLORS.sub).layoutWeight(1)
                .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
              Text(item.cat).fontSize(8).fontColor(catColor(item.cat))
                .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                .backgroundColor(COLORS.chip).borderRadius(5)
            }
            .width('100%')

            Text('▶ 进入直播间').fontSize(9).fontColor(COLORS.white)
              .width('100%').textAlign(TextAlign.Center)
              .padding({ top: 5, bottom: 5 }).backgroundColor(COLORS.red).borderRadius(8)
          }
          .width('48%').padding(10).backgroundColor(COLORS.card).borderRadius(12)
        }, (item: LiveItem) => item.title + item.tag)
      }
      .width('100%')
    }
    .width('100%')
  }

直播间 Tab 的构建分为三个区块。第一个区块是品类筛选 chips,与头部品类 chips 的实现方式一致,但数据源换成了 LIVE_CATS,状态绑定到了 liveCatIdx。选中态同样是白字红底圆角标签,点击切换筛选品类。第二个区块是标题行,左侧"热播直播间"加粗标题,右侧展示当前筛选下的直播间数量(通过 this.filteredLives().length.toString() 动态计算)。

第三个区块是核心的双列直播间卡片墙。它使用了 Flex 容器配合 FlexWrap.Wrap(换行)和 FlexAlign.SpaceBetween(两端对齐),使每个卡片宽度为 48% 时自动形成两列布局,列间距由 SpaceBetween 自动分配。每张卡片是一个 Column 容器,内部分为四个部分:

封面区是一个高度 96 的渐变色块(145 度红橙渐变),内部放置了 emoji 封面图标(字号 30,居中偏上),底部叠加了一行信息条——左侧是热度文本(红底白字小标签,如"🔴 1.2万人在看"),右侧是促销标签(橙底白字小标签,如"秒杀"、“清仓”)。封面的 145 度渐变角度比 Banner 的 135 度更倾斜,与 Banner 形成微妙的差异,避免视觉重复。

封面下方是直播间名(单行省略、加粗深色)、主播行(左侧"👤 主播名"灰色小字、右侧品类小标签使用 catColor 函数映射的品类色)、进入直播间按钮(红底白字居中全宽按钮)。整张卡片以纯白卡片底色(COLORS.card)和 12 的圆角呈现,内部 padding 为 10。ForEach 的 key 生成函数使用了 item.title + item.tag(直播间名+标签)的组合键,确保每个卡片有稳定的唯一标识。

十四、订单 Tab 构建

14.1 订单标题与票券订单卡

  @Builder
  tabOrder() {
    Column({ space: 12 }) {
      Row() {
        Text('🧾 我的直播订单').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text('近 30 天 · ' + this.orderList.length.toString() + ' 笔').fontSize(9).fontColor(COLORS.text3)
      }
      .width('100%')

      ForEach(this.orderList, (item: OrderItem) => {
        Column() {
          Row({ space: 10 }) {
            Column() {
              Text('🛍').fontSize(18)
            }
            .width(40).height(40).borderRadius(10).backgroundColor(COLORS.chip)
            .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)

            Column({ space: 4 }) {
              Text(item.goods).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
                .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
              Row({ space: 6 }) {
                Text(item.price).fontSize(13).fontColor(COLORS.red).fontWeight(FontWeight.Bold)
                Text(item.status).fontSize(8).fontColor(statusColor(item.status))
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .backgroundColor(COLORS.card).borderRadius(6)
                  .border({ width: 1, color: statusColor(item.status) })
              }
              .alignItems(VerticalAlign.Bottom)
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Start)
          }
          .width('100%').padding(12)

订单 Tab 的顶部是标题行("我的直播订单"加粗标题,右侧"近 30 天·8 笔"统计文本),下方是订单卡列表。每张订单卡采用了"两段式票券"设计——分为商品段(上半部分)和物流段(下半部分),中间通过打孔分隔行模拟票券撕口的视觉效果。

商品段是一个 Row,左侧是 40x40 的圆角方形容器,内部居中放置购物袋 emoji(🛍),底色为芯片色(COLORS.chip)。右侧是商品信息纵向容器:第一行是商品名(加粗深色、单行省略),第二行是价格和状态标签的横向排列——价格使用热销红加粗大字(字号 13),状态标签使用 statusColor 函数映射的语义色、白底加同色描边、圆角小标签。价格与状态标签通过 alignItems(VerticalAlign.Bottom) 底部对齐,视觉上更协调。

14.2 票券打孔分隔行与物流段

          Row() {
            Column().width(12).height(12).borderRadius(6).backgroundColor(COLORS.bg)
            Column().layoutWeight(1).height(1).backgroundColor(COLORS.line).margin({ left: 4, right: 4 })
            Column().width(12).height(12).borderRadius(6).backgroundColor(COLORS.bg)
          }
          .width('100%').padding({ left: 10, right: 10 })
          .backgroundColor(COLORS.card)

          Row({ space: 8 }) {
            Text('🚚').fontSize(12)
            Column({ space: 3 }) {
              Text(item.logistics).fontSize(9).fontColor(COLORS.sub)
                .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
              Text('下单时间 ' + item.time).fontSize(8).fontColor(COLORS.text3)
                .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Start)
            Text('详情 ›').fontSize(9).fontColor(COLORS.blue)
          }
          .width('100%').padding(12)
          .backgroundColor(COLORS.chip)
          .borderRadius({ bottomLeft: 11, bottomRight: 11 })
        }
        .width('100%').backgroundColor(COLORS.card).borderRadius(12)
      }, (item: OrderItem) => item.goods + item.status)

票券打孔分隔行是整个订单卡设计的点睛之笔。它是一个 Row,左右两端各放置一个 12x12 的圆形(borderRadius(6)),底色设为页面背景色(COLORS.bg),在视觉上形成两个"缺口"——仿佛票券被打了两个圆形撕孔。中间是一条 1 像素高的分隔线(COLORS.line 浅灰色),左右各留 4 的 margin 与圆孔衔接。整行的背景色设为卡片色(COLORS.card),使圆孔的背景色与页面底色一致,形成"卡片上开了两个洞"的视觉错觉。

物流段是票券的下半部分,底色改为芯片色(COLORS.chip 米色),底部圆角设为 11(与卡片整体的 12 圆角接近),使下半部分看起来像票券的副券。内容上,左侧是卡车 emoji(🚚),中间是物流信息文本(灰色小字,单行省略)和下单时间(更浅灰色更小字),右侧是"详情›"蓝色可点击链接文本。整段通过 padding(12) 保持内边距与商品段一致。

ForEach 的 key 使用了 item.goods + item.status(商品名+状态)的组合键。这种 key 设计在订单状态发生变化时(如从"待发货"变为"运输中")会生成新的 key,触发该卡片的完整重渲染,确保状态标签的颜色和文本同步更新。

14.3 月度成交额柱状图

订单 Tab 的最底部调用了 this.chartCard() 渲染月度成交额柱状图,该 Builder 的实现将在后文单独分析。

十五、AI 字幕 Tab 构建

AI 字幕 Tab 是整个应用的技术核心,完整展示了 Speech Kit 6.1.1 的四大新增字段。它由五个区块组成:组件实时预览卡、语言设置卡、外观设置卡、options 实时代码预览卡、字幕场景推荐列表。

15.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.85)
            .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
      }
      .width('100%').padding(10).borderRadius(10)
      .linearGradient({ angle: 135, colors: [[COLORS.redD, 0], [COLORS.orange, 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%')

        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.redD : COLORS.red)
            .borderRadius(10)
            .onClick(() => {
              this.captionShown = !this.captionShown;
            })

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

        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)

特性简介条是一个红橙渐变小条,左侧是 🗣 emoji,右侧是两行文本——第一行"Speech Kit·场景化语音服务"白色加粗标题,第二行"HarmonyOS 6.1.1:AI字幕支持源语言/目标语言/字体颜色/字体大小"白色半透明说明。这条简介以最精炼的文字概括了本 Tab 的技术核心。

组件实时预览卡是 AI 字幕 Tab 的第一块功能区。卡片顶部标题行右侧有一个状态标签,根据 captionReady 状态展示"已就绪"(绿色)或"初始化中"(金色),初始化中的状态还叠加了 breath 驱动的呼吸透明度变化(0.55 到 1 之间闪烁),传递出"正在加载"的动态感。

卡片中央是 AICaptionComponent 组件本体。它接收三个参数:isShown(布尔值,通过 @Link 双向绑定到 captionShown 状态,控制字幕的显示与隐藏)、controllerAICaptionController 实例,用于写入音频流)、options(调用 buildCaptionOptions() 方法实时组装的配置对象)。组件的宽度为 100%、高度 110、圆角 10、带有 1 像素的浅灰描边。当用户在下方修改语言、字号、颜色等设置时,buildCaptionOptions() 返回新的配置对象,组件的 options 参数变化触发内部重新应用配置,实现"所见即所得"的实时预览。

组件下方是两个控制按钮。左侧是"开启/隐藏字幕"切换按钮,文本随 captionShown 状态在"开启字幕"和"隐藏字幕"之间切换,背景色也随之在热销红和深红之间切换,点击翻转 captionShown 状态。右侧是"写入演示音频"按钮,底色为芯片色蓝字,旁边附带"×N"的已写入次数计数,点击调用 feedDemoAudio() 写入一帧 PCM 音频。当 captionErrMsg 非空时,卡片底部还会展示一行红色错误提示文本。

15.2 语言设置卡

      Column({ space: 10 }) {
        Text('🌐 语言设置').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

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

        Row({ space: 8 }) {
          ForEach(SRC_LANGS, (l: LangOption) => {
            Text(l.name).fontSize(11)
              .fontColor(this.srcLang === l.code ? COLORS.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.red : COLORS.chip)
              .borderRadius(10)
              .onClick(() => {
                this.switchSourceLang(l.code);
              })
          }, (l: LangOption) => l.code)
        }
        .width('100%')

        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.red : COLORS.chip)
                .borderRadius(10)
                .onClick(() => {
                  this.tgtLang = l.code;
                })
            }, (l: LangOption) => l.code)
          }
          .width('100%')
        }

        Row({ space: 6 }) {
          Circle().width(6).height(6).fill(COLORS.orange)
          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)

语言设置卡是 sourceLanguagetargetLanguage 两大新增字段的可视化配置区。卡片首先展示"源语言 sourceLanguage"标题行,右侧标注取值范围"取值 ‘zh’ | ‘en’"。下方是两个选项按钮(中文、英文),选中态为白字红底加粗,点击调用 switchSourceLang 方法切换源语言。

接着是"目标语言 targetLanguage"标题行,右侧的取值范围标注会根据源语言动态变化——中文源时显示"中文源已锁定",英文源时显示"取值 ‘zh’ | ‘en’ | ‘zh-en’"。下方的目标语言选择区域使用了条件渲染:当 srcLang === 'zh' 时,渲染一个锁定提示行(🔒 图标+“中文源锁定中文:无翻译方向,targetLanguage 固定为 zh”),不可选择;当 srcLang === 'en' 时,渲染三个目标语言选项(中文、英文、中英双语),选中态同样为白字红底加粗,点击直接设置 tgtLang 状态。

卡片底部是一个当前语言组合的摘要行——左侧一个小橙圆点,右侧是"当前组合:源 中文→目标 中文"(通过 langName 函数转换语言码为中文名)。这行摘要使用户在选择语言时始终能看到当前生效的组合,避免混淆。

15.3 外观设置卡

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

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

        Row({ space: 8 }) {
          ForEach(SIZE_OPTIONS, (s: SizeOption) => {
            Text(s.name).fontSize(11)
              .fontColor(this.captionSize === s.size ? COLORS.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.red : COLORS.chip)
              .borderRadius(10)
              .onClick(() => {
                this.captionSize = s.size;
              })
          }, (s: SizeOption) => s.name)
        }
        .width('100%')

        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.red : 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)

外观设置卡是 fontSizefontColor 两大新增字段的配置区。字体大小部分首先展示标题行(“字体大小 fontSize”,右侧标注类型名"AICaptionFontSize"),下方是四档字号选项按钮(小号、标准、大号、超大),选中态白字红底加粗,点击设置 captionSize 为对应的枚举值。

字体颜色部分同样先展示标题行(“字体颜色 fontColor”,右侧标注类型名"ResourceColor"),下方是五个圆形色块,分别对应 CAPTION_FONT_COLORS 中的五档预设色。每个色块是一个 26x26 的 Circle,填充色为对应的预设色,描边色在选中时为热销红(2 像素粗描边)、未选中时为浅灰分隔线色。点击色块设置 captionColor 为对应的色值。

色块下方是一行当前选中色的摘要行——左侧一个 10x10 的小圆点填充当前选中色,中间是色名加色值文本(如"经典白 #FFFFFF",通过 colorName 函数转换),右侧是说明文字"作用于字幕原文与译文",提示用户字体颜色同时作用于字幕的原文部分和译文部分。

15.4 options 实时代码预览卡

      Column({ space: 10 }) {
        Row() {
          Text('💻 AICaptionOptions 实时代码').fontSize(13)
            .fontColor(COLORS.title).fontWeight(FontWeight.Bold)
          Column().layoutWeight(1)
          Text('随设置联动').fontSize(8).fontColor(COLORS.red)
        }
        .width('100%')

        Column({ space: 5 }) {
          Text('AICaptionOptions = {').fontSize(9).fontColor(COLORS.text3).fontFamily('monospace')
          Row({ space: 4 }) {
            Text('● sourceLanguage:').fontSize(9).fontColor(COLORS.orange).fontFamily('monospace')
            Text("'" + this.srcLang + "'").fontSize(9).fontColor(COLORS.green).fontFamily('monospace')
          }
          .width('100%')
          Row({ space: 4 }) {
            Text('● targetLanguage:').fontSize(9).fontColor(COLORS.orange).fontFamily('monospace')
            Text("'" + this.tgtLang + "'").fontSize(9).fontColor(COLORS.green).fontFamily('monospace')
          }
          .width('100%')
          Row({ space: 4 }) {
            Text('● fontSize:').fontSize(9).fontColor(COLORS.orange).fontFamily('monospace')
            Text(sizeName(this.captionSize)).fontSize(9).fontColor(COLORS.gold).fontFamily('monospace')
          }
          .width('100%')
          Row({ space: 4 }) {
            Text('● fontColor:').fontSize(9).fontColor(COLORS.orange).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.title).borderRadius(10)
        .alignItems(HorizontalAlign.Start)

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

代码预览卡是整个 AI 字幕 Tab 的点睛之笔。它将当前四大字段的实际取值以"代码"的形式展示出来,使用等宽字体(fontFamily('monospace'))和深色背景(COLORS.title 深棕灰底色)模拟代码编辑器的视觉效果。代码块的内容是:

AICaptionOptions = {
  ● sourceLanguage: 'zh'
  ● targetLanguage: 'zh'
  ● fontSize: NORMAL
  ● fontColor: '#FFFFFF'
}

每一行的键名使用橙色高亮、字符串值使用绿色、枚举值使用金色,而 fontColor 的值则使用它自身的颜色值来渲染——即如果当前选中了"樱花粉"(#FFB3C1),那么这行代码中的色值文本就会以樱花粉色显示,形成"代码即预览"的巧妙联动。当用户在上方修改任何一个设置时,这块代码会实时更新对应行的值,使用户能够直观地看到自己的操作如何转化为传给组件的配置参数。

代码块下方还有一行说明文本"★ 6.1.1 新增字段:sourceLanguage / targetLanguage / fontSize / fontColor",以最明确的方式告知用户这四个字段是 HarmonyOS 6.1.1 版本新增的能力。

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

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

场景推荐列表是 AI 字幕 Tab 的最后一块功能区。它通过 ForEach 遍历 sceneList 数据,渲染 5 个直播字幕场景条目。每个条目是一个 Row,左侧是一个 4 像素宽、42 像素高的色条(颜色按 idx % 3 在红、橙、金三色之间轮换),中间是场景名(加粗深色)、场景描述(灰色小字)、语言组合文本(红色小字,如"源 en→目标 zh")的纵向排列,右侧是"套用›"红色可操作文本。

点击某个场景条目时,会调用 this.switchSourceLang(item.src) 切换源语言(这会联动设置目标语言的默认值),紧接着直接设置 this.tgtLang = item.tgt 覆盖为目标语言为该场景推荐的值。这种"一键套用"的设计使用户无需在语言设置卡和外观设置卡中逐项调整,只需点击一个场景即可完成语言组合的切换,极大地降低了配置成本。

十六、我的 Tab 构建

16.1 会员渐变大卡

  @Builder
  tabMine() {
    Column({ space: 12 }) {
      Column({ space: 12 }) {
        Row({ space: 12 }) {
          Column() {
            Text('🛍').fontSize(26)
          }
          .width(54).height(54).borderRadius(27).backgroundColor(COLORS.redD)
          .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)

          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Text('林晚晚').fontSize(16).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
              Text('白金会员 Lv.5').fontSize(8).fontColor(COLORS.redD)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .backgroundColor(COLORS.gold).borderRadius(7)
            }
            Text('播购购 ID:bogou_0825 · 本月已省 386 元')
              .fontSize(9).fontColor(COLORS.white).opacity(0.85)
              .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Start)
        }
        .width('100%')

        Row({ space: 10 }) {
          Column({ space: 3 }) {
            Text('12').fontSize(13).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
            Text('待收货订单').fontSize(8).fontColor(COLORS.white).opacity(0.8)
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)

          Column({ space: 3 }) {
            Text('86').fontSize(13).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
            Text('收藏宝贝').fontSize(8).fontColor(COLORS.white).opacity(0.8)
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)

          Column({ space: 3 }) {
            Text('8').fontSize(13).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
            Text('可用优惠券').fontSize(8).fontColor(COLORS.white).opacity(0.8)
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)
        }
        .width('100%').margin({ top: 2 })
      }
      .width('100%').padding(16).borderRadius(14)
      .linearGradient({ angle: 135, colors: [[COLORS.redD, 0], [COLORS.red, 0.6], [COLORS.orange, 1]] })

我的 Tab 的第一块是会员渐变大卡。整张卡片采用 135 度红橙渐变(深红→热销红→热销橙),与头部 Banner 的渐变方案一致,形成视觉呼应。卡片上半部分是用户信息行:左侧是 54x54 的圆形头像容器(深红底色,内部居中放置 🛍 emoji),右侧是用户名"林晚晚"(白色加粗大字)和会员等级标签"白金会员 Lv.5"(金底深红字小标签),下方是 ID 和本月省钱信息(白色半透明小字)。

卡片下半部分是三列数据统计行,分别是"12 待收货订单"、“86 收藏宝贝”、“8 可用优惠券”,每列数字为白色加粗、标签为白色半透明小字,三列等宽分布(layoutWeight(1))。这种"用户信息+数据统计"的组合大卡是电商个人中心的标准设计,在一张卡片内集中展示用户最关心的身份和消费概览信息。

16.2 收货地址管理

      Column({ space: 8 }) {
        Row() {
          Text('📍 收货地址').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
          Column().layoutWeight(1)
          Text(this.addrList.length.toString() + ' 个地址').fontSize(9).fontColor(COLORS.text3)
          Text('+ 新增').fontSize(9).fontColor(COLORS.white)
            .padding({ left: 9, right: 9, top: 4, bottom: 4 })
            .backgroundColor(COLORS.red).borderRadius(8)
            .onClick(() => {
              this.addModal = true;
            })
        }
        .width('100%')

        ForEach(this.addrList, (item: UserStat, idx: number) => {
          Column({ space: 6 }) {
            Row({ space: 8 }) {
              Text(item.icon).fontSize(14)
              Column({ space: 3 }) {
                Text(item.label).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
                  .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
                Text(item.value).fontSize(9).fontColor(COLORS.sub)
                  .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Start)
            }
            .width('100%')

            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.openEditAddr(idx);
                })
              Text('删除').fontSize(8).fontColor(COLORS.red)
                .layoutWeight(1).textAlign(TextAlign.Center)
                .padding({ top: 4, bottom: 4 }).backgroundColor(COLORS.chip).borderRadius(6)
                .onClick(() => {
                  this.delIdx = idx;
                  this.delModal = true;
                })
            }
            .width('100%')
          }
          .width('100%').padding(10).backgroundColor(COLORS.card).borderRadius(10)
        }, (item: UserStat) => item.label + item.value)
      }
      .width('100%')

收货地址管理区域包含标题行和地址卡片列表。标题行左侧是"收货地址"加粗标题,中间是地址数量统计,右侧是"+ 新增"红底白字按钮,点击打开新增地址弹窗(addModal = true)。

每张地址卡片是一个 Column,上半部分是地址信息行——左侧是地址类型 emoji(🏠 家、🏢 公司),右侧是收件人姓名加手机号(加粗深色,单行省略)和详细地址(灰色小字,单行省略)。下半部分是两个等宽操作按钮——“编辑”(灰色字芯片底色,点击调用 openEditAddr(idx) 打开编辑弹窗)和"删除"(红色字芯片底色,点击设置 delIdx 并打开删除确认弹窗)。两个按钮通过 layoutWeight(1) 等宽分布,textAlign(TextAlign.Center) 居中文字。

16.3 功能清单行

      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 渲染 8 个功能入口。每行是一个 Row,从左到右依次是:功能图标 emoji(字号 16)、功能名(深色正常字,layoutWeight(1) 占据中间空间)、状态/数值文本(灰色小字,单行省略)、右箭头(浅灰大字,仅当 item.arrowtrue 时渲染)。整行白底圆角卡片,padding 为 12。

item.arrow 的条件渲染体现了"可点击进入"与"纯展示"的视觉区分——有箭头的行表示点击后会进入二级页面,无箭头的行(如收货地址行)则只做信息展示。虽然当前所有功能清单条目的 arrow 均为 true,但这个条件判断保留了未来某些条目不需要箭头时的灵活性。

十七、图表卡与底部导航

17.1 月度成交额柱状图

  @Builder
  chartCard() {
    Column({ space: 10 }) {
      Row() {
        Text('📊 月度直播成交额').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text('单位:元').fontSize(9).fontColor(COLORS.text3)
      }
      .width('100%')

      Row({ space: 8 }) {
        ForEach(MONTH_IDX, (i: number) => {
          Column({ space: 5 }) {
            Text(MONTH_AMOUNTS[i].toString()).fontSize(8)
              .fontColor(this.breath ? COLORS.red : COLORS.sub)
            Column().width(18)
              .height(Math.max(20, MONTH_AMOUNTS[i] / MONTH_MAX * 110 * (this.breath ? 1.05 : 0.95)))
              .borderRadius(5)
              .linearGradient({ angle: 180, colors: [[COLORS.orange, 0], [COLORS.red, 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 月累计成交 2861 元').fontSize(8).fontColor(COLORS.sub)
        Column().layoutWeight(1)
        Text('环比 +24.7%').fontSize(8).fontColor(COLORS.red)
      }
      .width('100%')
    }
    .width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)
  }

图表卡是一个纯 ArkUI 组件手绘的柱状图,没有使用任何图表三方库。卡片顶部是标题行("月度直播成交额"加粗标题,右侧"单位:元"说明),中央是 6 根柱子横向排列的图表区域,底部是摘要行("近 6 月累计成交 2861 元"灰色小字,"环比 +24.7%"红色小字)。

每根柱子是一个 Column 容器,自上而下包含三部分:数值文本(字号 8,颜色随 breath 状态在红色和灰色之间切换)、柱体(Column 组件,宽度 18,高度通过公式计算)、月份标签(字号 8,浅灰色)。柱体的高度计算公式为 Math.max(20, MONTH_AMOUNTS[i] / MONTH_MAX * 110 * (this.breath ? 1.05 : 0.95))——先计算基准高度(金额 / 满量程 * 110),再乘以 breath 驱动的波动系数(1.05 或 0.95,即 ±5%),最后用 Math.max(20, ...) 确保最小高度为 20,避免金额过小时柱体不可见。

柱体使用了 180 度纵向渐变(从顶部的橙色到底部的红色),borderRadius(5) 给柱顶圆角。整个图表行设置了 alignItems(VerticalAlign.Bottom) 使所有柱子底部对齐,height(150) 固定图表区域高度。ForEach 的 key 使用 'm' + i.toString()(如"m0"、“m1”)确保每根柱子有稳定的唯一标识。

breath 状态每秒翻转一次,使柱体高度在 ±5% 范围内波动、数值文本颜色在红灰之间切换,形成一种"数据在呼吸"的动态视觉效果。这种动效虽然不改变实际数据,但赋予了静态图表一种活力感,使页面不会显得死板。

17.2 底部导航栏

  @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 容器,通过 ForEach 遍历 TAB_LIST 渲染 4 个 Tab 项。每个 Tab 项是一个 Column,上方是 emoji 图标,下方是标签文本。选中态(currentTab === idx)的视觉反馈有三处:图标字号从 17 放大到 20、不透明度从 0.65 提升到 1、标签文字颜色从浅灰(COLORS.text3)变为热销红(COLORS.tabOn)并加粗。这种"放大+提亮+变色"的三重反馈使选中态一目了然。

每个 Tab 项设置 layoutWeight(1) 等宽分布,padding({ top: 7, bottom: 7 }) 保证点击热区有足够的上下边距。点击事件直接设置 this.currentTab = idx 切换 Tab。整个导航栏白底,顶部有 1 像素的分隔线(border({ width: { top: 1 }, color: COLORS.line })),与内容区做视觉分隔。ForEach 的 key 使用 t.label(Tab 标签名),由于四个 Tab 的标签名各不相同,这个 key 是稳定的。

十八、弹窗系统

18.1 全屏遮罩

  @Builder
  modalOverlay(onClose: () => void) {
    Stack() {
      Column().width('100%').height('100%').backgroundColor(COLORS.mask)
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Center)
    .onClick(() => onClose())
  }

modalOverlay 是所有弹窗共享的全屏遮罩 Builder。它是一个 Stack 容器,内部填充一个 100% 宽高的 Column,背景色为 COLORS.mask(半透明深棕灰,rgba(51,42,36,0.5)),使背景内容被压暗但仍然可见。整个 Stack 设置了 onClick(() => onClose()),即用户点击遮罩区域时调用 onClose 回调关闭弹窗——这是弹窗交互的标准模式,允许用户通过点击弹窗外部快速关闭而不必寻找关闭按钮。

StackalignContent(Alignment.Center) 使后续叠加在上面的弹窗面板内容居中显示。这个遮罩 Builder 被三个弹窗面板(新增、编辑、删除)复用,体现了 Builder 函数的代码复用能力。

18.2 新增地址弹窗

  @Builder
  panelAdd(onClose: () => void) {
    Stack() {
      this.modalOverlay(onClose)
      Column({ space: 12 }) {
        Text('新增收货地址').fontSize(15).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

        Column({ space: 6 }) {
          Text('收件人姓名 + 手机号').fontSize(9).fontColor(COLORS.sub)
          TextInput({ text: this.formName, placeholder: '如:张三 138****5678' })
            .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.formAddr, placeholder: '如:省市区 + 街道门牌号' })
            .fontSize(11).fontColor(COLORS.title)
            .backgroundColor(COLORS.chip).borderRadius(8)
            .onChange((value: string) => {
              this.formAddr = 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.red).borderRadius(9)
            .onClick(() => {
              this.saveAddr();
            })
        }
        .width('100%')
      }
      .width('78%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Center)
  }

新增地址弹窗面板是一个 Stack,内部先叠加 modalOverlay 遮罩,再叠加弹窗内容 Column。弹窗内容宽度为 78%(使两侧露出遮罩,提示用户可以点击遮罩关闭),白色卡片底色,14 圆角,16 padding。

弹窗内部从上到下依次是:标题"新增收货地址"(加粗深色大字)、收件人输入区(标题+TextInput)、详细地址输入区(标题+TextInput)、操作按钮行(取消+保存)。两个 TextInput 组件分别绑定 formNameformAddr 状态,onChange 回调将输入值同步到状态中。TextInputtext 参数接收当前状态值(而非 placeholder),使得状态与输入框内容始终同步;placeholder 在输入为空时显示提示文案。

操作按钮行中,"取消"按钮为灰字芯片底色,点击调用 onClose 关闭弹窗;"保存"按钮为白字红底加粗,点击调用 saveAddr 方法保存地址。两个按钮通过 layoutWeight(1) 等宽分布。

18.3 编辑与删除地址弹窗

  @Builder
  panelEdit(onClose: () => void) {
    Stack() {
      this.modalOverlay(onClose)
      Column({ space: 12 }) {
        Text('编辑收货地址').fontSize(15).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

        Column({ space: 6 }) {
          Text('收件人姓名 + 手机号').fontSize(9).fontColor(COLORS.sub)
          TextInput({ text: this.editName, placeholder: '收件人姓名 + 手机号' })
            .fontSize(11).fontColor(COLORS.title)
            .backgroundColor(COLORS.chip).borderRadius(8)
            .onChange((value: string) => {
              this.editName = value;
            })
        }
        .width('100%').alignItems(HorizontalAlign.Start)

        Column({ space: 6 }) {
          Text('详细地址').fontSize(9).fontColor(COLORS.sub)
          TextInput({ text: this.editAddr, placeholder: '详细地址' })
            .fontSize(11).fontColor(COLORS.title)
            .backgroundColor(COLORS.chip).borderRadius(8)
            .onChange((value: string) => {
              this.editAddr = 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.orange).borderRadius(9)
            .onClick(() => {
              this.updateAddr();
            })
        }
        .width('100%')
      }
      .width('78%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Center)
  }

编辑地址弹窗的结构与新增弹窗几乎完全一致,差异在于:标题改为"编辑收货地址"、输入框绑定的是 editNameeditAddr 状态(在 openEditAddr 方法中被回填了当前地址的原有值)、保存按钮的文案改为"保存修改"且背景色改为橙色(COLORS.orange,与新增的红色按钮做视觉区分)、点击调用 updateAddr 方法。这种"同结构不同状态绑定"的复用方式是 Builder 模式的优势——视觉结构一致,仅数据源和回调不同。

  @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.delAddr();
            })
        }
        .width('100%')
      }
      .width('78%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Center)
  }
}

删除确认弹窗相比新增和编辑更为简洁——只有标题、警告文案和操作按钮行,没有输入框。警告文案"确认删除该收货地址吗?删除后已下单未发货的包裹将无法送达,且不可恢复。“使用了较长的描述,maxLines(2) 允许两行折行展示。操作按钮的文案为"取消"和"确认删除”,确认删除按钮为红底白字加粗,点击调用 delAddr 方法执行删除。这种二次确认的交互模式是破坏性操作的标准设计,避免用户误触导致数据不可逆丢失。

至此,整个组件的 build 方法和所有 @Builder 函数已全部分析完毕。三个弹窗面板(新增、编辑、删除)共享 modalOverlay 遮罩,在 buildStack 中按条件渲染叠加在主内容之上,形成完整的弹窗交互闭环。

十九、技术特性对比

特性维度 传统字幕方案 Speech Kit AICaptionComponent(6.1.1)
语言支持 仅源语言转写,无翻译能力 sourceLanguage + targetLanguage 双语联动,支持 zh/en/zh-en 三种目标模式
字体大小 固定字号或需自定义渲染 fontSize 提供 SMALL/NORMAL/BIG/LARGE 四档枚举,开箱即用
字体颜色 需自行处理文字渲染与着色 fontColor 支持 ResourceColor,任意颜色值直接生效
音频输入 需自行对接语音识别 SDK AICaptionController.writeAudio 直接接收 AudioData,内置识别引擎
渲染方式 需自行实现字幕 UI 组件 AICaptionComponent 开箱即用的可视化组件,isShown 控制显隐
错误处理 需自行捕获各环节异常 onError 回调统一返回 BusinessError,code + message 标准化
就绪感知 无标准化的初始化回调 onPrepared 回调精确告知字幕服务就绪时机
双语对照 需自行拼接原文译文 targetLanguage = ‘zh-en’ 自动输出双语对照字幕
状态驱动 命令式调用,需手动刷新 UI options 配置变化自动触发组件重新应用配置,声明式响应
集成成本 需引入三方 ASR/TTS/翻译服务 系统级 Kit,无需引入三方依赖,鸿蒙设备原生支持

二十、总结

本文完整剖析了一款基于 HarmonyOS ArkUI 框架开发的"播购购·直播带货平台"应用的全部源码。从技术栈角度看,这款应用集中展现了 ArkUI 声明式 UI 范式在复杂电商场景下的落地实践——通过 @State 状态驱动、@Builder 函数式构建、@Observed 数据观察三大核心装饰器,构建出一个包含四个差异化 Tab 页面、三个弹窗面板、一套柱状图图表和完整的 AI 字幕特性展示的完整应用,全程未引入任何三方 UI 库或图表库,纯 ArkUI 原生组件即实现了全部视觉效果。

在颜色系统设计上,应用采用了"接口约束+常量实例化"的工程化方案。ColorPalette 接口定义了 16 个颜色字段的类型约束,COLORS 常量填入了浅色主题的具体色值。活力白(#F8F7F5)作为主背景色营造柔和明亮的购物氛围,热销红橙(#E8442E + #F0821E)渐变作为核心视觉语言传递促销紧迫感,暖灰三级文字色保持与背景的色温一致,绿蓝金辅助色分别承载签收、运输、会员等语义状态。这套色彩体系既统一又有层次,使应用在视觉上既不过于花哨也不显得单调。

Speech Kit 6.1.1 的 AICaptionComponent 是本文的技术核心。四大新增字段——sourceLanguage(源语言,‘zh’/‘en’)、targetLanguage(目标语言,‘zh’/‘en’/‘zh-en’)、fontSize(字号,AICaptionFontSize 四档枚举)、fontColor(字体颜色,ResourceColor)——在 AI 字幕 Tab 中被完整地以可视化方式呈现给用户。用户可以通过语言设置卡配置源语言和目标语言(中文源时自动锁定目标语言为中文,英文源时可选择中文、英文或中英双语),通过外观设置卡选择四档字号和五档预设色,所有配置变化实时反映在 AICaptionOptions 代码预览块中,并通过 buildCaptionOptions() 方法实时组装为新的配置对象传给 AICaptionComponent,实现"所见即所得"的字幕预览体验。

应用的动效设计也值得称道。一个每秒翻转的 breath 布尔状态作为全局动效驱动源,同时驱动了柱状图柱高的 ±5% 波动、数值文本颜色的红灰切换、字幕就绪状态的透明度呼吸、直播角标的明暗闪烁等多处动效。这种"单状态多消费"的设计以极低的代码量实现了丰富的动态视觉效果,使应用始终保持一种"有生命"的活力感,避免了静态页面的沉闷。同时,aboutToAppear 启动定时器、aboutToDisappear 清理定时器的对称写法,确保了组件销毁时资源不泄漏。

弹窗系统的设计同样体现了工程化思维。modalOverlay 作为共享的全屏遮罩 Builder,被新增、编辑、删除三个弹窗面板复用,避免了遮罩代码的重复编写。三个弹窗面板在结构上高度一致(标题+表单/文案+操作按钮行),仅在数据源绑定和回调方法上有差异,通过 @Builder 函数参数化 onClose 回调实现了弹窗关闭逻辑的灵活注入。条件渲染(if (this.addModal))使弹窗仅在需要时才被构建,避免了不必要的组件开销。地址列表的增删改操作通过 pushspliceslice 等数组方法配合 @State 的可观测性实现 UI 自动刷新,其中 slice 创建新数组引用的技巧解决了直接修改数组元素属性不触发重渲染的问题。

从业务完整性角度看,这款应用虽然使用 Mock 数据,但在功能维度上覆盖了直播电商的核心链路——直播间浏览(双列卡片墙+品类筛选)、订单管理(票券订单卡+月度成交额图表)、AI 字幕(Speech Kit 四大字段可视化配置+场景推荐)、个人中心(会员大卡+地址管理+功能清单)。每一个功能区块都做到了信息密度适中、交互路径清晰、视觉层次分明,具备作为真实产品原型的可用性。尤其是 AI 字幕 Tab 的"代码预览联动"设计——将四大字段的当前取值以代码形式实时展示,且 fontColor 行使用自身颜色值渲染——堪称技术展示页面的设计范本,既教育了用户每个字段的作用和取值范围,又提供了直观的实时反馈。

总而言之,这份源码不仅是一份直播电商应用的 UI 实现,更是一份 HarmonyOS ArkUI 声明式开发范式的实践教材,以及 Speech Kit 6.1.1 新特性落地的参考范例。它展示了如何用纯原生组件构建复杂电商界面、如何将系统级 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 将自动执行以下操作:

  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、测试、元服务和应用上架分发等。

更多推荐