// widgets/ds/ds_bar_chart.dart — 原型 finance.html 收支趋势分组柱状图(镜像 .barchart)。 // 度量照原型:图高 180 / 柱宽 26 / pair 内 gap 6 / 列间 gap 18 / 柱顶 mono 数值标签 // (top:-17)/ 顶圆角 r-sm;收入柱=primary、支出柱=brand400;含图例(.chart-legend)。 import 'package:flutter/material.dart'; import '../../core/theme/app_dims.g.dart'; import '../../core/theme/context_tokens.dart'; import '../../core/theme/app_fonts.dart'; class DsBarGroup { final String label; // 列底标签(如 1月) final double a; // 收入(primary 柱) final double b; // 支出(brand400 柱) const DsBarGroup(this.label, this.a, this.b); } class DsBarChart extends StatelessWidget { final List groups; final String legendA; final String legendB; /// 柱顶数值标签格式化(原型显示「万元」整数)。 final String Function(double v) format; const DsBarChart({ super.key, required this.groups, this.legendA = '收入', this.legendB = '支出', required this.format, }); @override Widget build(BuildContext context) { final t = context.tokens; var max = 0.0; for (final g in groups) { if (g.a > max) max = g.a; if (g.b > max) max = g.b; } if (max <= 0) max = 1; return Container( // 原型 .chart-card:surface / border / r-lg / padding 18 20 14 padding: const EdgeInsets.fromLTRB(20, 18, 20, 14), decoration: BoxDecoration( color: t.surface, border: Border.all(color: t.border), borderRadius: BorderRadius.circular(AppDims.rLg), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // .chart-legend Row(children: [ _legend(t, t.primary, legendA), const SizedBox(width: 18), _legend(t, t.brand400, legendB), ]), const SizedBox(height: 16), // .barchart:高 180 + 顶部标签余量 6 SizedBox( height: 186, child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ for (var i = 0; i < groups.length; i++) ...[ if (i > 0) const SizedBox(width: 18), Expanded(child: _col(t, groups[i], max)), ], ], ), ), ], ), ); } Widget _legend(dynamic t, Color color, String label) => Row(mainAxisSize: MainAxisSize.min, children: [ Container( width: 11, height: 11, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(3))), const SizedBox(width: 7), Text(label, style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)), ]); Widget _col(dynamic t, DsBarGroup g, double max) { return LayoutBuilder(builder: (ctx, cons) { // 原型柱宽 26;窄屏(列宽不足)按列宽收缩,防横向溢出 final barW = ((cons.maxWidth - 6) / 2).clamp(8.0, 26.0); return Column( mainAxisAlignment: MainAxisAlignment.end, children: [ Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ _bar(t, g.a, max, t.primary, barW), const SizedBox(width: 6), _bar(t, g.b, max, t.brand400, barW), ], ), ), const SizedBox(height: 8), Text(g.label, style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)), ], ); }); } Widget _bar(dynamic t, double v, double max, Color color, double barW) { // 最高柱 140 + 顶标签 ~18 ≈ 原型 .bar-pair 可用高(180 - 月份标签行) final h = (v / max * 140).clamp(2.0, 140.0); return Column( mainAxisSize: MainAxisSize.min, children: [ // .bv 柱顶数值(mono fs-xs muted;原型绝对定位悬浮 → 布局占柱宽、绘制可溢出) SizedBox( width: barW, child: Center( child: Text(format(v), softWrap: false, overflow: TextOverflow.visible, style: TextStyle( fontSize: AppDims.fsXs, color: t.muted, fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback)), ), ), const SizedBox(height: 3), Container( width: barW, height: h, decoration: BoxDecoration( color: color, borderRadius: const BorderRadius.vertical(top: Radius.circular(AppDims.rSm)), ), ), ], ); } }