一、技术背景:鸿蒙生态的演进与声明式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 组件(如 TextColumnRow)在构建时返回一个组件实例,后续的 .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),ScrollscrollBar 属性可以通过 BarState.Off 完全隐藏滚动条以实现更干净的视觉呈现。ForEach 渲染控制函数在 API 24 中对列表项的 key 生成策略做了内部优化,减少了不必要的全量刷新。

1.5 应用业务背景:百亿补贴电商频道

在上述技术底座之上,本文分析的工程实践是一个仿照拼多多"百亿补贴"频道风格的电商应用页面。该应用以黑金视觉风格为基调,提供五个底部 Tab 频道(推荐、手机、电脑数码、美妆、订单),并集成了六种不同类型的弹窗交互(降价提醒、预约购买、编辑地址、删除浏览记录、历史价格图表、多平台比价)。

这一应用充分展现了 ArkTS 声明式 UI 在复杂业务场景下的架构能力。从多频道导航到状态驱动的弹窗系统,从 ForEach 列表渲染到自定义图表绘制,从链式动画到表单交互,它几乎覆盖了电商类应用最核心的 UI 交互模式。

渲染错误: Mermaid 渲染失败: Parse error on line 6: ...t/Scroll] C --> F[状态管理
@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 组件)。每一层都有明确的职责边界,上层依赖下层,形成清晰的依赖链。

数据模型层 - interface 定义
SubsidyGoods/BrandItem/RankGoods等

静态数据层 - const 数组
SUBSIDY_GOODS/BRAND_LIST等

导航枚举层 - SubsidyTab
RECOMMEND/PHONE/DIGITAL/BEAUTY/ORDER

入口组件层 - BigSubsidyApp
@Entry @Component

推荐频道
SubsidyHomeContent

手机频道
PhoneChannelContent

数码频道
DigitalGridContent

美妆频道
BeautyChannelContent

订单频道
SubsidyOrderContent

在这里插入图片描述

2.2 组件依赖关系图

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

渲染错误: Mermaid 渲染失败: Parse error on line 2: graph TD Root[BigSubsidyApp

在这里插入图片描述

三、数据模型层:接口定义深度解析

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 字符串作为商品图标的轻量替代方案,避免了图片资源加载的性能开销。

priceoriginal 分别代表补贴后价格和原价,两者的差值即为 subsidy(补贴金额)。这三个数字字段在前端需要配合渲染为"红色当前价 + 灰色删除线原价 + 补贴标签"的组合展示形态。tag 是营销标签字符串(如"国补+平台补"),sold 是销量文案(如"8.2万+"),brandcategory 用于品牌横滑和频道筛选。

这种设计体现了 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 描述。除了常规的 idnameiconprice 外,它特别包含 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 的基础。第二,文案类字段(如 soldhottrendgap)使用 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 条件表达式的常量性,在运行时通过短路求值快速跳过不满足条件的分支,只有满足条件的组件子树才会被构建和渲染。

应用启动

点击手机Tab

点击数码Tab

点击美妆Tab

点击订单Tab

点击推荐Tab

点击数码Tab

点击美妆Tab

点击订单Tab

点击推荐Tab

点击手机Tab

点击美妆Tab

点击订单Tab

点击推荐Tab

点击手机Tab

点击数码Tab

点击订单Tab

点击推荐Tab

点击手机Tab

点击数码Tab

点击美妆Tab

RECOMMEND

PHONE

DIGITAL

BEAUTY

ORDER


六、入口组件 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 片段重新渲染。在本例中,activeTabcontentArea 构建器中的条件分支和 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.CenterHorizontalAlign.StartHorizontalAlign.End)、justifyContent(设置子组件在垂直方向的排列方式,如 FlexAlign.CenterFlexAlign.SpaceBetweenFlexAlign.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.BoldFontWeight.NormalFontWeight.Medium)、maxLines(最大行数限制)、decoration(文本装饰,如删除线 TextDecorationType.LineThrough)、textAlign(文本对齐方式)。

在这段代码中,Tab 图标通过 opacity 属性实现选中态与未选中态的视觉区分——选中时透明度为 1.0(完全不透明),未选中时为 0.42(半透明灰暗效果)。Tab 文字通过 fontColorfontWeight 双重区分——选中态为金色加粗,未选中态为灰色常规字重。

条件渲染的装饰性应用: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 变量,分为三类:弹窗显示控制(showCompareshowHistoryshowPriceAlert,均为 boolean 类型)、弹窗内部状态(alertCut 控制降价幅度选中态、alertPrice 存储用户输入的期望价格)、动画状态(popScale 控制按钮缩放动画)。

boolean 状态的渲染控制模式: 在 ArkTS 中,@State boolean 变量最典型的用途是控制弹窗/浮层的显示与隐藏。当 showPriceAlerttrue 时,对应的弹窗组件子树被创建并渲染;当变为 false 时,组件子树被移除。这种"状态驱动显隐"的模式比传统的 display: nonevisibility: 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 })

这段代码实现了降价幅度的三选一标签组。每个标签的 fontColorbackgroundColor 都通过三元表达式与 this.alertCut 状态变量绑定——选中态为白字黑底,未选中态为灰字浅灰底。

