一、鸿蒙技术栈全景:从HarmonyOS 6.1.1到ArkTS API 24的声明式UI范式演进

HarmonyOS 6.1.1作为华为全场景分布式操作系统的最新迭代版本,在前端UI框架层面带来了显著的架构升级与能力增强。其核心UI开发语言ArkTS基于TypeScript超集扩展,融合了声明式编程范式与响应式状态管理机制,为开发者提供了一套类型安全、编译期校验完备的高性能UI开发体系。在HarmonyOS ArkTS API 24中,ArkUI框架的组件模型、状态管理装饰器体系、布局引擎以及动画子系统均经历了深度优化,使得复杂交互场景下的渲染性能与开发体验达到了全新的高度。

ArkTS语言的核心设计哲学在于"声明式描述UI结构,响应式驱动状态更新"。与传统的命令式UI编程不同,开发者不再需要手动操作DOM节点或调用增量更新接口,而是通过@Component装饰器标注自定义组件,在build()方法中以声明式语法描述UI的当前状态。当@State@Prop@Link等状态装饰器修饰的变量发生变化时,ArkUI框架的差分渲染引擎会自动计算出最小的UI更新补丁,并批量应用到组件树上。这种"状态驱动视图"的模式从根源上消除了状态与视图之间的同步不一致风险。

在HarmonyOS 6.1.1中,ArkUI框架引入了多项关键改进。首先是布局引擎的性能优化,Column、Row、Stack等基础容器组件的测量与布局流程经过了原生代码级的加速,在处理大规模列表和嵌套容器场景时帧率更加稳定。其次是动画系统的完善,animation属性修饰器和animateTo全局API提供了更丰富的动画曲线与回调机制。此外,API 24还增强了ForEach渲染控制组件的键值生成策略和diff算法效率,在大数据量场景下的列表更新性能有显著提升。

声明式UI范式在ArkTS中的实现依赖于编译期的代码转换。ArkTS编译器会将build()方法中声明的DSL语法链式调用转换为高效的组件创建指令。例如,Text('hello').fontSize(14)在编译期会被转换为创建Text组件实例并设置fontSize属性的字节码。这种编译期转换策略避免了运行时解析的开销,使得ArkUI的首次渲染速度和后续更新速度都具备了接近原生代码的执行效率。

ArkTS的类型系统在TypeScript基础上进一步收紧。在ArkUI上下文中,所有组件属性方法均接受强类型参数。例如fontSize()方法接受numberLength类型,fontColor()接受ResourceColor类型(即Color枚举值或字符串)。这种严格的类型约束在编译期即可捕获绝大多数的类型错误,大幅降低了运行时崩溃的风险。同时,ArkTS对interfaceclass的使用也施加了特定约束,例如接口属性不能使用!非空断言修饰符,且对象字面量必须符合接口定义的结构类型。

状态管理是ArkUI架构的核心支柱。@State装饰器为组件创建响应式状态变量,当变量被重新赋值时触发组件的重新渲染。@Prop用于父到子的单向同步,@Link用于父子双向同步。在HarmonyOS 6.1.1中,这些装饰器的实现底层依赖于ObservedObject的代理机制:框架会拦截被装饰变量的getter/setter,在setter被调用时通知所有依赖该变量的UI节点进行更新。这种细粒度的依赖追踪机制确保了状态更新时只有真正受影响的UI片段会重新渲染。

本文将以一个完整的社区团购电商应用——"多多买菜"为案例,深入剖析HarmonyOS ArkTS API 24在实际业务场景中的架构设计、状态管理策略、组件复用模式以及复杂交互的实现方案。该应用模拟了拼多多旗下"多多买菜"社区团购业务的核心功能,包含首页瀑布流商品展示、分类分栏浏览、购物袋管理、订单全生命周期追踪、个人中心数据可视化等模块,并集成了6种不同类型的弹窗交互、柱状图数据可视化、进度条动画等丰富的UI效果。

二、应用业务背景:社区团购次日自提电商模型

社区团购是一种以社区为单位、以自提点为履约终端的电商模式。用户在前一天下单,商品在次日清晨配送至社区附近的指定自提点,用户凭借提货码到自提点取货。这种模式省去了"最后一公里"的配送成本,通过集中采购和统仓统配实现了极致的价格优势。

多多买菜应用完整模拟了这一业务流程。应用采用底部双排8 Tab的导航架构:上排为运营位(签到、领券、客服),下排为主功能位(首页、分类、购物袋、订单、我的)。这种设计借鉴了拼多多成熟的运营策略,通过运营位引导用户参与营销活动,通过主功能位完成核心交易闭环。

应用包含18个SKU商品,覆盖时令水果、新鲜蔬菜、肉禽蛋品、海鲜水产、乳品烘焙、粮油调味、休闲零食、日用百货等8大品类。每个商品携带价格、原价、销量、标签、分类、单位等完整属性。此外还有6个秒杀商品、6个购物袋商品、8条订单记录、3个自提点,构成了一个完整的电商数据生态。

