// 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)),
),
),
],
);
}
}