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
+215
View File
@@ -0,0 +1,215 @@
// widgets/ds/ds_atoms.dart
// Flutter 组件库(ds=design-system)——一对一镜像原型 atoms.css 的原子。
// 单一组件源:尺寸/间距/圆角/字号全引 AppDims,颜色全引 context.tokens(禁硬编码)。
// 屏只组合这些组件,不再自己堆样式(见代码端真相源闸 check_ds_code)。
import 'package:flutter/material.dart';
import '../../core/theme/context_tokens.dart';
import '../../core/theme/app_dims.g.dart';
// ── .btn ──────────────────────────────────────────────────────────────────
enum DsBtnVariant { ghost, primary, danger }
/// 原型 .btnh38 / pad 0 16 / r-md / fs-body / fw600 / gap7,图标 16。
/// ghost=surface+border / primary=primary+on-primary / danger=danger+白。
class DsButton extends StatelessWidget {
final String label;
final IconData? icon;
final VoidCallback? onPressed;
final DsBtnVariant variant;
final bool small; // .btn.smh32 / pad 0 12 / fs-sm
const DsButton(
this.label, {
super.key,
this.icon,
this.onPressed,
this.variant = DsBtnVariant.ghost,
this.small = false,
});
@override
Widget build(BuildContext context) {
final t = context.tokens;
final (bg, fg, border) = switch (variant) {
DsBtnVariant.ghost => (t.surface, t.text, t.border),
DsBtnVariant.primary => (t.primary, t.onPrimary, t.primary),
DsBtnVariant.danger => (t.danger, t.onPrimary, t.danger),
};
return Material(
color: bg,
borderRadius: BorderRadius.circular(AppDims.rMd),
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(AppDims.rMd),
child: Container(
height: small ? 32 : 38,
padding: EdgeInsets.symmetric(horizontal: small ? 12 : 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppDims.rMd),
border: Border.all(color: border),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: 16, color: fg),
const SizedBox(width: 7),
],
Text(label,
style: TextStyle(
color: fg,
fontSize: small ? AppDims.fsSm : AppDims.fsBody,
fontWeight: FontWeight.w600)),
],
),
),
),
);
}
}
// ── .badge ────────────────────────────────────────────────────────────────
enum DsBadgeTone { ok, danger, warn, info, accent, muted }
/// 原型 .badgeh22 / pad 0 9 / r-pill / fs-sm fw600 + 前导圆点(6px, currentColor)。
class DsBadge extends StatelessWidget {
final String label;
final DsBadgeTone tone;
const DsBadge(this.label, {super.key, this.tone = DsBadgeTone.ok});
@override
Widget build(BuildContext context) {
final t = context.tokens;
final (bg, fg) = switch (tone) {
DsBadgeTone.ok => (t.successBg, t.success),
DsBadgeTone.danger => (t.dangerBg, t.danger),
DsBadgeTone.warn => (t.warnBg, t.warn),
DsBadgeTone.info => (t.infoSoft, t.primary),
DsBadgeTone.accent => (t.accentSoft, t.accent),
DsBadgeTone.muted => (t.borderSubtle, t.muted),
};
return Container(
height: 22,
padding: const EdgeInsets.symmetric(horizontal: 9),
decoration:
BoxDecoration(color: bg, borderRadius: BorderRadius.circular(AppDims.rPill)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(color: fg, shape: BoxShape.circle)),
const SizedBox(width: 5),
Text(label,
style: TextStyle(
color: fg,
fontSize: AppDims.fsSm,
fontWeight: FontWeight.w600)),
],
),
);
}
}
// ── .chip ─────────────────────────────────────────────────────────────────
/// 原型 .chiph34 / pad 0 12 / border r-md / surface / fs-sm / gap7。
/// 选中值用 .cvprimary fw600);激活(on)边框 primary。配 PopupMenuButton 用。
class DsChip extends StatelessWidget {
final String label;
final String? value; // 选中值(.cv);非空即 on 态
final VoidCallback? onTap;
const DsChip({super.key, required this.label, this.value, this.onTap});
@override
Widget build(BuildContext context) {
final t = context.tokens;
final on = value != null && value!.isNotEmpty;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppDims.rMd),
child: Container(
height: 34,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: t.surface,
border: Border.all(color: on ? t.primary : t.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(label,
style: TextStyle(fontSize: AppDims.fsSm, color: t.text)),
if (on) ...[
const SizedBox(width: 7),
Text(value!,
style: TextStyle(
fontSize: AppDims.fsSm,
color: t.primary,
fontWeight: FontWeight.w600)),
],
const SizedBox(width: 7),
Icon(Icons.keyboard_arrow_down, size: 14, color: t.faint),
],
),
),
);
}
}
// ── .searchbox ──────────────────────────────────────────────────────────────
/// 原型 .searchboxh34 / bg / border r-md / gap8 / pad 0 11,前导放大镜 14(faint)。
class DsSearchBox extends StatelessWidget {
final TextEditingController? controller;
final String hint;
final ValueChanged<String>? onChanged;
final ValueChanged<String>? onSubmitted;
final double? width;
const DsSearchBox({
super.key,
this.controller,
this.hint = '',
this.onChanged,
this.onSubmitted,
this.width,
});
@override
Widget build(BuildContext context) {
final t = context.tokens;
return SizedBox(
width: width,
height: 34,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 11),
decoration: BoxDecoration(
color: t.bg,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Row(
children: [
Icon(Icons.search, size: 14, color: t.faint),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: controller,
onChanged: onChanged,
onSubmitted: onSubmitted,
style: TextStyle(fontSize: AppDims.fsBody, color: t.text),
decoration: InputDecoration(
isCollapsed: true,
border: InputBorder.none,
hintText: hint,
hintStyle:
TextStyle(fontSize: AppDims.fsBody, color: t.faint),
),
),
),
],
),
),
);
}
}