在交互层面,应用实现了6种弹窗:规格选择底部抽屉(含数量步进器)、提货码中央弹窗、新增备注表单、编辑自提点单选列表、删除订单警示框、缺货赔付明细卡。这些弹窗覆盖了电商场景中绝大多数的模态交互模式,每一种都采用了不同的定位策略和布局方案。
在这里插入图片描述

三、架构层次总览

入口组件 DuoDuoGroceryApp

内容区域 contentArea

底部双排Tab导航

运营排 Row

主排 Row

签到 promoTabItem

领券 promoTabItem

客服 promoTabItem

首页 GroceryHomeContent

分类 CategorySplitContent

购物袋 ShoppingCartContent

订单 GroceryOrderContent

我的 GroceryMineContent

应用的整体架构清晰分为三层。最顶层是入口组件DuoDuoGroceryApp,它持有activeTab状态变量控制当前显示的页面,同时管理cartCount购物袋数量角标。中间层是内容区域contentArea,通过条件渲染根据activeTab的值动态切换5个页面组件。最底层是底部导航栏,包含运营排和主排两个Row容器。

入口组件采用@Entry装饰器标注,表示这是应用的根入口。@Component装饰器将其声明为ArkUI自定义组件。@State activeTab@State cartCount两个状态变量分别管理当前激活的Tab索引和购物袋商品数量。当用户点击底部Tab时,activeTab被更新,触发contentArea的条件渲染分支切换,从而显示对应的页面内容。

四、类型定义深度解析

ArkTS的类型系统是其区别于普通JavaScript的核心特征之一。应用首先定义了5个interface接口,为整个应用的数据层提供了严格的类型契约。

4.1 商品类型GoodsItem

interface GoodsItem {
  id: number
  name: string
  icon: string
  price: number
  original: number
  sold: string
  tag: string
  category: string
  unit: string
}

在这里插入图片描述

GoodsItem接口定义了商品的核心数据结构。id为数字类型的唯一标识符,用于ForEach渲染时的键值生成和商品查找。nameicon分别存储商品名称和Emoji图标——在真实业务中icon通常指向图片资源URL,此处使用Emoji是为了简化演示。priceoriginal为数字类型的现价与原价,类型选择number而非string是因为后续需要进行toFixed(2)格式化和金额计算。

sold字段定义为string类型而非number,这是一个值得注意的设计决策。在电商场景中,销量数字经常以"2.3万+“这样的格式展示,纯数字类型无法直接表达这种"万级+模糊后缀"的语义。因此将sold定义为字符串,直接存储展示文本,避免了在渲染时进行数字到文本的格式化转换。tag存储营销标签文本如"新人价”“热销”,category用于分类筛选,unit存储计量单位如"份"“箱”“盒”。

在ArkTS的接口约束下,任何声明为GoodsItem类型的对象字面量必须包含以上所有属性,且每个属性的类型必须与定义一致。编译器会在编译期检查所有对象字面量的结构合规性。如果某个商品缺少unit属性,或price被赋值为字符串,编译器会报错并阻止构建。

4.2 购物袋类型CartItem

interface CartItem {
  id: number
  name: string
  icon: string
  price: number
  count: number
  unit: string
  stock: number
}

CartItem接口描述购物袋中的商品条目。与GoodsItem相比,它去掉了originalsoldtagcategory字段——因为购物袋中不需要展示原价、销量、标签和分类信息。新增了count(购买数量)和stock(库存数量)两个字段。stock为0表示该商品暂时缺货,这一状态在UI层会触发"缺货赔付"弹窗的展示逻辑。

这种"按场景定制接口"的设计方式体现了ArkTS类型系统的灵活性。不同页面关注的数据维度不同,使用不同的接口定义可以避免携带冗余字段,同时让类型约束更加精准。

4.3 订单类型OrderItem

interface OrderItem {
  id: number
  no: string
  goodsText: string
  icon: string
  amount: number
  count: number
  status: string
  time: string
  station: string
}

OrderItem接口定义了订单的数据结构。no为订单编号字符串如"DD20260826001",采用字符串类型而非数字,因为订单编号通常包含日期信息和前缀,不适合用数字类型。goodsText是商品摘要文本如"蓝莓2盒、土鸡蛋20枚、鲜牛奶3组 等5件",将多商品信息压缩为一条可读字符串,这是电商订单列表的常见展示策略。

amount为订单总金额,count为商品件数,status为订单状态字符串。status字段使用字符串而非枚举,值为"待提货"“已提货”"退款中"等。在更严格的架构中,应使用联合类型或枚举来约束status的取值范围,但当前设计通过运行时条件判断实现状态路由,在可读性和类型安全性之间取得了平衡。time存储下单时间文本,station存储自提点名称。

4.4 秒杀商品类型SecKillItem

interface SecKillItem {
  id: number
  name: string
  icon: string
  price: number
  original: number
  progress: number
}

