HarmonyOS6 半年磨一剑:RcTag 组件实战案例(一)内容展示与商品筛选

HarmonyOS6 半年磨一剑:RcTag 组件实战案例(一)内容展示与商品筛选

文章目录

Hello 各位开发者们大家好,我是若城,本篇是 RcTag 实战系列的第一篇文章,将通过两个真实业务场景,展示如何用 RcTag 构建内容详情页标签区域和电商商品分类筛选功能。


一、场景一:内容详情页标签展示

1.1 场景描述

文章详情页顶部展示文章分类标签、内容标签和难度标签,纯展示用途,无交互。这类需求在技术博客、内容社区、资讯类 App 中极为普遍,标签本身承载的是信息分类职责,设计上需要做到层次分明而不杂乱。

1.2 设计思路

这个场景的核心是在有限的屏幕宽度内,用视觉差异区分出三种信息层级:主分类(主语意最强)、难度(次要信息)、内容关键词(辅助信息)。

主分类标签采用实色填充,这是最高强调等级,抢先抓住用户视线。难度标签采用胶囊形状,缩小尺寸至 mini,既保持可读性又不抢占主分类的主导地位。内容关键词数量最多,若同样使用实色会造成视觉噪声,因此采用 rcTagPlain: true + rcTagPlainFill: true 的镂空填充风格——有背景色但背景透明度低,整体轻量,适合数量多的场景。

布局方面,第一行使用 Row 横向排列分类标签和难度标签,这两个标签数量固定,不会换行。第二行使用 Flex + FlexWrap.Wrap 展示内容关键词,标签数量不确定,自动换行是必要的。

1.3 完整代码

