HarmonyOS 6 onApply: (idx: number) => void = (idx: number) => {}是一个回调函数属性子组件通过调用回调函数,将事件和数据传递给父组件处理
引言:鸿蒙生态下的ArkTS组件化开发实践
鸿蒙操作系统与ArkTS语言的技术背景
鸿蒙操作系统(HarmonyOS)是华为推出的面向全场景的分布式操作系统,其核心设计理念是"一次开发,多端部署"。在鸿蒙的开发体系中,ArkTS作为首选的应用开发语言,承载了连接UI描述与业务逻辑的关键使命。ArkTS基于TypeScript进行了扩展,引入了声明式UI范式、状态管理机制、自定义组件等一系列核心特性,使得开发者能够以更加直观、高效的方式构建跨设备的用户界面。
声明式UI是ArkTS最核心的特性之一。与传统的命令式编程不同,声明式UI允许开发者描述"界面应该是什么样子",而不是一步步告诉系统"如何构建界面"。在ArkTS中,开发者通过build()方法描述UI结构,框架会自动负责底层的渲染、更新和优化工作。这种范式的转变极大地提升了开发效率,同时也降低了UI状态不一致的风险。
声明式UI的核心哲学是:UI = f(State)。给定一个状态,UI的呈现就是确定的。当状态发生变化时,框架自动重新计算并更新UI。
组件化开发是鸿蒙ArkTS开发的另一大基石。在ArkTS中,一切皆组件——从最简单的Text文本组件,到复杂的页面结构,都可以抽象为一个@Component装饰的结构体。这种组件化思想带来了诸多优势:代码复用性高、职责单一明确、便于单元测试、易于团队协作。一个设计良好的组件应该是高内聚、低耦合的,它只关注自身的渲染逻辑和状态管理,通过属性参数和回调函数与外部世界交互。
状态管理是ArkTS开发中不可回避的核心话题。鸿蒙提供了多层次的状态管理装饰器:@State用于组件内部状态,@Prop用于父子组件间的单向数据传递,@Link用于双向数据绑定,@Provide/@Consume用于跨层级的状态共享,@Observed/@ObjectLink用于嵌套对象的观察。合理选择和使用这些状态管理机制,直接决定了应用的性能表现和可维护性。
布局系统是UI框架的基础能力。ArkTS提供了丰富的布局容器:Column用于垂直方向的线性布局,Row用于水平方向的线性布局,Stack用于层叠布局,Flex用于弹性布局,List/Grid用于滚动列表和网格布局。每种布局容器都有其特定的使用场景和性能特点,开发者需要根据界面结构灵活选择。
本文将以一个完整的配音社区应用"声临其境 VOICE LAB"为案例,从数据结构、组件设计、状态管理、布局实现、动画效果等多个维度,深入剖析ArkTS开发的技术细节和最佳实践。
应用整体架构概览
"声临其境 VOICE LAB"是一个面向声优和配音爱好者的社区平台,涵盖了试音接单、声优榜单、剧本浏览、作品管理、声音收藏、个人中心等核心功能模块。整个应用采用经典的底部Tab导航架构,共包含6个主功能Tab和6个弹框组件,形成了一个功能完整、交互丰富的移动端应用。
从架构图中可以清晰地看到,整个应用以Index组件为核心枢纽,向上管理6个Tab子组件的切换显示,向下管理6个弹框组件的弹出与关闭。这种集中式的状态管理模式使得应用的数据流更加清晰可追踪。
数据层设计:接口定义与静态数据
TypeScript接口在ArkTS中的应用
在开始构建UI之前,首先需要定义清晰的数据模型。TypeScript的接口(Interface)机制为我们提供了强大的类型检查能力,确保数据在传递和使用过程中始终符合预期的结构。
interface AuditT {
id: number
role: string
project: string
pay: string
words: string
deadline: string
style: string
color: string
}

AuditT接口定义了试音项目的数据结构。其中id作为唯一标识符,类型为number,用于在列表操作中精确匹配特定条目。role字段存储角色名称,如"冷淡系男主"、“元气少女"等,直接展示给用户。project字段表示所属项目名称,通常是广播剧、有声书等作品的标题。pay字段以字符串形式存储报酬信息,格式如"¥45/千字”,选择字符串类型而非数值类型是因为这里包含了货币符号和单位,更适合直接展示。words字段描述字数规模,如"约 1.2 万字"。deadline字段记录截止日期,采用月-日格式。style字段描述声音风格标签,如"低音·克制"。color字段则存储十六进制颜色值,用于卡片背景色的个性化展示。
为每个数据实体定义明确的接口是TypeScript/ArkTS开发的最佳实践。它不仅提供了编译时的类型检查,还能作为文档帮助团队成员理解数据结构,减少沟通成本。
interface RankT {
id: number
name: string
emoji: string
score: number
delta: number
works: number
color: string
}
RankT接口定义了声优榜单条目。name是声优昵称,emoji是头像表情符号,这种用emoji代替真实头像的设计在Demo或原型阶段非常常见,既美观又省事。score是积分,类型为number,用于排序和展示。delta字段非常有意思,它表示排名变化量——正数表示上升,负数表示下降,零表示持平。这个字段驱动了榜单中升降箭头的显示逻辑。works字段记录作品数量。color同样用于头像背景色。
interface ScriptT {
id: number
title: string
genre: string
chapters: number
roles: number
level: number
hot: string
color: string
}

ScriptT接口定义了剧本数据模型。title是剧本标题,genre是类型标签如"都市·悬疑"。chapters和roles都是数值类型,分别表示章节数和角色数。level字段是难度等级,取值为1到5的整数,对应五颗星的星级显示。hot字段以字符串形式存储热度信息,如"8.2 万人在读",选择字符串是因为它包含了单位和描述性文字。
interface VoiceT {
id: number
title: string
role: string
duration: string
status: string
likes: number
date: string
}
VoiceT接口定义了配音作品的数据结构。duration表示时长,如"04分12秒"。status字段是作品状态,有"已发布"、“审核中”、"草稿"三种取值,不同状态对应不同的显示样式和操作权限。likes是点赞数,数值类型,便于计算和格式化。date是发布日期。
interface SoundT {
id: number
title: string
author: string
duration: string
color: string
}
SoundT接口用于收藏的声音条目。相比VoiceT,它的字段更简洁——只有标题、作者、时长和颜色。这是因为收藏页面不需要展示状态、点赞数等管理属性,只需要最核心的展示信息。
接口设计的一个重要原则是:不同场景使用不同的数据模型。不要试图用一个"万能接口"满足所有需求,那样会导致字段冗余和语义模糊。为每个使用场景定义精准的数据接口,能够让代码更加清晰、更加类型安全。
静态数据的组织与设计
const AUDIT_LIST: Array<AuditT> = [
{ id: 1, role: '冷淡系男主', project: '广播剧《雪落无声》', pay: '¥45/千字', words: '约 1.2 万字', deadline: '09-02', style: '低音 · 克制', color: '#F8BBD0' },
{ id: 2, role: '元气少女', project: '动态漫《追风日记》', pay: '¥38/千字', words: '约 0.8 万字', deadline: '08-31', style: '明亮 · 活泼', color: '#FFCDD2' },
// ... 更多条目
]

AUDIT_LIST是一个类型为Array<AuditT>的常量数组,包含了8条试音项目数据。每条数据都精心设计了不同的角色类型、项目背景、报酬水平和颜色主题,模拟了真实平台上多样化的试音机会。使用const声明表明这是不可变的静态数据,在运行时不会被修改。
const RANK_LIST: Array<RankT> = [
{ id: 1, name: '清枕', emoji: '🎙️', score: 9862, delta: 2, works: 328, color: '#F8BBD0' },
{ id: 2, name: '苏打气泡音', emoji: '🫧', score: 9415, delta: 0, works: 291, color: '#E1BEE7' },
// ... 更多条目
]
RANK_LIST包含8名声优的榜单数据。值得注意的是delta字段的设计——有正有负有零,分别对应排名上升、持平、下降三种状态。这种设计使得UI层可以通过简单的条件判断展示不同的箭头图标和颜色。
const SCRIPT_LIST: Array<ScriptT> = [
{ id: 1, title: '雪落无声', genre: '都市 · 悬疑', chapters: 12, roles: 6, level: 4, hot: '8.2 万人在读', color: '#F8BBD0' },
// ... 更多条目
]
SCRIPT_LIST的8条剧本数据覆盖了都市、校园、科幻、古风、搞笑、美食、赛博等多种题材,level从2到5不等,展示了难度分级的设计思路。
const FANS_WEEK: Array<string> = ['一', '二', '三', '四', '五', '六', '日']
const FANS_WEEK_VAL: Array<number> = [120, 260, 180, 340, 210, 520, 380]
这两个数组用于粉丝增长柱状图。FANS_WEEK存储星期标签,FANS_WEEK_VAL存储对应的粉丝增长数值。两个数组通过索引一一对应,形成了图表的数据源。这种平行数组(Parallel Array)的设计在简单图表场景中非常常见,实现简单且易于理解。
const GENRE_CHIPS: Array<string> = ['全部', '都市', '校园', '科幻', '古风', '搞笑', '美食', '赛博']
GENRE_CHIPS是剧本分类的标签数组,用于横向滚动的筛选栏。"全部"作为第一个选项,是分类筛选的常见设计模式。
const TIP_LIST: Array<string> = [
'试音前请确保环境安静,避免喷麦',
'干声上传后甲方可自行后期处理',
// ... 更多贴士
]
TIP_LIST是新手贴士数组,这类提示性内容通常以数组形式存储,便于循环渲染。
纯函数工具:数字格式化
function fmtK(n: number): string {
if (n >= 10000) {
return (n / 10000).toFixed(1) + '万'
}
return n.toString()
}

