HarmonyOS pc 实战之角标、删除线、信息排版 文章目录前言整体卡片结构角标Stack 叠加 offset 超出边界删除线价格置顶大卡片Stack 文字压图信息层次感写在最后前言菜品卡片是外卖页里最密集的视觉单元每一行都要在有限空间里传达图片、名称、标签、价格、加购按钮这五类信息。信息太多就显得拥挤太少又不够吸引人。这篇文章就来拆解菜品卡片的视觉设计角标用 Stack 怎么叠删除线价格怎么写信息列的排版怎么建立层次感。整体卡片结构菜品卡片是一个水平 Row左侧菜品图固定 72×72vp右侧信息列用layoutWeight(1)弹性填满。完整示例PcFoodCardPage.etsimport{router}fromkit.ArkUIinterfaceFoodCard{id:numbername:stringdesc:stringprice:numberoriginalPrice:numberemoji:stringbgColor:stringtags:string[]isTop:booleansales:numbercartCount:number}EntryComponentstruct 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:Recordstring,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.foodListthis.foodList.map(ff.idfood.id?this.createFoodCard(f,f.cartCount1):f)}removeFromCart(food:FoodCard){if(food.cartCount0)returnthis.foodListthis.foodList.map(ff.idfood.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.originalPricefood.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.cartCount0){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].originalPricethis.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.length0){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.hoveredIdfood.id?#F9FAFB:Color.White).border({width:{bottom:1},color:#F9FAFB}).onHover((isHover){this.hoveredIdisHover?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.originalPricefood.price){Text(¥${food.originalPrice}).decoration({type:TextDecorationType.LineThrough})}置顶大卡片Stack 文字压图第一道菜用了 Stack 实现文字叠在背景色块上的效果这比单纯的图文并排更有视觉冲击力。关键是Stack({ alignContent: Alignment.BottomStart })——文字信息默认靠左下对齐emoji 通过position放在右侧两者在同一层叠加形成字左图右的视觉布局背景是统一的浅色块。信息层次感普通菜品行的信息列从上到下菜名14px/Medium→ 描述12px/灰色→ 标签10px/彩色背景→ 价格加购最后一行。字号从大到小颜色从深到浅重要信息靠上操作区靠下符合用户的视觉扫描顺序。PC 端在普通菜品行加了onHover悬停背景变色给鼠标操作提供反馈——手机端不需要这个但 PC 端必须有。写在最后菜品卡片的这几个视觉细节角标用 Stackoffset、删除线用.decoration、置顶大卡用 Stack 叠层单个拆出来都是一两行代码组合起来就是一个完整的菜品展示方案。PC 端额外要注意的是onHover悬停反馈这是 PC 交互与移动端最明显的差异之一。