SecKillItem接口专为秒杀商品设计。progress字段存储抢购进度百分比(0-100),用于在UI层渲染进度条。这是秒杀场景特有的数据维度——通过展示"已抢78%"这样的信息制造紧迫感,驱动用户快速决策。

4.5 自提点类型StationItem

interface StationItem {
  id: number
  name: string
  distance: string
  address: string
  hours: string
}

在这里插入图片描述

StationItem接口定义了自提点的数据结构。distance存储距离文本如"350m",使用字符串类型而非数字,原因与sold字段类似——距离值可能携带"m"或"km"等不同单位后缀,统一存储为字符串避免了单位转换逻辑。hours存储营业时间如"06:00 - 22:00"。

五、静态数据层架构

纯函数

数据池

GOODS_ALL 18条商品

CATEGORY_LIST 9个分类

SECKILL_LIST 6条秒杀

CART_LIST 6条购物袋

ORDER_LIST 8条订单

STATION_LIST 3个自提点

getGoodsByCategory

筛选结果 GoodsItem数组

应用的数据层完全由文件顶部的const常量定义,采用静态数据池的模式。这种模式在原型开发和功能演示中非常高效——所有数据在编译期即已确定,无需异步网络请求,UI可以同步渲染。

GOODS_ALL是一个包含18条GoodsItem记录的数组常量,覆盖了8大品类。每条记录的icon使用Emoji字符如’🫐’‘🍌’‘🍎’,在真实应用中这些会被替换为网络图片URL。商品价格从1.99元到49.9元不等,原价设置在现价的1.4-2倍区间,符合社区团购的折扣定价策略。

CATEGORY_LIST是一个字符串数组,包含9个分类项:8个品类名称加上1个"全部商品"选项。在ArkTS中,数组字面量['时令水果', '新鲜蔬菜', ...]的类型会被推断为string[],与const声明结合后成为一个不可变引用的字符串数组。注意ArkTS中const修饰的是引用不可变性,即变量不能重新赋值,但数组内容理论上仍可修改(不过在当前代码中没有任何地方修改这些常量数组的内容)。

5.1 分类筛选纯函数getGoodsByCategory

function getGoodsByCategory(cat: string): GoodsItem[] {
  const result: GoodsItem[] = []
  for (let i = 0; i < GOODS_ALL.length; i++) {
    if (cat === '全部商品' || GOODS_ALL[i].category === cat) {
      result.push(GOODS_ALL[i])
    }
  }
  return result
}

在这里插入图片描述

getGoodsByCategory是一个全局纯函数,接受分类名称字符串,返回该分类下的商品数组。函数内部使用传统的for循环遍历GOODS_ALL数组,通过条件判断筛选匹配分类的商品。当传入"全部商品"时,由于短路或运算符||的特性,条件直接为true,返回所有商品。

这个函数定义在组件外部,是ArkTS允许的顶层函数声明。在ArkTS中,function声明的函数可以在任何作用域定义,包括文件顶层。全局函数不依赖于组件实例,可以在任意组件的build()方法中直接调用。这种"数据逻辑与UI逻辑分离"的设计模式使得代码结构更加清晰,数据筛选逻辑可以独立测试和复用。

值得注意的是,该函数每次调用都会创建一个新的result数组。在ArkUI的响应式系统中,由于函数在build()方法内被调用时,其返回值直接参与UI渲染,每次重新渲染都会重新执行筛选。对于18条数据来说,这个开销可以忽略,但在大数据量场景下应考虑缓存优化。

六、入口组件与底部双排Tab导航深度剖析

运营排

主排

HOME

CATEGORY

CART

ORDER

MINE

用户点击Tab

哪个Tab?

签到/领券/客服

activeTab不变 仅视觉反馈

activeTab赋值

渲染 GroceryHomeContent

渲染 CategorySplitContent

渲染 ShoppingCartContent

渲染 GroceryOrderContent

渲染 GroceryMineContent

contentArea条件分支更新

ArkUI差分渲染引擎计算最小更新

页面切换完成

6.1 枚举定义GroceryTab

enum GroceryTab {
  HOME = 0,
  CATEGORY = 1,
  CART = 2,
  ORDER = 3,
  MINE = 4
}

在这里插入图片描述

ArkTS支持TypeScript风格的enum枚举类型。GroceryTab枚举定义了5个主Tab的索引值,分别为首页(0)、分类(1)、购物袋(2)、订单(3)、我的(4)。使用枚举而非魔法数字(如直接用0、1、2)的好处在于:代码可读性大幅提升,activeTab === GroceryTab.HOMEactiveTab === 0语义清晰得多;同时枚举值在编译期即被替换为数字常量,运行时无额外开销。

6.2 入口组件结构

@Entry
@Component
struct DuoDuoGroceryApp {
  @State activeTab: GroceryTab = GroceryTab.HOME
  @State cartCount: number = 10
  // ...
}

@Entry装饰器标记DuoDuoGroceryApp为应用的入口组件,整个页面树的根节点。@Component装饰器声明它是一个ArkUI自定义组件。这两个装饰器通常配合使用:@Entry确保该组件在应用启动时被自动渲染为页面根节点,@Component使其具备了自定义组件的全部能力(如build()方法、@Builder方法、状态装饰器等)。

