Files
jiu/client/lib/widgets/ds/ds_kpi.dart
wangjia 9ce8934412 feat(client): ds 组件库地基(镜像 atoms.css 原子,屏重写前置)
按「先建单一组件源、屏只组合不重写样式」重立组件层:
- 新建 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
2026-06-25 21:30:59 +08:00

116 lines
3.7 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 }
/// 原型 .kpisurface/border/r-lg/pad 15 16;右上 .ic(30×30 r-md)
/// .t(fs-sm muted) / .v(24 fw700 heading monoalert→accent) / .d(fs-xsup绿/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),
);
}
}