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
This commit is contained in:
wangjia
2026-06-25 21:30:59 +08:00
parent 7623a4c702
commit 9ce8934412
2 changed files with 330 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
// 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),
);
}
}