feat(client): 库存/出入库移动卡片与详情 sheet 逐一对齐原型结构

- 新增 MCard(原型 .m-card 双栏:mc-main 标题+副行 mono | mc-right 徽章行+
  金额 mono | › | mc-foot ⓘ脚注)+ DsIconBadge(.badge.ico 22px 纯图标)/
  DsTextIconBadge(.badge.bi 图标+文字) + statusToneOf 状态词配色表——
  替换原 MobileListCard「标签:值」布局与各屏本地 _icoBadge/_statusColors
- 出入库卡:单号 + 供应商/客户·N 项 + 状态/退单/待定价图标徽章 + 合计金额 +
  日期·经手人·审核 脚注;库存卡:品名 + 编码·规格 + 状态徽章 + 数量(缺货红/
  预警黄);移动卡不再有行内操作(对齐原型,操作统一在详情 sheet;打标签=
  打印,移动端裁剪)
- DsTable 窄屏卡片流去外层大卡容器(卡直接铺页面底色,对齐原型 m-scroll)——
  往来/基础数据等全部卡片流屏同步受益
- 出入库详情窄屏改 sheet 专属布局(原型 openOrder):drow 字段行 + 明细
  pe-card(头行 图标+「明细 · N 项」+ 行=品名+系列·×数量 | 金额/待定价徽章)+
  合计 drow + 等分按钮行(无组标签);桌面右滑抽屉布局不变