borderRadius 与 padding 的胶囊形设计: borderRadius(14) 配合 padding({ top: 6, bottom: 6 }) 的上下等量内边距,形成了一个胶囊形(pill-shaped)标签。borderRadius 值大于或等于元素高度的一半时,矩形边角变为完全的半圆形,形成胶囊外观。

onClick 与状态更新的响应链: 当用户点击"降500"标签时,this.alertCut = '降500' 触发 @State 更新。框架检测到 alertCut 变化后,重新执行依赖 alertCut 的所有 UI 片段——三个标签的 fontColorbackgroundColor 全部重新求值,选中态从"降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 限制为电话号码键盘)、backgroundColorborderRadius

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 内实现弹性布局——左侧"到价自动提醒"文字占据所有剩余空间,右侧"已开启"标签靠右对齐。这是 RowlayoutWeight 的经典用法:一端弹性伸缩,另一端固定宽度。

      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 属性可以附加到任何可动画的属性变化上,包括 heightwidthopacityscale 等。

borderRadius 的部分圆角: .borderRadius({ topLeft: 3, topRight: 3 }) 只设置了左上和右上的圆角,底部保持直角。这种"上圆下直"的圆角设置是柱状图柱子的标准设计——顶部圆润,底部与基线齐平。

历史价格弹窗渲染流程

showHistory = true

创建遮罩层

创建内容卡片

渲染标题栏

ForEach遍历12周数据

w === 11?

柱子高度 = 80vp
颜色 = 金色 #B8860B

柱子高度 = price/8999*80vp
颜色 = 浅棕 #D7CCC8

添加animation动画

渲染周标签文字

渲染统计卡片

渲染关闭按钮

      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 的每一项都能根据数据自身属性呈现差异化视觉。

是 本平台

否 竞品

COMPARE_LIST 数据源

ForEach遍历

c.id === 1?

平台名: 金色
价格: 金色加粗
差价: 绿色 已省
背景: 浅金 #FFF8E1

平台名: 深灰
价格: 深灰加粗
差价: 红色 贵
背景: 浅灰 #FAFAFA

渲染比价行

继续下一项

      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.HorizontalScrollDirection.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 实现懒加载。

BRAND_LIST 8条品牌数据

ForEach遍历

为每个品牌生成Column卡片

卡片1: Apple 🍎 38款

卡片2: 华为 🌸 52款

卡片3: 小米 🧡 64款

... 更多品牌

Row容器水平排列

Scroll横向滚动容器

用户左右滑动浏览

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(右对齐)。

推荐页大商品卡片

Row 左右两列布局

左列: 商品图标
固定110x110vp

右列: 商品信息
layoutWeight弹性填充

商品名称 加粗15vp

规格描述 灰色10vp

标签Row: 国补标签 + 销量

价格Row: ¥ + 7299 + ¥8999删除线

权益Row: 已省¥1700红标 + 分期免息绿标

功能入口Row 三等分按钮

🔔 降价提醒 → showPriceAlert=true

📉 历史价格 → showHistory=true

⚖️ 全网比价 → showCompare=true

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 组件直接作为可点击元素,而非包裹在 ColumnRow 中——在 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 包裹 RowRow 内是五个 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)。两个选择区域的状态相互独立——reserveColorreserveVer 是两个独立的 @State 变量,用户选择颜色不会影响版本的选择态,反之亦然。

预约弹窗交互流程

用户点击立即预约

showReserve = true 弹窗显示

用户选择颜色

reserveColor 更新
标签着色刷新

用户选择版本

reserveVer 更新
标签着色刷新

用户点击支付预约金

popScale = 0.92 缩小动画

setTimeout 180ms

popScale = 1.0 恢复

showReserve = false 弹窗关闭

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 } 对象,xy 分别是水平和垂直方向的缩放比例,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.reserveColorthis.reserveVer 实时反映当前选择。当用户在弹窗中改变颜色或版本时,这行文字会自动更新(因为两个变量都是 @State)。不过在当前实现中,弹窗关闭后这行文字才更新(因为弹窗显示时遮罩层覆盖了页面内容)。

justifyContent 在 Row 中的居中应用: .justifyContent(FlexAlign.Center) 使 Row 内的单一子组件(文字)水平居中。在 Row 中,justifyContent 控制子组件在主轴(水平方向)上的排列方式。

数码频道页面结构

频道头部 黑底
标题 + 副标题 + 倒计时

预约横幅 浅金底
信息 + 立即预约按钮

已选信息行 居中文字

2列网格商品列表

ForEach遍历SUBSIDY_GOODS

g.category === 数码?

生成网格卡片

跳过

图标区 + 名称 + 价格行 + 销量

预约弹窗 条件渲染

showReserve = true时显示

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 在网格布局中的行为:ForEachRow 内生成多个固定宽度的子组件时,它们会按声明顺序从左到右排列。超出屏幕宽度的部分会被截断(因为外层 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%’)
}
}


---
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b63001809b414fe3b774f19d87e70765.png#pic_center)

### 21.8 总结:

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

Logo

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

更多推荐