【共创季稿事节】鸿蒙原生 ArkTS 布局实战:使用 Stack 实现商品 Tag 标签叠加 鸿蒙原生 ArkTS 布局实战使用 Stack 实现商品 Tag 标签叠加一、背景与需求在移动端电商应用中商品卡片是最基础也最重要的信息载体。一个典型的商品卡片通常包含以下视觉层次商品图片— 占据卡片主要区域直观展示商品外观标签信息— 在图片之上叠加各种营销标签如新品、“-30%”、“热卖”价格信息— 展示原价、折后价、促销价等传统的前端布局方案实现图片 标签叠加通常需要position: absolute配合z-index来控制层级。而在鸿蒙 ArkUI 框架中Stack容器正是为解决此类层叠布局场景而设计的原生组件。本文将从零开始完整实现一个具备 4 种不同标签组合的商品卡片展示页面并深入解析 Stack 布局的核心机制与 ArkTS 开发中的关键注意事项。二、Stack 布局核心概念2.1 什么是 StackStack是 ArkUI 提供的一种堆叠容器其内部子组件按照书写顺序从底层到顶层依次堆叠。也就是说第一个子组件位于最底层Z 轴的最下方最后一个子组件位于最顶层Z 轴的最上方上层组件会覆盖下层组件的重叠区域这与 Web 开发中position: relative容器内放置position: absolute子元素的思路类似但 ArkUI 的 Stack 是原生组件性能更优、语义更清晰。2.2 Stack 的核心属性属性类型默认值说明alignAlignmentAlignment.Center控制未显式定位的子组件在 Stack 内的对齐方式widthLength自适应容器宽度heightLength自适应容器高度2.3 子组件定位方式Stack 中的子组件有两种定位策略策略一通过父容器的 align 属性统一对齐在子组件上使用.align(Alignment.TopStart)将其定位到 Stack 的左上角。这种方式适用于标签栏这类整体需要对齐到某个边缘的区域。策略二通过 position 属性绝对定位在子组件上使用.position({ top: 8, right: 8 })或.position({ bottom: 0 })进行精确的像素级定位。这种方式适用于折扣标签、价格栏等需要固定在某个具体位置的元素。值得注意的是.align()和.position()可以混合使用在同一个 Stack 的不同子组件上这为复杂布局提供了极大的灵活性。三、项目架构与组件设计3.1 整体组件树Index (页面入口) ├── Scroll (可滚动容器) │ └── Column (垂直布局) │ ├── 页面标题 │ ├── 副标题 │ ├── ForEach (遍历商品列表) │ │ └── ProductCard (商品卡片) │ │ └── Stack (核心堆叠区域) │ │ ├── 商品图片 (底层) │ │ ├── 标签区域 (左上角) │ │ ├── 折扣标签 (右上角) │ │ └── 价格栏 (底部) │ └── LayoutTipsCard (布局知识卡片) │ └── TipRow × 5 (提示条目)3.2 组件职责划分整个页面共拆分为 5 个自定义组件和 1 个数据接口组件 / 接口职责复用性Product(interface)定义商品数据结构全局数据模型ProductTag封装单个标签的外观可复用新品/热卖/折扣ProductCard实现 Stack 核心堆叠逻辑可复用任意商品LayoutTipsCard展示布局知识点一次性提示TipRow单行提示条目可复用Index页面入口 数据提供页面级这种组件化的设计遵循了 ArkUI 的推荐实践每个组件只关注自己的职责数据通过属性public字段从父组件流向子组件。四、代码深度解析4.1 数据模型定义interfaceProduct{name:string;// 商品名称price:number;// 商品价格元discount:number;// 折扣比例0.7 7折0 无折扣isNew:boolean;// 是否为新品isHot:boolean;// 是否为热卖款bgColor:string;// 背景色十六进制字符串icon:string;// 商品图标Emoji 模拟图片}这里有一个需要注意的 ArkTS 特性颜色值建议使用string类型如#F44336而不是Color枚举。因为在数据列表中我们通常从 JSON 或服务端获取颜色字符串而Color枚举只能表示有限的几个预设值Color.Red、Color.Green等无法表示任意十六进制颜色。ArkUI 的.backgroundColor()方法同时接受string和Color类型所以使用string更灵活。4.2 ProductTag — 可复用的标签组件Componentstruct ProductTag{publictext:string;publicbgColor:string#FF0000;publicfontColor:string#FFFFFF;publictagRadius:number8;build(){Text(this.text).fontSize(12).fontColor(this.fontColor).fontWeight(FontWeight.Bold).padding({left:8,right:8,top:4,bottom:4}).backgroundColor(this.bgColor).borderRadius(this.tagRadius)}}关键设计决策属性用public而非private这是 ArkTS 的重要约束 — 父组件通过构造器语法ProductTag({ text: ..., bgColor: ... })传入的属性必须声明为public。如果用private修饰编译时会报 “Property ‘text’ is private and can not be initialized through the component constructor” 警告。属性名避免与系统方法冲突这里使用tagRadius而不是borderRadius因为borderRadius是CommonAttribute上的链式方法名。如果定义一个同名的public属性编译器会报类型冲突错误Property ‘borderRadius’ in type ‘ProductTag’ is not assignable to the same property in base type ‘CustomComponent’。提供合理的默认值每个属性都赋予默认值这样即使父组件未传入某些属性子组件也能正常渲染。4.3 ProductCard — Stack 布局的核心实现这是整个示例最核心的组件一个Stack内包含 4 个堆叠层第 1 层商品图片底层// 第 1 层底层模拟商品图片区域Column(){Text(this.product.icon).fontSize(72)Text(商品示意图).fontSize(14).fontColor(Color.White)}.width(100%).height(200).backgroundColor(this.product.bgColor).borderRadius(12).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)这是 Stack 中第一个子组件因此位于 Z 轴的最底层。在实际项目中这里会替换为Image组件加载真实的商品图片。本示例使用 Emoji 色块来模拟图片区域方便读者在不依赖图片资源的情况下运行和调试。第 2 层新品 / 热卖标签左上角// 第 2 层顶层左侧新品 / 热卖 标签Column(){if(this.product.isNew){ProductTag({text:✨ 新品,bgColor:#4CAF50,...}).margin({bottom:6})}if(this.product.isHot){ProductTag({text: 热卖,bgColor:#FF9800,...})}}.align(Alignment.TopStart)// ← 关键定位.padding({left:8,top:8})定位要点.align(Alignment.TopStart)将整个标签列定位到 Stack 的左上角。Alignment枚举支持 9 个方位TopStart、TopCenter、TopEnd、Start、Center、End、BottomStart、BottomCenter、BottomEnd可以满足绝大多数对齐需求。条件渲染通过if (this.product.isNew)和if (this.product.isHot)实现标签的按需显示。这种声明式的条件渲染比命令式的visibility切换更符合 ArkUI 的设计理念。第 3 层折扣标签右上角// 第 3 层顶层右侧折扣标签if(this.product.discount0){ProductTag({text:-${this.getDiscountPercent()}%,bgColor:#F44336,...}).position({top:8,right:8})// ← 绝对定位到右上角}定位要点使用.position({ top: 8, right: 8 })进行绝对定位。position的坐标相对于父容器Stack的左上角。这里我们将折扣标签固定在右上角与左上角的新品标签形成视觉对称。注意position会使元素脱离 Stack 的默认流式布局不会影响其他子组件的位置。这也是为什么折扣标签和标签列可以分别定位在右上角和左上角而互不干扰。第 4 层名称和价格栏底部// 第 4 层底部商品名称和价格Row(){Text(this.product.name).fontSize(16).fontColor(Color.White)Blank()// ... 价格显示逻辑}.position({bottom:0})// ← 固定在底部.width(100%).backgroundColor(Color.Gray).opacity(0.85).borderRadius({bottomLeft:12,bottomRight:12})定位要点.position({ bottom: 0 })将价格栏固定在 Stack 的底部。这里有一个巧妙的设计价格栏设置了.opacity(0.85)半透明背景让底部的商品图片可以隐约透出增加视觉层次感。Stack 容器配置Stack(){// ... 4 个子层}.width(100%).height(200).borderRadius(12)Stack 容器本身设置了固定的高度200vp和圆角12vp其内部的子组件通过width(100%)和height(100%)来继承容器的尺寸确保各层能够完全覆盖。4.4 计算逻辑提取// 在 ProductCard 组件中getDiscountPercent():number{returnMath.round((1-this.product.discount)*100);}getDiscountedPrice():number{returnthis.product.price*this.product.discount;}为什么要提取为方法这是 ArkTS 的一个重要约束在build()方法的 UI 描述区内不允许声明let/const变量。如果直接在 build() 中写let discountPercent ...编译会报 “Only UI component syntax can be written here” 错误。因此所有计算逻辑都必须提取为组件的方法或者使用 getter 属性在 build() 中通过this.getDiscountPercent()的形式调用。4.5 页面入口与数据驱动EntryComponentstruct Index{StateproductList:Product[][{name:北欧风沙发,price:2999,discount:0.7,isNew:true,isHot:false,...},{name:智能手表 Pro,price:1299,discount:0,isNew:true,isHot:true,...},{name:无线降噪耳机,price:899,discount:0.8,isNew:false,isHot:true,...},{name:极简台灯,price:199,discount:0.55,isNew:false,isHot:false,...},];build(){Scroll(){Column(){// 标题// 商品列表 → ForEach// 布局提示}.constraintSize({minHeight:100%})}}}数据驱动通过State装饰器声明商品列表数据实现响应式更新。当productList的内容发生变化时框架会自动重新渲染 UI。ForEach 遍历使用ForEach遍历渲染商品卡片每次迭代创建一个ProductCard实例并通过{ product: item }传入数据。constraintSize({ minHeight: 100% })这是替代minHeight的 ArkTS API 写法。Column组件没有直接的minHeight属性需要通过constraintSize来设置最小尺寸约束。五、ArkTS 开发的 8 个关键注意事项通过这个示例的编写和调试过程我总结了以下 ArkTS 开发中容易踩坑的要点5.1 组件属性必须用 public规则父组件通过构造器语法ChildComponent({ prop: value })传入的属性在子组件中必须声明为public。原因ArkTS 的类型系统和编译期检查要求构造器初始化的目标属性可公开访问。private属性只能在组件内部访问不能通过外部构造器写入。例外State、Prop、Link等装饰器修饰的属性不受此限制它们有自己的数据传递机制。5.2 避免属性名与系统方法冲突规则组件中声明public属性时避免使用与CommonAttribute同名的方法名如borderRadius、width、height、padding等。原因这些方法名在编译时会被视为对基类方法的覆盖override造成类型签名不匹配的错误。最佳实践给属性名加上业务前缀如tagRadius、cardWidth、contentPadding等。5.3 build() 方法内不能声明变量规则build()方法的直接代码块中UI 描述区只能包含组件构造和链式调用不能出现let、const声明。原因ArkUI 的声明式 UI 语法要求在 build() 中只能描述 UI 结构任何计算逻辑都应提取到方法或计算属性中。解决方案将计算逻辑提取为组件方法// ❌ 错误写法build(){letpricethis.product.price*this.product.discount;// 编译错误Text(${price})}// ✅ 正确写法getFinalPrice():number{returnthis.product.price*this.product.discount;}build(){Text(${this.getFinalPrice()})}5.4 颜色值使用 string 而非 Color规则如果需要灵活的颜色值尤其是从数据或配置中读取使用string类型如#FF0000、rgb(255,0,0)而不是Color枚举。原因Color枚举只包含有限的预设值Color.Red、Color.Green、Color.Blue等无法表达任意十六进制颜色。而.backgroundColor()、.fontColor()等 API 同时接受string和Color类型。5.5 使用 constraintSize 替代 minHeight规则在Column或Row上设置最小高度时使用.constraintSize({ minHeight: 100% })而非.minHeight(100%)。原因ArkUI 的Column属性集中没有直接的minHeight属性需要通过constraintSize对象来设置尺寸约束。5.6 Stack 的子组件顺序决定 Z 轴层级规则Stack 内先写的子组件在底层后写的在顶层。常见误区很多开发者会误以为后写的在底层类似 CSS 的z-index思维。实际上Stack 的层级顺序是先写 底层后写 顶层与 HTML 的默认堆叠顺序一致。5.7 align 与 position 的适用场景规则当子组件是一个容器如 Column内部包含多个元素需要整体定位时使用.align(Alignment.Xx)当子组件是单个元素需要精确的像素级定位时使用.position({ top, right, bottom, left })示例新品/Hot 标签列 →.align(Alignment.TopStart)— 整体对齐到左上角折扣标签 →.position({ top: 8, right: 8 })— 精确固定在右上角价格栏 →.position({ bottom: 0 })— 精确固定在底部5.8 使用 ForEach 遍历列表时注意键值规则ForEach的第三个参数可以指定键值生成函数帮助框架高效识别哪个列表项发生了变化。ForEach(this.productList,(item:Product){ProductCard({product:item})},(item:Product)item.name)// 以 name 作为唯一键虽然本示例未显式传入键值函数使用了默认索引但在实际生产项目中建议为列表项提供稳定的唯一键以优化 diff 性能。六、运行效果与验证6.1 编译验证在项目根目录执行以下命令hvigorw assembleApp --no-daemon编译结果BUILD SUCCESSFUL in 8 s 556 ms0 个编译错误0 个代码警告。6.2 预期运行效果页面启动后将展示页面标题“️ 商品展示 — Stack Tag 标签叠加”4 张商品卡片每张卡片展示不同的标签组合商品折扣标签背景色️ 北欧风沙发-30%✨ 新品墨绿⌚ 智能手表 Pro无折扣✨ 新品 热卖深蓝 无线降噪耳机-20% 热卖紫色 极简台灯-45%无标签暖木色布局要点提示卡片展示 Stack 布局的 5 个核心技巧6.3 交互验证页面支持纵向滚动可浏览所有商品卡片每个卡片的标签根据数据自动按需显示带折扣的商品同时显示原价带删除线和折后价七、扩展与优化方向7.1 替换为真实图片将第 1 层的 Emoji 图标替换为Image组件Image(this.product.imageUrl).objectFit(ImageFit.Cover)// 图片裁剪方式.width(100%).height(100%)7.2 添加动画效果为标签添加入场动画提升用户体验ProductTag({...}).transition({type:TransitionType.Insert,scale:{x:0,y:0}})7.3 支持更多标签类型扩展Product接口支持更多标签类型interfaceProduct{// ... 现有字段tags:string[];// 自定义标签数组badges:Badge[];// 角标列表}7.4 响应式适配根据屏幕尺寸动态调整卡片大小和标签位置使用breakpoint或mediaQueryif(this.isWideScreen()){// 平板大卡片 横向布局}else{// 手机小卡片 网格布局}八、总结本文通过一个完整的电商商品卡片案例深入解析了 HarmonyOS NEXT 中Stack布局的使用方法和 ArkTS 开发的核心要点。核心收获Stack 的核心价值在同一区域叠加多层 UI 元素通过.align()和.position()灵活控制每层位置Z 轴层级规则Stack 内子组件按书写顺序从底到顶堆叠组件化设计将标签封装为独立组件ProductTag提高代码复用性ArkTS 语法约束了解public属性、避免命名冲突、build() 内禁止声明变量等关键规则数据驱动 UI通过StateForEach实现列表渲染和响应式更新Stack 布局是鸿蒙 ArkUI 中最常用也最强大的容器之一掌握它的使用方式和最佳实践可以轻松应对各种层叠叠加的 UI 场景——不仅仅是商品标签还包括头像叠加、通知角标、遮罩层、工具提示Tooltip、模态引导等。希望本文能帮助你更好地理解和使用鸿蒙原生 Stack 布局在实际项目中打造出既美观又高性能的叠加 UI。