基于HarmonyOS API 24构建百亿补贴电商频道:HarmonyOS 6.1.1 ArkTS声明式UI全架构深度解析——多Tab导航、六类弹窗交互与状态驱动的渲染实践
一、技术背景:鸿蒙生态的演进与声明式UI范式
1.1 HarmonyOS 6.1.1 技术栈全景
HarmonyOS 6.1.1 作为华为全场景智慧生活操作系统的最新迭代版本,承载着万物互联时代跨设备协同的核心使命。其技术栈自底向上涵盖了内核层(HarmonyOS 微内核 / Linux Kernel)、系统服务层(分布式软总线、分布式数据管理、分布式任务调度)、框架层(ArkUI 声明式框架、ArkTS 运行时、Ability 框架)以及应用层。在 HarmonyOS 6.1.1 中,ArkUI 框架迎来了全面的性能优化与 API 能力扩充,尤其是在 API 24 版本中,声明式 UI 的渲染管线经过重新设计,组件复用机制更加高效,状态管理响应速度显著提升。
ArkTS 是鸿蒙生态的官方应用开发语言,它在 TypeScript 的基础上进行了深度定制与扩展。ArkTS 保留了 TypeScript 的静态类型检查能力,同时引入了诸如 @Component、@Entry、@State、@Builder、@Extend、@Styles 等装饰器语法,将声明式 UI 范式与类型系统深度融合。在 HarmonyOS API 24 中,ArkTS 的编译器能够对装饰器修饰的结构体进行静态分析,在编译期生成高效的渲染更新函数,避免运行时反射开销。
1.2 声明式 UI 范式的核心理念
声明式 UI 范式(Declarative UI Paradigm)是 HarmonyOS ArkUI 框架的设计哲学基石。与传统的命令式 UI(如 Android View 体系)不同,声明式 UI 要求开发者描述"界面应该长什么样"而非"如何一步步构建界面"。在 ArkTS 中,开发者通过链式调用构建组件树,框架负责将组件树转换为底层渲染指令,并在状态变化时自动触发差分更新。
这种范式的核心优势在于:状态与视图的同步是自动的。当 @State 修饰的变量发生变更时,框架会自动找到依赖该变量的所有 UI 片段,执行最小粒度的重新渲染。在 HarmonyOS 6.1.1 的 ArkUI 引擎中,这一过程通过虚拟 DOM 的 diff 算法实现,性能损耗被控制在极低水平。
1.3 ArkTS 语言特性深度解析
ArkTS 在 TypeScript 基础上做了一系列语言层面的增强与约束。首先,ArkTS 要求所有组件必须以 struct 关键字声明,且每个 struct 必须被 @Component 装饰器修饰才能成为有效的 UI 组件。其次,ArkTS 的接口定义(interface)用于描述数据模型的形状,与 TypeScript 保持一致,但在编译期会被擦除为运行时类型守卫。
ArkTS 的链式属性设置语法是其 UI 构建的核心机制。每一个 UI 组件(如 Text、Column、Row)在构建时返回一个组件实例,后续的 .fontSize()、.fontColor()、.backgroundColor() 等方法在同一个实例上设置属性并返回自身,从而形成流畅的链式调用。这种设计在 HarmonyOS API 24 中得到了进一步优化,编译器能够对链式调用进行内联展开,减少方法调用栈深度。
1.4 HarmonyOS 6.1.1 新特性与 API 24 能力
HarmonyOS 6.1.1 在 API 24 级别上带来了多项重要更新。在 UI 层面,新增了更丰富的动画曲线类型(如 Curve.EaseOut 的精细控制)、scale 变换属性的动画支持增强、以及 position 绝对定位能力的完善。在状态管理层面,@State 装饰器的响应式追踪机制更加精确,支持更复杂的嵌套对象变更检测。
在组件层面,API 24 的 Scroll 组件支持更灵活的滚动方向控制(ScrollDirection.Horizontal / ScrollDirection.Vertical),Scroll 的 scrollBar 属性可以通过 BarState.Off 完全隐藏滚动条以实现更干净的视觉呈现。ForEach 渲染控制函数在 API 24 中对列表项的 key 生成策略做了内部优化,减少了不必要的全量刷新。
1.5 应用业务背景:百亿补贴电商频道
在上述技术底座之上,本文分析的工程实践是一个仿照拼多多"百亿补贴"频道风格的电商应用页面。该应用以黑金视觉风格为基调,提供五个底部 Tab 频道(推荐、手机、电脑数码、美妆、订单),并集成了六种不同类型的弹窗交互(降价提醒、预约购买、编辑地址、删除浏览记录、历史价格图表、多平台比价)。
这一应用充分展现了 ArkTS 声明式 UI 在复杂业务场景下的架构能力。从多频道导航到状态驱动的弹窗系统,从 ForEach 列表渲染到自定义图表绘制,从链式动画到表单交互,它几乎覆盖了电商类应用最核心的 UI 交互模式。
@State/@B ----------------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'LINK_ID'

二、整体架构层次分析
2.1 工程文件的组织结构
整个应用源码在单文件内完成了全部业务逻辑的编写,通过接口定义、常量数据池、枚举、入口组件和频道子组件的分层组织,构建了一个层次清晰的架构体系。这种单文件架构在演示原型阶段非常高效,它避免了跨文件引用的复杂性,同时保持了代码的逻辑内聚性。
从架构层次来看,代码自顶向下分为五层:数据模型层(interface 定义)、静态数据层(const 常量数组)、导航枚举层(SubsidyTab)、入口组件层(BigSubsidyApp)、频道内容组件层(五个 struct 组件)。每一层都有明确的职责边界,上层依赖下层,形成清晰的依赖链。

2.2 组件依赖关系图
应用的组件树以 BigSubsidyApp 为根节点,通过 activeTab 状态变量驱动条件渲染,切换五个频道子组件。每个频道子组件内部又通过 @Builder 方法定义了各自的弹窗构建器,形成"页面内容 + 弹窗叠加"的双层结构。

三、数据模型层:接口定义深度解析
3.1 类型系统在 ArkTS 中的角色
在 ArkTS 中,interface 是纯类型声明,用于约束对象字面量的形状。它不会编译为运行时实体,而是在编译期提供类型安全保障。对于电商应用而言,良好的接口设计是状态管理与列表渲染的基础——ForEach 渲染控制函数需要明确的数据类型来推导每个列表项的属性。
3.2 SubsidyGoods 接口:补贴商品模型
interface SubsidyGoods {
id: number
name: string
icon: string
price: number
original: number
subsidy: number
tag: string
sold: string
brand: string
category: string
}
这是整个应用最核心的数据模型,定义了补贴商品的完整字段。id 为数字主键,用于 ForEach 的列表项标识。name 是商品全称,通常包含品牌、型号、规格等复合信息。icon 使用 emoji 字符串作为商品图标的轻量替代方案,避免了图片资源加载的性能开销。
price 与 original 分别代表补贴后价格和原价,两者的差值即为 subsidy(补贴金额)。这三个数字字段在前端需要配合渲染为"红色当前价 + 灰色删除线原价 + 补贴标签"的组合展示形态。tag 是营销标签字符串(如"国补+平台补"),sold 是销量文案(如"8.2万+"),brand 与 category 用于品牌横滑和频道筛选。
这种设计体现了 ArkTS 类型系统在实际工程中的价值:通过接口约束,确保每一条商品数据都必须包含完整的十一个字段,编译器在构建静态数据数组时会进行逐字段校验,杜绝运行时因字段缺失导致的 undefined 错误。
3.3 BrandItem 接口:品牌横滑模型
interface BrandItem {
id: number
name: string
icon: string
count: string
}
品牌横滑区域的每个品牌卡片由 BrandItem 描述。id 作为唯一标识,name 是品牌名称,icon 是品牌 emoji 图标,count 是该品牌在补贴频道中的商品数量文案(如"38款")。这个接口结构简单,但它服务于一个高频交互的横滑组件——用户通过左右滑动浏览所有品牌,点击品牌进入品牌专场。
3.4 RankGoods 接口:补贴榜单模型
interface RankGoods {
id: number
name: string
icon: string
price: number
hot: string
trend: string
}

补贴热销榜单的每一行由 RankGoods 描述。除了常规的 id、name、icon、price 外,它特别包含 hot(热度值文案,如"热度98.2万")和 trend(趋势文案,如"↑12%")。这两个字段在前端渲染时需要配合排名序号实现"前三名红色高亮 + 趋势箭头"的视觉差异化。
3.5 SubsidyOrder 接口:订单模型
interface SubsidyOrder {
id: number
no: string
goods: string
icon: string
amount: number
subsidyCut: number
status: string
time: string
}

订单数据模型相对复杂,no 是订单编号字符串,goods 是商品简述,amount 是实付金额,subsidyCut 是补贴节省金额。status 是订单状态(“待发货”/“已发货”/“已签收”),它直接驱动了订单卡片底部操作按钮的条件渲染——不同状态展示不同的按钮(修改地址/查看物流/再次购买)。time 是订单时间文案。
3.6 BrowseItem 与 CompareItem 接口
interface BrowseItem {
id: number
name: string
icon: string
price: number
viewed: string
}
interface CompareItem {
id: number
platform: string
price: number
gap: string
icon: string
}

