// 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'; import '../../core/theme/app_fonts.dart'; /// 图标块 .ic 变体(软底 + 前景色)。 enum DsKpiTone { info, ok, blue, alert, warn } /// 环比方向 .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; // 原型 users.html 的 KPI 无右上图标块 → icon 可空时不渲染 .ic 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, 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), // 暗黄(草稿卡):icon 与数字走 warn token DsKpiTone.warn => (t.warnBg, t.warn), }; 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)), ), ), if (icon != null) 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: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, color: switch (tone) { DsKpiTone.alert => t.accent, DsKpiTone.warn => t.warn, _ => t.heading, })), if (delta != null) ...[ const SizedBox(height: 5), // 原型 .d 的箭头是文本字符 ▲/▼(非图标),gap4。 Text( switch (deltaTone) { DsKpiDelta.up => '▲ ${delta!}', DsKpiDelta.down => '▼ ${delta!}', DsKpiDelta.neutral => delta!, }, style: TextStyle(fontSize: AppDims.fsXs, color: deltaColor), ), ], ], ), ); if (onTap == null) return card; return MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector(onTap: onTap, child: card), ); } }