// widgets/ds/ds_kpi.dart — 原型 .kpi 卡(镜像 atoms.css)。 import 'package:flutter/material.dart'; import '../../core/theme/context_tokens.dart'; import '../../core/theme/app_dims.g.dart'; /// 图标块 .ic 变体(软底 + 前景色)。 enum DsKpiTone { info, ok, blue, alert } /// 环比方向 .d.up/.down(绿/红),neutral=纯文案(muted)。 enum DsKpiDelta { up, down, neutral } /// 原型 .kpi:surface/border/r-lg/pad 15 16;右上 .ic(30×30 r-md); /// .t(fs-sm muted) / .v(24 fw700 heading mono,alert→accent) / .d(fs-xs,up绿/down红)。 class DsKpi extends StatelessWidget { final String title; final String value; final IconData icon; final DsKpiTone tone; final String? delta; final DsKpiDelta deltaTone; final VoidCallback? onTap; const DsKpi({ super.key, required this.title, required this.value, required this.icon, this.tone = DsKpiTone.info, this.delta, this.deltaTone = DsKpiDelta.neutral, this.onTap, }); @override Widget build(BuildContext context) { final t = context.tokens; final (icBg, icFg) = switch (tone) { DsKpiTone.info => (t.infoSoft, t.primary), DsKpiTone.ok => (t.okSoft, t.success), DsKpiTone.blue => (t.infoSoft, t.brand400), DsKpiTone.alert => (t.accentSoft, t.accent), }; final deltaColor = switch (deltaTone) { DsKpiDelta.up => t.success, DsKpiDelta.down => t.danger, DsKpiDelta.neutral => t.muted, }; final card = Container( padding: const EdgeInsets.fromLTRB(16, 15, 16, 15), decoration: BoxDecoration( color: t.surface, border: Border.all(color: t.border), borderRadius: BorderRadius.circular(AppDims.rLg), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Padding( padding: const EdgeInsets.only(top: 2), child: Text(title, style: TextStyle( fontSize: AppDims.fsSm, color: t.muted)), ), ), Container( width: 30, height: 30, decoration: BoxDecoration( color: icBg, borderRadius: BorderRadius.circular(AppDims.rMd)), alignment: Alignment.center, child: Icon(icon, size: 17, color: icFg), ), ], ), const SizedBox(height: 7), Text(value, style: TextStyle( fontSize: 24, fontWeight: FontWeight.w700, letterSpacing: 0.3, fontFamily: 'monospace', color: tone == DsKpiTone.alert ? t.accent : t.heading)), if (delta != null) ...[ const SizedBox(height: 5), Row( mainAxisSize: MainAxisSize.min, children: [ if (deltaTone != DsKpiDelta.neutral) Icon( deltaTone == DsKpiDelta.up ? Icons.arrow_drop_up : Icons.arrow_drop_down, size: 14, color: deltaColor), Text(delta!, style: TextStyle(fontSize: AppDims.fsXs, color: deltaColor)), ], ), ], ], ), ); if (onTap == null) return card; return MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector(onTap: onTap, child: card), ); } }