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:
@@ -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 }
|
||||||
|
|
||||||
|
/// 原型 .btn:h38 / 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.sm:h32 / 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 }
|
||||||
|
|
||||||
|
/// 原型 .badge:h22 / 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 ─────────────────────────────────────────────────────────────────
|
||||||
|
/// 原型 .chip:h34 / pad 0 12 / border r-md / surface / fs-sm / gap7。
|
||||||
|
/// 选中值用 .cv(primary 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 ──────────────────────────────────────────────────────────────
|
||||||
|
/// 原型 .searchbox:h34 / 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
|
||||||
|
/// 原型 .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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user