BrowseItem 描述浏览足迹记录,viewed 是"5分钟前"这类相对时间文案。CompareItem 描述多平台比价数据,platform 是平台名称,gap 是差价文案(如"贵¥1200"或"已省¥1700"),id === 1 标识当前平台,在比价弹窗中以金色高亮显示。
3.7 接口设计原则总结
纵观六个接口定义,我们可以提炼出几条设计原则。第一,每个接口都包含 id: number 字段,这是 ForEach 渲染控制函数生成列表项 key 的基础。第二,文案类字段(如 sold、hot、trend、gap)使用 string 而非 number,因为它们在前端展示时已经包含单位符号和格式化文本,直接使用字符串避免了渲染时的类型转换。第三,接口之间保持扁平结构,没有嵌套对象,这有利于 ArkTS 编译器的类型推导和 ForEach 的 diff 优化。
四、静态数据层:常量数组详解
4.1 补贴商品池 SUBSIDY_GOODS
const SUBSIDY_GOODS: SubsidyGoods[] = [
{ id: 1, name: 'Apple iPhone 17 Pro 256G 原色钛金属', icon: '📱', price: 7299, original: 8999, subsidy: 1700, tag: '国补+平台补', sold: '8.2万+', brand: 'Apple', category: '手机' },
{ id: 2, name: '华为 Mate80 Pro 12+512G 曜金黑', icon: '📲', price: 6199, original: 7499, subsidy: 1300, tag: '旗舰现货', sold: '5.6万+', brand: '华为', category: '手机' },
// ... 共12条
]
SUBSIDY_GOODS 是全局常量数组,使用 const 声明并标注 SubsidyGoods[] 类型。在 ArkTS 中,const 声明的数组在编译期会被内联为静态数据表,运行时访问效率极高。这个数组包含 12 条商品数据,覆盖手机(4条)、数码(6条)、美妆(3条,含重叠)三个品类。
数据被多个频道共享使用——推荐页的双列瀑布流从中取全部商品按奇偶索引分列展示,手机频道通过 g.category === '手机' 条件筛选,数码频道通过 g.category === '数码' 筛选,美妆频道通过 g.category === '美妆' 筛选。这种"一份数据源、多频道条件消费"的设计避免了数据冗余,同时保证了各频道数据的一致性。
4.2 品牌横滑 BRAND_LIST
const BRAND_LIST: BrandItem[] = [
{ id: 1, name: 'Apple', icon: '🍎', count: '38款' },
{ id: 2, name: '华为', icon: '🌸', count: '52款' },
// ... 共8条
]
品牌横滑数据包含 8 个品牌,每个品牌以 emoji 图标 + 名称 + 商品数量的组合呈现在一个固定宽度的卡片中。这些卡片被放置在一个水平 Scroll 容器内,用户可以左右滑动浏览。
4.3 补贴榜单 RANK_GOODS
const RANK_GOODS: RankGoods[] = [
{ id: 1, name: 'iPhone 17 Pro', icon: '📱', price: 7299, hot: '热度98.2万', trend: '↑12%' },
// ... 共6条
]
补贴热销榜单包含 6 条排名数据。id 字段在这里兼任排名序号——r.id 在渲染时直接作为排名数字显示,且 r.id <= 3 时排名数字以红色('#E53935')显示,其余以灰色显示。这是一个巧妙的设计:利用 id 字段的双重语义,既作为 ForEach 的 key,又作为排名标识。
4.4 订单数据 SUBSIDY_ORDERS
const SUBSIDY_ORDERS: SubsidyOrder[] = [
{ id: 1, no: 'BB20260826001', goods: 'iPhone 17 Pro 256G 原色钛金属', icon: '📱', amount: 7299, subsidyCut: 1700, status: '待发货', time: '今天 11:23' },
// ... 共5条
]
订单数据包含 5 条记录,覆盖"待发货"(1条)、“已发货”(1条)、“已签收”(3条)三种状态。订单编号 no 使用 BB 前缀 + 日期 + 序号的格式,模拟真实电商平台的订单号生成规则。
4.5 浏览记录与比价数据
const BROWSE_LIST: BrowseItem[] = [
{ id: 1, name: '华为 FreeBuds Pro 4', icon: '🎵', price: 899, viewed: '5分钟前' },
// ... 共8条
]
const COMPARE_LIST: CompareItem[] = [
{ id: 1, platform: '本平台(百亿补贴)', price: 7299, gap: '已省¥1700', icon: '🏆' },
{ id: 2, platform: '某东自营', price: 8499, gap: '贵¥1200', icon: '🔴' },
// ... 共4条
]
浏览记录包含 8 条数据,viewed 字段使用相对时间文案(“5分钟前”/“1小时前”/“昨天”/“前天”/“3天前”),模拟真实的时间衰减展示。比价数据包含 4 个平台,其中 id === 1 标识本平台(百亿补贴),在比价弹窗中以金色背景高亮,其余平台以灰色背景显示,差价文案根据是否为本平台分别使用绿色或红色。
4.6 历史价格数据
const HISTORY_PRICES: number[] = [8999, 8899, 8799, 8899, 8699, 8599, 8499, 8299, 8199, 7899, 7499, 7299]
这是一个纯数字数组,包含 12 周的历史价格数据。从 8999 到 7299,呈现明显的下降趋势,最终达到当前补贴价。这个数组在历史价格弹窗中通过 ForEach 遍历渲染为柱状图,每根柱子的高度通过 HISTORY_PRICES[w] / 8999 * 80 计算得出——以历史最高价 8999 为基准,映射到 80vp 的最大柱高。
4.7 数据层设计的技术要点
从技术角度审视数据层设计,有几个值得关注的要点。首先,所有数据数组都使用 const 声明,在 ArkTS 中这意味着引用不可变但内容可变——不过在本应用中数据作为静态源使用,不会被运行时修改。其次,HISTORY_PRICES 使用 number[] 而非接口数组,因为它只包含单一数值,使用裸数组避免了不必要的接口包装开销。
五、导航枚举层:SubsidyTab 设计
enum SubsidyTab {
RECOMMEND = 0,
PHONE = 1,
DIGITAL = 2,
BEAUTY = 3,
ORDER = 4
}

5.1 枚举在 ArkTS 中的特性
ArkTS 的 enum 是 TypeScript 枚举的子集,支持数字枚举和字符串枚举。在本应用中,SubsidyTab 使用数字枚举,从 0 开始递增。枚举成员在编译期被替换为常量数字,运行时无额外内存开销。
使用枚举而非魔法数字(magic number)来标识 Tab 状态,是工程实践中的重要规范。它使得代码可读性大幅提升——this.activeTab === SubsidyTab.RECOMMEND 远比 this.activeTab === 0 更易理解。同时,枚举的增删不会破坏已有逻辑,新增一个 Tab 只需在枚举中添加一个成员并在条件分支中处理即可。
5.2 枚举与条件渲染的配合
在入口组件的 contentArea 构建器中,SubsidyTab 枚举值通过 if-else if-else 链驱动条件渲染。ArkTS 的条件渲染在编译期会分析 if 条件表达式的常量性,在运行时通过短路求值快速跳过不满足条件的分支,只有满足条件的组件子树才会被构建和渲染。
六、入口组件 BigSubsidyApp 深度解析
6.1 @Entry 与 @Component 装饰器
@Entry
@Component
struct BigSubsidyApp {
@State activeTab: SubsidyTab = SubsidyTab.RECOMMEND
// ...
}
@Entry 装饰器标记该组件为页面的入口组件。在 HarmonyOS ArkUI 框架中,每个页面有且仅有一个 @Entry 组件,它是整个组件树的根节点。框架在加载页面时会首先查找被 @Entry 修饰的 struct,创建其实例并执行 build() 方法生成组件树。
@Component 装饰器将一个普通 struct 声明为可复用的 UI 组件。被 @Component 修饰的 struct 必须实现 build() 方法,该方法返回组件树的最顶层容器。@Component 还启用了 ArkTS 的状态管理机制——struct 内部使用 @State、@Prop、@Link 等装饰器修饰的成员变量将被框架纳入响应式追踪。
6.2 @State 装饰器:响应式状态的核心
@State activeTab: SubsidyTab = SubsidyTab.RECOMMEND 是入口组件的核心状态变量。@State 装饰器是 ArkTS 状态管理体系中最基础也是最重要的一环。
@State 的作用机制: 当 @State 修饰的变量被赋以新值时,框架会自动检测变化并触发依赖该变量的所有 UI 片段重新渲染。在本例中,activeTab 被 contentArea 构建器中的条件分支和 bottomTabItem 构建器中的样式逻辑所依赖。当用户点击底部 Tab 时,this.activeTab = tab 的赋值会同时触发两处更新:内容区域切换到对应频道组件,底部 Tab 项的图标透明度和文字颜色同步变更。
@State 的技术要点: @State 对基本类型(number、string、boolean、enum)使用值比较,只有当新值与旧值不同时才触发更新。对于对象和数组类型,@State 会进行浅层引用比较——如果整个对象引用未变但内部属性变了,需要通过重新赋值整个对象或使用 @Observed 装饰器来触发更新。
6.3 @Builder contentArea():内容区域构建器
@Builder contentArea() {
Column() {
if (this.activeTab === SubsidyTab.RECOMMEND) {
SubsidyHomeContent()
} else if (this.activeTab === SubsidyTab.PHONE) {
PhoneChannelContent()
} else if (this.activeTab === SubsidyTab.DIGITAL) {
DigitalGridContent()
} else if (this.activeTab === SubsidyTab.BEAUTY) {
BeautyChannelContent()
} else {
SubsidyOrderContent()
}
}
.layoutWeight(1)
}