@State activeTab: GroceryTab = GroceryTab.HOME声明了一个响应式状态变量,初始值为首页。当activeTab的值被修改(例如用户点击Tab后执行this.activeTab = tab),ArkUI框架会检测到变化并重新执行依赖此变量的UI分支——即contentArea中的条件渲染逻辑。

@State cartCount: number = 10管理购物袋商品数量角标。初始值为10,表示购物袋中已有10件商品。这个值会影响底部Tab栏购物袋图标的角标显示:当cartCount > 0时,购物袋图标右上角显示红色数字角标。

6.3 内容区域条件渲染contentArea

@Builder contentArea() {
  Column() {
    if (this.activeTab === GroceryTab.HOME) {
      GroceryHomeContent()
    } else if (this.activeTab === GroceryTab.CATEGORY) {
      CategorySplitContent()
    } else if (this.activeTab === GroceryTab.CART) {
      ShoppingCartContent()
    } else if (this.activeTab === GroceryTab.ORDER) {
      GroceryOrderContent()
    } else {
      GroceryMineContent()
    }
  }
  .layoutWeight(1)
}

@Builder装饰器是ArkUI中用于声明可复用UI构建方法的关键装饰器。被@Builder标注的方法不会创建新的组件实例,而是将其中的UI描述内联到调用处。这与@Component标注的自定义组件不同——@Component组件是独立的可复用单元,拥有自己的状态和生命周期;而@Builder方法更像是UI片段的"宏展开"。

contentArea方法内部使用if-else if-else条件语句根据activeTab的值渲染不同的页面组件。ArkTS支持在build()方法和@Builder方法中使用条件语句(if)和循环语句(forwhile)来控制UI的结构。这与React JSX中的条件渲染类似,但ArkTS的条件渲染是在ArkUI框架的DSL层面实现的,编译器会将if分支转换为条件节点。

activeTab变化时,ArkUI的差分渲染引擎会比较新旧组件树的结构差异。例如从GroceryHomeContent()切换到CategorySplitContent()时,引擎会卸载首页组件树并挂载分类页组件树。Column().layoutWeight(1)确保内容区域占据底部Tab栏之外的所有剩余空间。layoutWeight(1)是ArkUI Flex布局的弹性权重属性,值为1表示该组件在主轴方向上尽可能扩展以填充父容器的剩余空间。

关于@Builder装饰器的技术要点:@Builder方法可以接受参数,参数可以是基本类型、对象或函数(回调)。在本例中,contentArea不接受参数,直接访问组件的this.activeTab状态变量。@Builder方法内部的UI描述与build()方法完全一致的语法,可以使用所有ArkUI组件和属性方法。

6.4 运营位Tab项promoTabItem

@Builder promoTabItem(icon: string, label: string) {
  Column() {
    Text(icon).fontSize(15)
    Text(label).fontSize(9).fontColor('#2E7D32').margin({ top: 1 })
  }
  .layoutWeight(1)
  .alignItems(HorizontalAlign.Center)
  .padding({ top: 4, bottom: 4 })
  .backgroundColor('#E8F5E9')
  .onClick(() => { this.activeTab = this.activeTab })
}

在这里插入图片描述

promoTabItem是一个参数化的@Builder方法,接受iconlabel两个字符串参数,分别作为Tab的图标和文字。方法内部构建了一个Column容器,垂直排列图标Text和文字Text

Column是ArkUI中最基础的布局容器之一,其作用是将子组件在垂直方向(从上到下)依次排列。Column的常用属性包括:alignItems设置子组件在水平方向的对齐方式(HorizontalAlign.Start居左、HorizontalAlign.Center居中、HorizontalAlign.End居右);justifyContent设置子组件在垂直方向的分布方式(FlexAlign.Start顶部对齐、FlexAlign.Center居中、FlexAlign.SpaceBetween两端对齐等);layoutWeight设置弹性权重。

此处Column().alignItems(HorizontalAlign.Center)使图标和文字水平居中对齐。padding({ top: 4, bottom: 4 })设置了上下4vp的内边距。backgroundColor('#E8F5E9')设置浅绿色背景。layoutWeight(1)使该Tab项在父Row中均分宽度——3个运营位Tab各占1/3宽度。

onClick(() => { this.activeTab = this.activeTab })是一个"空操作"——将activeTab赋值给它自身。这实际上是运营位Tab的占位逻辑,表示签到、领券、客服这些功能暂未实现页面跳转。在真实应用中,这里应替换为打开对应功能页面的导航逻辑。

6.5 主Tab项bottomTabItem与角标