fmtK函数是一个典型的纯函数——给定相同的输入永远产生相同的输出,没有任何副作用。它的功能是将大数字格式化为以"万"为单位的字符串。当数值大于等于10000时,除以10000并保留一位小数,然后追加"万"字;否则直接返回数字的字符串形式。
这个函数在代码中被多次调用,用于展示积分、粉丝数等可能很大的数值。将格式化逻辑抽取为独立的纯函数,是代码复用和关注点分离的经典实践。所有需要展示数字的地方都可以调用这个函数,确保了格式的一致性。
纯函数是函数式编程的核心概念。纯函数具有可预测性、可测试性、可缓存性等诸多优点。在UI开发中,将数据格式化、数据转换等逻辑封装为纯函数,能够显著提升代码质量。
试音Tab:AuditionTab组件深度解析
组件结构与状态定义
@Component
struct AuditionTab {
@State applied: Array<number> = []
onApply: (idx: number) => void = (idx: number) => {}
AuditionTab使用@Component装饰器标记为一个自定义组件。结构体名称遵循大驼峰命名法,这是ArkTS组件的命名规范。
@State applied: Array<number> = []定义了一个组件内部状态。applied是一个数字数组,存储了用户已经投递过的试音项目ID。初始值为空数组,表示用户尚未投递任何试音。@State装饰器告诉框架:这个变量是响应式的,当它发生变化时,所有引用它的UI都会自动更新。
onApply: (idx: number) => void = (idx: number) => {}是一个回调函数属性。它的类型是一个接受number参数、返回void的函数。默认值是一个空函数,这是一种防御性编程——即使父组件没有传入这个回调,调用它也不会报错。这种设计模式实现了子组件向父组件的通信:子组件通过调用回调函数,将事件和数据传递给父组件处理。
父子组件通信是组件化开发的核心模式。父传子通过属性参数(Props),子传父通过回调函数(Events)。这种单向数据流的设计使得组件间的依赖关系清晰明确。
外层滚动容器
build() {
Scroll() {
Column() {
// ... 内容
}
.padding({ left: 12, right: 12, top: 12, bottom: 16 })
}
.scrollBar(BarState.Off)
.width('100%')
.layoutWeight(1)
}

build()方法是组件的核心,它返回组件的UI描述。最外层使用Scroll组件包裹Column,使得整个页面内容可以垂直滚动。
Scroll组件的.scrollBar(BarState.Off)方法关闭了滚动条显示,这在移动端应用中很常见——用户通过滑动手势来滚动,不需要显示滚动条。
.width('100%')设置宽度占满父容器。.layoutWeight(1)是一个重要的布局属性,它表示该组件在弹性布局中占据剩余空间的权重。在父容器是Column且存在固定高度的兄弟组件时,layoutWeight(1)会让当前组件填满剩余的垂直空间。
内部的Column设置了四边的内边距(padding),左右各12像素,顶部12像素,底部16像素。这种边距设计为内容区域提供了舒适的呼吸空间,避免内容紧贴屏幕边缘。
横向滚动的"进行中的试音"卡片
Text('进行中的试音')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2430')
.width('100%')
.margin({ bottom: 10 })

首先是一个标题文本。.fontSize(15)设置字号为15像素,.fontWeight(FontWeight.Bold)设置为粗体。.fontColor('#3A2430')设置文字颜色为深棕紫色,这是整个应用的主文字色。.width('100%')让文本占满整行,.margin({ bottom: 10 })在底部留出10像素的外边距,与下方内容分隔。
Scroll() {
Row({ space: 10 }) {
ForEach(AUDIT_LIST, (a: AuditT, i: number) => {
if (i < 4) {
Column() {
Text(a.role)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text(a.project)
.fontSize(9)
.fontColor('rgba(255,255,255,0.85)')
.margin({ top: 5 })
.maxLines(1)
Text(a.pay + ' · 截止 ' + a.deadline)
.fontSize(9)
.fontColor('#FFFFFF')
.margin({ top: 5 })
Text(this.applied.indexOf(a.id) >= 0 ? '已投递' : '急招')
.fontSize(9)
.fontColor('#E91E63')
.fontWeight(FontWeight.Medium)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor('#FFFFFF')
.margin({ top: 8 })
}
.width(150)
.padding(12)
.borderRadius(16)
.backgroundColor(a.color)
.alignItems(HorizontalAlign.Start)
}
})
}
.padding({ left: 2, right: 2 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ bottom: 16 })

这是一个横向滚动的卡片列表,是本组件的亮点之一。
外层又是一个Scroll组件,但这次设置了.scrollable(ScrollDirection.Horizontal),使其变为水平滚动方向。.scrollBar(BarState.Off)同样隐藏滚动条。
内部的Row({ space: 10 })使用了space参数,设置子元素之间的间距为10像素。这比为每个子元素单独设置margin更加简洁。
ForEach(AUDIT_LIST, (a: AuditT, i: number) => { ... })是循环渲染的核心语法。它接受两个参数:第一个是要遍历的数组,第二个是渲染函数,函数参数为当前元素a和索引i。ForEach会为数组中的每个元素执行渲染函数,并将结果依次添加到父容器中。
if (i < 4)这个条件判断非常关键——它只渲染前4个元素。也就是说,虽然AUDIT_LIST有8条数据,但"进行中的试音"这个横向滚动区域只展示前4条。这是一种常见的UI设计模式:在页面顶部展示精选/热门内容,完整列表在下方展示。
每个卡片是一个Column,固定宽度150像素,内边距12像素,圆角16像素。背景色使用a.color,即数据中定义的颜色值,每张卡片有不同的主题色。.alignItems(HorizontalAlign.Start)设置子元素左对齐。
卡片内部有四个文本元素:
- 角色名:14号粗体白字,最醒目
- 项目名:9号半透明白字(85%不透明度),上方5像素间距,最多显示1行(超出省略)
- 报酬和截止日期:9号白字,上方5像素间距
- 状态标签:“已投递"或"急招”,9号粉色粗体字,白色背景,圆角8像素,内边距各方向不同
状态标签的文本通过三元表达式动态决定:this.applied.indexOf(a.id) >= 0 ? '已投递' : '急招'。它检查当前项目的ID是否存在于applied数组中——如果存在,说明用户已经投递过,显示"已投递";否则显示"急招"。这是响应式状态的典型应用:当applied数组变化时,所有相关标签的文本会自动更新。
.maxLines(1)是一个重要的文本属性,它限制文本最多显示1行,超出部分会以省略号结尾。在卡片布局中,这个属性可以防止长文本撑破布局。
ForEach是ArkTS中列表渲染的核心机制。它不是简单的循环,而是框架内部做了大量优化——包括虚拟滚动、差异更新等。对于大数据量列表,建议使用List组件配合ForEach以获得更好的性能。
"全部角色"列表头
Row() {
Text('全部角色')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2430')
.layoutWeight(1)
Text('今日新增 12 个')
.fontSize(10)
.fontColor('#E91E63')
}
.width('100%')
.margin({ bottom: 10 })
这是完整列表的标题行,使用Row水平排列两个文本。
第一个文本"全部角色"设置了.layoutWeight(1),这意味着它会占据Row中除了第二个文本宽度之外的所有剩余空间。效果就是"全部角色"左对齐,"今日新增 12 个"右对齐。这是Row布局中实现两端对齐的常用技巧。
"今日新增 12 个"使用粉色字体,10号字,作为辅助信息吸引用户注意。
试音列表项详解
Column() {
ForEach(AUDIT_LIST, (a: AuditT, i: number) => {
Column() {
// ... 列表项内容
}
.width('100%')
.padding(12)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.margin({ bottom: 8 })
})
}
.width('100%')
完整的试音列表使用ForEach遍历全部8条数据。每个列表项是一个白色背景的Column,宽度100%,内边距12像素,圆角14像素,底部外边距8像素。白色卡片加圆角加阴影(这里通过背景色和间距形成视觉层次)是移动端UI设计的经典模式。
Row() {
Text(a.role)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2430')
Text(a.style)
.fontSize(9)
.fontColor('#E91E63')
.padding({ left: 7, right: 7, top: 2, bottom: 2 })
.borderRadius(7)
.backgroundColor('#FDE4EF')
.margin({ left: 8 })
}

列表项第一行是角色名和风格标签。角色名使用14号粗体深色字,是主要信息。风格标签是一个粉色背景(#FDE4EF)的小标签,粉色文字,圆角7像素,内边距精心调整,左边距8像素与角色名分隔。这种标签式设计能够快速传达分类信息。
Text(a.project)
.fontSize(11)
.fontColor('#9A8794')
.margin({ top: 6 })
第二行是项目名称,11号字,浅紫色(#9A8794),顶部6像素间距。次要信息使用较小字号和较浅颜色是视觉层次设计的基本原则。
Row() {
Text('💰 ' + a.pay)
.fontSize(11)
.fontColor('#E91E63')
.fontWeight(FontWeight.Medium)
Text('📝 ' + a.words)
.fontSize(10)
.fontColor('#9A8794')
.margin({ left: 10 })
Text('⏰ 截止 ' + a.deadline)
.fontSize(10)
.fontColor('#FF7043')
.margin({ left: 10 })
}
.margin({ top: 8 })
第三行是三个关键信息标签,使用emoji图标增强视觉识别度:
- 报酬信息:钱袋emoji + 报酬金额,粉色粗体,最醒目(因为报酬是声优最关心的信息)
- 字数信息:纸笔emoji + 字数,浅灰色,次要
- 截止时间:闹钟emoji + 截止日期,橙色(
#FF7043),警示色提醒截止时间
三个标签水平排列,通过左边距分隔。这种信息密集型的行布局需要仔细权衡各元素的视觉权重。
Row() {
Text(i < 3 ? '🔥 热门' : '⭐ 推荐')
.fontSize(9)
.fontColor('#9A8794')
.layoutWeight(1)
Text(this.applied.indexOf(a.id) >= 0 ? '已投递' : '立即试音')
.fontSize(11)
.fontWeight(FontWeight.Medium)
.fontColor(this.applied.indexOf(a.id) >= 0 ? '#9A8794' : '#FFFFFF')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(this.applied.indexOf(a.id) >= 0 ? '#F3EDF1' : '#E91E63')
.onClick(() => {
this.onApply(a.id)
})
}
.width('100%')
.margin({ top: 10 })
第四行是操作区,左侧是标签,右侧是按钮。
左侧的标签根据索引判断:前3个显示"🔥 热门",其余显示"⭐ 推荐"。这是一种简单的推荐标记策略,通过位置来区分热门程度。.layoutWeight(1)让这个标签占据左侧剩余空间,实现右侧按钮的右对齐。
右侧的"立即试音"/"已投递"按钮是整个列表项的交互核心。按钮的样式完全由this.applied.indexOf(a.id) >= 0这个条件决定——如果已投递,按钮变为灰色背景(#F3EDF1)+ 灰色文字(#9A8794)的禁用样式;如果未投递,则是粉色背景(#E91E63)+ 白色文字的激活样式。
.onClick(() => { this.onApply(a.id) })绑定了点击事件。点击时调用父组件传入的onApply回调函数,并将当前项目的ID作为参数传递出去。这样,投递的具体逻辑由父组件决定,子组件只负责触发事件和传递数据,体现了单一职责原则。
新手贴士区域
Column() {
Text('新手贴士')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2430')
.width('100%')
.margin({ bottom: 8 })
ForEach(TIP_LIST, (t: string) => {
Row() {
Text('·')
.fontSize(12)
.fontColor('#E91E63')
.fontWeight(FontWeight.Bold)
Text(t)
.fontSize(10)
.fontColor('#9A8794')
.layoutWeight(1)
.margin({ left: 6 })
}
.width('100%')
.padding({ top: 4, bottom: 4 })
})
}
.width('100%')
.padding(12)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.margin({ top: 8 })
页面底部是"新手贴士"模块,同样是一个白色卡片。标题使用13号粗体字。
贴士列表通过ForEach(TIP_LIST, (t: string) => { ... })循环渲染。每条贴士是一个Row,左侧是一个粉色的圆点(用·字符模拟),右侧是贴士文本。圆点使用粗体增强视觉效果,文本使用浅灰色10号字。
.layoutWeight(1)让文本占据剩余空间,实现圆点左对齐。每条贴士的上下内边距为4像素,保证行间距舒适。
这种"标题 + 列表项"的卡片式布局在移动端非常普遍。通过统一的卡片样式(白色背景、圆角、内边距),整个页面形成了清晰的视觉节奏和信息层次。
从结构图中可以看出,AuditionTab采用了典型的"头部Banner + 分类列表 + 底部贴士"的信息架构,层次分明,逻辑清晰。每一个模块都是独立的视觉单元,通过间距和背景色进行区分。
榜单Tab:RankTab组件与动画系统
领奖台设计与动画实现
@Component
struct RankTab {
@State crownScale: number = 1
onReward: (id: number) => void = (id: number) => {}
aboutToAppear(): void {
this.getUIContext().animateTo({ duration: 800, iterations: -1, playMode: PlayMode.Alternate, curve: Curve.EaseInOut }, () => {
this.crownScale = 1.25
})
}
RankTab是榜单页面组件。它有一个@State crownScale: number = 1状态变量,用于控制冠军皇冠的缩放比例,初始值为1(原始大小)。
aboutToAppear()是组件的生命周期函数,在组件即将出现时被调用。这是执行初始化动画的最佳时机。
this.getUIContext().animateTo({...}, () => { ... })是ArkTS的显式动画API。animateTo方法接受两个参数:第一个是动画配置对象,第二个是动画结束状态的闭包函数。
动画配置包含以下关键参数:
duration: 800:动画持续时间800毫秒iterations: -1:迭代次数,-1表示无限循环playMode: PlayMode.Alternate:播放模式为交替,即正向播放完后反向播放curve: Curve.EaseInOut:动画曲线为缓入缓出,动画开始和结束时速度较慢,中间较快
闭包中的this.crownScale = 1.25定义了动画的目标状态——皇冠放大到1.25倍。由于设置了PlayMode.Alternate和无限循环,皇冠会在1倍和1.25倍之间来回脉动,形成呼吸灯般的脉冲效果。
animateTo是ArkTS中最核心的动画API。它的工作原理是:框架会自动计算初始状态和目标状态之间的差异,然后在指定的时间内平滑过渡。开发者只需要定义"变成什么样",而不需要关心"怎么变过去"。
领奖台布局结构
build() {
Scroll() {
Column() {
// 三甲领奖台
Column() {
Row({ }) {
// 第二名
Column() {
Text('🥈')
.fontSize(22)
Text(RANK_LIST[1].name)
.fontSize(11)
.fontWeight(FontWeight.Medium)
.fontColor('#3A2430')
.maxLines(1)
Text(fmtK(RANK_LIST[1].score) + ' 分')
.fontSize(9)
.fontColor('#9A8794')
.margin({ top: 2 })
Column()
.width(72)
.height(64)
.borderRadius({ topLeft: 10, topRight: 10 })
.backgroundColor('#E1BEE7')
.margin({ top: 8 })
.justifyContent(FlexAlign.Center)
Text('2')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.position({ x: 26, y: 34 })
}
.alignItems(HorizontalAlign.Center)
.margin({ right: 10 })
领奖台是整个榜单页面最具视觉冲击力的部分。它使用Row水平排列三个名次的展示,第二名在左,第一名在中,第三名在右。
第二名的领奖台由以下元素组成(从上到下):
- 银牌emoji 🥈,22号字
- 声优名字,11号中等粗细,最多一行
- 积分,9号浅灰色,调用
fmtK函数格式化 - 领奖台柱体:一个72x64的紫色(
#E1BEE7)矩形,顶部圆角10像素,顶部外边距8像素,内容居中 - 数字"2",22号粗体白字
.position({ x: 26, y: 34 })是绝对定位属性,它将数字"2"定位在领奖台柱体的特定坐标位置。这里的坐标是相对于父容器的,x=26, y=34的位置大约在柱体的中心。使用绝对定位可以精确控制元素的位置,不受正常布局流的影响。
.alignItems(HorizontalAlign.Center)设置整列内容水平居中对齐,这是领奖台对称美感的基础。
// 第一名
Column() {
Text('👑')
.fontSize(24)
.scale({ x: this.crownScale, y: this.crownScale })
Text(RANK_LIST[0].name)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2430')
.maxLines(1)
Text(fmtK(RANK_LIST[0].score) + ' 分')
.fontSize(9)
.fontColor('#9A8794')
.margin({ top: 2 })
Column()
.width(84)
.height(96)
.borderRadius({ topLeft: 10, topRight: 10 })
.backgroundColor('#E91E63')
.margin({ top: 8 })
.justifyContent(FlexAlign.Center)
Text('1')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.position({ x: 26, y: 56 })
}
.alignItems(HorizontalAlign.Center)
.margin({ right: 10 })
第一名的领奖台是视觉焦点,设计上有几个明显的强化:
- 皇冠emoji 👑 代替了奖牌,字号24比银牌铜牌的22更大
.scale({ x: this.crownScale, y: this.crownScale })应用了缩放变换,缩放值绑定到crownScale状态变量,从而实现脉冲动画- 名字字号12号(比第二三名的11号大),且是粗体(Bold)
- 领奖台柱体更宽(84px)更高(96px),颜色为主色粉色(
#E91E63) - 数字"1"字号30,比第二名的22更大
- 绝对定位的y坐标为56,对应更高的柱体
这种视觉层级的强化设计,通过尺寸、颜色、动画等多种手段,让第一名在视觉上脱颖而出,符合用户对领奖台的心理预期。
// 第三名
Column() {
Text('🥉')
.fontSize(22)
Text(RANK_LIST[2].name)
.fontSize(11)
.fontWeight(FontWeight.Medium)
.fontColor('#3A2430')
.maxLines(1)
Text(fmtK(RANK_LIST[2].score) + ' 分')
.fontSize(9)
.fontColor('#9A8794')
.margin({ top: 2 })
Column()
.width(72)
.height(46)
.borderRadius({ topLeft: 10, topRight: 10 })
.backgroundColor('#FFE0B2')
.margin({ top: 8 })
.justifyContent(FlexAlign.Center)
Text('3')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.position({ x: 27, y: 24 })
}
.alignItems(HorizontalAlign.Center)
第三名的领奖台高度最低(46px),颜色为橘黄色(#FFE0B2),数字字号20。三个领奖台的高度依次递减(96 > 64 > 46),形成阶梯状的视觉效果,完美模拟了现实中领奖台的形态。
.justifyContent(FlexAlign.Center)在领奖台柱体的Column上设置,它控制子元素在主轴(垂直方向)上的对齐方式为居中。虽然柱体内只有一个数字元素通过绝对定位放置,但这个属性为布局提供了灵活性。
.borderRadius({ topLeft: 10, topRight: 10 })这种分角设置圆角的方式在UI设计中非常实用。它允许我们为四个角分别设置不同的圆角半径,创造出各种形状的视觉元素。领奖台柱体只设置顶部圆角,底部保持直角,正是这种技巧的典型应用。
榜单列表详解
Text('声优总榜')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2430')
.width('100%')
.margin({ bottom: 10 })
Column() {
ForEach(RANK_LIST, (r: RankT, i: number) => {
Row() {
Text(i < 3 ? ['🥇', '🥈', '🥉'][i] : (i + 1).toString())
.fontSize(i < 3 ? 18 : 13)
.fontWeight(FontWeight.Bold)
.fontColor(i < 3 ? '#3A2430' : '#9A8794')
.width(30)
榜单列表的每一行是一个Row,包含四个主要部分:排名、头像、名字信息、升降排名和打赏按钮。
排名显示部分的设计很巧妙:对于前三名(i < 3),显示对应的金银铜牌emoji,使用数组['🥇', '🥈', '🥉'][i]来获取;对于第四名及以后,显示排名数字(i + 1,因为索引从0开始)。同时前三名的字号更大(18)、颜色更深(深紫色),其余的字号较小(13)、颜色较浅(浅灰色)。
.width(30)给排名区域一个固定宽度,确保所有行的排名列对齐,形成整齐的表格视觉效果。
Text(r.emoji)
.fontSize(20)
.width(42)
.height(42)
.borderRadius(21)
.backgroundColor(r.color)
.textAlign(TextAlign.Center)
头像是一个42x42的圆形区域(通过borderRadius(21)实现,半径为宽度的一半即得正圆),背景色是数据中定义的个人主题色,emoji作为头像内容居中显示。.textAlign(TextAlign.Center)确保文字水平居中。
Column() {
Text(r.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#3A2430')
Text(r.works + ' 部作品 · ' + fmtK(r.score) + ' 分')
.fontSize(9)
.fontColor('#9A8794')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
中间是声优信息列,使用Column垂直排列名字和作品/积分信息。.alignItems(HorizontalAlign.Start)设置左对齐。.layoutWeight(1)让这一列占据剩余空间,实现左右两侧的固定宽度、中间弹性填充的布局效果。
名字使用13号中等粗细字体,是行内的主要信息。下方的作品数和积分为9号浅灰色,是辅助信息。积分调用fmtK函数进行格式化,确保大数字以"万"为单位简洁展示。
Column() {
if (r.delta > 0) {
Text('↑' + r.delta.toString())
.fontSize(11)
.fontColor('#E5484D')
.fontWeight(FontWeight.Medium)
} else if (r.delta < 0) {
Text('↓' + (-r.delta).toString())
.fontSize(11)
.fontColor('#2E9E5B')
.fontWeight(FontWeight.Medium)
} else {
Text('—')
.fontSize(11)
.fontColor('#C9BCC5')
}
Text('打赏')
.fontSize(9)
.fontColor('#E91E63')
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor('#FDE4EF')
.margin({ top: 4 })
.onClick(() => {
this.onReward(r.id)
})
}
右侧是排名变化指示和打赏按钮,使用Column垂直排列。
排名变化通过if/else if/else条件渲染实现三种状态:
- 上升(
delta > 0):显示红色上箭头 + 上升位数 - 下降(
delta < 0):显示绿色下箭头 + 下降位数(取绝对值) - 持平(
delta = 0):显示灰色横线
这里有个有趣的设计选择:上升用红色,下降用绿色。这与股票市场的"涨红跌绿"一致,但与某些地区的习惯(涨绿跌红)相反。在国内互联网产品中,涨红跌绿是比较常见的配色方案。
下方的"打赏"按钮是一个粉色背景的小标签,点击后调用onReward回调,将声优ID传递给父组件,由父组件弹出打赏弹框。
}
.width('100%')
.padding({ top: 10, bottom: 10 })
.border({ width: { bottom: 0.5 }, color: '#F6ECF1' })
})
}
.width('100%')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor('#FFFFFF')
每一行的底部有一条0.5像素的浅粉色分隔线(.border({ width: { bottom: 0.5 }, color: '#F6ECF1' })),这是列表设计的经典做法。0.5像素的细线在高清屏幕上显示为1物理像素,非常精致。
整个列表被包裹在一个白色背景、圆角16像素的Column中,左右内边距12像素,上下内边距6像素,形成一个完整的卡片容器。
剧本Tab:ScriptTab组件与分类筛选
分类标签栏(Chips)
@Component
struct ScriptTab {
@State cate: number = 0
onOpenScript: (idx: number) => void = (idx: number) => {}
build() {
Scroll() {
Column() {
Scroll() {
Row({ space: 8 }) {
ForEach(GENRE_CHIPS, (c: string, i: number) => {
Text(c)
.fontSize(12)
.fontColor(this.cate === i ? '#FFFFFF' : '#6E5A66')
.padding({ left: 13, right: 13, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(this.cate === i ? '#E91E63' : '#FFFFFF')
.onClick(() => {
this.cate = i
})
})
}
.padding({ left: 2, right: 2 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ bottom: 14 })
ScriptTab是剧本浏览页面。@State cate: number = 0维护当前选中的分类索引,初始值为0(对应"全部")。
分类筛选栏是一个横向滚动的标签(Chips)列表。外层Scroll设置水平滚动,内层Row设置8像素间距。
每个标签是一个Text组件,通过this.cate === i判断是否为当前选中状态,动态切换样式:
- 选中态:白色文字 + 粉色背景(
#E91E63) - 未选中态:深灰色文字(
#6E5A66) + 白色背景
标签的内边距和圆角经过精心调校,形成圆润的胶囊形状。.onClick(() => { this.cate = i })在点击时更新选中索引,触发UI重新渲染。
这种"选中状态由索引驱动"的模式是Tab/Chips类组件的标准实现方式。通过一个简单的数值状态变量,即可控制整组标签的选中样式,逻辑清晰且易于维护。
剧本卡片列表
Text('热门剧本库')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2430')
.width('100%')
.margin({ bottom: 10 })
Column() {
ForEach(SCRIPT_LIST, (s: ScriptT, i: number) => {
Column() {
Row() {
Column()
.width(52)
.height(72)
.borderRadius(12)
.backgroundColor(s.color)
.justifyContent(FlexAlign.Center)
剧本列表的每张卡片是一个Row,左侧是封面占位,右侧是剧本信息和操作按钮。
左侧封面是一个52x72的矩形色块,圆角12像素,背景色使用剧本的主题色。这个色块模拟了书籍封面的视觉效果,在没有真实封面图的情况下,用纯色块作为占位是一种简洁有效的设计手法。.justifyContent(FlexAlign.Center)让内容垂直居中(虽然当前没有内容,但为后续添加图标或文字预留了位置)。
Column() {
Text('《' + s.title + '》')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2430')
Text(s.genre + ' · ' + s.chapters + ' 章 · ' + s.roles + ' 个角色')
.fontSize(10)
.fontColor('#9A8794')
.margin({ top: 5 })
中间信息列的第一行是剧本标题,使用书名号包裹,14号粗体字。第二行是剧本的基本信息:类型、章节数、角色数,用中间点(·)分隔,10号浅灰色字。
Row() {
Text('难度')
.fontSize(9)
.fontColor('#9A8794')
Row({ space: 2 }) {
ForEach([1, 2, 3, 4, 5], (n: number) => {
Text('★')
.fontSize(9)
.fontColor(n <= s.level ? '#FFB300' : '#E8DEE4')
})
}
.margin({ left: 4 })
Text(s.hot)
.fontSize(9)
.fontColor('#E91E63')
.margin({ left: 8 })
}
.margin({ top: 6 })
第三行包含难度星级和热度信息,是剧本卡片中最具技术含量的部分。
难度星级通过ForEach([1, 2, 3, 4, 5], (n: number) => { ... })渲染五颗星星。每颗星的颜色由n <= s.level决定:如果星的序号小于等于难度等级,则显示金色(#FFB300),否则显示浅灰色(#E8DEE4)。这种通过循环和条件判断实现星级评分的模式在UI开发中非常经典。
Row({ space: 2 })设置星星之间的间距为2像素,让它们紧密排列但不重叠。
热度信息显示在右侧,粉色字体,如"8.2 万人在读",吸引用户关注热门内容。
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Text('详情')
.fontSize(11)
.fontColor('#E91E63')
.padding({ left: 11, right: 11, top: 5, bottom: 5 })
.borderRadius(11)
.backgroundColor('#FDE4EF')
.onClick(() => {
this.onOpenScript(i)
})
}
.width('100%')
}
.width('100%')
.padding(12)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.margin({ bottom: 8 })
最右侧是"详情"按钮,粉色文字配浅粉背景,圆角胶囊形状。点击后调用onOpenScript(i)回调,将当前剧本的索引传递给父组件,由父组件打开剧本详情弹框。
信息列使用.layoutWeight(1)占据中间剩余空间,实现左侧封面固定、中间信息弹性填充、右侧按钮固定的经典三栏布局。
作品Tab:VoiceTab组件与波形动画
组件属性与状态设计
@Component
struct VoiceTab {
@Prop voices: Array<VoiceT> = VOICE_INIT
@State playingId: number = -1
@State waveH: number = 6
onUpload: () => void = () => {}
onEdit: (idx: number) => void = (idx: number) => {}
onDelete: (idx: number) => void = (idx: number) => {}
VoiceTab是作品管理页面,这里引入了新的装饰器@Prop。
@Prop voices: Array<VoiceT> = VOICE_INIT定义了一个从父组件传入的属性。@Prop装饰器表示这是一个单向数据绑定——父组件的数据变化会同步到子组件,但子组件不能直接修改这个属性。默认值为VOICE_INIT,在父组件没有传入时使用。
@Prop和@State的核心区别在于:@State是组件内部自有状态,组件可以自由修改;@Prop是从父组件传入的属性,子组件只能读取不能修改。如果子组件需要修改@Prop的值,应该通过回调函数通知父组件,由父组件修改后再通过@Prop同步回来。这就是单向数据流的原则。
@State playingId: number = -1记录当前正在播放的作品ID,初始值-1表示没有在播放的作品。这个状态用于控制播放按钮的显示(播放/暂停图标切换)和波形动画的激活状态。
@State waveH: number = 6是波形条的高度,初始值6像素。这个值会在动画中不断变化,驱动波形条的上下起伏。
三个回调函数分别对应上传、编辑、删除三个操作,遵循子组件触发事件、父组件处理逻辑的通信模式。
生命周期与动画
aboutToAppear(): void {
this.getUIContext().animateTo({ duration: 420, iterations: -1, playMode: PlayMode.Alternate, curve: Curve.EaseInOut }, () => {
this.waveH = 16
})
}
与榜单页面的皇冠动画类似,作品页面在aboutToAppear生命周期中启动了波形动画。动画参数:
- 持续时间420毫秒(比皇冠的800毫秒快,模拟波形的快速跳动)
- 无限循环
- 交替播放模式
- 缓入缓出曲线
目标状态是waveH = 16,即波形高度从6像素变化到16像素。由于交替播放,实际效果是波形高度在6和16之间来回变化,形成音频播放时的跳动视觉效果。
@Builder装饰器:波形条组件
@Builder waveBars(active: boolean) {
Row({ space: 2 }) {
Column().width(3).height(active ? this.waveH : 5).borderRadius(2).backgroundColor(active ? '#E91E63' : '#D9C8D2')
Column().width(3).height(active ? this.waveH * 0.7 : 5).borderRadius(2).backgroundColor(active ? '#E91E63' : '#D9C8D2')
Column().width(3).height(active ? this.waveH * 1.1 : 5).borderRadius(2).backgroundColor(active ? '#E91E63' : '#D9C8D2')
Column().width(3).height(active ? this.waveH * 0.8 : 5).borderRadius(2).backgroundColor(active ? '#E91E63' : '#D9C8D2')
Column().width(3).height(active ? this.waveH : 5).borderRadius(2).backgroundColor(active ? '#E91E63' : '#D9C8D2')
}
.height(18)
.alignItems(VerticalAlign.Center)
}
@Builder是ArkTS中一个非常实用的装饰器,它用于将可复用的UI片段封装为函数。@Builder函数可以像组件一样调用,但写法更轻量,适合封装那些不需要独立状态、只是纯展示的UI片段。
waveBars函数接受一个active: boolean参数,表示波形是否处于激活状态(即是否正在播放)。
波形由5个竖条组成,每个竖条是一个3像素宽的Column(用作矩形色块),圆角2像素。5个竖条的高度各不相同:
- 第1条:完整高度
waveH - 第2条:0.7倍高度
- 第3条:1.1倍高度(最高)
- 第4条:0.8倍高度
- 第5条:完整高度
这种错落有致的高度设计模拟了真实音频波形的视觉特征——中间高、两边低,富有节奏感。
当active为true时(正在播放):
- 高度使用
this.waveH及其倍数(动态变化,产生动画效果) - 颜色为粉色(
#E91E63)
当active为false时(未播放):
- 高度固定为5像素(静止的矮条)
- 颜色为浅灰色(
#D9C8D2)
外层Row的高度固定为18像素,.alignItems(VerticalAlign.Center)让竖条在垂直方向居中对齐。这样无论波形条怎么变化高度,整体的位置都是居中的。
@Builder函数是ArkTS中代码复用的重要手段。与自定义组件(@Component)相比,@Builder更轻量、更灵活——它可以定义在组件内部、可以访问组件的状态、可以接受参数。对于那些只在一个组件内复用、且不需要独立状态的UI片段,@Builder是比@Component更好的选择。
这张状态图清晰地展示了波形条的两种状态及其切换条件。播放状态的变化通过playingId控制,而动画的驱动力则来自waveH的持续变化。
作品列表头部
build() {
Scroll() {
Column() {
Row() {
Text('我的作品')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2430')
.layoutWeight(1)
Text('+ 上传')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor('#E91E63')
.onClick(() => {
this.onUpload()
})
}
.width('100%')
.margin({ bottom: 10 })
页面头部是标题和上传按钮的组合。“我的作品"标题使用.layoutWeight(1)占据左侧空间,”+ 上传"按钮固定在右侧。
上传按钮是一个粉色背景的圆角矩形,白色文字,点击后触发onUpload回调。按钮前的"+"号是操作按钮的常见设计模式,直观地表达"添加/创建"的含义。
作品列表项详解
Column() {
ForEach(this.voices, (v: VoiceT, i: number) => {
Column() {
Row() {
Text(this.playingId === v.id ? '⏸' : '▶')
.fontSize(13)
.fontColor('#FFFFFF')
.width(34)
.height(34)
.borderRadius(17)
.borderRadius(17)
.backgroundColor(this.playingId === v.id ? '#C2185B' : '#E91E63')
.textAlign(TextAlign.Center)
.onClick(() => {
if (this.playingId === v.id) {
this.playingId = -1
} else {
this.playingId = v.id
}
})
每个作品卡片的第一行是播放控制区域,包含播放按钮、作品信息和波形条。
播放按钮是一个34x34的圆形按钮(borderRadius(17)),背景色有两种状态:播放中使用深粉色(#C2185B),未播放使用主色粉色(#E91E63)。按钮上的图标通过三元表达式切换:正在播放显示暂停图标⏸,未播放显示播放图标▶。
点击事件的逻辑是:如果当前作品正在播放,则设置playingId = -1(暂停播放);否则设置playingId = v.id(开始播放当前作品)。由于playingId是全局唯一的,同时只能有一个作品处于播放状态,这符合音频播放器的常见行为。
Column() {
Text(v.title)
.fontSize(12)
.fontWeight(FontWeight.Medium)
.fontColor('#3A2430')
.maxLines(1)
Row() {
Text(v.role)
.fontSize(9)
.fontColor('#E91E63')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(6)
.backgroundColor('#FDE4EF')
Text(v.date + ' · ' + v.duration)
.fontSize(9)
.fontColor('#9A8794')
.margin({ left: 6 })
}
.margin({ top: 5 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
中间是作品信息列,使用.layoutWeight(1)占据弹性空间。
作品标题使用12号中等粗细字体,最多显示1行。下方一行包含角色标签和日期/时长信息。角色标签是一个粉色的小胶囊,日期和时长用中间点分隔,浅灰色字体。
this.waveBars(this.playingId === v.id)
}
.width('100%')
最右侧是波形条,调用之前定义的@Builder waveBars函数。传入的参数this.playingId === v.id表示当前作品是否正在播放——如果是,则波形处于激活状态,显示动画效果。
这里有一个值得注意的设计细节:虽然所有作品的波形条都引用了同一个
waveH状态变量,但由于active参数的控制,只有当前播放的作品的波形才会显示动画效果,其他作品的波形保持静止。这种"一个动画源驱动多个显示"的模式既高效又简洁。
Row() {
Text(v.status)
.fontSize(9)
.fontColor(v.status === '已发布' ? '#2E9E5B' : v.status === '审核中' ? '#FF9F43' : '#9A8794')
.padding({ left: 7, right: 7, top: 2, bottom: 2 })
.borderRadius(7)
.backgroundColor(v.status === '已发布' ? '#E4F7F0' : v.status === '审核中' ? '#FFF3E0' : '#F3EDF1')
if (v.status === '已发布') {
Text('❤ ' + fmtK(v.likes))
.fontSize(9)
.fontColor('#9A8794')
.margin({ left: 8 })
}
第二行是状态标签、点赞数和操作按钮。
状态标签的样式根据状态动态变化,使用嵌套的三元表达式:
- 已发布:绿色文字(
#2E9E5B)+ 浅绿背景(#E4F7F0) - 审核中:橙色文字(
#FF9F43)+ 浅橙背景(#FFF3E0) - 其他(草稿):灰色文字 + 浅灰背景
不同状态使用不同颜色是信息可视化的重要手段,用户可以一眼识别出作品的状态。
if (v.status === '已发布')这个条件判断很有意思——只有已发布的作品才显示点赞数。审核中和草稿状态的作品没有公开,自然也就没有点赞数据。这种"数据和状态匹配"的逻辑设计体现了对业务场景的深入理解。
Row() {
Text('编辑')
.fontSize(10)
.fontColor('#E91E63')
.padding({ left: 9, right: 9, top: 4, bottom: 4 })
.borderRadius(9)
.backgroundColor('#FDE4EF')
.onClick(() => {
this.onEdit(i)
})
Text('删除')
.fontSize(10)
.fontColor('#E5484D')
.padding({ left: 9, right: 9, top: 4, bottom: 4 })
.borderRadius(9)
.backgroundColor('#FDECEC')
.margin({ left: 6 })
.onClick(() => {
this.onDelete(i)
})
}
.layoutWeight(1)
.justifyContent(FlexAlign.End)
}
.width('100%')
.margin({ top: 10 })
右侧是编辑和删除两个操作按钮,使用Row包裹并设置.justifyContent(FlexAlign.End)靠右对齐。.layoutWeight(1)让按钮组占据右侧剩余空间。
"编辑"按钮使用粉色配色方案,"删除"按钮使用红色配色方案(#E5484D文字 + #FDECEC背景)。删除操作使用红色是UI设计的通用规范,警示用户这是一个危险操作。
两个按钮都调用对应的回调函数,并将当前索引i作为参数传递出去。
收藏Tab:FavTab组件与Flex布局
Flex宫格布局
@Component
struct FavTab {
@State playingId: number = -1
build() {
Scroll() {
Column() {
Text('收藏的声音')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2430')
.width('100%')
.margin({ bottom: 10 })
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(FAV_SOUNDS, (s: SoundT) => {
Column() {
// ... 卡片内容
}
.width('31%')
.padding(10)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.margin({ bottom: 10 })
.alignItems(HorizontalAlign.Start)
.onClick(() => {
if (this.playingId === s.id) {
this.playingId = -1
} else {
this.playingId = s.id
}
})
})
}
.width('100%')
FavTab是收藏页面,最大的技术亮点是使用Flex组件实现了流式宫格布局。
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween })创建了一个弹性布局容器。两个关键参数:
wrap: FlexWrap.Wrap:开启换行。当一行的内容超出容器宽度时,自动换行到下一行。如果不设置这个属性,默认是NoWrap,所有子元素会挤在一行(可能溢出或被压缩)。justifyContent: FlexAlign.SpaceBetween:主轴对齐方式为两端对齐。第一个元素靠左,最后一个元素靠右,中间元素均匀分布。
每个卡片的宽度设置为'31%',即父容器宽度的31%。这样一行可以放下3个卡片(31% × 3 = 93%),剩余的7%空间通过SpaceBetween均匀分配到卡片之间,形成两列间隙。底部外边距10像素,控制行与行之间的垂直间距。
Flex布局是现代UI框架的标配能力。ArkTS的
Flex组件完全兼容CSS Flexbox的核心概念——主轴(Main Axis)、交叉轴(Cross Axis)、flex-direction、justify-content、align-items、flex-wrap等。掌握Flex布局是成为熟练ArkTS开发者的必备技能。
从原理图可以看到,Flex布局会自动计算每一行能容纳的元素数量,超出的元素自动换行。SpaceBetween的对齐方式使得每行的首尾元素贴边,中间间距均匀。
声音卡片内容
Column() {
Row() {
Text(this.playingId === s.id ? '⏸' : '▶')
.fontSize(14)
.fontColor('#FFFFFF')
.width(30)
.height(30)
.borderRadius(15)
.backgroundColor('rgba(0,0,0,0.25)')
.textAlign(TextAlign.Center)
Text(s.duration)
.fontSize(8)
.fontColor('#FFFFFF')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(5)
.backgroundColor('rgba(0,0,0,0.25)')
}
.width('100%')
.padding(8)
.justifyContent(FlexAlign.SpaceBetween)
每个声音卡片的顶部是一个操作栏,包含播放按钮和时长标签。
播放按钮是一个30x30的圆形,半透明黑色背景(rgba(0,0,0,0.25)),白色图标。半透明背景的设计让按钮既能被看见,又不会完全遮挡下方的内容。播放/暂停图标的切换逻辑与作品页面一致。
时长标签同样使用半透明黑色背景,8号白色字,圆角5像素,非常精致。
顶部的Row设置了.justifyContent(FlexAlign.SpaceBetween),使得播放按钮在左、时长标签在右,两端对齐。
rgba(red, green, blue, alpha)是CSS/ArkTS中表示颜色的一种方式,前三个参数是红、绿、蓝三原色(0-255),第四个参数是透明度(0-1,0为完全透明,1为完全不透明)。使用半透明颜色可以创造出层次感和玻璃质感的视觉效果。
Text(s.title)
.fontSize(11)
.fontWeight(FontWeight.Medium)
.fontColor('#3A2430')
.margin({ top: 8 })
.maxLines(1)
Text('@' + s.author)
.fontSize(9)
.fontColor('#9A8794')
.margin({ top: 3 })
卡片下方是标题和作者信息。标题11号中等粗细,最多一行。作者信息9号浅灰色,前面加"@"符号,这是社交媒体中常见的用户标识方式。
.alignItems(HorizontalAlign.Start)设置卡片内容左对齐,符合阅读习惯。
整个卡片的点击事件绑定在外层Column上——点击卡片任意位置都可以切换播放状态。这种扩大点击区域的设计提升了移动端的触控体验。
最近循环列表
Text('最近循环')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2430')
.width('100%')
.margin({ top: 10, bottom: 10 })
Column() {
ForEach(FAV_SOUNDS, (s: SoundT, i: number) => {
if (i < 5) {
Row() {
Text((i + 1).toString())
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#E91E63')
.width(20)
Text(s.title)
.fontSize(12)
.fontColor('#3A2430')
.layoutWeight(1)
.maxLines(1)
Text('循环 ' + (i + 3) * 7 + ' 次')
.fontSize(9)
.fontColor('#9A8794')
}
.width('100%')
.padding({ top: 9, bottom: 9 })
.border({ width: { bottom: 0.5 }, color: '#F6ECF1' })
}
})
}
.width('100%')
.padding({ left: 12, right: 12 })
.borderRadius(14)
.backgroundColor('#FFFFFF')
页面下方是"最近循环"排行榜,展示循环播放次数最多的5首声音。
if (i < 5)限制只显示前5条,这是"排行榜"类组件的常见做法——展示前N名而非全部数据。
每一行的结构是:排名序号(左,固定宽度20px)+ 标题(中,弹性宽度)+ 循环次数(右,固定宽度)。排名序号使用粉色粗体,与右侧的灰色循环次数形成对比。
行之间通过0.5像素的底部边框分隔,与之前的列表设计保持一致。整个列表包裹在白色圆角卡片中。
收藏页面展示了两种不同的列表布局方式:上方的宫格布局(Flex + Wrap)和下方的行式布局(Column + ForEach)。在同一个页面中根据内容特点选择合适的布局方式,是良好UI设计的体现。
我的Tab:MyTab组件与数据可视化
用户信息卡片
@Component
struct MyTab {
build() {
Scroll() {
Column() {
Row() {
Text('🎙️')
.fontSize(26)
.width(58)
.height(58)
.borderRadius(29)
.backgroundColor('#FDE4EF')
.textAlign(TextAlign.Center)
Column() {
Text('青栀未眠')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2430')
Text('Lv.5 进阶声优 · 中标 23 单')
.fontSize(10)
.fontColor('#9A8794')
.margin({ top: 5 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
Text('认证声优')
.fontSize(10)
.fontColor('#FFFFFF')
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.borderRadius(8)
.backgroundColor('#E91E63')
}
.width('100%')
.padding(14)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.margin({ bottom: 14 })
MyTab是个人中心页面。顶部的用户信息卡片是个人中心的标志性元素。
卡片使用Row水平排列三个部分:头像(左)、用户信息(中)、认证标签(右)。
头像是一个58x58的圆形(borderRadius(29)),浅粉色背景(#FDE4EF),麦克风emoji作为头像内容,26号字,居中显示。
中间信息列使用.layoutWeight(1)弹性填充。用户名16号粗体,等级和中标数10号浅灰色。"Lv.5 进阶声优 · 中标 23 单"这种格式同时展示了用户等级和成就数据,给用户明确的成长反馈。
右侧的"认证声优"标签是一个粉色背景的胶囊标签,白色文字,表明用户的身份认证状态。
个人中心页面的设计核心是"用户画像"——通过头像、昵称、等级、认证等信息,让用户快速感知自己的身份状态和成长进度。良好的个人中心设计能够提升用户的归属感和活跃度。
粉丝增长柱状图
Column() {
Text('本周新增粉丝')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2430')
.width('100%')
Row({ space: 8 }) {
ForEach(FANS_WEEK, (d: string, i: number) => {
Column() {
Text(FANS_WEEK_VAL[i].toString())
.fontSize(8)
.fontColor('#9A8794')
Column()
.width(16)
.width(16)
.height(FANS_WEEK_VAL[i] * 0.28)
.borderRadius(6)
.backgroundColor(i === 5 ? '#E91E63' : '#F3B3CF')
Text('周' + d)
.fontSize(9)
.fontColor('#9A8794')
.margin({ top: 6 })
}
})
}
.alignItems(VerticalAlign.Bottom)
.height(110)
.margin({ top: 12 })
Text('周六直播翻牌活动带来 520 个新粉丝')
.fontSize(10)
.fontColor('#9A8794')
.margin({ top: 8 })
}
.width('100%')
.padding(14)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.margin({ bottom: 14 })
粉丝增长柱状图是个人中心的数据可视化模块,展示了一周七天的粉丝增长情况。这是纯代码实现数据可视化的一个优秀案例。
柱状图的实现原理非常巧妙:
- 使用
Row水平排列7天的数据列 - 每个数据列是一个
Column,包含数值标签、柱体、星期标签 - 柱体的高度由数据值决定:
FANS_WEEK_VAL[i] * 0.28,即数值乘以一个比例系数 - 整个
Row的高度固定为110像素,设置.alignItems(VerticalAlign.Bottom)——底部对齐,这样柱体就会从底部向上生长
.alignItems(VerticalAlign.Bottom)是实现柱状图"从底部升起"效果的关键。默认的交叉轴对齐方式是顶部对齐,如果不修改这个属性,柱子会从上往下悬挂,看起来就像倒过来的柱状图。
高亮效果通过条件判断实现:i === 5 ? '#E91E63' : '#F3B3CF'——周六(索引5)的柱子使用主色粉色高亮,其余使用浅粉色。这是因为周六的数据最高(520),高亮突出最大值是数据可视化的常见手法。
底部的说明文字"周六直播翻牌活动带来 520 个新粉丝"解释了数据峰值的原因,将数据与业务场景关联起来。
纯代码实现的图表虽然功能相对简单,但具有零依赖、加载快、完全可定制的优势。对于简单的数据展示场景,使用基础布局组件手写图表是一种轻量高效的方案。
钱包模块
Row() {
Column() {
Text('¥1,286.50')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#E91E63')
Text('可提现余额')
.fontSize(10)
.fontColor('#9A8794')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('提现')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.borderRadius(14)
.backgroundColor('#E91E63')
}
.width('100%')
.padding(14)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.margin({ bottom: 14 })
钱包模块简洁明了:左侧显示余额信息,右侧是提现按钮。
余额数字使用18号粉色粗体字,是整个卡片的视觉焦点。"可提现余额"的标签用浅灰色小字,作为辅助说明。这种"大数字 + 小标签"的信息层级设计在金融类UI中非常典型。
提现按钮是一个粉色的胶囊按钮,白色文字,左右内边距14像素,上下7像素,圆角14像素。按钮尺寸经过精心设计,既保证了可点击区域足够大,又不会显得臃肿。
设备列表
Column() {
Text('录音设备')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2430')
.width('100%')
.margin({ bottom: 8 })
Row() {
Text('🎤')
.fontSize(16)
Text('电容麦 NT1 · 已连接')
.fontSize(12)
.fontColor('#3A2430')
.layoutWeight(1)
.margin({ left: 10 })
Text('●')
.fontSize(12)
.fontColor('#2E9E5B')
}
.padding({ top: 8, bottom: 8 })
.width('100%')
设备列表展示了用户的录音设备连接状态。每个设备一行,包含设备图标、设备名称和状态、连接状态指示。
连接状态通过圆点的颜色来表示:
- 绿色(
#2E9E5B)实心圆"●":已连接 - 灰色(
#C9BCC5)空心圆"○":未连接
这种"绿点表示在线、灰点表示离线"的设计是即时通讯和IoT设备UI的通用模式,用户几乎不需要学习就能理解其含义。
三个设备(电容麦、监听耳机、声卡)依次排列,其中前两个已连接(绿色),第三个未连接(灰色)。设备列表的设计体现了配音应用的专业性——关注用户的硬件设备状态。
主页面Index:状态管理与弹框架构
@Entry装饰器与组件状态
@Entry
@Component
struct Index {
@State currentTab: number = 0
@State voices: Array<VoiceT> = VOICE_INIT
@State showApply: boolean = false
@State applyId: number = 1
@State applySample: number = 0
@State applyPrice: number = 45
@State applyNote: string = ''
@State showScript: boolean = false
@State scriptIdx: number = 0
@State showUpload: boolean = false
@State uploadTitle: string = ''
@State uploadRole: string = ''
@State uploadTag: string = ''
@State uploadPub: boolean = true
@State showEdit: boolean = false
@State editIdx: number = -1
@State editTitle: string = ''
@State editRole: string = ''
@State editIntro: string = ''
@State showDel: boolean = false
@State delIdx: number = -1
@State showReward: boolean = false
@State rewardId: number = 1
@State rewardAmount: number = 1
@State rewardWay: number = 0
Index是应用的主入口组件,使用@Entry装饰器标记。@Entry表示这个组件是页面的根组件,应用启动时会从这里开始渲染。一个页面只能有一个@Entry组件。
Index组件管理着大量的状态变量,可以分为几类:
导航状态:
currentTab:当前选中的Tab索引,初始值0(试音页)
业务数据:
voices:作品列表数据,初始值为VOICE_INIT。这个状态由父组件管理,通过@Prop传递给子组件VoiceTab。当需要修改数据时(增删改),在父组件中修改后自动同步到子组件。
弹框显示状态(6个弹框,每个对应一个boolean状态):
showApply:试音申请抽屉showScript:剧本详情大卡showUpload:上传配音表单showEdit:编辑配音抽屉showDel:删除确认弹框showReward:打赏结算卡
弹框数据状态(每个弹框维护自己的表单数据):
- 试音申请:
applyId、applySample、applyPrice、applyNote - 剧本详情:
scriptIdx - 上传表单:
uploadTitle、uploadRole、uploadTag、uploadPub - 编辑表单:
editIdx、editTitle、editRole、editIntro - 删除确认:
delIdx - 打赏结算:
rewardId、rewardAmount、rewardWay
将所有弹框的状态集中在父组件管理,是一种常见的架构模式。虽然状态变量看起来很多,但这种集中式管理的好处是:数据流清晰、弹框之间的互斥关系易于控制、子组件保持纯粹(只负责展示和触发事件)。
Stack布局与弹框层叠
build() {
Stack() {
Column() {
// 头部 + 内容区 + 底部tab
// ...
}
.width('100%')
.height('100%')
// 弹框层
if (this.showApply) {
this.applyOverlay()
}
if (this.showScript) {
this.scriptOverlay()
}
if (this.showUpload) {
this.uploadOverlay()
}
if (this.showEdit) {
this.editOverlay()
}
if (this.showDel) {
this.delOverlay()
}
if (this.showReward) {
this.rewardOverlay()
}
}
.width('100%')
.height('100%')
}
Stack是实现弹框效果的核心布局组件。Stack的特点是:子元素按照顺序层叠排列,后写的元素覆盖在先写的元素上面。第一个子元素在最底层,最后一个在最顶层。
在这个应用中,Stack包含两层内容:
- 底层:主界面(头部 + 内容区 + 底部Tab),占满整个屏幕
- 顶层:弹框层,通过条件渲染决定显示哪个弹框
每个弹框通过if (this.showXxx)条件判断来控制显示和隐藏。当对应的状态变量为true时,弹框被渲染出来,覆盖在主界面上方;为false时,弹框不被渲染。
Stack布局的层叠特性使其成为实现弹框、浮层、遮罩等效果的首选。与CSS中的position: fixed或z-index不同,Stack的层级关系完全由子元素的书写顺序决定,更加直观和可预测。
头部区域
Column() {
// 头部(玫红影视风,无动画)
Column() {
Row() {
Text('🎙️')
.fontSize(22)
Column() {
Text('声临其境')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('VOICE LAB')
.fontSize(8)
.fontColor('rgba(255,255,255,0.7)')
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 8 })
Row() {
Text('🔍 搜剧本 / 声优')
.fontSize(11)
.fontColor('rgba(255,255,255,0.75)')
.layoutWeight(1)
}
.layoutWeight(1)
.height(32)
.borderRadius(16)
.height(32)
.borderRadius(16)
.backgroundColor('rgba(255,255,255,0.22)')
.padding({ left: 12, right: 12 })
.margin({ left: 14 })
Text('💬')
.fontSize(16)
.margin({ left: 10 })
}
.width('100%')
头部区域采用渐变的玫红色背景,营造出"影视风"的视觉氛围。顶部一行包含Logo、搜索框和消息图标。
Logo部分由麦克风emoji和应用名称组成。应用名称"声临其境"是15号粗体白字,英文副标题"VOICE LAB"是8号半透明白字(70%不透明度)。主副标题的大小和透明度差异形成了清晰的视觉层次。
搜索框是一个半透明白色背景(22%不透明度)的圆角矩形,高度32像素,圆角16像素(完全圆角)。内部使用放大镜emoji和占位文字"搜剧本 / 声优",文字也是半透明白色(75%不透明度)。.layoutWeight(1)让搜索框占据中间剩余空间。
消息图标(💬)在最右侧,16号字,左边距10像素。
Row() {
Text('🎧 新人首单立减 ¥20 · 声库装备节进行中')
.fontSize(10)
.fontColor('#FFFFFF')
.layoutWeight(1)
Text('去逛逛 >')
.fontSize(10)
.fontColor('rgba(255,255,255,0.9)')
}
.width('100%')
.padding({ left: 10, right: 10, top: 8, bottom: 8 })
.borderRadius(10)
.backgroundColor('rgba(255,255,255,0.16)')
.margin({ top: 10 })
}
.width('100%')
.padding({ left: 14, right: 14, top: 12, bottom: 12 })
.linearGradient({ angle: 120, colors: [['#E91E63', 0], ['#AD1457', 1]] })
头部下方是一个活动横幅(Banner),同样是半透明白色背景(16%不透明度),圆角10像素。
横幅左侧是活动文案,使用耳机emoji开头,10号白色字,.layoutWeight(1)占据左侧空间。右侧是"去逛逛 >"的引导文字,90%不透明度的白色字。
整个头部的背景使用了线性渐变:.linearGradient({ angle: 120, colors: [['#E91E63', 0], ['#AD1457', 1]] })。渐变角度120度,从亮粉色(#E91E63)过渡到深粉色(#AD1457)。渐变背景比纯色背景更有视觉冲击力和层次感。
linearGradient是ArkTS中实现渐变背景的方法。参数包括渐变角度和颜色停止点数组。每个颜色停止点是一个二元数组,第一个元素是颜色值,第二个是位置比例(0到1)。合理使用渐变可以大幅提升UI的视觉品质。
内容区与Tab切换
// 内容区
Column() {
if (this.currentTab === 0) {
AuditionTab({
onApply: (id: number) => {
this.applyId = id
this.showApply = true
}
})
}
if (this.currentTab === 1) {
RankTab({
onReward: (id: number) => {
this.rewardId = id
this.showReward = true
}
})
}
if (this.currentTab === 2) {
ScriptTab({
onOpenScript: (idx: number) => {
this.scriptIdx = idx
this.showScript = true
}
})
}
if (this.currentTab === 3) {
VoiceTab({
voices: this.voices,
onUpload: () => {
this.showUpload = true
},
onEdit: (idx: number) => {
this.editIdx = idx
this.editTitle = this.voices[idx].title
this.editRole = this.voices[idx].role
this.editIntro = ''
this.showEdit = true
},
onDelete: (idx: number) => {
this.delIdx = idx
this.showDel = true
}
})
}
if (this.currentTab === 4) {
FavTab()
}
if (this.currentTab === 5) {
MyTab()
}
}
.width('100%')
.layoutWeight(1)
.backgroundColor('#FFF0F5')
内容区通过if条件判断,根据currentTab的值渲染对应的Tab组件。每次只渲染一个Tab组件,其他Tab不被渲染,保证了性能和内存的高效利用。
每个子组件通过属性参数接收回调函数,实现子组件向父组件的事件传递。以AuditionTab为例:
onApply回调被触发时,父组件执行两个操作:设置applyId为传入的ID,设置showApply = true显示试音申请弹框
VoiceTab的回调更多,因为它涉及数据增删改操作:
onUpload:打开上传弹框onEdit:设置编辑索引,预填编辑表单数据(标题、角色),打开编辑弹框onDelete:设置删除索引,打开删除确认弹框
特别值得注意的是onEdit回调中的数据预填逻辑:
this.editIdx = idx
this.editTitle = this.voices[idx].title
this.editRole = this.voices[idx].role
this.editIntro = ''
在打开编辑弹框之前,父组件从voices数组中取出对应的数据,填充到编辑表单的状态变量中。这样弹框打开时就已经有了原始数据,用户可以在基础上修改。这是编辑功能的标准实现模式。
内容区的背景色是浅粉色(#FFF0F5),比纯白色更有温度感,与应用的整体粉色主题保持一致。.layoutWeight(1)让内容区占据头部和底部Tab之间的所有剩余空间。
底部Tab导航
// 底部 tab
Row() {
this.tabItem('📢', '试音', 0)
this.tabItem('🏆', '榜单', 1)
this.tabItem('📖', '剧本', 2)
this.tabItem('🎧', '作品', 3)
this.tabItem('💗', '收藏', 4)
this.tabItem('👤', '我的', 5)
}
.width('100%')
.backgroundColor('#FFFFFF')
.border({ width: { top: 0.5 }, color: '#FDE4EF' })
底部导航栏是一个白色背景的Row,包含6个Tab项。顶部有一条0.5像素的浅粉色边框,与内容区分隔。
this.tabItem(...)调用了一个@Builder函数来创建每个Tab项,避免了重复代码。
@Builder tabItem函数
@Builder tabItem(icon: string, label: string, idx: number) {
Column() {
Text(icon)
.fontSize(19)
Text(label)
.fontSize(9)
.fontColor(this.currentTab === idx ? '#E91E63' : '#9A8794')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center)
.layoutWeight(1)
.padding({ top: 7, bottom: 6 })
.onClick(() => {
this.currentTab = idx
})
}
tabItem是一个@Builder函数,接受三个参数:图标emoji、标签文字、索引值。
每个Tab项是一个垂直排列的Column:上方是图标(19号字),下方是文字标签(9号字)。
文字颜色由选中状态决定:当前选中的Tab显示粉色(#E91E63),未选中的显示浅灰色(#9A8794)。判断条件是this.currentTab === idx。
.layoutWeight(1)让每个Tab项平均分配宽度,6个Tab各占1/6的宽度。.alignItems(HorizontalAlign.Center)让内容水平居中。
点击事件.onClick(() => { this.currentTab = idx })将当前Tab索引设置为被点击项的索引,触发内容区的切换。
底部Tab导航是移动端应用最经典的导航模式之一。通过
@Builder封装Tab项,既减少了代码重复,又保证了所有Tab项的样式一致性。如果需要修改Tab的样式(如字号、间距),只需要修改一处即可。
弹框系统:六大弹框深度解析
弹框的通用架构模式
六个弹框虽然功能各异,但都遵循相同的架构模式:
- 遮罩层:全屏半透明黑色背景,点击可关闭弹框
- 内容层:白色背景的内容容器,包含标题、表单/内容、操作按钮
- zIndex层级控制:确保弹框在最上层
每个弹框都是一个@Builder函数,定义在Index组件内部,可以访问组件的所有状态和方法。
弹框1:试音申请抽屉
@Builder applyOverlay() {
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(40,15,28,0.55)')
.position({ x: 0, y: 0 })
.zIndex(999)
.onClick(() => {
this.showApply = false
})
}

总结:
综观整个应用,我们可以看到ArkTS开发技术体系的各个组成部分是如何协同工作的。TypeScript的类型系统为数据提供了安全保障,接口定义确保了数据结构的一致性。声明式UI的build方法让界面描述更加直观,开发者只需要关注"是什么"而不需要关心"怎么做"。装饰器(@Component、@State、@Prop、@Builder、@Entry)为不同的编程元素提供了明确的身份标记,框架根据这些标记执行相应的处理。布局系统(Column、Row、Stack、Flex、Scroll)提供了丰富的布局能力,几乎可以实现任何设计稿。状态管理机制让数据变化自动驱动UI更新,开发者不需要手动操作DOM或控件。动画系统提供了简洁的animateTo API,几行代码就能实现流畅的过渡效果。事件系统通过onClick等方法让用户交互与业务逻辑关联起来。
更多推荐


所有评论(0)