import{ RcTag }from'rchoui'@Entry@ComponentV2 struct ArticleDetailTagsDemo {// 文章元数据private articleCategory:string='鸿蒙开发'private articleTags:string[]=['ArkTS','ArkUI','组件库','UI设计','开源']private articleDifficulty:string='进阶'private articleDifficultyType:'primary'|'warning'|'error'='warning'build(){Column({ space:16}){// 文章标题Text('HarmonyOS6 组件库实战:RcTag 深度解析').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#1a1a1a')// 标签区域Column({ space:8}){// 第一行:主分类(实色)+ 难度(胶囊)Row({ space:8}){RcTag({ rcTagText:this.articleCategory, rcTagType:'primary', rcTagSize:'medium'})RcTag({ rcTagText:this.articleDifficulty, rcTagType:this.articleDifficultyType, rcTagShape:'circle', rcTagSize:'mini'})}// 第二行:内容标签(镂空填充)Flex({ wrap: FlexWrap.Wrap }){ForEach(this.articleTags,(tag:string)=>{RcTag({ rcTagText: tag, rcTagType:'info', rcTagPlain:true, rcTagPlainFill:true, rcTagSize:'mini', rcTagMargin:{ right:6, bottom:6}})},(tag:string)=> tag)}}.alignItems(HorizontalAlign.Start).width('100%')// 文章摘要Text('本文深度解析 RcTag 组件的架构设计、色彩系统...').fontSize(14).fontColor('#606266').lineHeight(22)}.padding(16).width('100%').backgroundColor(Color.White)}}

1.4 代码详解

数据结构设计

组件定义了三个私有变量来存储文章元数据:articleCategory 是文章主分类,固定为字符串;articleTags 是内容关键词数组,数量动态;articleDifficultyType 的类型是联合类型 'primary' | 'warning' | 'error',这使得难度类型与 RcTagrcTagType 直接兼容,无需任何转换就可以透传给组件。这个设计细节值得学习:让数据层的类型定义与 UI 层的 Props 保持一致,能大幅减少映射代码。

第一行:主分类与难度标签

主分类标签只配置了 rcTagType: 'primary'rcTagSize: 'medium',未设置 rcTagShape,因此使用默认的方形(square),整体感觉更稳重。难度标签则配置了 rcTagShape: 'circle',呈现胶囊形状,视觉上比方形更活泼。rcTagSize: 'mini' 使难度标签明显小于主分类标签,这种尺寸差异主动建立了视觉层级。

两个标签放在 Row({ space: 8 }) 中,space: 8 在两标签之间产生 8vp 的间距,无需借助 rcTagMargin 来处理间距,代码更简洁。

第二行:内容关键词标签

内容关键词数组用 ForEach 遍历渲染,key 函数直接返回标签文本本身 (tag: string) => tag。这在标签文本唯一时是没问题的——文章的内容标签通常不会重复,这个假设是合理的。若标签可能重复,则应改用索引或其他唯一标识。

每个标签设置了 rcTagMargin: { right: 6, bottom: 6 },右边距和下边距共同作用于 Flex 换行场景:右边距控制同行标签间的水平间距,下边距则在换行后产生行间距,最终标签无论在哪一行哪一位置,与邻近元素的距离都是一致的 6vp。

rcTagPlain: true 开启镂空描边模式,rcTagPlainFill: true 在镂空基础上增加浅色背景填充。这两个属性必须同时设置才能产生镂空填充效果——单独设置 rcTagPlainFill 而不设置 rcTagPlain,浅色背景不会生效。rcTagType: 'info' 决定了浅色背景和描边的颜色基调,这里选用 info 是因为其灰蓝色调不强调、不抢眼,恰好适合次要信息的展示。

布局容器的 alignItems

标签区域的 Column 设置了 alignItems(HorizontalAlign.Start),这确保内部的 RowFlex 都左对齐。如果不设置,默认居中对齐,标签会出现在屏幕中央,与正文排版惯例不符。


二、场景二:商品分类筛选

2.1 场景描述

电商 App 的商品列表页,顶部提供分类筛选标签,支持单选切换,点击标签实时过滤商品列表。这是电商 App 中最常见的交互模式之一,分类标签横向排列,超出屏幕宽度时可以横向滑动,点击某分类后列表立即刷新。

2.2 设计思路

这个场景的技术核心有两点:一是用 rcTagPlain 的动态值来表达"选中/未选中"两种视觉状态,二是用 rcTagName 携带分类 ID,通过回调事件更新状态变量从而驱动列表过滤。

选中状态的实现思路是:当某个分类标签的 id 等于当前 selectedCategory 时,rcTagPlainfalse(实色,表示选中);否则为 true(镂空,表示未选中)。这样一行表达式 rcTagPlain: this.selectedCategory !== cat.id 就完整描述了选中状态,逻辑非常清晰。

2.3 完整代码

import{ RcTag }from'rchoui'interfaceProductCategory{ id:number label:string}interfaceProduct{ id:number name:string category:number price:string}@Entry@ComponentV2 struct ProductFilterDemo {@Local selectedCategory:number=0// 0 = 全部private categories: ProductCategory[]=[{ id:0, label:'全部'},{ id:1, label:'手机数码'},{ id:2, label:'家用电器'},{ id:3, label:'服装鞋包'},{ id:4, label:'美妆护肤'},{ id:5, label:'图书'}]private products: Product[]=[{ id:1, name:'HarmonyOS 手机', category:1, price:'3999'},{ id:2, name:'智能冰箱', category:2, price:'4299'},{ id:3, name:'运动夹克', category:3, price:'299'},{ id:4, name:'精华液套装', category:4, price:'599'},{ id:5, name:'ArkTS 实战', category:5, price:'89'},{ id:6, name:'鸿蒙平板', category:1, price:'2599'}]privategetfilteredProducts(): Product[]{if(this.selectedCategory ===0){returnthis.products }returnthis.products.filter(p => p.category ===this.selectedCategory)}build(){Column(){// 分类筛选栏Scroll(){Row({ space:8}){ForEach(this.categories,(cat: ProductCategory)=>{RcTag({ rcTagText: cat.label, rcTagType:'primary', rcTagShape:'circle', rcTagPlain:this.selectedCategory !== cat.id, rcTagName: cat.id,onRcTagClick:(name)=>{this.selectedCategory = name asnumber}})},(cat: ProductCategory)=>String(cat.id))}.padding({ left:16, right:16, top:12, bottom:12})}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width('100%').backgroundColor(Color.White)// 商品列表List({ space:0}){ForEach(this.filteredProducts,(product: Product)=>{ListItem(){Row({ space:12}){Column({ space:4}){Text(product.name).fontSize(15).fontColor('#1a1a1a')Text(`¥${product.price}`).fontSize(14).fontColor('#f56c6c').fontWeight(FontWeight.Medium)}.alignItems(HorizontalAlign.Start).layoutWeight(1)// 状态标签RcTag({ rcTagText:this.categories.find(c => c.id === product.category)?.label ??'', rcTagType:'info', rcTagSize:'mini', rcTagPlain:true})}.padding({ left:16, right:16, top:14, bottom:14}).width('100%').backgroundColor(Color.White)}},(product: Product)=>String(product.id))}.divider({ strokeWidth:0.5, color:'#f0f0f0', startMargin:16, endMargin:16}).backgroundColor(Color.White).margin({ top:8})}.width('100%').height('100%').backgroundColor('#f5f5f5')}}

2.4 代码详解

接口定义

代码在组件外定义了 ProductCategoryProduct 两个接口。ProductCategoryid: number 作为分类的唯一标识,id: 0 被约定为"全部"这一特殊分类。Productcategory 字段存储的就是 ProductCategoryid,二者通过数字 ID 关联,这是一种轻量的关联数据结构,不引入复杂的嵌套。

状态变量设计

selectedCategory 使用 @Local 装饰,初始值为 0,对应"全部"分类。@Local 是 ComponentV2 中的局部状态装饰器,修改该变量会触发组件重新渲染,这是驱动筛选栏视觉更新和列表内容刷新的根本机制。

计算属性 filteredProducts

filteredProducts 定义为 private get,即 getter 计算属性。每次 build() 执行时都会调用它,根据当前 selectedCategory 过滤商品数组并返回结果。当 selectedCategory 为 0 时直接返回全量数据,否则用 Array.filter 过滤出匹配分类的商品。这个设计的好处是将过滤逻辑与 UI 布局代码分离,build() 里只需读取 filteredProducts 即可,无需关心过滤细节。

筛选栏的横向滚动

分类标签放在 Scroll 容器中,scrollable(ScrollDirection.Horizontal) 指定为横向滚动,scrollBar(BarState.Off) 隐藏滚动条(滚动条在手机端通常视觉上不美观)。内部使用 Row({ space: 8 }) 横向排列所有分类标签,设置了 padding 为标签四周预留呼吸空间。

rcTagPlain 驱动选中状态

这是本案例最核心的技术点。rcTagPlain: this.selectedCategory !== cat.id 这行代码的含义:当前分类的 ID 不等于已选分类 ID 时,标签处于镂空状态(未选中样式);等于时 rcTagPlainfalse,标签呈现实色填充(选中样式)。由于 selectedCategory@Local 状态变量,每次点击都会更新它,进而触发 ForEach 所有标签重新计算 rcTagPlain 的值,选中/未选中状态即时切换。

rcTagName 携带业务标识

rcTagName: cat.id 将分类 ID 作为标签的"名字"附加到组件上。onRcTagClick 回调的参数就是 rcTagName 的值,因此在回调中可以直接拿到被点击分类的 ID,执行 this.selectedCategory = name as number 完成状态更新。这避免了在回调闭包中捕获整个 cat 对象,是更干净的传参方式。

ForEach 的 key 函数

筛选栏的 ForEach 使用 (cat: ProductCategory) => String(cat.id) 作为 key 函数,商品列表使用 (product: Product) => String(product.id) 作为 key。数字 ID 需要通过 String() 转换为字符串,因为 ForEach 的 key 函数返回值类型是 string。用 ID 作为 key 的好处是:即使数组顺序发生变化,ArkTS 框架也能精确识别哪些节点需要更新,而不会触发全量重渲染,性能更优。

商品列表中的分类标签

商品列表每一行右侧也有一个小标签,显示该商品所属的分类名称。this.categories.find(c => c.id === product.category)?.label ?? '' 通过分类 ID 在分类数组中查找对应名称,?. 可选链保证了 find 返回 undefined 时不报错,?? '' 则提供了空字符串兜底。这个标签使用 rcTagType: 'info'rcTagPlain: truercTagSize: 'mini',视觉上轻量,不与左侧的商品主信息竞争注意力。

商品列表的布局权重

商品信息的 Column 设置了 .layoutWeight(1),这使其在 Row 中占据除右侧标签以外的所有剩余宽度。layoutWeight 是 ArkUI 中类似 CSS flex: 1 的属性,配合 Row 使用时效果等同于弹性布局中的按比例分配空间,保证标签始终靠右对齐,商品名称可以充分利用剩余空间。


总结

本文介绍的两个案例分别代表了 RcTag 的两种典型用法:纯展示(通过类型和风格传达信息层级)和交互筛选(通过状态变量与 rcTagPlain 联动实现选中态)。rcTagMargin 解决了多标签排列的间距问题,rcTagName + 事件回调解决了 ForEach 场景下的标识传递问题。这两个模式在大多数 App 的标签需求中都能复用。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!
Could not load content