@Builder bottomTabItem(icon: string, label: string, tab: GroceryTab) {
  Column() {
    Stack() {
      Column() {
        Text(icon).fontSize(19).opacity(this.activeTab === tab ? 1.0 : 0.42)
      }
      .alignItems(HorizontalAlign.Center)
      if (tab === GroceryTab.CART && this.cartCount > 0) {
        Text(this.cartCount.toString()).fontSize(8).fontColor('#FFFFFF')
          .backgroundColor('#E53935').borderRadius(8)
          .padding({ left: 4, right: 4, top: 1, bottom: 1 })
          .position({ x: 22, y: -4 })
      }
    }
    .height(24)
    Text(label).fontSize(9)
      .fontColor(this.activeTab === tab ? '#07C160' : '#999999')
      .fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
      .margin({ top: 1 })
  }
  .layoutWeight(1)
  .alignItems(HorizontalAlign.Center)
  .padding({ top: 5, bottom: 5 })
  .onClick(() => { this.activeTab = tab })
}

在这里插入图片描述

bottomTabItem是主Tab项的构建方法,比运营位Tab更复杂。它接受3个参数:icon图标、label文字、tab对应的枚举值。方法内部使用了Stack容器来叠加图标和数量角标。

Stack是ArkUI的层叠布局容器,其作用是将子组件堆叠在一起,后声明的子组件覆盖在先声明的子组件之上。Stack的常用属性包括:alignContent设置子组件的对齐方式(默认为Alignment.Center居中);可以通过子组件的position属性实现绝对定位。在当前代码中,Stack内放置了图标Column和(条件渲染的)角标Text,角标通过position({ x: 22, y: -4 })绝对定位到图标右上方,实现了红点角标效果。

opacity(this.activeTab === tab ? 1.0 : 0.42)通过三元表达式动态控制图标透明度。当前激活的Tab图标完全不透明(1.0),非激活Tab图标半透明(0.42),实现了选中态的视觉区分。opacity属性接受0到1之间的浮点数,0完全透明、1完全不透明。

角标的条件渲染逻辑if (tab === GroceryTab.CART && this.cartCount > 0)只有在当前Tab是购物袋且购物数量大于0时才显示角标。this.cartCount.toString()将数字转为字符串显示。backgroundColor('#E53935')设置红色背景,borderRadius(8)使角标呈圆角矩形。padding({ left: 4, right: 4, top: 1, bottom: 1 })设置内边距使文字与边缘有适当间距。position({ x: 22, y: -4 })将角标相对于Stack左上角偏移到右上方位置,y: -4为负值表示超出Stack上边界,营造角标"悬浮"在图标右上角的视觉效果。

文字标签的颜色和字重也通过三元表达式动态切换:激活态为绿色#07C160加粗FontWeight.Bold,非激活态为灰色#999999常规FontWeight.NormalFontWeight是ArkUI内置的字体粗细枚举,包含NormalBoldMediumLight等值。

onClick(() => { this.activeTab = tab })是Tab切换的核心逻辑。用户点击Tab后,将activeTab赋值为该Tab对应的枚举值,触发contentArea的条件渲染更新和所有bottomTabItem的选中态样式更新。这是ArkUI响应式状态管理的经典应用场景:一次状态变更同时驱动内容区域和导航栏的UI更新。

6.6 build方法的整体布局

build() {
  Column() {
    this.contentArea()
    Column() {
      Row() {
        this.promoTabItem('📅', '签到')
        this.promoTabItem('🎟️', '领券')
        this.promoTabItem('💬', '客服')
      }
      .width('100%').backgroundColor('#E8F5E9')
      Row() {
        this.bottomTabItem('🏠', '首页', GroceryTab.HOME)
        this.bottomTabItem('🗂️', '分类', GroceryTab.CATEGORY)
        this.bottomTabItem('🛒', '购物袋', GroceryTab.CART)
        this.bottomTabItem('📦', '订单', GroceryTab.ORDER)
        this.bottomTabItem('👤', '我的', GroceryTab.MINE)
      }
      .width('100%').backgroundColor('#FFFFFF')
      .padding({ top: 3, bottom: 6 })
      .shadow({ radius: 10, color: '#14000000', offsetY: -3 })
    }
    .width('100%')
  }
  .width('100%').height('100%')
  .backgroundColor('#F5FFF7')
}

build()方法是每个ArkUI自定义组件必须实现的核心方法,返回该组件的UI描述。入口组件的build()方法构建了一个最外层Column,内部依次包含内容区域和底部导航栏。

Row是ArkUI的另一个基础布局容器,与Column相对,其作用是将子组件在水平方向(从左到右)依次排列。Row的常用属性包括:alignItems设置子组件在垂直方向的对齐方式(VerticalAlign.Top顶对齐、VerticalAlign.Center居中、VerticalAlign.Bottom底对齐);justifyContent设置子组件在水平方向的分布方式。

底部导航栏由两个Row组成:上方的运营排和下方的主排。运营排Row使用浅绿色背景#E8F5E9,内含3个promoTabItem。主排Row使用白色背景,内含5个bottomTabItem

shadow({ radius: 10, color: '#14000000', offsetY: -3 })为主排Tab栏添加阴影效果。radius控制模糊半径(值越大阴影越模糊),color使用8位十六进制颜色#14000000——前两位14为alpha通道(约8%不透明度),后六位000000为黑色。offsetY: -3使阴影向上偏移3vp,模拟光线从下方照射的效果,使Tab栏产生"悬浮"在内容上方的视觉层次感。

