HarmonyOS6 半年磨一剑:RcTag 组件实战案例(一)内容展示与商品筛选
文章目录
- 一、场景一:内容详情页标签展示
- 1.1 场景描述
- 1.2 设计思路
- 1.3 完整代码
- 1.4 代码详解
- 二、场景二:商品分类筛选
- 2.1 场景描述
- 2.2 设计思路
- 2.3 完整代码
- 2.4 代码详解
- 总结
Hello 各位开发者们大家好,我是若城,本篇是RcTag实战系列的第一篇文章,将通过两个真实业务场景,展示如何用RcTag构建内容详情页标签区域和电商商品分类筛选功能。
一、场景一:内容详情页标签展示
1.1 场景描述
文章详情页顶部展示文章分类标签、内容标签和难度标签,纯展示用途,无交互。这类需求在技术博客、内容社区、资讯类 App 中极为普遍,标签本身承载的是信息分类职责,设计上需要做到层次分明而不杂乱。
1.2 设计思路
这个场景的核心是在有限的屏幕宽度内,用视觉差异区分出三种信息层级:主分类(主语意最强)、难度(次要信息)、内容关键词(辅助信息)。
主分类标签采用实色填充,这是最高强调等级,抢先抓住用户视线。难度标签采用胶囊形状,缩小尺寸至 mini,既保持可读性又不抢占主分类的主导地位。内容关键词数量最多,若同样使用实色会造成视觉噪声,因此采用rcTagPlain: true+rcTagPlainFill: true的镂空填充风格——有背景色但背景透明度低,整体轻量,适合数量多的场景。
布局方面,第一行使用Row横向排列分类标签和难度标签,这两个标签数量固定,不会换行。第二行使用Flex+FlexWrap.Wrap展示内容关键词,标签数量不确定,自动换行是必要的。
1.3 完整代码
import{RcTag}from'rchoui'@Entry@ComponentV2struct ArticleDetailTagsDemo{// 文章元数据privatearticleCategory:string='鸿蒙开发'privatearticleTags:string[]=['ArkTS','ArkUI','组件库','UI设计','开源']privatearticleDifficulty:string='进阶'privatearticleDifficultyType:'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',这使得难度类型与RcTag的rcTagType直接兼容,无需任何转换就可以透传给组件。这个设计细节值得学习:让数据层的类型定义与 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),这确保内部的Row和Flex都左对齐。如果不设置,默认居中对齐,标签会出现在屏幕中央,与正文排版惯例不符。
二、场景二:商品分类筛选
2.1 场景描述
电商 App 的商品列表页,顶部提供分类筛选标签,支持单选切换,点击标签实时过滤商品列表。这是电商 App 中最常见的交互模式之一,分类标签横向排列,超出屏幕宽度时可以横向滑动,点击某分类后列表立即刷新。
2.2 设计思路
这个场景的技术核心有两点:一是用rcTagPlain的动态值来表达"选中/未选中"两种视觉状态,二是用rcTagName携带分类 ID,通过回调事件更新状态变量从而驱动列表过滤。
选中状态的实现思路是:当某个分类标签的id等于当前selectedCategory时,rcTagPlain为false(实色,表示选中);否则为true(镂空,表示未选中)。这样一行表达式rcTagPlain: this.selectedCategory !== cat.id就完整描述了选中状态,逻辑非常清晰。
2.3 完整代码
import{RcTag}from'rchoui'interfaceProductCategory{id:numberlabel:string}interfaceProduct{id:numbername:stringcategory:numberprice:string}@Entry@ComponentV2struct ProductFilterDemo{@LocalselectedCategory:number=0// 0 = 全部privatecategories:ProductCategory[]=[{id:0,label:'全部'},{id:1,label:'手机数码'},{id:2,label:'家用电器'},{id:3,label:'服装鞋包'},{id:4,label:'美妆护肤'},{id:5,label:'图书'}]privateproducts: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=nameasnumber}})},(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 代码详解
接口定义
代码在组件外定义了ProductCategory和Product两个接口。ProductCategory用id: number作为分类的唯一标识,id: 0被约定为"全部"这一特殊分类。Product的category字段存储的就是ProductCategory的id,二者通过数字 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 时,标签处于镂空状态(未选中样式);等于时rcTagPlain为false,标签呈现实色填充(选中样式)。由于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: true、rcTagSize: 'mini',视觉上轻量,不与左侧的商品主信息竞争注意力。
商品列表的布局权重
商品信息的Column设置了.layoutWeight(1),这使其在Row中占据除右侧标签以外的所有剩余宽度。layoutWeight是 ArkUI 中类似 CSSflex: 1的属性,配合Row使用时效果等同于弹性布局中的按比例分配空间,保证标签始终靠右对齐,商品名称可以充分利用剩余空间。
总结
本文介绍的两个案例分别代表了RcTag的两种典型用法:纯展示(通过类型和风格传达信息层级)和交互筛选(通过状态变量与 rcTagPlain 联动实现选中态)。rcTagMargin解决了多标签排列的间距问题,rcTagName+ 事件回调解决了 ForEach 场景下的标识传递问题。这两个模式在大多数 App 的标签需求中都能复用。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!