@Builder 装饰器用于声明一个可复用的 UI 构建片段。与直接在 build() 中编写不同,@Builder 方法可以被多次调用复用,并且支持参数传递。
Column 组件技术详解: Column 是 ArkUI 最核心的线性布局容器之一,它将其子组件在垂直方向上从上到下排列。Column 的常用属性包括:layoutWeight(设置组件在父容器中的权重占比,用于弹性分配剩余空间)、alignItems(设置子组件在水平方向的对齐方式,如 HorizontalAlign.Center、HorizontalAlign.Start、HorizontalAlign.End)、justifyContent(设置子组件在垂直方向的排列方式,如 FlexAlign.Center、FlexAlign.SpaceBetween、FlexAlign.SpaceAround)。
在这段代码中,Column 包裹了五个频道组件的条件渲染,.layoutWeight(1) 使内容区域占据底部 Tab 栏之外的所有剩余空间。这是一个典型的弹性布局模式——内容区域自适应填充,底部 Tab 栏固定高度。
条件渲染的编译期优化: ArkTS 编译器会对 if-else if-else 条件渲染生成高效的更新函数。当 activeTab 变化时,框架只需要比较新旧枚举值,移除旧分支的组件子树,创建新分支的组件子树,而不需要重建整个 Column。
6.4 @Builder bottomTabItem():底部 Tab 项构建器
@Builder bottomTabItem(icon: string, label: string, tab: SubsidyTab) {
Column() {
Text(icon).fontSize(19).opacity(this.activeTab === tab ? 1.0 : 0.42)
Text(label).fontSize(9)
.fontColor(this.activeTab === tab ? '#B8860B' : '#999999')
.fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: 1 })
if (this.activeTab === tab) {
Row() {
Column().width(4).height(4).backgroundColor('#FFD700').borderRadius(2)
Column().width(4).height(4).backgroundColor('#FFD700').borderRadius(2).margin({ left: 2 })
Column().width(4).height(4).backgroundColor('#FFD700').borderRadius(2).margin({ left: 2 })
}
.margin({ top: 2 })
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 5, bottom: 5 })
.onClick(() => { this.activeTab = tab })
}
这个构建器接收三个参数:icon(emoji 图标字符串)、label(Tab 文字标签)、tab(对应的枚举值)。它展示了 @Builder 带参数的能力——同一个构建器被调用五次,每次传入不同的参数生成五个 Tab 项,极大减少了代码重复。
Text 组件技术详解: Text 是 ArkUI 的基础文本组件,用于显示一段不可编辑的文本。其常用属性包括:fontSize(字体大小,支持 number 类型的 vp 单位或 Length 对象)、fontColor(文字颜色,支持十六进制字符串如 '#B8860B')、fontWeight(字重,枚举值如 FontWeight.Bold、FontWeight.Normal、FontWeight.Medium)、maxLines(最大行数限制)、decoration(文本装饰,如删除线 TextDecorationType.LineThrough)、textAlign(文本对齐方式)。
在这段代码中,Tab 图标通过 opacity 属性实现选中态与未选中态的视觉区分——选中时透明度为 1.0(完全不透明),未选中时为 0.42(半透明灰暗效果)。Tab 文字通过 fontColor 和 fontWeight 双重区分——选中态为金色加粗,未选中态为灰色常规字重。
条件渲染的装饰性应用: 当 this.activeTab === tab 为真时,额外渲染三个金色小圆点组成的指示器。这种将条件渲染用于装饰性元素显示/隐藏的手法,在 ArkTS 中非常常见且高效——框架在状态变更时只创建或移除这三个小圆点组件,不影响其他部分。
onClick 事件回调: .onClick(() => { this.activeTab = tab }) 是 ArkUI 的点击事件监听器。箭头函数捕获了外层的 tab 参数和 this 上下文,当用户点击 Tab 项时,将 activeTab 状态变量赋值为对应的枚举值,触发 @State 的响应式更新。
6.5 build() 方法:入口组件的渲染入口
build() {
Column() {
this.contentArea()
Row() {
this.bottomTabItem('🛍️', '推荐', SubsidyTab.RECOMMEND)
this.bottomTabItem('📱', '手机', SubsidyTab.PHONE)
this.bottomTabItem('💻', '电脑数码', SubsidyTab.DIGITAL)
this.bottomTabItem('💄', '美妆', SubsidyTab.BEAUTY)
this.bottomTabItem('📦', '订单', SubsidyTab.ORDER)
}
.width('100%')
.backgroundColor('#FFFFFF')
.padding({ top: 4, bottom: 6 })
.shadow({ radius: 10, color: '#14000000', offsetY: -3 })
}
.width('100%').height('100%')
.backgroundColor('#F7F7F7')
}
build() 方法的契约: 每个被 @Component 修饰的 struct 必须实现 build() 方法。该方法返回一个单一根节点的组件树。在本例中,根节点是一个 Column,它包含两部分:内容区域(通过 this.contentArea() 调用构建器)和底部 Tab 栏(一个包含五个 bottomTabItem 调用的 Row)。
Row 组件技术详解: Row 是 ArkUI 的另一个核心线性布局容器,与 Column 互补——它将子组件在水平方向上从左到右排列。Row 的常用属性与 Column 类似但方向不同:alignItems 控制子组件在垂直方向的对齐方式(VerticalAlign 枚举),justifyContent 控制子组件在水平方向的排列方式(FlexAlign 枚举)。
在这段代码中,Row 包裹五个 Tab 项,每个 Tab 项内部设置了 .layoutWeight(1),使得五个 Tab 项等分屏幕宽度。Row 本身设置了白色背景和内边距,并通过 shadow 属性添加了向上投射的阴影效果(offsetY: -3 使阴影向上偏移,模拟从底部投射的视觉效果)。
shadow 属性解析: shadow 接收一个对象参数,包含 radius(模糊半径)、color(阴影颜色,这里使用 '#14000000'——前两位 14 是十六进制的透明度,约为 8%)、offsetY(Y 轴偏移量,负值表示向上偏移)。这种参数化的阴影定义比 CSS 的 box-shadow 更加灵活。
Column 与 Row 的嵌套模式: 外层 Column 设置了 width('100%') 和 height('100%'),占据整个屏幕。它的第一个子组件(内容区域)通过 layoutWeight(1) 弹性填充,第二个子组件(底部 Tab 栏的 Row)按内容高度固定。这是 ArkUI 中最经典的"头部/内容 + 底部导航"布局模式。
七、推荐频道 SubsidyHomeContent 逐行深度解析
7.1 组件声明与状态变量
@Component
struct SubsidyHomeContent {
@State showCompare: boolean = false
@State showHistory: boolean = false
@State showPriceAlert: boolean = false
@State alertCut: string = '降500元'
@State alertPrice: string = ''
@State popScale: number = 0.85
推荐频道组件声明了六个 @State 变量,分为三类:弹窗显示控制(showCompare、showHistory、showPriceAlert,均为 boolean 类型)、弹窗内部状态(alertCut 控制降价幅度选中态、alertPrice 存储用户输入的期望价格)、动画状态(popScale 控制按钮缩放动画)。
boolean 状态的渲染控制模式: 在 ArkTS 中,@State boolean 变量最典型的用途是控制弹窗/浮层的显示与隐藏。当 showPriceAlert 为 true 时,对应的弹窗组件子树被创建并渲染;当变为 false 时,组件子树被移除。这种"状态驱动显隐"的模式比传统的 display: none 或 visibility: hidden 更加彻底——不显示时组件完全不存在于渲染树中,不占用任何渲染资源。
7.2 @Builder modalOverlay():通用遮罩层构建器
@Builder modalOverlay(onClose: () => void) {
Column()
.width('100%').height('100%')
.backgroundColor('rgba(15,15,15,0.6)')
.onClick(onClose)
}
这是一个接收函数类型参数的构建器。onClose: () => void 是一个回调函数签名,调用方传入具体的关闭逻辑。modalOverlay 构建了一个全屏半透明遮罩层——使用 rgba(15,15,15,0.6) 设置 60% 透明度的深灰色背景,点击遮罩层任意区域触发 onClose 回调关闭弹窗。
函数作为参数的 ArkTS 特性: ArkTS 支持函数类型作为 @Builder 方法的参数。在 HarmonyOS API 24 中,这种模式被广泛用于回调机制——构建器定义了 UI 结构和交互行为框架,具体的业务逻辑由调用方通过函数参数注入。这是一种 UI 层面的依赖反转,使得 modalOverlay 可以被推荐频道的三个弹窗复用,每个弹窗传入自己的关闭逻辑。
7.3 弹窗一:降价提醒 priceAlertModal()
@Builder priceAlertModal() {
Column() {
this.modalOverlay(() => { this.showPriceAlert = false })
Column() {
Text('🔔 降价提醒').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ top: 22 })
Text('iPhone 17 Pro 256G').fontSize(12).fontColor('#999999').margin({ top: 4 })
Row() {
Text('当前补贴价 ').fontSize(12).fontColor('#666666')
Text('¥7299').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#B8860B')
}
.margin({ top: 12 })
降价提醒弹窗的结构采用"遮罩层 + 内容卡片"的双层叠加模式。外层 Column 是全屏容器,第一个子元素是 modalOverlay 遮罩层,第二个子元素是弹窗内容卡片。
Column 嵌套的布局原理: 外层 Column 的两个子元素(遮罩层和内容卡片)在垂直方向上叠放。但由于遮罩层设置了 width('100%') 和 height('100%'),它会占据全部空间,内容卡片需要在视觉上浮于遮罩层之上。这里通过 position 绝对定位来实现叠加效果。
Row 组件在价格展示中的应用: Row 将"当前补贴价"文字和"¥7299"价格数字水平排列。价格使用 18vp 字号和金色(#B8860B)加粗显示,与前缀文字的 12vp 灰色形成强烈的视觉对比。这种"小灰文字 + 大彩色数字"的组合是电商价格展示的标准模式。
Text('期望降价幅度').fontSize(12).fontColor('#666666').margin({ top: 18 })
Row() {
Text('降200').fontSize(12)
.fontColor(this.alertCut === '降200' ? '#FFFFFF' : '#333333')
.backgroundColor(this.alertCut === '降200' ? '#1A1A1A' : '#F5F5F5')
.borderRadius(14).padding({ left: 14, right: 14, top: 6, bottom: 6 }).margin({ right: 8 })
.onClick(() => { this.alertCut = '降200' })
Text('降500').fontSize(12)
.fontColor(this.alertCut === '降500' ? '#FFFFFF' : '#333333')
.backgroundColor(this.alertCut === '降500' ? '#1A1A1A' : '#F5F5F5')
.borderRadius(14).padding({ left: 14, right: 14, top: 6, bottom: 6 }).margin({ right: 8 })
.onClick(() => { this.alertCut = '降500' })
Text('降1000').fontSize(12)
.fontColor(this.alertCut === '降1000' ? '#FFFFFF' : '#333333')
.backgroundColor(this.alertCut === '降1000' ? '#1A1A1A' : '#F5F5F5')
.borderRadius(14).padding({ left: 14, right: 14, top: 6, bottom: 6 })
.onClick(() => { this.alertCut = '降1000' })
}
.margin({ top: 8 })
这段代码实现了降价幅度的三选一标签组。每个标签的 fontColor 和 backgroundColor 都通过三元表达式与 this.alertCut 状态变量绑定——选中态为白字黑底,未选中态为灰字浅灰底。
borderRadius 与 padding 的胶囊形设计: borderRadius(14) 配合 padding({ top: 6, bottom: 6 }) 的上下等量内边距,形成了一个胶囊形(pill-shaped)标签。borderRadius 值大于或等于元素高度的一半时,矩形边角变为完全的半圆形,形成胶囊外观。
onClick 与状态更新的响应链: 当用户点击"降500"标签时,this.alertCut = '降500' 触发 @State 更新。框架检测到 alertCut 变化后,重新执行依赖 alertCut 的所有 UI 片段——三个标签的 fontColor 和 backgroundColor 全部重新求值,选中态从"降500"之前的标签转移到"降500"标签上。整个过程是自动且高效的。
TextInput({ placeholder: '或直接输入期望价格,如 6999' })
.placeholderColor('#CCCCCC').fontSize(13)
.backgroundColor('#F5F5F5').borderRadius(10)
.width('82%').margin({ top: 14 })
.onChange((v: string) => { this.alertPrice = v })
TextInput 组件技术详解: TextInput 是 ArkUI 的文本输入组件,用于接收用户键盘输入。其构造函数接收一个配置对象,placeholder 设置占位提示文字。常用属性包括:placeholderColor(占位文字颜色)、type(输入类型枚举,如 InputType.PhoneNumber 限制为电话号码键盘)、backgroundColor、borderRadius。
onChange 事件回调接收一个 (value: string) 参数,每次输入内容变化时触发。在本例中,用户输入的期望价格通过 this.alertPrice = v 存入状态变量。在真实应用中,这个值可以在用户点击"设为提醒"按钮时被提交到后端。
Row() {
Text('🔔 到价自动提醒').fontSize(12).fontColor('#333333').layoutWeight(1)
Text('已开启').fontSize(11).fontColor('#B8860B')
.backgroundColor('#FFF8E1').borderRadius(10)
.padding({ left: 10, right: 10, top: 3, bottom: 3 })
}
.width('82%').margin({ top: 14 })
这里使用 layoutWeight(1) 在 Row 内实现弹性布局——左侧"到价自动提醒"文字占据所有剩余空间,右侧"已开启"标签靠右对齐。这是 Row 中 layoutWeight 的经典用法:一端弹性伸缩,另一端固定宽度。
Text('设为提醒').fontSize(14).fontColor('#FFD700')
.backgroundColor('#1A1A1A').borderRadius(20)
.padding({ left: 40, right: 40, top: 11, bottom: 11 }).margin({ top: 20, bottom: 22 })
.onClick(() => { this.showPriceAlert = false })
}
.width('82%').backgroundColor('#FFFFFF').borderRadius(20)
.alignItems(HorizontalAlign.Center)
.position({ x: '9%', y: '20%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
position 绝对定位技术详解: position 属性接收一个 { x, y } 对象,将组件从正常文档流中脱离,相对于父容器进行绝对定位。内容卡片通过 position({ x: '9%', y: '20%' }) 定位在屏幕上方 20% 处,左右各留 9% 的边距。
zIndex 层叠控制: zIndex(999) 确保弹窗在所有正常流组件之上渲染。在 ArkUI 中,默认的渲染顺序是 DOM 顺序(后声明的组件在上层),但通过 zIndex 可以显式控制层叠顺序,确保弹窗始终覆盖在页面内容之上。
7.4 弹窗二:历史价格柱状图 historyPriceModal()
@Builder historyPriceModal() {
Column() {
this.modalOverlay(() => { this.showHistory = false })
Column() {
Row() {
Text('📉 历史价格走势').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#333333')
Column().layoutWeight(1)
Text('✕').fontSize(16).fontColor('#999999')
.onClick(() => { this.showHistory = false })
}
.width('100%').padding({ left: 18, right: 18, top: 18 })
历史价格弹窗的头部使用 Row 实现三段式布局:左侧标题、中间弹性占位(Column().layoutWeight(1),一个空的 Column 占据剩余空间)、右侧关闭按钮。这是一种非常实用的布局模式——通过一个空的弹性组件在两个固定内容之间撑开空间。
Text('iPhone 17 Pro 256G · 近12周').fontSize(11).fontColor('#999999')
.width('100%').padding({ left: 18, bottom: 8 })
Row() {
ForEach([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], (w: number) => {
Column() {
Column()
.width(12)
.height((HISTORY_PRICES[w] / 8999 * 80).toFixed(0) + 'vp')
.backgroundColor(w === 11 ? '#B8860B' : '#D7CCC8')
.borderRadius({ topLeft: 3, topRight: 3 })
.animation({ duration: 300 })
Text((12 + w).toString() + '周').fontSize(7).fontColor('#BDBDBD').margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
})
}
.padding({ left: 12, right: 12 })
这是整个应用中最精妙的 UI 实现之一——一个手工编织的 12 周柱状图。
ForEach 渲染控制函数技术详解: ForEach 是 ArkUI 的核心列表渲染控制函数,它接收三个参数:数据源数组、项目生成函数((item, index) => Component)、可选的键值生成函数((item) => string)。ForEach 会对数据源的每个元素执行项目生成函数,生成对应的组件列表。
在这段代码中,ForEach 的数据源是一个数字字面量数组 [0, 1, 2, ..., 11],代表 12 周的索引。项目生成函数接收周索引 w,返回一个 Column 包裹的柱状条。这种使用索引数组而非数据数组的方式,使得 ForEach 更像一个"循环控制流"而非纯粹的数据映射。
柱状图的高度计算: .height((HISTORY_PRICES[w] / 8999 * 80).toFixed(0) + 'vp') 是柱子高度的核心计算。以历史最高价 8999 为分母,当前周价格为分子,比值乘以 80vp 得到柱子高度。.toFixed(0) 将浮点数取整为字符串,再拼接 'vp' 后缀形成合法的长度值。例如第一周(8999)的柱高为 80vp,最后一周(7299)的柱高约为 65vp。
条件着色的技术实现: w === 11 判断是否为最后一周(即当前价),最后一周的柱子使用金色(#B8860B)高亮,其余使用浅棕色(#D7CCC8)。通过索引条件判断实现差异化着色,简洁有效。
animation 属性: .animation({ duration: 300 }) 为柱子添加了 300 毫秒的过渡动画。当柱子从初始高度(0vp 或默认值)过渡到计算高度时,会有一个平滑的"长高"动画效果。在 HarmonyOS API 24 中,animation 属性可以附加到任何可动画的属性变化上,包括 height、width、opacity、scale 等。
borderRadius 的部分圆角: .borderRadius({ topLeft: 3, topRight: 3 }) 只设置了左上和右上的圆角,底部保持直角。这种"上圆下直"的圆角设置是柱状图柱子的标准设计——顶部圆润,底部与基线齐平。
Row() {
Column() {
Text('历史最高').fontSize(10).fontColor('#999999')
Text('¥8999').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#E53935').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('历史最低').fontSize(10).fontColor('#999999')
Text('¥7299').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#43A047').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('12周降幅').fontSize(10).fontColor('#999999')
Text('-18.9%').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#B8860B').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%').backgroundColor('#FAFAFA').borderRadius(10).padding({ top: 10, bottom: 10 })
.margin({ left: 18, right: 18, top: 12 })
统计卡片区域使用 Row 内嵌三个等宽 Column 的布局,每个 Column 包含标签文字和数值文字两行。三个数值分别使用红色(最高价)、绿色(最低价)、金色(降幅)着色,形成直观的红绿金三色对比。layoutWeight(1) 确保三个 Column 等分 Row 的宽度。
Text('当前为近一年最低价,比价后可放心入手').fontSize(11).fontColor('#43A047')
.margin({ top: 10 })
Text('关闭').fontSize(13).fontColor('#666666')
.backgroundColor('#F5F5F5').borderRadius(18)
.padding({ left: 40, right: 40, top: 9, bottom: 9 }).margin({ top: 14, bottom: 18 })
.onClick(() => { this.showHistory = false })
}
.width('88%').backgroundColor('#FFFFFF').borderRadius(18)
.position({ x: '6%', y: '14%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
7.5 弹窗三:多平台比价 compareModal()
@Builder compareModal() {
Column() {
this.modalOverlay(() => { this.showCompare = false })
Column() {
Text('⚖️ 全网比价').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ top: 20 })
Text('iPhone 17 Pro 256G 原色钛金属').fontSize(11).fontColor('#999999').margin({ top: 4 })
Column() {
ForEach(COMPARE_LIST, (c: CompareItem) => {
Row() {
Text(c.icon).fontSize(20)
Column() {
Text(c.platform).fontSize(12).fontWeight(FontWeight.Medium).fontColor(c.id === 1 ? '#B8860B' : '#333333')
Text('含运费 · 含税').fontSize(9).fontColor('#BDBDBD').margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 10 })
Column() {
Text('¥' + c.price.toString()).fontSize(15).fontWeight(FontWeight.Bold)
.fontColor(c.id === 1 ? '#B8860B' : '#333333')
Text(c.gap).fontSize(10)
.fontColor(c.id === 1 ? '#43A047' : '#E53935').margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding({ top: 11, bottom: 11, left: 14, right: 14 })
.backgroundColor(c.id === 1 ? '#FFF8E1' : '#FAFAFA')
.borderRadius(10).margin({ top: 6 })
})
}
.width('88%')
比价弹窗通过 ForEach 遍历 COMPARE_LIST 数据源,为每个平台生成一个比价行。每个比价行使用 Row 实现三段式横向布局:左侧平台图标、中间平台名称与说明(layoutWeight(1) 弹性填充)、右侧价格与差价。
ForEach 与条件着色的组合模式: 在 ForEach 的项目生成函数内部,通过 c.id === 1 条件判断区分本平台与竞品平台。本平台的名称、价格使用金色(#B8860B),差价使用绿色(#43A047,表示"已省"),背景使用浅金色(#FFF8E1)。竞品平台使用灰色文字、红色差价(表示"贵")、灰色背景。这种数据驱动的条件着色使得 ForEach 的每一项都能根据数据自身属性呈现差异化视觉。
Text('数据来源公开比价渠道,每小时更新').fontSize(9).fontColor('#CCCCCC').margin({ top: 12 })
Text('查看商品').fontSize(14).fontColor('#FFD700')
.backgroundColor('#1A1A1A').borderRadius(20)
.padding({ left: 40, right: 40, top: 10, bottom: 10 }).margin({ top: 12, bottom: 20 })
.onClick(() => { this.showCompare = false })
}
.width('84%').backgroundColor('#FFFFFF').borderRadius(20)
.alignItems(HorizontalAlign.Center)
.position({ x: '8%', y: '16%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
7.6 推荐页 build() 方法:页面主体结构
build() {
Stack() {
Scroll() {
Column() {
// 头部:黑金风格
Column() {
Row() {
Column() {
Row() {
Text('百亿').fontSize(21).fontWeight(FontWeight.Bold).fontColor('#FFD700')
Text('补贴').fontSize(21).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
}
Text('大牌官方直降 · 全场包邮 · 买贵必赔').fontSize(9).fontColor('#BDBDBD').margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Column() {
Text('🧧').fontSize(20)
Text('领券').fontSize(9).fontColor('#FFD700').margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center)
}
.width('100%').padding({ left: 16, right: 16, top: 14 })
Stack 组件技术详解: Stack 是 ArkUI 的层叠布局容器,它将子组件堆叠在一起,后声明的子组件在上层。Stack 的常用属性包括:alignContent(设置子组件的对齐方式,默认为 Alignment.Center)。在本应用中,Stack 被用于实现"滚动内容 + 弹窗叠加"的双层结构——页面内容在底层,弹窗通过条件渲染叠加在上层。
Scroll 组件技术详解: Scroll 是 ArkUI 的可滚动容器组件,它将其唯一的子组件包裹在一个可滚动的视口中。Scroll 的常用属性和参数包括:scrollable(设置滚动方向,ScrollDirection.Horizontal 或 ScrollDirection.Vertical,默认为垂直)、scrollBar(设置滚动条显示状态,BarState.Auto 自动显示/隐藏、BarState.On 始终显示、BarState.Off 始终隐藏)、layoutWeight(在父容器中的弹性权重)。
在这段代码中,外层 Stack 包含一个 Scroll(页面滚动内容)和三个条件渲染的弹窗。当弹窗显示时,它叠加在 Scroll 内容之上;当弹窗关闭时,只有 Scroll 内容可见。这是 Stack 的层叠特性在弹窗管理中的典型应用。
黑金风格头部的视觉构建: 头部使用深色背景(#1A1A1A),"百亿"使用金色文字(#FFD700),"补贴"使用白色文字,形成"金白对比"的强烈视觉冲击。副标题"大牌官方直降 · 全场包邮 · 买贵必赔"使用极小字号(9vp)的灰色文字,在深色背景上形成低调的辅助说明。
// 头部品类横滑
Scroll() {
Row() {
Text('🔥 补贴榜').fontSize(11).fontColor('#FFD700')
.backgroundColor('rgba(255,215,0,0.16)').borderRadius(12)
.padding({ left: 10, right: 10, top: 5, bottom: 5 }).margin({ left: 12 })
Text('📱 手机').fontSize(11).fontColor('#FFFFFF')
.backgroundColor('rgba(255,255,255,0.1)').borderRadius(12)
.padding({ left: 10, right: 10, top: 5, bottom: 5 }).margin({ left: 8 })
// ... 更多品类标签
}
}
.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
.margin({ top: 12, bottom: 14 })
}
.width('100%').backgroundColor('#1A1A1A')
横向 Scroll 的技术要点: 通过 .scrollable(ScrollDirection.Horizontal) 将 Scroll 设置为水平滚动模式,内部的 Row 包含多个品类标签,超出屏幕宽度的部分可以通过左右滑动浏览。.scrollBar(BarState.Off) 隐藏滚动条,使横滑区域看起来更加干净。
这里第一个标签"🔥 补贴榜"使用半透明金色背景(rgba(255,215,0,0.16))和金色文字,视觉上突出为主选标签。其余标签使用半透明白色背景(rgba(255,255,255,0.1))和白色文字,作为次级标签。通过 rgba 颜色的透明度控制,在深色背景上实现了柔和的层次感。
// 品牌横滑
Column() {
Text('🏅 官方补贴品牌').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333')
.width('100%').padding({ left: 16, top: 12, bottom: 8 })
Scroll() {
Row() {
ForEach(BRAND_LIST, (b: BrandItem) => {
Column() {
Text(b.icon).fontSize(24)
Text(b.name).fontSize(11).fontWeight(FontWeight.Medium).fontColor('#333333').margin({ top: 4 })
Text(b.count + '补贴中').fontSize(9).fontColor('#B8860B').margin({ top: 2 })
}
.width(78).alignItems(HorizontalAlign.Center)
.backgroundColor('#FFFFFF').borderRadius(12).padding({ top: 10, bottom: 8 })
.margin({ left: 5, right: 5 })
})
}
}
.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).height(84)
}
.width('100%').backgroundColor('#FFFFFF')
.margin({ left: 12, right: 12, top: 10 })
.borderRadius(14)
品牌横滑区域使用 ForEach 遍历 BRAND_LIST 数据源,为每个品牌生成一个固定宽度(78vp)的卡片。每个卡片是一个 Column,从上到下排列品牌 emoji 图标、品牌名称、补贴商品数量。
ForEach 在横滑列表中的应用模式: 在水平 Scroll 内部使用 Row 包裹 ForEach 生成的列表项,是 ArkUI 中实现横向滚动列表的标准模式。ForEach 会将所有列表项一次性渲染(因为品牌数量有限),Scroll 负责处理滚动视口。对于数据量较大的场景,应考虑使用 List 组件配合 LazyForEach 实现懒加载。
7.7 大商品卡片:iPhone 主推 + 功能入口
// 大商品卡(iPhone 主推 + 功能入口)
Column() {
Row() {
Column() {
Text('📱').fontSize(52)
}
.width(110).height(110).backgroundColor('#FAFAFA').borderRadius(14)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text('iPhone 17 Pro 256G').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#333333')
Text('原色钛金属 · 全网通 · 官方直营').fontSize(10).fontColor('#999999').margin({ top: 3 })
Row() {
Text('国补+平台补贴').fontSize(8).fontColor('#B8860B').backgroundColor('#FFF8E1')
.padding({ left: 4, right: 4, top: 1, bottom: 1 }).borderRadius(4)
Text('已补8.2万台').fontSize(9).fontColor('#BDBDBD').margin({ left: 6 })
}
.margin({ top: 6 })
Row() {
Text('¥').fontSize(11).fontColor('#E53935')
Text('7299').fontSize(24).fontWeight(FontWeight.Bold).fontColor('#E53935')
Text('¥8999').fontSize(10).fontColor('#CCCCCC').margin({ left: 6 })
.decoration({ type: TextDecorationType.LineThrough })
}
.margin({ top: 8 })
这是推荐页的核心商品卡片,使用 Row 实现左右两列布局:左侧是商品图标区域(固定 110vp x 110vp),右侧是商品信息区域(layoutWeight(1) 弹性填充)。
decoration 删除线技术详解: TextDecorationType.LineThrough 是 ArkUI 的文本装饰类型之一,它在文字中间绘制一条删除线。电商应用中,原价通常以删除线形式呈现,与当前补贴价形成对比。decoration 属性接收一个 { type, color } 对象,type 可以是 LineThrough(删除线)、Underline(下划线)或 None(无装饰)。
价格展示的视觉层次构建: "¥"符号使用 11vp 红色小字,"7299"使用 24vp 红色加粗大字,"¥8999"使用 10vp 灰色删除线小字。三个 Text 在同一个 Row 中水平排列,通过字号和颜色的极端对比(24vp vs 10vp,红色 vs 灰色),形成"当前价为主角、原价为配角"的视觉焦点。这是电商价格展示的经典设计语言。
Row() {
Text('已省¥1700').fontSize(10).fontColor('#FFFFFF').backgroundColor('#E53935')
.borderRadius(8).padding({ left: 6, right: 6, top: 2, bottom: 2 })
Text('分期免息').fontSize(10).fontColor('#43A047').backgroundColor('#E8F5E9')
.borderRadius(8).padding({ left: 6, right: 6, top: 2, bottom: 2 }).margin({ left: 6 })
}
.margin({ top: 6 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 12 })
}
.width('100%').padding(14)
标签的色彩语义系统: "已省¥1700"使用红色文字白色不可用——实际上这里使用红色文字配红色背景,白色文字在红色背景上,形成警示性的红色标签。"分期免息"使用绿色文字配浅绿色背景(#E8F5E9),形成正向的绿色标签。这种"红=价格优势/紧迫感、绿=附加权益/正面信息"的色彩语义系统贯穿整个应用。
Row() {
Text('🔔 降价提醒').fontSize(12).fontColor('#333333')
.layoutWeight(1).textAlign(TextAlign.Center)
.backgroundColor('#F5F5F5').borderRadius(10).padding({ top: 8, bottom: 8 })
.onClick(() => { this.showPriceAlert = true })
Text('📉 历史价格').fontSize(12).fontColor('#333333')
.layoutWeight(1).textAlign(TextAlign.Center)
.backgroundColor('#F5F5F5').borderRadius(10).padding({ top: 8, bottom: 8 }).margin({ left: 8 })
.onClick(() => { this.showHistory = true })
Text('⚖️ 全网比价').fontSize(12).fontColor('#FFD700')
.layoutWeight(1).textAlign(TextAlign.Center)
.backgroundColor('#1A1A1A').borderRadius(10).padding({ top: 8, bottom: 8 }).margin({ left: 8 })
.onClick(() => { this.showCompare = true })
}
.width('100%').padding({ left: 14, right: 14, bottom: 14 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(14)
.margin({ left: 12, right: 12, top: 10 })
功能入口区域使用三个等宽按钮(layoutWeight(1)),分别打开三个弹窗。前两个按钮使用浅灰背景配深色文字,第三个按钮(全网比价)使用黑色背景配金色文字,视觉上区分为主推操作(比价)与辅助操作(降价提醒、历史价格)。
textAlign 在按钮文字中的应用: textAlign(TextAlign.Center) 确保文字在弹性宽度的按钮中居中对齐。在 ArkUI 中,Text 组件默认左对齐,通过 textAlign 可以修改为 Center(居中)或 End(右对齐)。
7.8 补贴热销榜
// 补贴榜
Column() {
Row() {
Text('🔥 补贴热销榜').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333')
Column().layoutWeight(1)
Text('每10分钟更新').fontSize(9).fontColor('#BDBDBD')
}
.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 4 })
ForEach(RANK_GOODS, (r: RankGoods) => {
Column() {
Row() {
Text(r.id.toString()).fontSize(14).fontWeight(FontWeight.Bold).width(24)
.fontColor(r.id <= 3 ? '#E53935' : '#BDBDBD')
Text(r.icon).fontSize(26)
Column() {
Text(r.name).fontSize(13).fontWeight(FontWeight.Medium).fontColor('#333333')
Text(r.hot + ' · 趋势 ' + r.trend).fontSize(9).fontColor('#BDBDBD').margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 10 })
Column() {
Text('¥' + r.price.toString()).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#E53935')
Text('百亿补贴').fontSize(8).fontColor('#B8860B').margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding({ top: 10, bottom: 10, left: 16, right: 16 })
Divider().color('#F5F5F5').margin({ left: 56 })
}
.width('100%').alignItems(HorizontalAlign.Start)
})
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(14)
.margin({ left: 12, right: 12, top: 10 })
Divider 组件技术详解: Divider 是 ArkUI 的分隔线组件,用于在列表项之间添加视觉分隔。Divider 的常用属性包括:color(分隔线颜色)、strokeWidth(线宽,默认 1px)、margin(外边距,这里设置 left: 56 使分隔线从排名图标之后开始,而非从最左侧开始)。
排名条件着色的设计巧思: r.id <= 3 ? '#E53935' : '#BDBDBD' 将前三名的排名数字显示为红色,第四名及以后显示为灰色。这是一个利用 id 字段双重语义(既是主键又是排名序号)实现条件着色的经典案例。
7.9 双列瀑布流
// 双列瀑布(全部商品)
Row() {
Text('✨ 更多补贴好物').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333')
Column().layoutWeight(1)
Text('买贵必赔').fontSize(9).fontColor('#BDBDBD')
}
.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 2 })
Row() {
Column() {
this.goodsCard(SUBSIDY_GOODS[0])
this.goodsCard(SUBSIDY_GOODS[2])
this.goodsCard(SUBSIDY_GOODS[4])
this.goodsCard(SUBSIDY_GOODS[6])
this.goodsCard(SUBSIDY_GOODS[8])
this.goodsCard(SUBSIDY_GOODS[10])
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
this.goodsCard(SUBSIDY_GOODS[1])
this.goodsCard(SUBSIDY_GOODS[3])
this.goodsCard(SUBSIDY_GOODS[5])
this.goodsCard(SUBSIDY_GOODS[7])
this.goodsCard(SUBSIDY_GOODS[9])
this.goodsCard(SUBSIDY_GOODS[11])
}
.layoutWeight(1).alignItems(HorizontalAlign.End)
}
.width('100%').padding({ bottom: 20 })
双列瀑布流通过手动将 SUBSIDY_GOODS 数组按奇偶索引分配到左右两列 Column 中。左列使用偶数索引(0, 2, 4, 6, 8, 10),右列使用奇数索引(1, 3, 5, 7, 9, 11)。两个 Column 都设置 layoutWeight(1) 等分宽度,左列子项左对齐,右列子项右对齐。
@Builder 方法调用的渲染机制: this.goodsCard(SUBSIDY_GOODS[0]) 调用了 @Builder 修饰的 goodsCard 方法,传入一个 SubsidyGoods 对象。在 ArkUI 中,@Builder 方法的调用会在调用位置内联展开组件树,相当于将构建器内容直接粘贴到调用处。这种内联展开机制确保了构建器调用的零运行时开销。
7.10 @Builder goodsCard():商品卡片构建器
@Builder goodsCard(g: SubsidyGoods) {
Column() {
Column() {
Text(g.icon).fontSize(42)
}
.width('100%').height(86).backgroundColor('#FAFAFA').borderRadius(10)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Text(g.name).fontSize(11).fontColor('#333333').maxLines(2)
.width('100%').margin({ top: 6 })
Text(g.brand + ' · ' + g.sold + '人已补').fontSize(9).fontColor('#BDBDBD').margin({ top: 3 })
Row() {
Text('¥').fontSize(9).fontColor('#E53935')
Text(g.price.toString()).fontSize(17).fontWeight(FontWeight.Bold).fontColor('#E53935')
Text('¥' + g.original.toString()).fontSize(9).fontColor('#CCCCCC').margin({ left: 4 })
.decoration({ type: TextDecorationType.LineThrough })
}
.margin({ top: 5 })
Text('补贴¥' + g.subsidy.toString()).fontSize(9).fontColor('#FFFFFF').backgroundColor('#1A1A1A')
.borderRadius(6).padding({ left: 6, right: 6, top: 2, bottom: 2 }).margin({ top: 5 })
}
.width('48%').backgroundColor('#FFFFFF').borderRadius(12).padding(8)
.margin({ left: 4, right: 4, top: 8 })
}
商品卡片构建器接收一个 SubsidyGoods 对象参数,返回一个包含商品图标、名称、品牌信息、价格和补贴标签的 Column 卡片。
maxLines 属性详解: maxLines(2) 限制商品名称最多显示 2 行。当文本超出限制时,ArkUI 会自动截断并在末尾添加省略号。这是电商商品列表中控制卡片高度一致性的重要手段——商品名称长度不一,通过 maxLines 限制行数可以确保每个卡片的高度可控。
justifyContent 在居中布局中的应用: 图标区域的 Column 使用 .justifyContent(FlexAlign.Center) 将 emoji 图标在垂直方向居中。在 ArkUI 的 Column 中,justifyContent 控制子组件在主轴(垂直方向)上的排列方式,FlexAlign.Center 表示居中排列。
7.11 弹窗条件渲染汇总
if (this.showPriceAlert) { this.priceAlertModal() }
if (this.showHistory) { this.historyPriceModal() }
if (this.showCompare) { this.compareModal() }
}
.width('100%').height('100%')
}
}
三个弹窗的显隐通过三个独立的 @State boolean 变量控制,使用 if 条件渲染在 Stack 内层叠。这种设计允许三个弹窗理论上可以同时显示(虽然实际业务中一次只显示一个),每个弹窗的显隐相互独立、互不干扰。
八、手机频道 PhoneChannelContent 逐行深度解析
8.1 组件声明与状态
@Component
struct PhoneChannelContent {
@State filterBrand: string = '全部'
@State showCompare: boolean = false
手机频道声明了两个状态变量:filterBrand 控制品牌筛选标签的选中态(初始为"全部"),showCompare 控制比价弹窗的显隐。与推荐页不同,手机频道只有一个弹窗(比价弹窗),但多了一个筛选状态。
8.2 @Builder filterChip():筛选标签构建器
@Builder filterChip(label: string) {
Text(label)
.fontSize(11)
.fontColor(this.filterBrand === label ? '#FFD700' : '#333333')
.backgroundColor(this.filterBrand === label ? '#1A1A1A' : '#FFFFFF')
.borderRadius(12).padding({ left: 12, right: 12, top: 6, bottom: 6 })
.margin({ left: 6 })
.onClick(() => { this.filterBrand = label })
}
筛选标签构建器接收一个 label 字符串参数,生成一个可点击的标签。标签的选中态通过 this.filterBrand === label 判断——选中时金字黑底,未选中时灰字白底。
这种通过 @Builder 参数化生成可复用标签的模式,与入口组件中的 bottomTabItem 异曲同工。注意这里使用了 Text 组件直接作为可点击元素,而非包裹在 Column 或 Row 中——在 ArkUI 中,任何组件都可以添加 onClick 事件,不限于容器组件。
8.3 手机频道 build() 方法
build() {
Stack() {
Column() {
// 频道头
Column() {
Text('📱 手机补贴专场').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('旗舰直降最高2000元 · 12期免息').fontSize(10).fontColor('#BDBDBD').margin({ top: 3 })
}
.width('100%').backgroundColor('#1A1A1A')
.alignItems(HorizontalAlign.Start).padding({ left: 16, top: 12, bottom: 12 })
// 筛选条
Scroll() {
Row() {
this.filterChip('全部')
this.filterChip('Apple')
this.filterChip('华为')
this.filterChip('小米')
this.filterChip('OPPO')
}
.padding({ left: 8, right: 8 })
}
.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
.margin({ top: 10 })
手机频道的布局使用 Column 从上到下排列三个部分:频道头部(黑底白字标题)、筛选条(横向可滑动的品牌标签)、商品列表(纵向可滑动)。
筛选条使用水平 Scroll 包裹 Row,Row 内是五个 filterChip 构建器调用。通过横滑可以浏览所有品牌筛选选项,scrollBar(BarState.Off) 隐藏滚动条保持视觉简洁。
Scroll() {
Column() {
ForEach(SUBSIDY_GOODS, (g: SubsidyGoods) => {
if (g.category === '手机') {
Row() {
Column() {
Text(g.icon).fontSize(38)
}
.width(86).height(86).backgroundColor('#FAFAFA').borderRadius(12)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text(g.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#333333').maxLines(1)
Row() {
Text(g.tag).fontSize(8).fontColor('#B8860B').backgroundColor('#FFF8E1')
.padding({ left: 4, right: 4, top: 1, bottom: 1 }).borderRadius(4)
Text(g.sold + '人已补').fontSize(9).fontColor('#BDBDBD').margin({ left: 6 })
}
.margin({ top: 5 })
Row() {
Text('¥').fontSize(10).fontColor('#E53935')
Text(g.price.toString()).fontSize(19).fontWeight(FontWeight.Bold).fontColor('#E53935')
Text('¥' + g.original.toString()).fontSize(10).fontColor('#CCCCCC').margin({ left: 5 })
.decoration({ type: TextDecorationType.LineThrough })
Column().layoutWeight(1)
Text('补贴¥' + g.subsidy.toString()).fontSize(9).fontColor('#FFFFFF').backgroundColor('#E53935')
.borderRadius(8).padding({ left: 5, right: 5, top: 2, bottom: 2 })
}
.margin({ top: 7 })
ForEach 内的条件渲染: if (g.category === '手机') 在 ForEach 的项目生成函数内部使用条件渲染,只渲染品类为"手机"的商品。这种"全量遍历 + 条件过滤"的模式在数据量较小时性能足够,但对于大数据集,更优的做法是先在数据层过滤再渲染。
价格行的弹性布局巧思: 在价格 Row 中,Column().layoutWeight(1) 作为弹性占位符被插入在原价和补贴标签之间。这使得"¥"符号、当前价、原价在左侧聚拢,补贴标签被推到右侧。这种通过空弹性组件实现左右分推的技巧在 ArkUI 中非常实用。
Row() {
Text('⚖️ 比价').fontSize(10).fontColor('#333333')
.backgroundColor('#F5F5F5').borderRadius(9)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.onClick(() => { this.showCompare = true })
Text('分期免息').fontSize(10).fontColor('#43A047')
.backgroundColor('#E8F5E9').borderRadius(9)
.padding({ left: 10, right: 10, top: 4, bottom: 4 }).margin({ left: 8 })
Text('以旧换新').fontSize(10).fontColor('#1565C0')
.backgroundColor('#E3F2FD').borderRadius(9)
.padding({ left: 10, right: 10, top: 4, bottom: 4 }).margin({ left: 8 })
}
.margin({ top: 8 })
操作按钮行使用三个小标签按钮,分别使用灰色(比价)、绿色(分期免息)、蓝色(以旧换新)的色彩系统。只有"比价"按钮绑定了 onClick 事件打开弹窗,其余两个为纯展示标签。
色彩语义的扩展: 蓝色(#1565C0 文字配 #E3F2FD 背景)在这里被引入作为"以旧换新"的标识色。至此,应用的色彩语义系统已包含红色(价格/紧迫)、绿色(权益/正面)、金色(品牌/补贴)、蓝色(服务/附加功能)四类。
8.4 比价弹窗的差异化实现
@Builder compareModal() {
Column() {
this.modalOverlay(() => { this.showCompare = false })
Column() {
Text('⚖️ 全网比价').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ top: 20 })
Text('华为 Mate80 Pro 12+512G').fontSize(11).fontColor('#999999').margin({ top: 4 })
Column() {
ForEach(COMPARE_LIST, (c: CompareItem) => {
Row() {
Text(c.icon).fontSize(18)
Text(c.platform).fontSize(12).fontColor(c.id === 1 ? '#B8860B' : '#333333')
.layoutWeight(1).margin({ left: 10 })
Text('¥' + (c.price - 1100).toString()).fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(c.id === 1 ? '#B8860B' : '#333333')
Text(c.gap).fontSize(10).fontColor(c.id === 1 ? '#43A047' : '#E53935').margin({ left: 8 })
}
.width('100%').padding({ top: 12, bottom: 12, left: 14, right: 14 })
.backgroundColor(c.id === 1 ? '#FFF8E1' : '#FAFAFA').borderRadius(10).margin({ top: 6 })
})
}
.width('88%')
手机频道的比价弹窗与推荐页的比价弹窗结构相似,但有两处关键差异。第一,商品名称改为"华为 Mate80 Pro",比价数据中的价格通过 c.price - 1100 进行了差异化处理(模拟不同商品的价格不同)。第二,比价行的布局更紧凑——平台名称和价格在同一个 Row 中水平排列,而非使用嵌套 Column 分层展示。
数据复用与差异化的平衡: 两个频道的比价弹窗复用了同一个 COMPARE_LIST 数据源,但通过价格偏移(-1100)和布局差异实现了商品级差异化。在实际工程中,更优的做法是为不同商品传入不同的比价数据,但作为演示原型,这种"一数据多用 + 运行时偏移"的方式展示了数据复用的灵活性。
九、数码频道 DigitalGridContent 逐行深度解析
9.1 组件声明与状态
@Component
struct DigitalGridContent {
@State showReserve: boolean = false
@State reserveColor: string = '钛金属'
@State reserveVer: string = '256G'
@State popScale: number = 0.85
数码频道声明了四个状态变量:showReserve 控制预约弹窗显隐,reserveColor 存储选中的颜色(初始为"钛金属"),reserveVer 存储选中的版本(初始为"256G"),popScale 存储按钮缩放动画的当前值(初始为 0.85)。
9.2 弹窗四:预约购买 reserveModal()
@Builder reserveModal() {
Column() {
this.modalOverlay(() => { this.showReserve = false })
Column() {
Row() {
Column() {
Text('💻').fontSize(40)
}
.width(80).height(80).backgroundColor('#FAFAFA').borderRadius(12)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text('MacBook Air M5 13寸').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333')
Text('预约享首发价 ¥7899(官方价¥9499)').fontSize(10).fontColor('#999999').margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 12 })
Text('✕').fontSize(15).fontColor('#999999')
.onClick(() => { this.showReserve = false })
}
.width('100%').padding(16)
预约弹窗的头部使用三段式 Row 布局:左侧商品图标(固定 80vp x 80vp)、中间商品信息(弹性填充)、右侧关闭按钮。这是弹窗头部的标准布局模式。
Column() {
Text('颜色').fontSize(12).fontColor('#666666').width('100%')
Row() {
Text('午夜黑').fontSize(12)
.fontColor(this.reserveColor === '午夜黑' ? '#FFD700' : '#333333')
.backgroundColor(this.reserveColor === '午夜黑' ? '#1A1A1A' : '#F5F5F5')
.borderRadius(8).padding({ left: 14, right: 14, top: 6, bottom: 6 }).margin({ right: 8 })
.onClick(() => { this.reserveColor = '午夜黑' })
Text('星光色').fontSize(12)
.fontColor(this.reserveColor === '星光色' ? '#FFD700' : '#333333')
.backgroundColor(this.reserveColor === '星光色' ? '#1A1A1A' : '#F5F5F5')
.borderRadius(8).padding({ left: 14, right: 14, top: 6, bottom: 6 }).margin({ right: 8 })
.onClick(() => { this.reserveColor = '星光色' })
Text('钛金属').fontSize(12)
.fontColor(this.reserveColor === '钛金属' ? '#FFD700' : '#333333')
.backgroundColor(this.reserveColor === '钛金属' ? '#1A1A1A' : '#F5F5F5')
.borderRadius(8).padding({ left: 14, right: 14, top: 6, bottom: 6 })
.onClick(() => { this.reserveColor = '钛金属' })
}
.margin({ top: 8 })
}
.width('100%').padding({ left: 16, right: 16 })
颜色选择区域使用三个可点击的 Text 标签,选中态为金字黑底,未选中态为灰字浅灰底。这种标签选择器的模式与降价提醒弹窗中的"降200/降500/降1000"标签组完全一致,体现了 ArkUI 状态驱动 UI 的一致性设计语言。
Column() {
Text('版本').fontSize(12).fontColor('#666666').width('100%')
Row() {
Text('256G').fontSize(12)
.fontColor(this.reserveVer === '256G' ? '#FFD700' : '#333333')
.backgroundColor(this.reserveVer === '256G' ? '#1A1A1A' : '#F5F5F5')
.borderRadius(8).padding({ left: 16, right: 16, top: 6, bottom: 6 }).margin({ right: 8 })
.onClick(() => { this.reserveVer = '256G' })
Text('512G').fontSize(12)
.fontColor(this.reserveVer === '512G' ? '#FFD700' : '#333333')
.backgroundColor(this.reserveVer === '512G' ? '#1A1A1A' : '#F5F5F5')
.borderRadius(8).padding({ left: 16, right: 16, top: 6, bottom: 6 }).margin({ right: 8 })
.onClick(() => { this.reserveVer = '512G' })
Text('1TB').fontSize(12)
.fontColor(this.reserveVer === '1TB' ? '#FFD700' : '#333333')
.backgroundColor(this.reserveVer === '1TB' ? '#1A1A1A' : '#F5F5F5')
.borderRadius(8).padding({ left: 16, right: 16, top: 6, bottom: 6 })
.onClick(() => { this.reserveVer = '1TB' })
}
.margin({ top: 8 })
}
.width('100%').padding({ left: 16, right: 16, top: 14 })
版本选择区域与颜色选择区域结构完全相同,只是选项内容不同(256G/512G/1TB)。两个选择区域的状态相互独立——reserveColor 和 reserveVer 是两个独立的 @State 变量,用户选择颜色不会影响版本的选择态,反之亦然。
9.3 预约按钮的缩放动画
Row() {
Text('预约金').fontSize(12).fontColor('#666666')
Text('¥100(可退)').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#B8860B').margin({ left: 6 })
Column().layoutWeight(1)
Text('已预约2.3万人').fontSize(10).fontColor('#BDBDBD')
}
.width('100%').padding({ left: 16, right: 16, top: 14 })
Text('支付预约金锁定首发价').fontSize(14).fontColor('#FFD700')
.backgroundColor('#1A1A1A').borderRadius(20)
.padding({ left: 46, right: 46, top: 11, bottom: 11 }).margin({ top: 18, bottom: 20 })
.scale({ x: this.popScale, y: this.popScale })
.animation({ duration: 200, curve: Curve.EaseOut })
.onClick(() => {
this.popScale = 0.92
setTimeout(() => { this.popScale = 1.0; this.showReserve = false }, 180)
})
scale 变换属性技术详解: .scale({ x: this.popScale, y: this.popScale }) 对组件进行二维缩放变换。scale 接收一个 { x, y } 对象,x 和 y 分别是水平和垂直方向的缩放比例,1.0 为原始大小。在本例中,popScale 初始为 0.85,意味着按钮初始状态是正常大小的 85%。
animation 与 curve 的配合: .animation({ duration: 200, curve: Curve.EaseOut }) 为 scale 属性的变化添加 200 毫秒的过渡动画,使用 EaseOut 缓动曲线。EaseOut 曲线的特点是变化先快后慢——动画开始时快速变化,接近目标值时减速,模拟物理世界中物体落地的减速感。
onClick 中的动画序列: 当用户点击"支付预约金"按钮时,执行三步操作:第一步 this.popScale = 0.92 将按钮缩小到 92%(模拟按下效果);第二步通过 setTimeout 延迟 180 毫秒后执行 this.popScale = 1.0 恢复大小并关闭弹窗。这种"缩小→延迟→恢复+关闭"的动画序列为按钮交互提供了物理反馈感。
Curve 缓动曲线类型详解: HarmonyOS API 24 的 Curve 枚举提供了多种缓动曲线:Linear(匀速)、Ease(先加速后减速)、EaseIn(先慢后快)、EaseOut(先快后慢)、EaseInOut(两端慢中间快)、FastOutSlowIn(快速起步慢速结束)、LinearOutSlowIn(线性起步慢速结束)、FastOutLinearIn(快速起步线性结束)等。选择合适的曲线可以增强 UI 交互的自然感。
9.4 数码频道 build() 方法
build() {
Stack() {
Scroll() {
Column() {
Column() {
Text('💻 电脑数码馆').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('M5芯片新品 · 预约享首发价').fontSize(10).fontColor('#BDBDBD').margin({ top: 3 })
Text('⏰ 距首发结束 02:14:36').fontSize(11).fontColor('#FFD700').margin({ top: 8 })
}
.width('100%').backgroundColor('#1A1A1A')
.alignItems(HorizontalAlign.Start).padding({ left: 16, top: 12, bottom: 14 })
数码频道头部在黑底白字标题基础上,额外增加了一行倒计时文字"⏰ 距首发结束 02:14:36",使用金色显示以营造紧迫感。在实际应用中,这个倒计时应该通过定时器实时更新,但作为静态演示,这里使用固定文案。
// 新品预约横幅
Row() {
Column() {
Text('🆕 新品首发预约').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#333333')
Text('MacBook Air M5 · 最低¥7899').fontSize(10).fontColor('#B8860B').margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Text('立即预约').fontSize(12).fontColor('#FFD700')
.backgroundColor('#1A1A1A').borderRadius(16)
.padding({ left: 16, right: 16, top: 7, bottom: 7 })
.onClick(() => { this.showReserve = true })
}
.width('100%').backgroundColor('#FFF8E1').borderRadius(12).padding(12)
.margin({ left: 12, right: 12, top: 10 })
预约横幅使用浅金色背景(#FFF8E1),左侧是预约信息(弹性填充),右侧是"立即预约"按钮。点击按钮设置 showReserve = true 打开预约弹窗。
Row() {
Text('已选 ' + this.reserveColor + ' · ' + this.reserveVer).fontSize(10).fontColor('#BDBDBD')
.margin({ top: 8 })
}
.width('100%').justifyContent(FlexAlign.Center)
这行文字展示了用户在弹窗中的选择结果——通过字符串拼接 this.reserveColor 和 this.reserveVer 实时反映当前选择。当用户在弹窗中改变颜色或版本时,这行文字会自动更新(因为两个变量都是 @State)。不过在当前实现中,弹窗关闭后这行文字才更新(因为弹窗显示时遮罩层覆盖了页面内容)。
justifyContent 在 Row 中的居中应用: .justifyContent(FlexAlign.Center) 使 Row 内的单一子组件(文字)水平居中。在 Row 中,justifyContent 控制子组件在主轴(水平方向)上的排列方式。
9.5 两列网格布局
// 2列网格
Row() {
ForEach(SUBSIDY_GOODS, (g: SubsidyGoods) => {
if (g.category === '数码') {
Column() {
Column() {
Text(g.icon).fontSize(38)
}
.width('100%').height(80).backgroundColor('#FAFAFA').borderRadius(10)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Text(g.name).fontSize(11).fontColor('#333333').maxLines(2)
.width('100%').margin({ top: 6 })
Row() {
Text('¥').fontSize(9).fontColor('#E53935')
Text(g.price.toString()).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E53935')
Text('省' + g.subsidy.toString()).fontSize(9).fontColor('#FFFFFF').backgroundColor('#E53935')
.borderRadius(6).padding({ left: 4, right: 4, top: 1, bottom: 1 }).margin({ left: 6 })
}
.margin({ top: 5 })
Text(g.sold + '人付款').fontSize(9).fontColor('#BDBDBD').margin({ top: 3 })
}
.width('48%').backgroundColor('#FFFFFF').borderRadius(12).padding(10)
.margin({ left: 4, right: 4, top: 8 })
}
})
}
.width('100%').padding({ left: 8, right: 8, bottom: 20 })
与推荐页的双列瀑布流不同,数码频道使用 ForEach 在一个 Row 内直接生成所有数码品类商品卡片。每个卡片宽度设为 48%,加上左右各 4vp 的外边距,两列卡片刚好填满屏幕宽度。
ForEach 在网格布局中的行为: 当 ForEach 在 Row 内生成多个固定宽度的子组件时,它们会按声明顺序从左到右排列。超出屏幕宽度的部分会被截断(因为外层 Row 没有设置 scrollable)。在实际工程中,如果数据量可能超出屏幕,应该使用 Grid 组件或 List 组件配合 columnsTemplate 实现真正的网格布局。
十、美妆频道 BeautyChannelContent 逐行深度解析
10.1 组件声明与无状态设计
@Component
struct BeautyChannelContent {
build() {
Scroll() {
Column() {
美妆频道是五个频道中唯一没有 @State 变量的组件——它是一个纯展示型页面,没有任何用户可交互的状态变更。这使得它成为 ArkUI 中"无状态组件"的典型示例。
无状态组件的渲染优势: 没有 @State 变量意味着该
})
}
.width(‘100%’).padding({ left: 8, right: 8, bottom: 20 })
}
.width('100%
if (this.showDeleteBrowse) { this.deleteBrowseModal() }
}
.width(‘100%’).height(‘100%’)
}
}
---

### 21.8 总结:
从工程实践角度,本应用展示了多项值得借鉴的实践规范:使用 `enum` 代替魔法数字提升可读性;使用 `interface` 约束数据模型形状保障类型安全;使用 `const` 声明静态数据实现编译期内联;使用 `@Builder` 参数化构建器减少代码重复;使用 `layoutWeight` 弹性布局适配不同屏幕尺寸;使用 `maxLines` 限制文本行数保障卡片高度一致性;使用"遮罩层 + 内容卡片"通用模式统一弹窗架构;使用三元表达式实现数据驱动的条件样式。这些实践规范在小型原型和大型工程中同样适用,是 ArkTS 开发的通用方法论。
更多推荐


所有评论(0)