外层Column设置width('100%').height('100%')使其占满整个屏幕。backgroundColor('#F5FFF7')设置极浅绿色背景作为应用底色。widthheight属性接受字符串(百分比或带单位的长度值)或数字(默认单位vp)。'100%'表示占满父容器。

七、首页模块深度剖析

首页布局结构

Stack根容器

Scroll滚动容器

条件: specModal弹窗

头部渐变区

Banner横滑区

金刚区2行4列

今日秒杀横滑

猜你喜欢标题

双列瀑布流

定位信息

搜索栏

新人1分钱

限时秒杀

次日达免运费

ForEach秒杀商品

进度条动画

左列9个goodsCard

右列9个goodsCard

7.1 首页组件状态定义

@Component
struct GroceryHomeContent {
  @State showSpec: boolean = false
  @State specCount: number = 1
  @State selectedGoods: GoodsItem = GOODS_ALL[0]
  @State specUnit: string = '1份'
  // ...
}

首页组件GroceryHomeContent使用@Component装饰器声明为自定义组件(注意没有@Entry,因为它不是入口组件,而是被contentArea调用的子组件)。它管理4个响应式状态变量。

@State showSpec: boolean = false控制规格选择弹窗的显示/隐藏。boolean类型的初始值为false表示弹窗默认不显示。当用户点击商品卡片的"+"按钮时,showSpec被设为true,弹窗渲染显示。

@State specCount: number = 1管理规格弹窗中数量步进器的当前值,初始为1。用户在弹窗中点击"+"或"−"按钮时修改此值,弹窗中的数量显示文本同步更新。

@State selectedGoods: GoodsItem = GOODS_ALL[0]存储当前选中的商品对象。初始值为GOODS_ALL数组的第一个元素。当用户点击不同商品时,selectedGoods被重新赋值,弹窗中的商品名称、价格、销量等信息同步更新。这里体现了ArkTS状态管理的重要特性:@State修饰的引用类型变量,当整个引用被替换时(this.selectedGoods = g),框架检测到引用变化并触发重新渲染。

@State specUnit: string = '1份'管理规格弹窗中选中的规格单位文本。用户可以在"1份"“2份装”"家庭装"三个选项间切换,选中项的背景色变为绿色,文字变为白色。

7.2 遮罩层Builder modalOverlay

@Builder modalOverlay(onClose: () => void) {
  Column()
    .width('100%').height('100%')
    .backgroundColor('rgba(10,50,20,0.55)')
    .onClick(onClose)
}

modalOverlay是一个参数化的@Builder方法,接受一个onClose回调函数作为参数。它构建一个全屏半透明遮罩层ColumnbackgroundColor('rgba(10,50,20,0.55)')使用RGBA颜色格式,最后一个参数0.55表示55%不透明度,实现半透明遮罩效果。颜色值(10,50,20)偏向深绿色,与应用的绿色主题协调。

onClick(onClose)将点击事件直接绑定到传入的回调函数。这个设计实现了遮罩层的通用化——任何弹窗都可以复用这个遮罩层,只需传入自己的关闭逻辑。点击遮罩层时调用onClose回调,通常将对应的show*状态变量设为false来关闭弹窗。

在ArkTS中,@Builder方法接受函数类型的参数是合法的。onClose: () => void表示一个无参数无返回值的回调函数。这种"将行为作为参数传入"的设计模式使得UI组件的可复用性大幅提升。

7.3 规格选择弹窗specModal

用户点击商品+

设置selectedGoods

重置specCount=1 specUnit=1份

showSpec=true

specModal渲染

modalOverlay遮罩层

底部抽屉Column

商品信息行: 图标+价格+库存

规格选择行: 1份/2份装/家庭装

数量步进行: - 数字 +

底部按钮: 加入购物袋/立即下单

点击遮罩 showSpec=false

点击按钮 showSpec=false

规格选择弹窗是应用中最复杂的弹窗之一,采用底部抽屉式设计,包含商品信息、规格选择、数量步进和操作按钮四个区域。

@Builder specModal() {
  Column() {
    this.modalOverlay(() => { this.showSpec = false })
    Column() {
      Row() {
        Column() {
          Text(this.selectedGoods.icon).fontSize(40)
        }
        .width(84).height(84).backgroundColor('#E8F5E9').borderRadius(12)
        .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
        Column() {
          Text('¥' + this.selectedGoods.price.toFixed(2)).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#E53935')
          Text('已选:' + this.specUnit + ' × ' + this.specCount.toString()).fontSize(11).fontColor('#666666').margin({ top: 3 })
          Text('库存 ' + this.selectedGoods.sold + '人已买').fontSize(10).fontColor('#BDBDBD').margin({ top: 2 })
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 12 })
        Text('✕').fontSize(16).fontColor('#999999')
          .onClick(() => { this.showSpec = false })
      }
      .width('100%').padding(16)
      // ...规格选择和数量步进...
    }
    .width('100%').backgroundColor('#FFFFFF')
    .borderRadius({ topLeft: 22, topRight: 22 })
    .position({ x: 0, y: '55%' })
  }
  .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}

