diff --git a/client/lib/screens/inventory/inventory_list_screen.dart b/client/lib/screens/inventory/inventory_list_screen.dart index 5b5f2d9..f5ff6cc 100644 --- a/client/lib/screens/inventory/inventory_list_screen.dart +++ b/client/lib/screens/inventory/inventory_list_screen.dart @@ -8,26 +8,19 @@ import '../../core/theme/context_tokens.dart'; import '../../core/theme/app_dims.g.dart'; import '../../core/storage/column_prefs.dart'; import '../../core/utils/export_util.dart'; -import '../../core/utils/print_util.dart'; -import '../../core/utils/dialog_util.dart'; import '../../models/inventory.dart'; import '../../providers/inventory_provider.dart'; -import '../../providers/product_provider.dart'; import '../../providers/product_option_provider.dart'; -import '../../providers/shop_provider.dart' show shopInfoProvider; import '../../widgets/write_guard.dart'; -import '../../widgets/kpi_card.dart' show StatusPill; // 移动卡片状态药丸 import '../../widgets/ds/ds_atoms.dart'; import '../../widgets/ds/ds_kpi.dart'; import '../../widgets/ds/ds_menu.dart'; import '../../widgets/ds/ds_table.dart'; +import '../../widgets/ds/m_card.dart'; import '../../widgets/ds/m_kpi_grid.dart'; import '../../widgets/ds/m_search_row.dart'; import '../../widgets/ds/m_sheet.dart'; -import '../../widgets/ds/status_icon_map.dart'; -import '../../widgets/mobile_list_card.dart'; import '../../widgets/multi_select_dropdown.dart' show ColDef; -import '../../widgets/label_preview_dialog.dart'; import '../../widgets/product_editor_drawer.dart'; import '../../core/theme/app_fonts.dart'; import '../../widgets/ds/ds_toast.dart'; @@ -363,33 +356,6 @@ class _InventoryListScreenState extends ConsumerState { } } - Future _printLabel(BuildContext context, Inventory item) async { - if (item.productId == null) return; - final qrBytes = await ref - .read(productRepositoryProvider) - .getQRCodeBytes(item.productId!); - final shopInfo = ref.read(shopInfoProvider).valueOrNull; - if (!context.mounted) return; - final label = LabelData( - productId: item.productId, - qrBytes: qrBytes, - name: item.productName, - code: item.productCode, - series: item.series.isEmpty ? null : item.series, - spec: item.spec.isEmpty ? null : item.spec, - batchNo: item.batchNo.isEmpty ? null : item.batchNo, - productionDate: item.productionDate, - remark: item.remark.isEmpty ? null : item.remark, - shopName: shopInfo?.name ?? '', - shopAddress: shopInfo?.address ?? '', - shopPhone: shopInfo?.phone ?? '', - ); - showAppDialog( - context: context, - builder: (_) => LabelPreviewDialog(labels: [label]), - ); - } - void _exportInventory(List items) { exportExcel( filename: '库存查询', @@ -551,41 +517,40 @@ class _InventoryListScreenState extends ConsumerState { }; } - /// 库存查询:窄屏卡片 + /// 库存查询:窄屏卡片(原型 m-inventory .m-card 双栏: + /// 品名+编码·规格 | 状态徽章+数量 | ›;操作统一收进详情 sheet,移动无打印)。 Widget _inventoryCard(Inventory item) { - return MobileListCard( + final t = context.tokens; + final status = _statusOf(item); + final qtyColor = switch (status) { + '缺货' => t.danger, + '预警' => t.warn, + _ => t.heading, + }; + final sub = [ + if (item.productCode.isNotEmpty) item.productCode, + if (item.spec.isNotEmpty) item.spec, + ].join(' · '); + return MCard( onTap: item.productId != null ? () => _openEditor(item) : null, - title: Text(item.productName.isEmpty ? '-' : item.productName), - subtitle: item.productCode.isEmpty ? null : Text(item.productCode), - trailing: _InventoryStatusBadge(item), - fields: [ - if (item.spec.isNotEmpty) MobileCardField('规格', item.spec), - if (item.series.isNotEmpty) MobileCardField('系列', item.series), - if (item.batchNo.isNotEmpty) MobileCardField('批次', item.batchNo), - MobileCardField( - '仓库', item.warehouseName.isEmpty ? '-' : item.warehouseName), - MobileCardField( - '单价', - item.unitPrice != null - ? '¥${item.unitPrice!.toStringAsFixed(2)}' - : '待定价'), - if (item.supplierName.isNotEmpty) - MobileCardField('供应商', item.supplierName), - ], - actions: [ - if (!WriteGuard.isReadonly(ref)) - WriteGuard( - child: TextButton( - onPressed: () => _editRemark(context, item), - child: const Text('备注', style: TextStyle(fontSize: 13)), - ), - ), - if (item.productId != null) - TextButton( - onPressed: () => _printLabel(context, item), - child: const Text('打标签', style: TextStyle(fontSize: 13)), - ), - ], + nm: item.productName.isEmpty ? '-' : item.productName, + sub: sub.isEmpty ? null : sub, + badges: [DsTextIconBadge(status)], + amtWidget: Text.rich( + TextSpan(children: [ + TextSpan( + text: item.quantity.toStringAsFixed(0), + style: TextStyle(color: qtyColor)), + TextSpan( + text: ' ${item.unit}'.trimRight(), + style: TextStyle(color: t.heading, fontWeight: FontWeight.w400)), + ]), + style: TextStyle( + fontSize: AppDims.fsBody, + fontWeight: FontWeight.w700, + fontFamily: AppFonts.mono, + fontFamilyFallback: AppFonts.monoFallback), + ), ); } @@ -678,46 +643,46 @@ class _InventoryListScreenState extends ConsumerState { // 头部(原型 .head{margin-bottom:18px}):标题 + SKU 数 + 列设置/导出 // 窄屏隐藏(原型 m-inventory:标题在壳顶栏 m-top) if (!mobile) - Container( - width: double.infinity, - color: context.tokens.bg, - padding: const EdgeInsets.only(bottom: 18), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text('库存管理', - style: TextStyle( - fontSize: AppDims.fsH1, - fontWeight: FontWeight.w700, - color: context.tokens.heading)), - const SizedBox(width: AppDims.sp3), - Padding( - padding: const EdgeInsets.only(bottom: 2), - child: Text( - '共 ${NumberFormat.decimalPattern().format(result.total)} 个 SKU · 数据实时', + Container( + width: double.infinity, + color: context.tokens.bg, + padding: const EdgeInsets.only(bottom: 18), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text('库存管理', style: TextStyle( - fontSize: AppDims.fsSm, - color: context.tokens.muted)), - ), - const Spacer(), - if (!context.isMobile) ...[ - Builder( - builder: (bctx) => DsButton( - '列设置', - icon: LucideIcons.alignLeft, - onPressed: () => _openColMenu(bctx), + fontSize: AppDims.fsH1, + fontWeight: FontWeight.w700, + color: context.tokens.heading)), + const SizedBox(width: AppDims.sp3), + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Text( + '共 ${NumberFormat.decimalPattern().format(result.total)} 个 SKU · 数据实时', + style: TextStyle( + fontSize: AppDims.fsSm, + color: context.tokens.muted)), + ), + const Spacer(), + if (!context.isMobile) ...[ + Builder( + builder: (bctx) => DsButton( + '列设置', + icon: LucideIcons.alignLeft, + onPressed: () => _openColMenu(bctx), + ), ), - ), - const SizedBox(width: AppDims.sp2), - DsButton( - '导出', - icon: LucideIcons.download, - onPressed: () => _exportInventory(filteredItems), - ), + const SizedBox(width: AppDims.sp2), + DsButton( + '导出', + icon: LucideIcons.download, + onPressed: () => _exportInventory(filteredItems), + ), + ], ], - ], + ), ), - ), // KPI 卡(还原原型 .kpi):宽屏 4 等分,窄屏横向滚动 Builder(builder: (context) { // KPI 走全店汇总接口(不随分页),环比对月初快照。 @@ -771,8 +736,7 @@ class _InventoryListScreenState extends ConsumerState { delta: '需补货 ${summary?.warningCount ?? 0} 项', deltaTone: MKpiDeltaTone.warn, selected: _statusFilter == '缺货', - onTap: () => - setState(() => _statusFilter = '缺货'), + onTap: () => setState(() => _statusFilter = '缺货'), ), ]), ); @@ -1059,34 +1023,3 @@ class _RowActEyeState extends State<_RowActEye> { ); } } - -class _InventoryStatusBadge extends StatelessWidget { - final Inventory item; - const _InventoryStatusBadge(this.item); - - @override - Widget build(BuildContext context) { - final t = context.tokens; - // 状态派生 qty vs min_stock(在售 / 预警 / 缺货),软底 + 图标变体 - //(2026-07-04 拍板:圆点 → 代表图标,映射走 status_icon_map)。 - if (item.quantity == 0) { - return StatusPill( - label: '缺货', - color: t.danger, - background: t.dangerBg, - icon: statusIcon('缺货')); - } - if (item.minStock != null && item.quantity < item.minStock!) { - return StatusPill( - label: '预警', - color: t.warn, - background: t.warnBg, - icon: statusIcon('预警')); - } - return StatusPill( - label: '在售', - color: t.success, - background: t.okSoft, - icon: statusIcon('在售')); - } -} diff --git a/client/lib/screens/stock_in/stock_in_list_screen.dart b/client/lib/screens/stock_in/stock_in_list_screen.dart index 14ad77a..2fed780 100644 --- a/client/lib/screens/stock_in/stock_in_list_screen.dart +++ b/client/lib/screens/stock_in/stock_in_list_screen.dart @@ -19,9 +19,8 @@ import '../../widgets/ds/ds_kpi.dart'; import '../../widgets/ds/ds_table.dart'; import '../../widgets/ds/m_kpi_grid.dart'; import '../../widgets/ds/m_search_row.dart'; +import '../../widgets/ds/m_card.dart'; import '../../widgets/ds/m_sheet.dart'; -import '../../widgets/ds/status_icon_map.dart'; -import '../../widgets/mobile_list_card.dart'; import '../../widgets/searchable_option_field.dart'; import '../../widgets/combo_search_field.dart'; import '../../widgets/status_badge.dart'; @@ -577,34 +576,7 @@ class _StockInListScreenState extends ConsumerState { // ── 窄屏形态(对齐移动原型 m-stock-in-list.html)────────────────── /// 状态词 → (前景, 软底):镜像原型 .badge.b-*,全走 token。 - (Color, Color) _statusColors(String label) { - final t = context.tokens; - return switch (label) { - '待审核' => (t.warn, t.warnBg), - '已审核' => (t.success, t.successBg), - '已拒绝' => (t.danger, t.dangerBg), - '部分退单' => (t.warn, t.warnBg), - '已退单' => (t.danger, t.dangerBg), - '待定价' => (t.warn, t.warnBg), - _ => (t.muted, t.infoSoft), // 草稿 - }; - } - /// 原型 .badge.ico:纯图标小徽章(卡片右上态标 / 状态 sheet 选项行)。 - Widget _icoBadge(String label) { - final (fg, bg) = _statusColors(label); - return Container( - height: 22, - width: 26, - alignment: Alignment.center, - decoration: BoxDecoration( - color: bg, - borderRadius: BorderRadius.circular(AppDims.rPill), - ), - child: Icon(statusIcon(label), size: 12, color: fg), - ); - } - /// 原型 .m-kpi 2×2:近30天笔数/金额 + 可点筛选的待审核/草稿(再点取消)。 Widget _buildMobileKpis(StockSummary? summary, int total) { String yuanWan(double v) => v >= 10000 @@ -720,7 +692,7 @@ class _StockInListScreenState extends ConsumerState { ), child: Row(children: [ if (_statusOptions[i].$2 != '全部') ...[ - _icoBadge(_statusOptions[i].$2), + DsIconBadge(_statusOptions[i].$2), const SizedBox(width: 10), ], Text(_statusOptions[i].$2, @@ -924,41 +896,29 @@ class _StockInListScreenState extends ConsumerState { } } - /// 入库单:窄屏卡片(原型 .m-card:图标徽章 + › 箭头;操作统一收进详情 sheet)。 + /// 入库单:窄屏卡片(原型 .m-card 双栏:单号+供应商·N 项 | 图标徽章+金额 | ›; + /// mc-foot = 日期 · 经手人 · 审核)。 Widget _orderCard(BuildContext context, StockInOrder o) { final hasPending = o.items.any((it) => it.costPrice == 0); - return MobileListCard( + final foot = [ + if (o.orderDate != null) o.orderDate!.substring(0, 10), + if (o.operatorName != null) '经手人 ${o.operatorName}', + if (o.reviewerName != null) '审核 ${o.reviewerName}', + ].join(' · '); + return MCard( onTap: () => _showDetail(context, o.id), - title: Text(o.orderNo, - style: const TextStyle( - fontFamily: AppFonts.mono, - fontFamilyFallback: AppFonts.monoFallback, - fontSize: 14)), - trailing: Row(mainAxisSize: MainAxisSize.min, children: [ - _icoBadge(_apiStatusToEnum(o.status).label), - if (o.returnState == 'partial') ...[ - const SizedBox(width: 5), - _icoBadge('部分退单'), - ] else if (o.returnState == 'full') ...[ - const SizedBox(width: 5), - _icoBadge('已退单'), - ], - if (hasPending) ...[ - const SizedBox(width: 5), - _icoBadge('待定价'), - ], - const SizedBox(width: 6), - Icon(LucideIcons.chevronRight, size: 18, color: context.tokens.faint), - ]), - fields: [ - MobileCardField('供应商', o.partnerName ?? '-'), - MobileCardField('仓库', o.warehouseName ?? '-'), - MobileCardField('合计金额', - o.costTotal != null ? '¥${o.costTotal!.toStringAsFixed(2)}' : '-'), - MobileCardField('入库时间', o.orderDate?.substring(0, 10) ?? '-'), - MobileCardField('入库员', o.operatorName ?? '-'), - MobileCardField('审核员', o.reviewerName ?? '-'), + nm: o.orderNo, + sub: '${o.partnerName ?? '—'} · ${o.items.length} 项', + badges: [ + DsIconBadge(_apiStatusToEnum(o.status).label), + if (o.returnState == 'partial') + const DsIconBadge('部分退单') + else if (o.returnState == 'full') + const DsIconBadge('已退单'), + if (hasPending) const DsIconBadge('待定价'), ], + amt: o.costTotal != null ? _money.format(o.costTotal) : '—', + foot: foot.isEmpty ? null : foot, ); } diff --git a/client/lib/screens/stock_out/stock_out_list_screen.dart b/client/lib/screens/stock_out/stock_out_list_screen.dart index a64d72f..6555c2a 100644 --- a/client/lib/screens/stock_out/stock_out_list_screen.dart +++ b/client/lib/screens/stock_out/stock_out_list_screen.dart @@ -33,9 +33,8 @@ import '../../widgets/ds/ds_kpi.dart'; import '../../widgets/ds/ds_table.dart'; import '../../widgets/ds/m_kpi_grid.dart'; import '../../widgets/ds/m_search_row.dart'; +import '../../widgets/ds/m_card.dart'; import '../../widgets/ds/m_sheet.dart'; -import '../../widgets/ds/status_icon_map.dart'; -import '../../widgets/mobile_list_card.dart'; import '../../widgets/status_badge.dart'; import '../../widgets/searchable_option_field.dart'; import '../../widgets/combo_search_field.dart'; @@ -607,34 +606,7 @@ class _StockOutListScreenState extends ConsumerState { } /// 状态词 → (前景, 软底):镜像原型 .badge.b-*,全走 token。 - (Color, Color) _statusColors(String label) { - final t = context.tokens; - return switch (label) { - '待审核' => (t.warn, t.warnBg), - '已审核' => (t.success, t.successBg), - '已拒绝' => (t.danger, t.dangerBg), - '部分退单' => (t.warn, t.warnBg), - '已退单' => (t.danger, t.dangerBg), - '待定价' => (t.warn, t.warnBg), - _ => (t.muted, t.infoSoft), // 草稿 - }; - } - /// 原型 .badge.ico:纯图标小徽章(卡片右上态标 / 状态 sheet 选项行)。 - Widget _icoBadge(String label) { - final (fg, bg) = _statusColors(label); - return Container( - height: 22, - width: 26, - alignment: Alignment.center, - decoration: BoxDecoration( - color: bg, - borderRadius: BorderRadius.circular(AppDims.rPill), - ), - child: Icon(statusIcon(label), size: 12, color: fg), - ); - } - /// 原型 .m-kpi 2×2:近30天笔数/金额 + 可点筛选的待审核/草稿(再点取消)。 Widget _buildMobileKpis(int total) { final StockSummary? summary = @@ -761,7 +733,7 @@ class _StockOutListScreenState extends ConsumerState { ), child: Row(children: [ if (_statusOptions[i].$2 != '全部') ...[ - _icoBadge(_statusOptions[i].$2), + DsIconBadge(_statusOptions[i].$2), const SizedBox(width: 10), ], Text(_statusOptions[i].$2, @@ -998,39 +970,29 @@ class _StockOutListScreenState extends ConsumerState { // ── 窄屏卡片 ───────────────────────────────────────────────── - /// 出库单:窄屏卡片(原型 .m-card:图标徽章 + › 箭头;操作统一收进详情 sheet)。 + /// 出库单:窄屏卡片(原型 .m-card 双栏:单号+客户·N 项 | 图标徽章+金额 | ›; + /// mc-foot = 日期 · 经手人 · 审核)。 Widget _orderCard(BuildContext context, StockOutOrder o) { final hasPending = o.items.any((it) => it.salePrice <= 0); - return MobileListCard( + final foot = [ + if (o.orderDate != null) o.orderDate!.substring(0, 10), + if (o.operatorName != null) '经手人 ${o.operatorName}', + if (o.reviewerName != null) '审核 ${o.reviewerName}', + ].join(' · '); + return MCard( onTap: () => _showDetail(context, o.id), - title: Text(o.orderNo, - style: const TextStyle( - fontFamily: AppFonts.mono, - fontFamilyFallback: AppFonts.monoFallback, - fontSize: 14)), - trailing: Row(mainAxisSize: MainAxisSize.min, children: [ - _icoBadge(_apiStatusToEnum(o.status).label), - if (o.returnState == 'partial') ...[ - const SizedBox(width: 5), - _icoBadge('部分退单'), - ] else if (o.returnState == 'full') ...[ - const SizedBox(width: 5), - _icoBadge('已退单'), - ], - if (hasPending) ...[ - const SizedBox(width: 5), - _icoBadge('待定价'), - ], - const SizedBox(width: 6), - Icon(LucideIcons.chevronRight, size: 18, color: context.tokens.faint), - ]), - fields: [ - MobileCardField('客户', o.partnerName ?? '-'), - MobileCardField('仓库', o.warehouseName ?? '-'), - MobileCardField('金额', - o.saleTotal != null ? '¥${o.saleTotal!.toStringAsFixed(2)}' : '-'), - MobileCardField('出库时间', o.orderDate?.substring(0, 10) ?? '-'), + nm: o.orderNo, + sub: '${o.partnerName ?? '—'} · ${o.items.length} 项', + badges: [ + DsIconBadge(_apiStatusToEnum(o.status).label), + if (o.returnState == 'partial') + const DsIconBadge('部分退单') + else if (o.returnState == 'full') + const DsIconBadge('已退单'), + if (hasPending) const DsIconBadge('待定价'), ], + amt: o.saleTotal != null ? _money.format(o.saleTotal) : '—', + foot: foot.isEmpty ? null : foot, ); } diff --git a/client/lib/widgets/ds/ds_table.dart b/client/lib/widgets/ds/ds_table.dart index 3d37066..cd9daa1 100644 --- a/client/lib/widgets/ds/ds_table.dart +++ b/client/lib/widgets/ds/ds_table.dart @@ -85,18 +85,22 @@ class _DsTableState extends State { @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: diff --git a/client/lib/widgets/ds/m_card.dart b/client/lib/widgets/ds/m_card.dart new file mode 100644 index 0000000..935b7ed --- /dev/null +++ b/client/lib/widgets/ds/m_card.dart @@ -0,0 +1,230 @@ +// widgets/ds/m_card.dart — 移动列表卡(镜像原型 mobile-atoms .m-card / .m-row 结构) +// 与 .badge.ico(22px 纯图标徽章)/.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 圆 pill,13px 状态图标,长按出状态词。 +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 pill,12px 图标替圆点。 +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 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)), + ), + ]), + ), + ], + ), + ), + ), + ); + } +} diff --git a/client/lib/widgets/order_detail_drawer.dart b/client/lib/widgets/order_detail_drawer.dart index cafdfa1..b56b65d 100644 --- a/client/lib/widgets/order_detail_drawer.dart +++ b/client/lib/widgets/order_detail_drawer.dart @@ -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 openOrder:drow 字段 + + // 明细 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: [ + // 头:单号 + X(grip 由 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), + ]), + ), + ], + ), + ), + // 合计(原型 drow:label 左 / 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 { diff --git a/client/test/golden/goldens/inventory_list_mobile_a.png b/client/test/golden/goldens/inventory_list_mobile_a.png index 4adb732..81a26cd 100644 Binary files a/client/test/golden/goldens/inventory_list_mobile_a.png and b/client/test/golden/goldens/inventory_list_mobile_a.png differ diff --git a/client/test/golden/goldens/inventory_list_mobile_b.png b/client/test/golden/goldens/inventory_list_mobile_b.png index d1bc4bf..7152ee8 100644 Binary files a/client/test/golden/goldens/inventory_list_mobile_b.png and b/client/test/golden/goldens/inventory_list_mobile_b.png differ diff --git a/client/test/golden/goldens/inventory_list_mobile_c.png b/client/test/golden/goldens/inventory_list_mobile_c.png index b4d0fbe..b9ec78f 100644 Binary files a/client/test/golden/goldens/inventory_list_mobile_c.png and b/client/test/golden/goldens/inventory_list_mobile_c.png differ diff --git a/client/test/golden/goldens/m_inventory_a.png b/client/test/golden/goldens/m_inventory_a.png index 9d006a8..79fb85c 100644 Binary files a/client/test/golden/goldens/m_inventory_a.png and b/client/test/golden/goldens/m_inventory_a.png differ diff --git a/client/test/golden/goldens/m_inventory_b.png b/client/test/golden/goldens/m_inventory_b.png index ba6794a..26aa41d 100644 Binary files a/client/test/golden/goldens/m_inventory_b.png and b/client/test/golden/goldens/m_inventory_b.png differ diff --git a/client/test/golden/goldens/m_inventory_c.png b/client/test/golden/goldens/m_inventory_c.png index 965f27c..c45b919 100644 Binary files a/client/test/golden/goldens/m_inventory_c.png and b/client/test/golden/goldens/m_inventory_c.png differ diff --git a/client/test/golden/goldens/m_partners_a.png b/client/test/golden/goldens/m_partners_a.png index fb42549..fa5229e 100644 Binary files a/client/test/golden/goldens/m_partners_a.png and b/client/test/golden/goldens/m_partners_a.png differ diff --git a/client/test/golden/goldens/m_partners_b.png b/client/test/golden/goldens/m_partners_b.png index e56453a..2465f6e 100644 Binary files a/client/test/golden/goldens/m_partners_b.png and b/client/test/golden/goldens/m_partners_b.png differ diff --git a/client/test/golden/goldens/m_partners_c.png b/client/test/golden/goldens/m_partners_c.png index 53b4a5b..9efbbab 100644 Binary files a/client/test/golden/goldens/m_partners_c.png and b/client/test/golden/goldens/m_partners_c.png differ diff --git a/client/test/golden/goldens/m_products_a.png b/client/test/golden/goldens/m_products_a.png index 223f75f..c0abbf2 100644 Binary files a/client/test/golden/goldens/m_products_a.png and b/client/test/golden/goldens/m_products_a.png differ diff --git a/client/test/golden/goldens/m_products_b.png b/client/test/golden/goldens/m_products_b.png index 681f937..cc78a1a 100644 Binary files a/client/test/golden/goldens/m_products_b.png and b/client/test/golden/goldens/m_products_b.png differ diff --git a/client/test/golden/goldens/m_products_c.png b/client/test/golden/goldens/m_products_c.png index f6e2714..5ff6144 100644 Binary files a/client/test/golden/goldens/m_products_c.png and b/client/test/golden/goldens/m_products_c.png differ diff --git a/client/test/golden/goldens/m_products_series_a.png b/client/test/golden/goldens/m_products_series_a.png index 2c30797..54a620e 100644 Binary files a/client/test/golden/goldens/m_products_series_a.png and b/client/test/golden/goldens/m_products_series_a.png differ diff --git a/client/test/golden/goldens/m_products_series_b.png b/client/test/golden/goldens/m_products_series_b.png index 3d87f84..3a3b67a 100644 Binary files a/client/test/golden/goldens/m_products_series_b.png and b/client/test/golden/goldens/m_products_series_b.png differ diff --git a/client/test/golden/goldens/m_products_series_c.png b/client/test/golden/goldens/m_products_series_c.png index 5fa0b1c..73c2793 100644 Binary files a/client/test/golden/goldens/m_products_series_c.png and b/client/test/golden/goldens/m_products_series_c.png differ diff --git a/client/test/golden/goldens/m_stock_in_list_a.png b/client/test/golden/goldens/m_stock_in_list_a.png index 0073081..23fe9a1 100644 Binary files a/client/test/golden/goldens/m_stock_in_list_a.png and b/client/test/golden/goldens/m_stock_in_list_a.png differ diff --git a/client/test/golden/goldens/m_stock_in_list_b.png b/client/test/golden/goldens/m_stock_in_list_b.png index 142f5c2..80363f1 100644 Binary files a/client/test/golden/goldens/m_stock_in_list_b.png and b/client/test/golden/goldens/m_stock_in_list_b.png differ diff --git a/client/test/golden/goldens/m_stock_in_list_c.png b/client/test/golden/goldens/m_stock_in_list_c.png index a5a9ebe..127dadc 100644 Binary files a/client/test/golden/goldens/m_stock_in_list_c.png and b/client/test/golden/goldens/m_stock_in_list_c.png differ diff --git a/client/test/golden/goldens/m_stock_out_list_a.png b/client/test/golden/goldens/m_stock_out_list_a.png index d04ac7b..68f4c89 100644 Binary files a/client/test/golden/goldens/m_stock_out_list_a.png and b/client/test/golden/goldens/m_stock_out_list_a.png differ diff --git a/client/test/golden/goldens/m_stock_out_list_b.png b/client/test/golden/goldens/m_stock_out_list_b.png index 7ddc999..54664f3 100644 Binary files a/client/test/golden/goldens/m_stock_out_list_b.png and b/client/test/golden/goldens/m_stock_out_list_b.png differ diff --git a/client/test/golden/goldens/m_stock_out_list_c.png b/client/test/golden/goldens/m_stock_out_list_c.png index 9452fba..c2cadbb 100644 Binary files a/client/test/golden/goldens/m_stock_out_list_c.png and b/client/test/golden/goldens/m_stock_out_list_c.png differ diff --git a/client/test/golden/goldens/partners_mobile_a.png b/client/test/golden/goldens/partners_mobile_a.png index 0f2c5e2..8a6e0bb 100644 Binary files a/client/test/golden/goldens/partners_mobile_a.png and b/client/test/golden/goldens/partners_mobile_a.png differ diff --git a/client/test/golden/goldens/partners_mobile_b.png b/client/test/golden/goldens/partners_mobile_b.png index 1fd7005..3f20d2d 100644 Binary files a/client/test/golden/goldens/partners_mobile_b.png and b/client/test/golden/goldens/partners_mobile_b.png differ diff --git a/client/test/golden/goldens/partners_mobile_c.png b/client/test/golden/goldens/partners_mobile_c.png index 98640b8..f108c6c 100644 Binary files a/client/test/golden/goldens/partners_mobile_c.png and b/client/test/golden/goldens/partners_mobile_c.png differ diff --git a/client/test/golden/goldens/products_archive_mobile_a.png b/client/test/golden/goldens/products_archive_mobile_a.png index 443e215..07f262b 100644 Binary files a/client/test/golden/goldens/products_archive_mobile_a.png and b/client/test/golden/goldens/products_archive_mobile_a.png differ diff --git a/client/test/golden/goldens/products_archive_mobile_b.png b/client/test/golden/goldens/products_archive_mobile_b.png index 067a575..e94f6b8 100644 Binary files a/client/test/golden/goldens/products_archive_mobile_b.png and b/client/test/golden/goldens/products_archive_mobile_b.png differ diff --git a/client/test/golden/goldens/products_archive_mobile_c.png b/client/test/golden/goldens/products_archive_mobile_c.png index 6d31dba..b544fc8 100644 Binary files a/client/test/golden/goldens/products_archive_mobile_c.png and b/client/test/golden/goldens/products_archive_mobile_c.png differ diff --git a/client/test/golden/goldens/stock_in_list_mobile_a.png b/client/test/golden/goldens/stock_in_list_mobile_a.png index a1359d9..ec97077 100644 Binary files a/client/test/golden/goldens/stock_in_list_mobile_a.png and b/client/test/golden/goldens/stock_in_list_mobile_a.png differ diff --git a/client/test/golden/goldens/stock_in_list_mobile_b.png b/client/test/golden/goldens/stock_in_list_mobile_b.png index 224fd2b..adce576 100644 Binary files a/client/test/golden/goldens/stock_in_list_mobile_b.png and b/client/test/golden/goldens/stock_in_list_mobile_b.png differ diff --git a/client/test/golden/goldens/stock_in_list_mobile_c.png b/client/test/golden/goldens/stock_in_list_mobile_c.png index dfc6b36..4eba4f9 100644 Binary files a/client/test/golden/goldens/stock_in_list_mobile_c.png and b/client/test/golden/goldens/stock_in_list_mobile_c.png differ diff --git a/client/test/golden/goldens/stock_out_list_mobile_a.png b/client/test/golden/goldens/stock_out_list_mobile_a.png index a2f4760..bcff332 100644 Binary files a/client/test/golden/goldens/stock_out_list_mobile_a.png and b/client/test/golden/goldens/stock_out_list_mobile_a.png differ diff --git a/client/test/golden/goldens/stock_out_list_mobile_b.png b/client/test/golden/goldens/stock_out_list_mobile_b.png index 0a5a458..9e8c625 100644 Binary files a/client/test/golden/goldens/stock_out_list_mobile_b.png and b/client/test/golden/goldens/stock_out_list_mobile_b.png differ diff --git a/client/test/golden/goldens/stock_out_list_mobile_c.png b/client/test/golden/goldens/stock_out_list_mobile_c.png index 7aecec4..3d6d420 100644 Binary files a/client/test/golden/goldens/stock_out_list_mobile_c.png and b/client/test/golden/goldens/stock_out_list_mobile_c.png differ