- 相关移动 golden 全量重生成(出入库/库存/往来/基础数据 ×3 主题),
  桌面 golden 零漂移;283 测试全绿

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-04 19:30:40 +08:00
parent 145bf06ff8
commit 980e38e020
39 changed files with 509 additions and 263 deletions
+10 -6
View File
@@ -85,18 +85,22 @@ class _DsTableState extends State<DsTable> {
@override
Widget build(BuildContext context) {
final t = context.tokens;
// 窄屏卡片流对齐原型 m-scroll:卡直接铺页面底色,无外层大卡容器
final mobileCardsMode = context.isMobile && widget.mobileCards != null;
return Column(
mainAxisSize: widget.shrinkWrap ? MainAxisSize.min : MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_maybeExpand(
Container(
decoration: BoxDecoration(
color: t.surface,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rLg),
),
clipBehavior: Clip.antiAlias,
decoration: mobileCardsMode
? null
: BoxDecoration(
color: t.surface,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rLg),
),
clipBehavior: mobileCardsMode ? Clip.none : Clip.antiAlias,
child: Column(
// 整宽拉伸:toolbar/表格都填满卡片宽度(否则 toolbar 按内容宽居中→左侧留白)。
mainAxisSize:
+230
View File
@@ -0,0 +1,230 @@
// widgets/ds/m_card.dart — 移动列表卡(镜像原型 mobile-atoms .m-card / .m-row 结构)
// 与 .badge.ico22px 纯图标徽章)/.badge.bi(图标+文字徽章)。
//
// 卡结构:mc-main(mc-nm 标题 + mc-sub 副行 mono) | mc-right(徽章行 + mc-amt 金额)
// | mc-chev ›;可选 mc-foot 脚注行(ⓘ 前缀,上分隔线)。
// 徽章色调按状态词查表(statusToneOf),图标按 status_icon_map(原型 BADGE_ICON)。
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../core/theme/app_dims.g.dart';
import '../../core/theme/app_fonts.dart';
import '../../core/theme/context_tokens.dart';
import 'status_icon_map.dart';
/// 状态词 → (前景, 软底)。对齐原型 .badge.b-* 的语义配色。
(Color fg, Color bg) statusToneOf(BuildContext context, String label) {
final t = context.tokens;
switch (label) {
case '待审核':
case '待定价':
case '部分退单':
case '预警':
case '进行中':
case '未结清':
return (t.warn, t.warnBg);
case '已审核':
case '已通过':
case '在售':
case '启用':
case '已完成':
case '已结清':
case '已兑换':
case '在线':
return (t.success, t.successBg);
case '已拒绝':
case '已驳回':
case '已退单':
case '缺货':
case '停用':
case '作废':
case '离线':
return (t.danger, t.dangerBg);
case '草稿':
return (t.muted, t.infoSoft);
default:
return (t.muted, t.borderSubtle);
}
}
/// 纯图标徽章(原型 .badge.ico):22×22 圆 pill13px 状态图标,长按出状态词。
class DsIconBadge extends StatelessWidget {
final String label;
const DsIconBadge(this.label, {super.key});
@override
Widget build(BuildContext context) {
final (fg, bg) = statusToneOf(context, label);
return Tooltip(
message: label,
child: Container(
width: 22,
height: 22,
decoration: BoxDecoration(
color: bg, borderRadius: BorderRadius.circular(AppDims.rPill)),
alignment: Alignment.center,
child:
Icon(statusIcon(label) ?? LucideIcons.circle, size: 13, color: fg),
),
);
}
}
/// 图标 + 文字徽章(原型 .badge.bi):h22 pill12px 图标替圆点。
class DsTextIconBadge extends StatelessWidget {
final String label;
const DsTextIconBadge(this.label, {super.key});
@override
Widget build(BuildContext context) {
final (fg, bg) = statusToneOf(context, label);
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: [
Icon(statusIcon(label) ?? LucideIcons.circle, size: 12, color: fg),
const SizedBox(width: 5),
Text(label,
style: TextStyle(
color: fg,
fontSize: AppDims.fsSm,
fontWeight: FontWeight.w600)),
]),
);
}
}
/// 移动列表卡(原型 .m-card 双栏结构)。
class MCard extends StatelessWidget {
/// mc-nm 标题(600 heading)。
final String nm;
/// mc-sub 副行(xs muted mono);null 不渲染。
final String? sub;
/// mc-right 上行徽章(DsIconBadge / DsBadge 等),横排 gap5。
final List<Widget> badges;
/// mc-amt 金额/数量(700 heading mono);amtWidget 优先。
final String? amt;
final Widget? amtWidget;
/// mc-foot 脚注(ⓘ 前缀 + xs muted,上分隔线);null 不渲染。
final String? foot;
final VoidCallback? onTap;
const MCard({
super.key,
required this.nm,
this.sub,
this.badges = const [],
this.amt,
this.amtWidget,
this.foot,
this.onTap,
});
@override
Widget build(BuildContext context) {
final t = context.tokens;
return Material(
color: t.surface,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
side: BorderSide(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rLg),
),
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.fromLTRB(14, 13, 14, 13),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
// mc-main
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(nm,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: AppDims.fsBody,
fontWeight: FontWeight.w600,
color: t.heading)),
if (sub != null) ...[
const SizedBox(height: 3),
Text(sub!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: AppDims.fsXs,
color: t.muted,
fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback)),
],
],
),
),
const SizedBox(width: 12),
// mc-right
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (badges.isNotEmpty)
Row(mainAxisSize: MainAxisSize.min, children: [
for (var i = 0; i < badges.length; i++) ...[
if (i > 0) const SizedBox(width: 5),
badges[i],
],
]),
if (amtWidget != null || amt != null) ...[
if (badges.isNotEmpty) const SizedBox(height: 5),
amtWidget ??
Text(amt!,
style: TextStyle(
fontSize: AppDims.fsBody,
fontWeight: FontWeight.w700,
color: t.heading,
fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback)),
],
],
),
const SizedBox(width: 8),
// mc-chev
Icon(LucideIcons.chevronRight, size: 18, color: t.faint),
],
),
if (foot != null)
Container(
margin: const EdgeInsets.only(top: 10),
padding: const EdgeInsets.only(top: 10),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: t.borderSubtle)),
),
child: Row(children: [
Icon(LucideIcons.info, size: 13, color: t.muted),
const SizedBox(width: 10),
Expanded(
child: Text(foot!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: AppDims.fsXs, color: t.muted)),
),
]),
),
],
),
),
),
);
}
}
+157
View File
@@ -7,6 +7,7 @@ import '../core/responsive/responsive.dart';
import '../core/theme/context_tokens.dart';
import '../core/theme/app_dims.g.dart';
import '../core/theme/app_fonts.dart';
import 'ds/m_card.dart';
import 'ds/m_sheet.dart';
/// 右侧滑入抽屉呈现(对齐原型 .drawer / .drawer-mask)。
@@ -126,6 +127,9 @@ class OrderDetailDrawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
final t = context.tokens;
// 窄屏:sheet 形态布局(对齐原型 m-stock-*-list openOrderdrow 字段 +
// 明细 pe-card + 合计 drow + 等分按钮行),桌面右滑抽屉布局不变。
if (context.isMobile) return _buildMobileSheet(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@@ -225,6 +229,159 @@ class OrderDetailDrawer extends StatelessWidget {
],
);
}
// ── 窄屏 sheet 形态(原型 openOrder)────────────────────────────────────
Widget _buildMobileSheet(BuildContext context) {
final t = context.tokens;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 头:单号 + Xgrip 由 showMSheet 提供)
Container(
padding: const EdgeInsets.fromLTRB(16, 4, 8, 8),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: t.borderSubtle))),
child: Row(children: [
Expanded(
child: Text(title,
style: TextStyle(
fontSize: AppDims.fsTitle,
fontWeight: FontWeight.w700,
color: t.heading)),
),
IconButton(
icon: Icon(LucideIcons.x, size: 20, color: t.muted),
splashRadius: 18,
onPressed: () => Navigator.of(context).pop(),
),
]),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 2, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (final r in infoRows)
_DrawerRow(label: r.label, value: r.value),
// 明细卡(原型 pe-card:头行 图标+「入库明细 · N 项」+ 行列表)
Container(
margin: const EdgeInsets.only(top: 14),
decoration: BoxDecoration(
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rLg),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.fromLTRB(14, 11, 14, 11),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: t.borderSubtle))),
child: Row(children: [
Icon(LucideIcons.tableProperties,
size: 15, color: t.primary),
const SizedBox(width: 8),
Text('$linesLabel · ${lines.length}',
style: TextStyle(
fontSize: AppDims.fsSm,
fontWeight: FontWeight.w600,
color: t.heading)),
]),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 2),
child: Column(children: [
for (var i = 0; i < lines.length; i++)
_mLineRow(t, lines[i], i == lines.length - 1),
]),
),
],
),
),
// 合计(原型 drowlabel 左 / mono 值右)
_mTotalRow(t, '合计金额', totalText, t.heading),
if (profitText != null)
_mTotalRow(t, '合计利润', profitText!,
profitText!.startsWith('-') ? t.danger : t.success),
// 操作按钮(原型:等分按钮行,无组标签)
if (actionGroups.isNotEmpty) const SizedBox(height: 16),
for (var g = 0; g < actionGroups.length; g++) ...[
if (g > 0) const SizedBox(height: 10),
Row(children: [
for (var b = 0;
b < actionGroups[g].buttons.length;
b++) ...[
if (b > 0) const SizedBox(width: 10),
Expanded(child: actionGroups[g].buttons[b]),
],
]),
],
],
),
),
),
],
);
}
/// 窄屏明细行(原型:左 商品名 + 系列·×数量 小字,右 金额 mono / 待定价徽章)。
Widget _mLineRow(dynamic t, OrderLine l, bool last) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 11),
decoration: last
? null
: BoxDecoration(
border: Border(bottom: BorderSide(color: t.borderSubtle))),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l.name,
style: TextStyle(
fontSize: AppDims.fsBody, color: t.text, height: 1.45)),
const SizedBox(height: 2),
Text('${l.series} · ×${l.qty}',
style: TextStyle(fontSize: AppDims.fsXs, color: t.faint)),
],
),
),
const SizedBox(width: 10),
l.pending
? const DsTextIconBadge('待定价')
: Text(l.amount.isNotEmpty ? l.amount : l.price,
style: TextStyle(
fontSize: AppDims.fsBody,
fontWeight: FontWeight.w600,
color: t.heading,
fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback)),
],
),
);
}
Widget _mTotalRow(dynamic t, String label, String value, Color color) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 11),
child: Row(children: [
Text(label, style: TextStyle(fontSize: AppDims.fsBody, color: t.muted)),
const Spacer(),
Text(value,
style: TextStyle(
fontSize: AppDims.fsBody,
fontWeight: FontWeight.w700,
color: color,
fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback)),
]),
);
}
}
class _DrawerRow extends StatelessWidget {