弹窗的最外层是一个Column,包含两部分:遮罩层modalOverlay和实际内容Column。外层Column设置position({ x: 0, y: 0 })使其脱离正常文档流,从屏幕左上角开始定位。zIndex(999)设置层级为999,确保弹窗覆盖在所有正常流组件之上。zIndex属性在ArkUI中用于控制兄弟组件的渲染顺序,值越大的组件越晚渲染(即在视觉上覆盖在上方)。

内容Column使用position({ x: 0, y: '55%' })将其固定在屏幕55%高度处,即从屏幕中偏下位置开始向上展开。这种底部抽屉定位方式是移动端弹窗的经典设计模式。borderRadius({ topLeft: 22, topRight: 22 })只设置左上和右上的圆角,营造从底部"滑出"的视觉效果。

商品信息行使用Row水平排列:左侧84×84的商品图标方块,中间的价格和库存信息,右侧的关闭按钮。Column内的商品图标使用justifyContent(FlexAlign.Center)实现图标在垂直方向的居中。FlexAlign是ArkUI Flex布局的对齐枚举,Center表示居中对齐。

价格文本Text('¥' + this.selectedGoods.price.toFixed(2))使用字符串拼接和toFixed(2)方法将价格格式化为两位小数。toFixed()是JavaScript/TypeScript的Number原型方法,在ArkTS中同样可用,返回保留指定小数位数的字符串表示。fontSize(20).fontWeight(FontWeight.Bold).fontColor('#E53935')设置20vp大字号、加粗、红色(电商价格标配色)。

规格选择区域
Column() {
  Text('规格').fontSize(12).fontColor('#666666').width('100%')
  Row() {
    Text('1份').fontSize(12)
      .fontColor(this.specUnit === '1份' ? '#FFFFFF' : '#333333')
      .backgroundColor(this.specUnit === '1份' ? '#07C160' : '#F5F5F5')
      .borderRadius(8).padding({ left: 16, right: 16, top: 6, bottom: 6 }).margin({ right: 8 })
      .onClick(() => { this.specUnit = '1份' })
    Text('2份装').fontSize(12)
      .fontColor(this.specUnit === '2份装' ? '#FFFFFF' : '#333333')
      .backgroundColor(this.specUnit === '2份装' ? '#07C160' : '#F5F5F5')
      .borderRadius(8).padding({ left: 16, right: 16, top: 6, bottom: 6 }).margin({ right: 8 })
      .onClick(() => { this.specUnit = '2份装' })
    Text('家庭装').fontSize(12)
      .fontColor(this.specUnit === '家庭装' ? '#FFFFFF' : '#333333')
      .backgroundColor(this.specUnit === '家庭装' ? '#07C160' : '#F5F5F5')
      .borderRadius(8).padding({ left: 16, right: 16, top: 6, bottom: 6 })
      .onClick(() => { this.specUnit = '家庭装' })
  }
  .margin({ top: 8 })
}

