9ce8934412
按「先建单一组件源、屏只组合不重写样式」重立组件层: - 新建 widgets/ds/:DsButton(.btn ghost/primary/danger/sm) · DsBadge(.badge+圆点) · DsChip(.chip 状态筹码) · DsSearchBox(.searchbox) · DsKpi(.kpi 含 .ic 右上块+环比 .d) - 每个组件一对一对照 atoms.css 尺寸/间距/圆角/字号,颜色/尺寸全引 token(0 硬编码,颜色闸过) - 现有 KpiCard/StatusPill 等逐步迁入 ds/ 废弃 下一步:建 DsTable(表格+列头漏斗+分页+状态徽章用 DsBadge)→ 用 ds 组件重写库存屏。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSKEiHsvauyxYUW2itzUXX
116 lines
3.7 KiB
Dart
116 lines
3.7 KiB
Dart
// 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),
|
||
);
|
||
}
|
||
}
|