Files
jiu/client/lib/widgets/ds/ds_atoms.dart
T
wangjia a3a722689e feat(client): 出入库列表按原型重建(KPI/详情抽屉/详细搜索/工具栏/选中复制/窗口尺寸)
- 列表重建:KPI 卡、状态+时间列、操作改眼睛开右侧详情抽屉、重置右置
- 详情抽屉(order_detail_drawer) 100% 还原原型 + SelectionArea 可选中复制
- 详细搜索弹窗:ComboSearchField/滚轮日期、字段重构、白底盒子按钮
- 工具栏:搜索框×清除+占位精简、供应商/客户下拉取全部、状态菜单收窄
- 加载不白屏(半透明遮罩)、输入框与下拉等高、windows/macOS 默认窗口尺寸调大

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 08:48:01 +08:00

275 lines
9.7 KiB
Dart
Raw 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;
final VoidCallback? onClear; // 选中态下点 × 单独清除该筛选(不触发展开菜单)
const DsChip(
{super.key,
required this.label,
this.value,
this.onTap,
this.onClear});
@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),
// 选中且提供 onClear → 展示可点 ×(清该筛选);否则展示下拉箭头。
if (on && onClear != null)
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onClear,
child: Icon(Icons.close, size: 14, color: t.muted),
)
else
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),
),
),
),
// 有输入时显示 × 清除(清空并触发一次空关键词搜索)。
if (controller != null)
ValueListenableBuilder<TextEditingValue>(
valueListenable: controller!,
builder: (context, value, _) {
if (value.text.isEmpty) return const SizedBox.shrink();
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
controller!.clear();
onChanged?.call('');
onSubmitted?.call('');
},
child: Padding(
padding: const EdgeInsets.only(left: 6),
child: Icon(Icons.close, size: 14, color: t.muted),
),
);
},
),
],
),
),
);
}
}
/// 加载遮罩(reload 时叠在旧内容之上):轻微半透明底 + 屏幕中心进度圈。
/// 搜索/筛选刷新时用它替代整屏白屏(见列表屏 skipLoadingOnReload)。
class DsLoadingScrim extends StatelessWidget {
const DsLoadingScrim({super.key});
@override
Widget build(BuildContext context) {
final t = context.tokens;
return GestureDetector(
behavior: HitTestBehavior.opaque, // 加载期间拦截点击,避免误触
onTap: () {},
child: ColoredBox(
color: t.bg.withValues(alpha: 0.55),
child: Center(
child: SizedBox(
width: 34,
height: 34,
child: CircularProgressIndicator(strokeWidth: 3, color: t.primary),
),
),
),
);
}
}