规格选择区域展示了ArkUI中"按钮组"的常见实现方式。三个Text组件作为规格选项,每个选项的样式通过三元表达式动态切换:选中时白字绿底(#FFFFFF文字 + #07C160背景),未选中时深字灰底(#333333文字 + #F5F5F5背景)。borderRadius(8)使选项呈圆角矩形。paddingmargin的组合控制选项的内部留白和选项间距。

每个选项的onClick回调将该选项的规格文本赋值给specUnit状态变量。当specUnit变化时,三个选项的样式条件重新求值,选中态自动切换到新选中的选项。这是ArkUI响应式系统在交互场景中的典型应用——通过状态变量驱动UI样式,无需手动操作DOM或调用样式API。

数量步进器
Row() {
  Text('数量').fontSize(13).fontColor('#333333').layoutWeight(1)
  Row() {
    Text('−').fontSize(18).fontColor('#666666')
      .width(30).height(30).backgroundColor('#F5F5F5').borderRadius(15)
      .textAlign(TextAlign.Center)
      .onClick(() => {
        if (this.specCount > 1) { this.specCount = this.specCount - 1 }
      })
    Text(this.specCount.toString()).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#333333')
      .width(44).textAlign(TextAlign.Center)
      .scale({ x: 1.0, y: 1.0 })
    Text('+').fontSize(18).fontColor('#FFFFFF')
      .width(30).height(30).backgroundColor('#07C160').borderRadius(15)
      .textAlign(TextAlign.Center)
      .onClick(() => { this.specCount = this.specCount + 1 })
  }
}

数量步进器由减号按钮、数字显示和加号按钮三部分组成。减号按钮的onClick逻辑包含边界检查if (this.specCount > 1),确保数量不会降到1以下。加号按钮无上限检查(在真实应用中应增加库存上限校验)。

TextAlign.Center是ArkUI的文本对齐枚举,使文字在组件范围内居中。减号按钮使用灰色背景#F5F5F5表示"次要操作",加号按钮使用绿色背景#07C160表示"主要操作",这种视觉区分符合电商场景中鼓励用户增加购买量的设计意图。

scale({ x: 1.0, y: 1.0 })是ArkUI的变换属性,可以缩放组件。当前值为1.0表示无缩放。在更高级的实现中,可以通过animateTo在数量变化时触发缩放动画,实现"点击+数字弹跳"的微交互效果。

底部操作按钮
Row() {
  Text('加入购物袋').fontSize(14).fontColor('#07C160')
    .layoutWeight(1).textAlign(TextAlign.Center)
    .backgroundColor('#E8F5E9').borderRadius(20)
    .padding({ top: 11, bottom: 11 })
    .onClick(() => { this.showSpec = false })
  Text('立即下单').fontSize(14).fontColor('#FFFFFF')
    .layoutWeight(1).textAlign(TextAlign.Center)
    .backgroundColor('#07C160').borderRadius(20)
    .padding({ top: 11, bottom: 11 }).margin({ left: 10 })
    .onClick(() => { this.showSpec = false })
}

底部两个按钮使用layoutWeight(1)均分宽度,中间用margin({ left: 10 })留出间距。"加入购物袋"使用浅绿底绿字(次要操作),"立即下单"使用实心绿底白字(主要操作)。两个按钮的onClick都只是关闭弹窗(在真实应用中应执行加入购物车或下单的业务逻辑)。

7.4 商品瀑布卡goodsCard

goodsCard Builder

Column卡片容器

Column图标区 90高度

Text商品名 maxLines=2

Row标签行

Row价格+加购按钮

Text Emoji fontSize 46

Text 标签 红底

Text 销量 灰字

Column 价格+原价划线

Column +按钮

Text ¥ + 价格

Text 原价 划线装饰

onClick 触发specModal

@Builder goodsCard(g: GoodsItem) {
  Column() {
    Column() {
      Text(g.icon).fontSize(46)
    }
    .width('100%').height(90).backgroundColor('#F1F8E9')
    .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
    Text(g.name).fontSize(12).fontColor('#333333').maxLines(2)
      .width('100%').margin({ top: 6 })
    Row() {
      Text(g.tag).fontSize(8).fontColor('#E53935').backgroundColor('#FFEBEE')
        .padding({ left: 4, right: 4, top: 1, bottom: 1 }).borderRadius(4)
      Text(g.sold + '人付款').fontSize(9).fontColor('#BDBDBD').margin({ left: 4 })
    }
    .width('100%').margin({ top: 4 })
    Row() {
      Column() {
        Row() {
          Text('¥').fontSize(10).fontColor('#E53935')
          Text(g.price.toFixed(2)).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E53935')
        }
        Text('¥' + g.original.toFixed(2)).fontSize(9).fontColor('#CCCCCC')
          .decoration({ type: TextDecorationType.LineThrough })
      }
      .alignItems(HorizontalAlign.Start).layoutWeight(1)
      Column() {
        Text('+').fontSize(14).fontColor('#FFFFFF')
          .width(24).height(24).borderRadius(12)
          .backgroundColor('#07C160').textAlign(TextAlign.Center)
      }
      .onClick(() => {
        this.selectedGoods = g
        this.specCount = 1
        this.specUnit = '1份'
        this.showSpec = true
      })
    }
    .width('100%').alignItems(VerticalAlign.Bottom).margin({ top: 6 })
  }
  .width('48%').backgroundColor('#FFFFFF').borderRadius(12).padding(8)
  .margin({ left: 4, right: 4, top: 8 })
  .onC
  goodsText: string
  icon: string
  amount: number
  count: number


      if (this.showStation) { this.stationModal() }
    }
    .width('100%').height('100%')
  }
}


在这里插入图片描述

19.10 总结:

应用实现了6种弹窗交互、步进器交互、Tab切换交互、列表选择交互、文本输入交互等多种交互模式。所有交互都遵循ArkUI的"事件触发→状态更新→UI重渲染"三步范式:用户操作触发onClick/onChange事件回调,回调中修改@State变量,框架自动重新渲染受影响的UI。这种统一的交互处理范式使得不同类型的交互(点击、输入、选择)在代码层面具有一致的结构,降低了理解和维护成本。

综上所述,本应用全面展示了HarmonyOS ArkTS API 24在电商场景中的技术实践能力。从声明式UI范式到响应式状态管理,从布局容器体系到弹窗架构设计,从数据可视化到动画特效,HarmonyOS 6.11的ArkUI框架提供了一套完整、高效、类型安全的移动应用开发解决方案。开发者通过掌握@State/@Builder/@Component装饰器体系、Column/Row/Stack布局容器、ForEach/if渲染控制、linearGradient/shadow/animation视觉属性等核心技术,即可构建出功能丰富、交互流畅、视觉精美的移动应用。

Logo

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

更多推荐