HarmonyOS PC实战之Flex 柱状图——用 alignItems.End 让双柱从底部对齐

📅 2026/6/16 0:03:15
HarmonyOS PC实战之Flex 柱状图——用 alignItems.End 让双柱从底部对齐
文章目录前言alignItems.End 为什么能让柱子底部对齐柱子高度的动态计算完整代码Y轴网格线的实现小结前言数据可视化是 PC 端应用的常见需求但大多数场景不需要引入专业图表库——几根柱子、一条折线完全可以用 Flex 布局手写性能更好样式完全可控。这篇做一个收支柱状图每组有两根柱子收入 支出高度根据数据比例动态计算。柱子高低不一关键问题是怎么让它们都从底部站在同一条基准线上答案是alignItems: ItemAlign.End。alignItems.End 为什么能让柱子底部对齐Flex 容器默认的alignItems是ItemAlign.Center——所有子项垂直居中对齐。对于高度不同的柱子居中对齐意味着顶部和底部都参差不齐。ItemAlign.End让所有子项靠底部对齐。外层容器固定高度所有柱子都站在容器底部高的柱子向上伸矮的柱子靠底这正是我们想要的效果Row(){// 收入柱Column().width(20).height(incomeHeight)// 动态高度.backgroundColor(#10B981).borderRadius({topLeft:4,topRight:4})// 支出柱Column().width(20).height(expenseHeight)// 动态高度.backgroundColor(#EF4444).borderRadius({topLeft:4,topRight:4})}.height(160)// ← 固定容器高度.alignItems(VerticalAlign.Bottom)// ← 底部对齐.space(4)柱子高度的动态计算柱子高度 容器高度 × (当月数值 / 最大值)。先找出所有月份中的最大值再按比例缩放constmaxValueMath.max(...this.chartData.map(dMath.max(d.income,d.expense)))constbarHeight(value:number)(value/maxValue)*this.chartHeight最大值的柱子占满容器高度其他柱子按比例缩短。完整代码interfaceMonthData{month:stringincome:numberexpense:number}EntryComponentstruct PcBarChartPage{StateselectedMonth:number-1StatechartHeight:number160chartData:MonthData[][{month:1月,income:12800,expense:8600},{month:2月,income:9200,expense:11400},{month:3月,income:15600,expense:9800},{month:4月,income:13400,expense:10200},{month:5月,income:18200,expense:12600},{month:6月,income:16800,expense:13800},{month:7月,income:14200,expense:9400},{month:8月,income:19600,expense:15200},{month:9月,income:17400,expense:11600},{month:10月,income:21000,expense:14800},{month:11月,income:16400,expense:12400},{month:12月,income:22800,expense:18600},]getmaxValue():number{returnMath.max(...this.chartData.map(dMath.max(d.income,d.expense)))}gettotalIncome():number{returnthis.chartData.reduce((sum,d)sumd.income,0)}gettotalExpense():number{returnthis.chartData.reduce((sum,d)sumd.expense,0)}getnetBalance():number{returnthis.totalIncome-this.totalExpense}barHeight(value:number):number{returnMath.max(4,Math.floor((value/this.maxValue)*this.chartHeight))}formatMoney(amount:number):string{return¥${(amount/10000).toFixed(1)}万}BuildersummaryCard(label:string,value:number,color:string,icon:string){Column({space:4}){Row({space:6}){Text(icon).fontSize(16)Text(label).fontSize(12).fontColor(#6B7280)}Text(this.formatMoney(value)).fontSize(20).fontWeight(FontWeight.Bold).fontColor(color)}.padding({left:16,right:16,top:14,bottom:14}).backgroundColor(Color.White).borderRadius(12).shadow({radius:6,color:#08000000,offsetY:2}).layoutWeight(1).alignItems(HorizontalAlign.Start)}BuilderbarGroup(data:MonthData,index:number){Column({space:6}){// 选中时显示数值 tooltipif(this.selectedMonthindex){Column({space:2}){Text(收${this.formatMoney(data.income)}).fontSize(9).fontColor(#10B981)Text(支${this.formatMoney(data.expense)}).fontSize(9).fontColor(#EF4444)}.padding({left:4,right:4,top:3,bottom:3}).backgroundColor(#111827E6).borderRadius(4).margin({bottom:4})}// 柱子组底部对齐的关键Row({space:4}){// 收入柱Column().width(12).height(this.barHeight(data.income)).backgroundColor(this.selectedMonthindex?#059669:#10B981).borderRadius({topLeft:3,topRight:3}).animation({duration:300})// 支出柱Column().width(12).height(this.barHeight(data.expense)).backgroundColor(this.selectedMonthindex?#DC2626:#EF4444).borderRadius({topLeft:3,topRight:3}).animation({duration:300})}.height(this.chartHeight).alignItems(VerticalAlign.Bottom)// ← 核心底部对齐// X轴月份标签Text(data.month).fontSize(10).fontColor(this.selectedMonthindex?#111827:#9CA3AF).fontWeight(this.selectedMonthindex?FontWeight.Medium:FontWeight.Normal)}.alignItems(HorizontalAlign.Center).layoutWeight(1).onClick((){this.selectedMonththis.selectedMonthindex?-1:index})}build(){Scroll(){Column({space:20}){// 标题Column({space:4}){Text(年度收支总览).fontSize(22).fontWeight(FontWeight.Bold).fontColor(#111827)Text(2024年全年财务数据).fontSize(14).fontColor(#6B7280)}.alignItems(HorizontalAlign.Start).width(100%)// 汇总卡片Row({space:12}){this.summaryCard(总收入,this.totalIncome,#10B981,)this.summaryCard(总支出,this.totalExpense,#EF4444,)this.summaryCard(净结余,this.netBalance,this.netBalance0?#3B82F6:#F59E0B,)}.width(100%)// 图表区域Column({space:12}){Row(){Text(月度收支对比).fontSize(15).fontWeight(FontWeight.Medium).fontColor(#374151).layoutWeight(1)// 图例Row({space:16}){Row({space:4}){Row().width(10).height(10).borderRadius(2).backgroundColor(#10B981)Text(收入).fontSize(11).fontColor(#6B7280)}Row({space:4}){Row().width(10).height(10).borderRadius(2).backgroundColor(#EF4444)Text(支出).fontSize(11).fontColor(#6B7280)}}}.width(100%)// Y轴参考线 柱状图Stack({alignContent:Alignment.BottomStart}){// Y轴网格线Column(){ForEach([1.0,0.75,0.5,0.25,0],(ratio:number){Row(){Text(this.formatMoney(this.maxValue*ratio)).fontSize(9).fontColor(#D1D5DB).width(40).textAlign(TextAlign.End)Divider().layoutWeight(1).strokeWidth(1).color(ratio0?#9CA3AF:#F3F4F6)}.width(100%).alignItems(VerticalAlign.Center)if(ratio0){Blank().layoutWeight(1)}})}.width(100%).height(this.chartHeight24).justifyContent(FlexAlign.SpaceBetween)// 柱状图叠在网格线上Row({space:0}){ForEach(this.chartData,(data:MonthData,index:number){this.barGroup(data,index)})}.width(100%).padding({left:44})}.width(100%)}.padding(20).backgroundColor(Color.White).borderRadius(16).shadow({radius:8,color:#08000000})// 选中月份详情if(this.selectedMonth0){Row({space:16}){Column({space:4}){Text(this.chartData[this.selectedMonth].month收入).fontSize(12).fontColor(#6B7280)Text(this.formatMoney(this.chartData[this.selectedMonth].income)).fontSize(18).fontWeight(FontWeight.Bold).fontColor(#10B981)}.layoutWeight(1).alignItems(HorizontalAlign.Center)Divider().vertical(true).height(40).strokeWidth(1).color(#E5E7EB)Column({space:4}){Text(this.chartData[this.selectedMonth].month支出).fontSize(12).fontColor(#6B7280)Text(this.formatMoney(this.chartData[this.selectedMonth].expense)).fontSize(18).fontWeight(FontWeight.Bold).fontColor(#EF4444)}.layoutWeight(1).alignItems(HorizontalAlign.Center)Divider().vertical(true).height(40).strokeWidth(1).color(#E5E7EB)Column({space:4}){Text(结余).fontSize(12).fontColor(#6B7280)Text(this.formatMoney(Math.abs(this.chartData[this.selectedMonth].income-this.chartData[this.selectedMonth].expense))).fontSize(18).fontWeight(FontWeight.Bold).fontColor((this.chartData[this.selectedMonth].income-this.chartData[this.selectedMonth].expense)0?#3B82F6:#F59E0B)}.layoutWeight(1).alignItems(HorizontalAlign.Center)}.padding(16).backgroundColor(Color.White).borderRadius(12).shadow({radius:6,color:#08000000}).width(100%)}}.padding({left:32,right:32,top:32,bottom:32}).constraintSize({minWidth:700,maxWidth:1100}).margin({left:auto,right:auto})}.width(100%).height(100%).backgroundColor(#F9FAFB)}}Y轴网格线的实现Y轴不用真正的绘图 API用绝对定位的 Divider 模拟// 容器高度对应最大值// 4 条参考线75%、50%、25%、0Column(){ForEach([1.0,0.75,0.5,0.25,0],(ratio){Row(){Text(formatMoney(maxValue*ratio)).width(40)// Y轴标签Divider().layoutWeight(1).color(ratio0?#9CA3AF:#F3F4F6)}})}.justifyContent(FlexAlign.SpaceBetween)// ← 均匀分布在容器高度柱状图用Stack叠在网格线上视觉上柱子就立在网格里了。小结柱状图的关键技巧alignItems: VerticalAlign.Bottom让柱子底部对齐柱子高度 容器高度 × 比例Stack把网格线和柱子叠在一起。不需要 Canvas纯 Flex Column 就能做出够用的柱状图。