HarmonyOS pc 实战之角标、删除线、信息排版
文章目录
- 前言
- 整体卡片结构
- 角标:Stack 叠加 + offset 超出边界
- 删除线价格
- 置顶大卡片:Stack 文字压图
- 信息层次感
- 写在最后
前言
菜品卡片是外卖页里最密集的视觉单元,每一行都要在有限空间里传达图片、名称、标签、价格、加购按钮这五类信息。信息太多就显得拥挤,太少又不够吸引人。
这篇文章就来拆解菜品卡片的视觉设计:角标用 Stack 怎么叠,删除线价格怎么写,信息列的排版怎么建立层次感。
整体卡片结构
菜品卡片是一个水平 Row:左侧菜品图固定 72×72vp,右侧信息列用layoutWeight(1)弹性填满。
完整示例:PcFoodCardPage.ets
import{router}from'@kit.ArkUI'interfaceFoodCard{id:numbername:stringdesc:stringprice:numberoriginalPrice:numberemoji:stringbgColor:stringtags:string[]isTop:booleansales:numbercartCount:number}@Entry@Componentstruct PcFoodCardPage{@StatefoodList:FoodCard[]=[{id:1,name:'宫保鸡丁盖饭',desc:'花生米、嫩鸡丁、经典川味',price:28,originalPrice:35,emoji:'🍛',bgColor:'#FEF3C7',tags:['热销','今日特惠'],isTop:true,sales:680,cartCount:0},{id:2,name:'鱼香肉丝套餐',desc:'木耳、胡萝卜、嫩肉丝',price:25,originalPrice:25,emoji:'🍚',bgColor:'#F0FDF4',tags:['招牌'],isTop:false,sales:420,cartCount:0},{id:3,name:'红烧肉双拼饭',desc:'五花肉、白米饭、时令蔬菜',price:38,originalPrice:45,emoji:'🥩',bgColor:'#FFF1F2',tags:['新品','限时折扣'],isTop:false,sales:230,cartCount:0},{id:4,name:'番茄炒蛋盖饭',desc:'新鲜番茄、土鸡蛋、嫩滑米饭',price:22,originalPrice:22,emoji:'🍅',bgColor:'#FFF7ED',tags:[],isTop:false,sales:510,cartCount:0},]@StatehoveredId:number=-1privatetagColors:Record<string,string>={'热销':'#EF4444','招牌':'#F59E0B','新品':'#3B82F6','今日特惠':'#10B981','限时折扣':'#8B5CF6',}createFoodCard(food:FoodCard,cartCount:number):FoodCard{return{id:food.id,name:food.name,desc:food.desc,price:food.price,originalPrice:food.originalPrice,emoji:food.emoji,bgColor:food.bgColor,tags:food.tags,isTop:food.isTop,sales:food.sales,cartCount:cartCount}}addToCart(food:FoodCard){this.foodList=this.foodList.map(f=>f.id===food.id?this.createFoodCard(f,f.cartCount+1):f)}removeFromCart(food:FoodCard){if(food.cartCount<=0)returnthis.foodList=this.foodList.map(f=>f.id===food.id?this.createFoodCard(f,f.cartCount-1):f)}@BuildertagBadge(tag:string){Text(tag).fontSize(10).fontColor(this.tagColors[tag]||'#6B7280').backgroundColor(`${this.tagColors[tag]||'#6B7280'}15`).borderRadius(4).padding({left:5,right:5,top:2,bottom:2})}@BuilderpriceRow(food:FoodCard){Row({space:6}){// 现价Row({space:1}){Text('¥').fontSize(12).fontColor('#EF4444').baselineOffset(2)Text(`${food.price}`).fontSize(18).fontColor('#EF4444').fontWeight(FontWeight.Bold)}// 划线原价(仅有折扣时显示)if(food.originalPrice>food.price){Text(`¥${food.originalPrice}`).fontSize(12).fontColor('#D1D5DB').decoration({type:TextDecorationType.LineThrough,color:'#D1D5DB'})}Blank()// 销量Text(`月售${food.sales}`).fontSize(11).fontColor('#9CA3AF')}.width('100%')}@BuildercounterWidget(food:FoodCard){Row({space:8}){if(food.cartCount>0){Text('−').fontSize(18).width(26).height(26).textAlign(TextAlign.Center).fontColor('#3B82F6').border({width:1.5,color:'#3B82F6'}).borderRadius(13).onClick(()=>this.removeFromCart(food))Text(`${food.cartCount}`).fontSize(14).fontColor('#111827').fontWeight(FontWeight.Bold).width(22).textAlign(TextAlign.Center)}Text('+').fontSize(18).width(26).height(26).textAlign(TextAlign.Center).fontColor(Color.White).backgroundColor('#3B82F6').borderRadius(13).onClick(()=>this.addToCart(food))}}build(){Column(){// 标题栏Row(){Text('今日推荐').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#111827')Blank()Text('查看全部 ›').fontSize(13).fontColor('#3B82F6')}.width('100%').padding({left:24,right:24,top:20,bottom:16})// 置顶大卡片(第一道菜特殊展示)Stack({alignContent:Alignment.BottomStart}){// 背景色块Column().width('100%').height(160).backgroundColor(this.foodList[0].bgColor).borderRadius(12)// 右侧大 emojiText(this.foodList[0].emoji).fontSize(72).position({x:'60%',y:20})// 左侧信息叠层Column({space:8}){Row({space:6}){ForEach(this.foodList[0].tags,(tag:string)=>{this.tagBadge(tag)})}Text(this.foodList[0].name).fontSize(18).fontColor('#111827').fontWeight(FontWeight.Bold)Text(this.foodList[0].desc).fontSize(12).fontColor('#6B7280')Row({space:8}){Row({space:2}){Text('¥').fontSize(13).fontColor('#EF4444')Text(`${this.foodList[0].price}`).fontSize(22).fontColor('#EF4444').fontWeight(FontWeight.Bold)}if(this.foodList[0].originalPrice>this.foodList[0].price){Text(`¥${this.foodList[0].originalPrice}`).fontSize(13).fontColor('#D1D5DB').decoration({type:TextDecorationType.LineThrough,color:'#D1D5DB'})}Blank()this.counterWidget(this.foodList[0])}.width('100%')}.padding({left:16,right:16,bottom:16}).width('70%')}.width('100%').margin({left:16,right:16}).padding({left:8,right:8})// 普通菜品行列表Scroll(){Column({space:0}){ForEach(this.foodList.slice(1),(food:FoodCard)=>{Row({space:12}){// 菜品图 + "NEW"角标Stack({alignContent:Alignment.TopEnd}){Stack(){Column().width(72).height(72).backgroundColor(food.bgColor).borderRadius(8)Text(food.emoji).fontSize(32)}.width(72).height(72)// 角标(仅新品显示)if(food.tags.includes('新品')){Text('NEW').fontSize(9).fontColor(Color.White).backgroundColor('#3B82F6').borderRadius(4).padding({left:4,right:4,top:2,bottom:2}).offset({x:4,y:-4})}}.width(72).height(72)// 右侧信息列Column({space:4}){// 菜名 + 标签Row({space:6}){Text(food.name).fontSize(14).fontColor('#111827').fontWeight(FontWeight.Medium).layoutWeight(1).maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis})}// 描述Text(food.desc).fontSize(12).fontColor('#9CA3AF').maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis})// 标签行if(food.tags.length>0){Row({space:4}){ForEach(food.tags,(tag:string)=>{this.tagBadge(tag)})}}// 价格行Row(){this.priceRow(food)this.counterWidget(food)}.width('100%').alignItems(VerticalAlign.Bottom)}.layoutWeight(1).alignItems(HorizontalAlign.Start)}.width('100%').padding({left:20,right:20,top:14,bottom:14}).backgroundColor(this.hoveredId===food.id?'#F9FAFB':Color.White).border({width:{bottom:1},color:'#F9FAFB'}).onHover((isHover)=>{this.hoveredId=isHover?food.id:-1}).animation({duration:150,curve:Curve.EaseOut})})}}.layoutWeight(1).scrollBar(BarState.Off)}.width('100%').height('100%').backgroundColor(Color.White)}}角标:Stack 叠加 + offset 超出边界
"NEW"标签这种角标,标准做法是 Stack 叠加 +offset让它超出图片边界:
Stack({alignContent:Alignment.TopEnd}){// 底层:菜品图FoodImage().width(72).height(72)// 顶层:角标Text('NEW').fontSize(9).backgroundColor('#3B82F6').borderRadius(4).padding({left:4,right:4,top:2,bottom:2}).offset({x:4,y:-4})// 向右 4、向上 4,超出图片边界}.width(72).height(72)Stack的alignContent: Alignment.TopEnd把角标默认定位到右上角,offset再微调让它露出边界。不需要position绝对定位,代码更简洁。
删除线价格
ArkUI 的 Text 有.decoration()属性支持文字装饰线:
Text(`¥${food.originalPrice}`).fontSize(12).fontColor('#D1D5DB').decoration({type:TextDecorationType.LineThrough,color:'#D1D5DB'})TextDecorationType.LineThrough就是删除线。颜色设为和字体颜色一致(灰色),视觉上统一。
只有折扣商品才显示原价,用if (food.originalPrice > food.price)条件判断:
if(food.originalPrice>food.price){Text(`¥${food.originalPrice}`).decoration({type:TextDecorationType.LineThrough})}置顶大卡片:Stack 文字压图
第一道菜用了 Stack 实现"文字叠在背景色块上"的效果,这比单纯的图文并排更有视觉冲击力。
关键是Stack({ alignContent: Alignment.BottomStart })——文字信息默认靠左下对齐,emoji 通过position放在右侧,两者在同一层叠加,形成"字左图右"的视觉布局,背景是统一的浅色块。
信息层次感
普通菜品行的信息列从上到下:菜名(14px/Medium)→ 描述(12px/灰色)→ 标签(10px/彩色背景)→ 价格+加购(最后一行)。
字号从大到小,颜色从深到浅,重要信息靠上,操作区靠下,符合用户的视觉扫描顺序。
PC 端在普通菜品行加了onHover悬停背景变色,给鼠标操作提供反馈——手机端不需要这个,但 PC 端必须有。
写在最后
菜品卡片的这几个视觉细节:角标用 Stack+offset、删除线用.decoration、置顶大卡用 Stack 叠层,单个拆出来都是一两行代码,组合起来就是一个完整的菜品展示方案。PC 端额外要注意的是onHover悬停反馈,这是 PC 交互与移动端最明显的差异之一。
