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>
@@ -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<InventoryListScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _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<Inventory> items) {
|
||||
exportExcel(
|
||||
filename: '库存查询',
|
||||
@@ -551,41 +517,40 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
};
|
||||
}
|
||||
|
||||
/// 库存查询:窄屏卡片
|
||||
/// 库存查询:窄屏卡片(原型 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<InventoryListScreen> {
|
||||
// 头部(原型 .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<InventoryListScreen> {
|
||||
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('在售'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<StockInListScreen> {
|
||||
// ── 窄屏形态(对齐移动原型 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<StockInListScreen> {
|
||||
),
|
||||
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<StockInListScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 入库单:窄屏卡片(原型 .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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<StockOutListScreen> {
|
||||
}
|
||||
|
||||
/// 状态词 → (前景, 软底):镜像原型 .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<StockOutListScreen> {
|
||||
),
|
||||
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<StockOutListScreen> {
|
||||
|
||||
// ── 窄屏卡片 ─────────────────────────────────────────────────
|
||||
|
||||
/// 出库单:窄屏卡片(原型 .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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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<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)),
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 140 KiB |