Files
jiu/client/lib/widgets/ds/ds_atoms.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

216 lines
7.6 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_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),
),
),
),
],
),
),
);
}
}