From f2b2f419293d76f970176a8a37b31900657c4ecb Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Thu, 21 May 2026 08:35:45 +0800 Subject: [PATCH] refactor: extract boxing page into modular components Split the monolithic boxing_page.dart into separate files: - boxing_models.dart: data models (_ManyToOnePackedItem, _Phase) - widgets/boxing_shared_widgets.dart: shared UI components - widgets/single_code_body.dart: single-code boxing UI - widgets/multi_code_body.dart: multi-code boxing UI - widgets/multi_packed_row.dart: multi-code packed row widget - widgets/single_assigned_row.dart: single-code assigned row widget Co-Authored-By: Claude Opus 4.6 --- lib/pages/boxing/boxing_models.dart | 35 + .../boxing/widgets/boxing_shared_widgets.dart | 161 ++ lib/pages/boxing/widgets/multi_code_body.dart | 606 +++++++ .../boxing/widgets/multi_packed_row.dart | 239 +++ .../boxing/widgets/single_assigned_row.dart | 231 +++ .../boxing/widgets/single_code_body.dart | 600 +++++++ lib/pages/boxing_page.dart | 1403 ++--------------- 7 files changed, 1982 insertions(+), 1293 deletions(-) create mode 100644 lib/pages/boxing/boxing_models.dart create mode 100644 lib/pages/boxing/widgets/boxing_shared_widgets.dart create mode 100644 lib/pages/boxing/widgets/multi_code_body.dart create mode 100644 lib/pages/boxing/widgets/multi_packed_row.dart create mode 100644 lib/pages/boxing/widgets/single_assigned_row.dart create mode 100644 lib/pages/boxing/widgets/single_code_body.dart diff --git a/lib/pages/boxing/boxing_models.dart b/lib/pages/boxing/boxing_models.dart new file mode 100644 index 0000000..08d193d --- /dev/null +++ b/lib/pages/boxing/boxing_models.dart @@ -0,0 +1,35 @@ +class ManyToOnePackedItem { + final int boxItemId; + final String zongpaiNo; + final String? paichanNo; + final String? workOrderNo; + final int boxNo; + final int quantity; + final int? totalQuantity; + + const ManyToOnePackedItem({ + required this.boxItemId, + required this.zongpaiNo, + this.paichanNo, + required this.workOrderNo, + required this.boxNo, + required this.quantity, + this.totalQuantity, + }); + + ManyToOnePackedItem copyWith({ + int? boxNo, + int? quantity, + int? totalQuantity, + }) { + return ManyToOnePackedItem( + boxItemId: boxItemId, + zongpaiNo: zongpaiNo, + paichanNo: paichanNo, + workOrderNo: workOrderNo, + boxNo: boxNo ?? this.boxNo, + quantity: quantity ?? this.quantity, + totalQuantity: totalQuantity ?? this.totalQuantity, + ); + } +} diff --git a/lib/pages/boxing/widgets/boxing_shared_widgets.dart b/lib/pages/boxing/widgets/boxing_shared_widgets.dart new file mode 100644 index 0000000..0a0f96b --- /dev/null +++ b/lib/pages/boxing/widgets/boxing_shared_widgets.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; + +const boxingHeaderStyle = TextStyle(fontSize: 10, fontWeight: FontWeight.w700); + +class BoxingDetailButton extends StatelessWidget { + final bool enabled; + final ColorScheme colorScheme; + final VoidCallback onPressed; + + const BoxingDetailButton({ + super.key, + required this.enabled, + required this.colorScheme, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + final foreground = enabled ? colorScheme.primary : Colors.grey.shade400; + final background = enabled ? Colors.blue.shade50 : Colors.grey.shade100; + final border = enabled ? colorScheme.primary : Colors.grey.shade300; + + return SizedBox( + height: 34, + child: OutlinedButton.icon( + onPressed: enabled ? onPressed : null, + icon: Icon(Icons.receipt_long, size: 15, color: foreground), + label: Text( + '详情', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: foreground, + ), + ), + style: OutlinedButton.styleFrom( + backgroundColor: background, + side: BorderSide(color: border), + padding: const EdgeInsets.symmetric(horizontal: 10), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), + ), + ), + ); + } +} + +class BoxingModePill extends StatelessWidget { + final bool isSingle; + final String label; + final bool enabled; + final VoidCallback onTap; + + const BoxingModePill({ + super.key, + required this.isSingle, + required this.label, + required this.enabled, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final bgColor = isSingle ? Colors.blue.shade700 : Colors.orange.shade700; + + return Material( + borderRadius: BorderRadius.circular(8), + color: bgColor, + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: enabled ? onTap : null, + child: Container( + height: 36, + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.swap_horiz, color: Colors.white, size: 18), + const SizedBox(width: 6), + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + label, + maxLines: 1, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w800, + ), + ), + ), + ), + const SizedBox(width: 8), + Text( + 'P2', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.88), + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ), + ); + } +} + +class CompactIconButton extends StatelessWidget { + final IconData icon; + final Color? color; + final VoidCallback? onTap; + + const CompactIconButton({ + super.key, + required this.icon, + this.color, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 28, + height: 28, + child: IconButton( + icon: Icon(icon, size: 16), + color: color, + onPressed: onTap, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 28, minHeight: 28), + ), + ); + } +} + +InputDecoration compactInputDecoration({ + required bool disabled, + required Color borderColor, +}) { + final disabledBorder = OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: BorderSide(color: Colors.grey.shade300), + ); + final activeBorder = OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: BorderSide(color: borderColor, width: 1.5), + ); + return InputDecoration( + contentPadding: EdgeInsets.zero, + border: activeBorder, + enabledBorder: activeBorder, + focusedBorder: activeBorder, + disabledBorder: disabledBorder, + filled: disabled, + fillColor: Colors.grey.shade100, + ); +} diff --git a/lib/pages/boxing/widgets/multi_code_body.dart b/lib/pages/boxing/widgets/multi_code_body.dart new file mode 100644 index 0000000..a116b4d --- /dev/null +++ b/lib/pages/boxing/widgets/multi_code_body.dart @@ -0,0 +1,606 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pad_scanner/pages/boxing/boxing_models.dart'; +import 'package:pad_scanner/pages/boxing/widgets/boxing_shared_widgets.dart'; +import 'package:pad_scanner/pages/boxing/widgets/multi_packed_row.dart'; +import 'package:pad_scanner/services/api_service.dart'; + +class MultiCodeBody extends StatelessWidget { + final bool hasScan; + final String? zongpaiNo; + final String? paichanNo; + final String? workOrderNo; + final int? erpQuantity; + final int maxBoxNo; + final List existingBoxes; + final List visibleItems; + final TextEditingController boxNoController; + final TextEditingController quantityController; + final FocusNode boxNoFocusNode; + final FocusNode quantityFocusNode; + final bool isSubmitting; + final bool quantityTooHigh; + final bool canSubmit; + final int? editingPackedItemId; + final String editingPackedQuantity; + final int? deletingPackedItemId; + final VoidCallback onOpenDetail; + final VoidCallback onSubmitFromKeyboard; + final VoidCallback onSubmit; + final VoidCallback onQuantityChanged; + final VoidCallback onFinishBox; + final ValueChanged onEditingPackedQuantityChanged; + final ValueChanged onSavePackedItem; + final VoidCallback onCancelEditPackedItem; + final ValueChanged onStartEditPackedItem; + final ValueChanged onStartDeletePackedItem; + final ValueChanged onDeletePackedItem; + final VoidCallback onCancelDeletePackedItem; + + const MultiCodeBody({ + super.key, + required this.hasScan, + required this.zongpaiNo, + required this.paichanNo, + required this.workOrderNo, + required this.erpQuantity, + required this.maxBoxNo, + required this.existingBoxes, + required this.visibleItems, + required this.boxNoController, + required this.quantityController, + required this.boxNoFocusNode, + required this.quantityFocusNode, + required this.isSubmitting, + required this.quantityTooHigh, + required this.canSubmit, + required this.editingPackedItemId, + required this.editingPackedQuantity, + required this.deletingPackedItemId, + required this.onOpenDetail, + required this.onSubmitFromKeyboard, + required this.onSubmit, + required this.onQuantityChanged, + required this.onFinishBox, + required this.onEditingPackedQuantityChanged, + required this.onSavePackedItem, + required this.onCancelEditPackedItem, + required this.onStartEditPackedItem, + required this.onStartDeletePackedItem, + required this.onDeletePackedItem, + required this.onCancelDeletePackedItem, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + _MultiHeaderBar( + paichanNo: paichanNo, + maxBoxNo: maxBoxNo, + existingBoxes: existingBoxes, + boxNoController: boxNoController, + boxNoFocusNode: boxNoFocusNode, + isSubmitting: isSubmitting, + onSubmitFromKeyboard: onSubmitFromKeyboard, + onOpenDetail: onOpenDetail, + ), + const _MultiListHeader(), + Expanded( + child: visibleItems.isEmpty + ? Center( + child: Text( + '尚未装入任何物料', + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade500, + fontStyle: FontStyle.italic, + ), + ), + ) + : ListView.builder( + itemCount: visibleItems.length, + itemBuilder: (context, index) { + final item = visibleItems[index]; + return MultiPackedRow( + item: item, + editing: editingPackedItemId == item.boxItemId, + deleting: deletingPackedItemId == item.boxItemId, + isSubmitting: isSubmitting, + editingQuantity: editingPackedQuantity, + onEditingQuantityChanged: onEditingPackedQuantityChanged, + onSave: () => onSavePackedItem(item), + onCancelEdit: onCancelEditPackedItem, + onStartEdit: () => onStartEditPackedItem(item), + onStartDelete: () => onStartDeletePackedItem(item), + onDelete: () => onDeletePackedItem(item), + onCancelDelete: onCancelDeletePackedItem, + ); + }, + ), + ), + _MultiBottomArea( + hasScan: hasScan, + hasItems: visibleItems.isNotEmpty, + zongpaiNo: zongpaiNo, + workOrderNo: workOrderNo, + erpQuantity: erpQuantity, + quantityController: quantityController, + quantityFocusNode: quantityFocusNode, + isSubmitting: isSubmitting, + quantityTooHigh: quantityTooHigh, + canSubmit: canSubmit, + onSubmitFromKeyboard: onSubmitFromKeyboard, + onSubmit: onSubmit, + onQuantityChanged: onQuantityChanged, + onFinishBox: onFinishBox, + ), + ], + ); + } +} + +class _MultiHeaderBar extends StatelessWidget { + final String? paichanNo; + final int maxBoxNo; + final List existingBoxes; + final TextEditingController boxNoController; + final FocusNode boxNoFocusNode; + final bool isSubmitting; + final VoidCallback onSubmitFromKeyboard; + final VoidCallback onOpenDetail; + + const _MultiHeaderBar({ + required this.paichanNo, + required this.maxBoxNo, + required this.existingBoxes, + required this.boxNoController, + required this.boxNoFocusNode, + required this.isSubmitting, + required this.onSubmitFromKeyboard, + required this.onOpenDetail, + }); + + @override + Widget build(BuildContext context) { + final hasDetail = existingBoxes.isNotEmpty; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.grey.shade50, + border: Border(bottom: BorderSide(color: Colors.grey.shade300)), + ), + child: Row( + children: [ + const Text( + '箱号', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Colors.black54, + ), + ), + const SizedBox(width: 6), + SizedBox( + width: 52, + height: 32, + child: TextField( + controller: boxNoController, + focusNode: boxNoFocusNode, + enabled: !isSubmitting, + keyboardType: TextInputType.none, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + textAlign: TextAlign.center, + onEditingComplete: () {}, + onSubmitted: (_) => onSubmitFromKeyboard(), + decoration: InputDecoration( + contentPadding: EdgeInsets.zero, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: BorderSide(color: Colors.grey.shade400), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: const BorderSide(color: Colors.blue, width: 1.5), + ), + ), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w800), + ), + ), + Container( + width: 1, + height: 28, + margin: const EdgeInsets.symmetric(horizontal: 10), + color: Colors.grey.shade300, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + paichanNo ?? '--', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + ), + overflow: TextOverflow.ellipsis, + ), + Text( + '已有 ${existingBoxes.length} 箱 · 最大箱号 $maxBoxNo', + style: TextStyle(fontSize: 11, color: Colors.grey.shade500), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(width: 10), + BoxingDetailButton( + enabled: hasDetail, + colorScheme: Theme.of(context).colorScheme, + onPressed: onOpenDetail, + ), + ], + ), + ); + } +} + +class _MultiListHeader extends StatelessWidget { + const _MultiListHeader(); + + @override + Widget build(BuildContext context) { + return Container( + height: 28, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.grey.shade200, + border: Border(bottom: BorderSide(color: Colors.grey.shade400)), + ), + child: const Row( + children: [ + Expanded( + flex: 26, + child: Text( + '总排号', + textAlign: TextAlign.center, + style: boxingHeaderStyle, + ), + ), + Expanded( + flex: 30, + child: Text( + '工令号', + textAlign: TextAlign.center, + style: boxingHeaderStyle, + ), + ), + Expanded( + flex: 23, + child: Text( + '数量', + textAlign: TextAlign.center, + style: boxingHeaderStyle, + ), + ), + SizedBox( + width: 54, + child: Text( + '操作', + textAlign: TextAlign.center, + style: boxingHeaderStyle, + ), + ), + ], + ), + ); + } +} + +class _MultiBottomArea extends StatelessWidget { + final bool hasScan; + final bool hasItems; + final String? zongpaiNo; + final String? workOrderNo; + final int? erpQuantity; + final TextEditingController quantityController; + final FocusNode quantityFocusNode; + final bool isSubmitting; + final bool quantityTooHigh; + final bool canSubmit; + final VoidCallback onSubmitFromKeyboard; + final VoidCallback onSubmit; + final VoidCallback onQuantityChanged; + final VoidCallback onFinishBox; + + const _MultiBottomArea({ + required this.hasScan, + required this.hasItems, + required this.zongpaiNo, + required this.workOrderNo, + required this.erpQuantity, + required this.quantityController, + required this.quantityFocusNode, + required this.isSubmitting, + required this.quantityTooHigh, + required this.canSubmit, + required this.onSubmitFromKeyboard, + required this.onSubmit, + required this.onQuantityChanged, + required this.onFinishBox, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Colors.grey.shade300)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _MultiScanBar( + hasScan: hasScan, + zongpaiNo: zongpaiNo, + workOrderNo: workOrderNo, + erpQuantity: erpQuantity, + ), + _MultiSubmitRow( + hasScan: hasScan, + hasItems: hasItems, + quantityController: quantityController, + quantityFocusNode: quantityFocusNode, + isSubmitting: isSubmitting, + quantityTooHigh: quantityTooHigh, + canSubmit: canSubmit, + onSubmitFromKeyboard: onSubmitFromKeyboard, + onSubmit: onSubmit, + onQuantityChanged: onQuantityChanged, + onFinishBox: onFinishBox, + ), + ], + ), + ); + } +} + +class _MultiScanBar extends StatelessWidget { + final bool hasScan; + final String? zongpaiNo; + final String? workOrderNo; + final int? erpQuantity; + + const _MultiScanBar({ + required this.hasScan, + required this.zongpaiNo, + required this.workOrderNo, + required this.erpQuantity, + }); + + @override + Widget build(BuildContext context) { + if (!hasScan) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), + color: Colors.grey.shade50, + child: const Row( + children: [ + Icon(Icons.qr_code_scanner, color: Colors.grey, size: 16), + SizedBox(width: 8), + Text( + '请扫描执行卡二维码', + style: TextStyle( + color: Colors.grey, + fontSize: 13, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), + decoration: BoxDecoration( + color: Colors.green.shade50, + border: Border(bottom: BorderSide(color: Colors.green.shade200)), + ), + child: Row( + children: [ + const Icon(Icons.check_circle, color: Colors.green, size: 15), + const SizedBox(width: 6), + Expanded( + child: Text( + '${zongpaiNo ?? ""} · ${workOrderNo ?? "--"} · ${erpQuantity ?? "--"}件', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.green.shade700, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } +} + +class _MultiSubmitRow extends StatelessWidget { + final bool hasScan; + final bool hasItems; + final TextEditingController quantityController; + final FocusNode quantityFocusNode; + final bool isSubmitting; + final bool quantityTooHigh; + final bool canSubmit; + final VoidCallback onSubmitFromKeyboard; + final VoidCallback onSubmit; + final VoidCallback onQuantityChanged; + final VoidCallback onFinishBox; + + const _MultiSubmitRow({ + required this.hasScan, + required this.hasItems, + required this.quantityController, + required this.quantityFocusNode, + required this.isSubmitting, + required this.quantityTooHigh, + required this.canSubmit, + required this.onSubmitFromKeyboard, + required this.onSubmit, + required this.onQuantityChanged, + required this.onFinishBox, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(12, 7, 12, 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + '数量', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: hasScan ? Colors.black54 : Colors.grey.shade400, + ), + ), + const SizedBox(width: 6), + SizedBox( + width: 52, + height: 34, + child: TextField( + controller: quantityController, + focusNode: quantityFocusNode, + enabled: hasScan && !isSubmitting, + keyboardType: TextInputType.none, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + textAlign: TextAlign.center, + onChanged: (_) => onQuantityChanged(), + onEditingComplete: () {}, + onSubmitted: (_) => onSubmitFromKeyboard(), + decoration: InputDecoration( + contentPadding: EdgeInsets.zero, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: BorderSide( + color: quantityTooHigh ? Colors.red : Colors.grey.shade300, + ), + ), + disabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + filled: !hasScan, + fillColor: Colors.grey.shade100, + ), + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), + ), + ), + const SizedBox(width: 8), + SizedBox( + height: 34, + child: ElevatedButton( + onPressed: canSubmit ? onSubmit : null, + style: ElevatedButton.styleFrom( + backgroundColor: canSubmit + ? Theme.of(context).colorScheme.primary + : Colors.grey.shade200, + foregroundColor: canSubmit + ? Colors.white + : Colors.grey.shade400, + padding: const EdgeInsets.symmetric(horizontal: 18), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: isSubmitting + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text( + '确认', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: SizedBox( + height: 34, + child: OutlinedButton( + onPressed: hasItems ? onFinishBox : null, + style: OutlinedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + side: BorderSide( + color: hasItems + ? Colors.grey.shade600 + : Colors.grey.shade300, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '完成本箱', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: hasItems ? Colors.black87 : Colors.grey.shade400, + ), + ), + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 1, + ), + decoration: BoxDecoration( + color: hasItems + ? Colors.grey.shade200 + : Colors.grey.shade100, + borderRadius: BorderRadius.circular(3), + ), + child: Text( + 'P3', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + color: hasItems + ? Colors.black54 + : Colors.grey.shade400, + ), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/boxing/widgets/multi_packed_row.dart b/lib/pages/boxing/widgets/multi_packed_row.dart new file mode 100644 index 0000000..b5a499b --- /dev/null +++ b/lib/pages/boxing/widgets/multi_packed_row.dart @@ -0,0 +1,239 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pad_scanner/pages/boxing/boxing_models.dart'; +import 'package:pad_scanner/pages/boxing/widgets/boxing_shared_widgets.dart'; + +class MultiPackedRow extends StatelessWidget { + final ManyToOnePackedItem item; + final bool editing; + final bool deleting; + final bool isSubmitting; + final String editingQuantity; + final ValueChanged onEditingQuantityChanged; + final VoidCallback onSave; + final VoidCallback onCancelEdit; + final VoidCallback onStartEdit; + final VoidCallback onStartDelete; + final VoidCallback onDelete; + final VoidCallback onCancelDelete; + + const MultiPackedRow({ + super.key, + required this.item, + required this.editing, + required this.deleting, + required this.isSubmitting, + required this.editingQuantity, + required this.onEditingQuantityChanged, + required this.onSave, + required this.onCancelEdit, + required this.onStartEdit, + required this.onStartDelete, + required this.onDelete, + required this.onCancelDelete, + }); + + @override + Widget build(BuildContext context) { + final totalText = item.totalQuantity?.toString() ?? '--'; + final barColor = editing + ? Colors.amber + : (deleting ? Colors.red : Colors.green); + + final Color bgColor; + if (editing) { + bgColor = Colors.yellow.shade50; + } else if (deleting) { + bgColor = Colors.red.shade50; + } else { + bgColor = Colors.transparent; + } + + final rowStyle = TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Colors.black87, + ); + + return Column( + children: [ + Container( + constraints: const BoxConstraints(minHeight: 36), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: bgColor, + border: Border( + left: BorderSide(color: barColor, width: 3), + bottom: BorderSide(color: Colors.grey.shade200), + ), + ), + child: Row( + children: [ + Expanded( + flex: 26, + child: Text( + item.zongpaiNo, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: rowStyle.copyWith(fontWeight: FontWeight.w700), + ), + ), + Expanded( + flex: 30, + child: Text( + item.workOrderNo ?? '--', + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: rowStyle, + ), + ), + Expanded( + flex: 23, + child: editing + ? SizedBox( + width: 52, + height: 26, + child: TextField( + keyboardType: TextInputType.none, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + textAlign: TextAlign.center, + controller: + TextEditingController(text: editingQuantity) + ..selection = TextSelection.collapsed( + offset: editingQuantity.length, + ), + onChanged: onEditingQuantityChanged, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 4, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(4), + borderSide: const BorderSide( + color: Colors.amber, + width: 1.5, + ), + ), + ), + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ) + : Text( + '${item.quantity} / $totalText', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: deleting ? Colors.red : Colors.green.shade700, + ), + ), + ), + SizedBox( + width: 54, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (editing) ...[ + CompactIconButton( + icon: Icons.check, + color: Colors.green, + onTap: isSubmitting ? null : onSave, + ), + CompactIconButton( + icon: Icons.close, + color: Colors.grey, + onTap: isSubmitting ? null : onCancelEdit, + ), + ] else ...[ + CompactIconButton( + icon: Icons.edit, + color: Colors.grey.shade700, + onTap: isSubmitting ? null : onStartEdit, + ), + CompactIconButton( + icon: Icons.delete_outline, + color: Colors.red, + onTap: isSubmitting ? null : onStartDelete, + ), + ], + ], + ), + ), + ], + ), + ), + if (deleting) + Container( + height: 28, + padding: const EdgeInsets.only(left: 15, right: 12), + decoration: BoxDecoration( + color: Colors.red.shade50, + border: Border(bottom: BorderSide(color: Colors.red.shade200)), + ), + child: Row( + children: [ + Expanded( + child: Text( + '确认删除?', + style: const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w500, + ), + ), + ), + TextButton( + onPressed: isSubmitting ? null : onDelete, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 3, + ), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + backgroundColor: Colors.red.shade100, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + child: const Text( + '删除', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: Colors.red, + ), + ), + ), + const SizedBox(width: 4), + TextButton( + onPressed: isSubmitting ? null : onCancelDelete, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 3, + ), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: const Text( + '取消', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: Colors.grey, + ), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages/boxing/widgets/single_assigned_row.dart b/lib/pages/boxing/widgets/single_assigned_row.dart new file mode 100644 index 0000000..782ae5c --- /dev/null +++ b/lib/pages/boxing/widgets/single_assigned_row.dart @@ -0,0 +1,231 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pad_scanner/pages/boxing/widgets/boxing_shared_widgets.dart'; +import 'package:pad_scanner/services/api_service.dart'; + +class SingleAssignedRow extends StatelessWidget { + final CurrentZongpaiBoxData item; + final String? workOrderNo; + final bool editing; + final bool deleting; + final bool isSubmitting; + final String editingQuantity; + final ValueChanged onEditingQuantityChanged; + final VoidCallback onSave; + final VoidCallback onCancelEdit; + final VoidCallback onStartEdit; + final VoidCallback onStartDelete; + final VoidCallback onDelete; + final VoidCallback onCancelDelete; + + const SingleAssignedRow({ + super.key, + required this.item, + required this.workOrderNo, + required this.editing, + required this.deleting, + required this.isSubmitting, + required this.editingQuantity, + required this.onEditingQuantityChanged, + required this.onSave, + required this.onCancelEdit, + required this.onStartEdit, + required this.onStartDelete, + required this.onDelete, + required this.onCancelDelete, + }); + + @override + Widget build(BuildContext context) { + final barColor = editing + ? Colors.amber + : (deleting ? Colors.red : Colors.green); + + final Color bgColor; + if (editing) { + bgColor = Colors.yellow.shade50; + } else if (deleting) { + bgColor = Colors.red.shade50; + } else { + bgColor = Colors.transparent; + } + + final rowStyle = TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Colors.black87, + ); + + return Column( + children: [ + Container( + constraints: const BoxConstraints(minHeight: 36), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: bgColor, + border: Border( + left: BorderSide(color: barColor, width: 3), + bottom: BorderSide(color: Colors.grey.shade200), + ), + ), + child: Row( + children: [ + Expanded( + flex: 22, + child: Text( + '${item.boxNo} 号箱', + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: rowStyle.copyWith(fontWeight: FontWeight.w700), + ), + ), + Expanded( + flex: 30, + child: Text( + workOrderNo ?? '--', + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: rowStyle, + ), + ), + Expanded( + flex: 23, + child: editing + ? SizedBox( + width: 52, + height: 28, + child: TextField( + keyboardType: TextInputType.none, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + textAlign: TextAlign.center, + controller: + TextEditingController(text: editingQuantity) + ..selection = TextSelection.collapsed( + offset: editingQuantity.length, + ), + onChanged: onEditingQuantityChanged, + onSubmitted: (_) => onSave(), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 4, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + borderSide: const BorderSide( + color: Colors.amber, + width: 1.5, + ), + ), + ), + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + ), + ), + ) + : Text( + '${item.quantity} 件', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: deleting ? Colors.red : Colors.black54, + ), + ), + ), + SizedBox( + width: 54, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (editing) ...[ + CompactIconButton( + icon: Icons.check, + color: Colors.green, + onTap: isSubmitting ? null : onSave, + ), + CompactIconButton( + icon: Icons.close, + color: Colors.grey, + onTap: isSubmitting ? null : onCancelEdit, + ), + ] else ...[ + CompactIconButton( + icon: Icons.edit, + color: Colors.grey.shade700, + onTap: isSubmitting ? null : onStartEdit, + ), + CompactIconButton( + icon: Icons.delete_outline, + color: Colors.red, + onTap: isSubmitting ? null : onStartDelete, + ), + ], + ], + ), + ), + ], + ), + ), + if (deleting) + Container( + height: 28, + padding: const EdgeInsets.only(left: 15, right: 12), + decoration: BoxDecoration( + color: Colors.red.shade50, + border: Border(bottom: BorderSide(color: Colors.red.shade200)), + ), + child: Row( + children: [ + Expanded( + child: Text( + '确认删除 ${item.boxNo} 号箱记录?', + style: const TextStyle(fontSize: 12, color: Colors.red), + overflow: TextOverflow.ellipsis, + ), + ), + TextButton( + onPressed: isSubmitting ? null : onDelete, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 2, + ), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + backgroundColor: Colors.red, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + child: const Text( + '删除', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700), + ), + ), + const SizedBox(width: 6), + TextButton( + onPressed: isSubmitting ? null : onCancelDelete, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 2, + ), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: const Text( + '取消', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages/boxing/widgets/single_code_body.dart b/lib/pages/boxing/widgets/single_code_body.dart new file mode 100644 index 0000000..82e3dbb --- /dev/null +++ b/lib/pages/boxing/widgets/single_code_body.dart @@ -0,0 +1,600 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pad_scanner/pages/boxing/widgets/boxing_shared_widgets.dart'; +import 'package:pad_scanner/pages/boxing/widgets/single_assigned_row.dart'; +import 'package:pad_scanner/services/api_service.dart'; + +class SingleCodeBody extends StatelessWidget { + final bool isWaiting; + final bool isFinished; + final String? zongpaiNo; + final String? paichanNo; + final String? workOrderNo; + final int? erpQuantity; + final int packedQuantity; + final int maxBoxNo; + final List assignedItems; + final List existingBoxes; + final TextEditingController boxNoController; + final TextEditingController quantityController; + final FocusNode boxNoFocusNode; + final FocusNode quantityFocusNode; + final bool isSubmitting; + final bool isDuplicateBoxNo; + final bool quantityTooHigh; + final bool canSubmit; + final int? editingAssignedItemId; + final String editingAssignedQuantity; + final int? deletingAssignedItemId; + final VoidCallback onOpenDetail; + final VoidCallback onCheckDuplicateBoxNo; + final VoidCallback onSubmitFromKeyboard; + final VoidCallback onSubmit; + final VoidCallback onQuantityChanged; + final ValueChanged onEditingAssignedQuantityChanged; + final ValueChanged onSaveAssignedItem; + final VoidCallback onCancelEditAssigned; + final ValueChanged onStartEditAssigned; + final ValueChanged onStartDeleteAssigned; + final ValueChanged onDeleteAssignedItem; + final VoidCallback onCancelDeleteAssigned; + + const SingleCodeBody({ + super.key, + required this.isWaiting, + required this.isFinished, + required this.zongpaiNo, + required this.paichanNo, + required this.workOrderNo, + required this.erpQuantity, + required this.packedQuantity, + required this.maxBoxNo, + required this.assignedItems, + required this.existingBoxes, + required this.boxNoController, + required this.quantityController, + required this.boxNoFocusNode, + required this.quantityFocusNode, + required this.isSubmitting, + required this.isDuplicateBoxNo, + required this.quantityTooHigh, + required this.canSubmit, + required this.editingAssignedItemId, + required this.editingAssignedQuantity, + required this.deletingAssignedItemId, + required this.onOpenDetail, + required this.onCheckDuplicateBoxNo, + required this.onSubmitFromKeyboard, + required this.onSubmit, + required this.onQuantityChanged, + required this.onEditingAssignedQuantityChanged, + required this.onSaveAssignedItem, + required this.onCancelEditAssigned, + required this.onStartEditAssigned, + required this.onStartDeleteAssigned, + required this.onDeleteAssignedItem, + required this.onCancelDeleteAssigned, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Column( + children: [ + _SingleScanBar( + isWaiting: isWaiting, + zongpaiNo: zongpaiNo, + workOrderNo: workOrderNo, + erpQuantity: erpQuantity, + packedQuantity: packedQuantity, + ), + _SingleInfoBar( + isWaiting: isWaiting, + paichanNo: paichanNo, + packedQuantity: packedQuantity, + erpQuantity: erpQuantity, + existingBoxes: existingBoxes, + maxBoxNo: maxBoxNo, + colorScheme: colorScheme, + onOpenDetail: onOpenDetail, + ), + _SingleAssignedList( + isWaiting: isWaiting, + isFinished: isFinished, + items: assignedItems, + workOrderNo: workOrderNo, + isSubmitting: isSubmitting, + editingAssignedItemId: editingAssignedItemId, + editingAssignedQuantity: editingAssignedQuantity, + deletingAssignedItemId: deletingAssignedItemId, + onEditingAssignedQuantityChanged: onEditingAssignedQuantityChanged, + onSaveAssignedItem: onSaveAssignedItem, + onCancelEditAssigned: onCancelEditAssigned, + onStartEditAssigned: onStartEditAssigned, + onStartDeleteAssigned: onStartDeleteAssigned, + onDeleteAssignedItem: onDeleteAssignedItem, + onCancelDeleteAssigned: onCancelDeleteAssigned, + ), + _SingleBottomArea( + disabled: isWaiting || isFinished, + boxNoController: boxNoController, + quantityController: quantityController, + boxNoFocusNode: boxNoFocusNode, + quantityFocusNode: quantityFocusNode, + isSubmitting: isSubmitting, + isDuplicateBoxNo: isDuplicateBoxNo, + quantityTooHigh: quantityTooHigh, + canSubmit: canSubmit, + onCheckDuplicateBoxNo: onCheckDuplicateBoxNo, + onSubmitFromKeyboard: onSubmitFromKeyboard, + onSubmit: onSubmit, + onQuantityChanged: onQuantityChanged, + ), + ], + ); + } +} + +class _SingleScanBar extends StatelessWidget { + final bool isWaiting; + final String? zongpaiNo; + final String? workOrderNo; + final int? erpQuantity; + final int packedQuantity; + + const _SingleScanBar({ + required this.isWaiting, + required this.zongpaiNo, + required this.workOrderNo, + required this.erpQuantity, + required this.packedQuantity, + }); + + @override + Widget build(BuildContext context) { + if (isWaiting) { + return Container( + height: 50, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + border: Border(bottom: BorderSide(color: Colors.grey.shade300)), + ), + child: const Row( + children: [ + Icon(Icons.qr_code_scanner, color: Colors.grey, size: 16), + SizedBox(width: 8), + Text( + '请扫描执行卡二维码', + style: TextStyle(color: Colors.grey, fontSize: 13), + ), + ], + ), + ); + } + + return Container( + height: 50, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.green.shade50, + border: Border(bottom: BorderSide(color: Colors.green.shade200)), + ), + child: Row( + children: [ + const Icon(Icons.check_circle, color: Colors.green, size: 18), + const SizedBox(width: 8), + Expanded( + child: Text( + zongpaiNo ?? '', + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + Text( + '${workOrderNo ?? "--"} / ${erpQuantity ?? 0}件 / 已装$packedQuantity', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Colors.black54, + ), + ), + ], + ), + ); + } +} + +class _SingleInfoBar extends StatelessWidget { + final bool isWaiting; + final String? paichanNo; + final int packedQuantity; + final int? erpQuantity; + final List existingBoxes; + final int maxBoxNo; + final ColorScheme colorScheme; + final VoidCallback onOpenDetail; + + const _SingleInfoBar({ + required this.isWaiting, + required this.paichanNo, + required this.packedQuantity, + required this.erpQuantity, + required this.existingBoxes, + required this.maxBoxNo, + required this.colorScheme, + required this.onOpenDetail, + }); + + @override + Widget build(BuildContext context) { + final total = erpQuantity ?? 0; + final packed = isWaiting ? 0 : packedQuantity; + final progress = total <= 0 ? 0.0 : (packed / total).clamp(0.0, 1.0); + final finished = !isWaiting && total > 0 && packed >= total; + final detailEnabled = !isWaiting && existingBoxes.isNotEmpty; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.grey.shade50, + border: Border(bottom: BorderSide(color: Colors.grey.shade300)), + ), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Text( + isWaiting ? '排产号 --' : (paichanNo ?? '--'), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: isWaiting + ? Colors.grey.shade400 + : Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + isWaiting + ? '已有 -- 箱 · 最大箱号 --' + : '已有 ${existingBoxes.length} 箱 · 最大箱号 $maxBoxNo', + style: TextStyle( + fontSize: 10, + color: isWaiting + ? Colors.grey.shade400 + : Colors.grey.shade600, + ), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.right, + ), + ), + ], + ), + const SizedBox(height: 4), + ClipRRect( + borderRadius: BorderRadius.circular(2), + child: LinearProgressIndicator( + value: progress, + minHeight: 3, + backgroundColor: Colors.grey.shade300, + valueColor: AlwaysStoppedAnimation( + finished ? Colors.green.shade700 : colorScheme.primary, + ), + ), + ), + ], + ), + ), + const SizedBox(width: 10), + BoxingDetailButton( + enabled: detailEnabled, + colorScheme: colorScheme, + onPressed: onOpenDetail, + ), + ], + ), + ), + ); + } +} + +class _SingleAssignedList extends StatelessWidget { + final bool isWaiting; + final bool isFinished; + final List items; + final String? workOrderNo; + final bool isSubmitting; + final int? editingAssignedItemId; + final String editingAssignedQuantity; + final int? deletingAssignedItemId; + final ValueChanged onEditingAssignedQuantityChanged; + final ValueChanged onSaveAssignedItem; + final VoidCallback onCancelEditAssigned; + final ValueChanged onStartEditAssigned; + final ValueChanged onStartDeleteAssigned; + final ValueChanged onDeleteAssignedItem; + final VoidCallback onCancelDeleteAssigned; + + const _SingleAssignedList({ + required this.isWaiting, + required this.isFinished, + required this.items, + required this.workOrderNo, + required this.isSubmitting, + required this.editingAssignedItemId, + required this.editingAssignedQuantity, + required this.deletingAssignedItemId, + required this.onEditingAssignedQuantityChanged, + required this.onSaveAssignedItem, + required this.onCancelEditAssigned, + required this.onStartEditAssigned, + required this.onStartDeleteAssigned, + required this.onDeleteAssignedItem, + required this.onCancelDeleteAssigned, + }); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Column( + children: [ + const _SingleAssignedHeader(), + Expanded( + child: items.isEmpty + ? Center( + child: Text( + isFinished + ? '该总排号已全部装箱完毕' + : (isWaiting ? '扫码后显示已分配箱号记录' : '本次扫码尚未分配箱号'), + style: TextStyle( + fontSize: 13, + color: isFinished + ? Colors.green.shade700 + : Colors.grey.shade500, + fontStyle: FontStyle.italic, + ), + ), + ) + : ListView.builder( + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + return SingleAssignedRow( + item: item, + workOrderNo: workOrderNo, + editing: editingAssignedItemId == item.boxItemId, + deleting: deletingAssignedItemId == item.boxItemId, + isSubmitting: isSubmitting, + editingQuantity: editingAssignedQuantity, + onEditingQuantityChanged: + onEditingAssignedQuantityChanged, + onSave: () => onSaveAssignedItem(item), + onCancelEdit: onCancelEditAssigned, + onStartEdit: () => onStartEditAssigned(item), + onStartDelete: () => onStartDeleteAssigned(item), + onDelete: () => onDeleteAssignedItem(item), + onCancelDelete: onCancelDeleteAssigned, + ); + }, + ), + ), + ], + ), + ); + } +} + +class _SingleAssignedHeader extends StatelessWidget { + const _SingleAssignedHeader(); + + @override + Widget build(BuildContext context) { + return Container( + height: 28, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.grey.shade200, + border: Border(bottom: BorderSide(color: Colors.grey.shade400)), + ), + child: const Row( + children: [ + Expanded( + flex: 22, + child: Text( + '箱号', + textAlign: TextAlign.center, + style: boxingHeaderStyle, + ), + ), + Expanded( + flex: 30, + child: Text( + '工令号', + textAlign: TextAlign.center, + style: boxingHeaderStyle, + ), + ), + Expanded( + flex: 23, + child: Text( + '数量', + textAlign: TextAlign.center, + style: boxingHeaderStyle, + ), + ), + SizedBox( + width: 54, + child: Text( + '操作', + textAlign: TextAlign.center, + style: boxingHeaderStyle, + ), + ), + ], + ), + ); + } +} + +class _SingleBottomArea extends StatelessWidget { + final bool disabled; + final TextEditingController boxNoController; + final TextEditingController quantityController; + final FocusNode boxNoFocusNode; + final FocusNode quantityFocusNode; + final bool isSubmitting; + final bool isDuplicateBoxNo; + final bool quantityTooHigh; + final bool canSubmit; + final VoidCallback onCheckDuplicateBoxNo; + final VoidCallback onSubmitFromKeyboard; + final VoidCallback onSubmit; + final VoidCallback onQuantityChanged; + + const _SingleBottomArea({ + required this.disabled, + required this.boxNoController, + required this.quantityController, + required this.boxNoFocusNode, + required this.quantityFocusNode, + required this.isSubmitting, + required this.isDuplicateBoxNo, + required this.quantityTooHigh, + required this.canSubmit, + required this.onCheckDuplicateBoxNo, + required this.onSubmitFromKeyboard, + required this.onSubmit, + required this.onQuantityChanged, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Colors.grey.shade300)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + Text( + '箱号', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: disabled ? Colors.grey.shade400 : Colors.black54, + ), + ), + const SizedBox(width: 4), + SizedBox( + width: 52, + height: 34, + child: TextField( + controller: boxNoController, + focusNode: boxNoFocusNode, + enabled: !disabled && !isSubmitting, + keyboardType: TextInputType.none, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + textAlign: TextAlign.center, + onChanged: (_) => onCheckDuplicateBoxNo(), + onSubmitted: (_) => onSubmitFromKeyboard(), + decoration: compactInputDecoration( + disabled: disabled || isSubmitting, + borderColor: isDuplicateBoxNo + ? Colors.amber + : Colors.blue.shade700, + ), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w800, + ), + ), + ), + const SizedBox(width: 8), + Text( + '数量', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: disabled ? Colors.grey.shade400 : Colors.black54, + ), + ), + const SizedBox(width: 4), + SizedBox( + width: 52, + height: 34, + child: TextField( + controller: quantityController, + focusNode: quantityFocusNode, + enabled: !disabled && !isSubmitting, + keyboardType: TextInputType.none, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + textAlign: TextAlign.center, + onChanged: (_) => onQuantityChanged(), + onSubmitted: (_) => onSubmitFromKeyboard(), + decoration: compactInputDecoration( + disabled: disabled || isSubmitting, + borderColor: quantityTooHigh + ? Colors.red + : Colors.blue.shade700, + ), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + ), + ), + ), + const Spacer(), + SizedBox( + height: 34, + child: ElevatedButton( + onPressed: canSubmit ? onSubmit : null, + style: ElevatedButton.styleFrom( + backgroundColor: canSubmit + ? Theme.of(context).colorScheme.primary + : Colors.grey.shade300, + foregroundColor: canSubmit + ? Colors.white + : Colors.grey.shade600, + padding: const EdgeInsets.symmetric(horizontal: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: isSubmitting + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text( + '确认', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/boxing_page.dart b/lib/pages/boxing_page.dart index a3ebae0..b37cb6b 100644 --- a/lib/pages/boxing_page.dart +++ b/lib/pages/boxing_page.dart @@ -8,6 +8,10 @@ import 'package:pad_scanner/services/code_parser.dart'; import 'package:pad_scanner/services/api_service.dart'; import 'package:pad_scanner/services/boxing_context.dart'; import 'package:pad_scanner/services/feedback_service.dart'; +import 'package:pad_scanner/pages/boxing/boxing_models.dart'; +import 'package:pad_scanner/pages/boxing/widgets/boxing_shared_widgets.dart'; +import 'package:pad_scanner/pages/boxing/widgets/multi_code_body.dart'; +import 'package:pad_scanner/pages/boxing/widgets/single_code_body.dart'; import 'package:pad_scanner/pages/boxing_detail_page.dart'; import 'package:pad_scanner/widgets/status_bar.dart'; @@ -36,42 +40,6 @@ enum _Phase { submitted, // 已提交成功 } -class _ManyToOnePackedItem { - final int boxItemId; - final String zongpaiNo; - final String? paichanNo; - final String? workOrderNo; - final int boxNo; - final int quantity; - final int? totalQuantity; - - const _ManyToOnePackedItem({ - required this.boxItemId, - required this.zongpaiNo, - this.paichanNo, - required this.workOrderNo, - required this.boxNo, - required this.quantity, - this.totalQuantity, - }); - - _ManyToOnePackedItem copyWith({ - int? boxNo, - int? quantity, - int? totalQuantity, - }) { - return _ManyToOnePackedItem( - boxItemId: boxItemId, - zongpaiNo: zongpaiNo, - paichanNo: paichanNo, - workOrderNo: workOrderNo, - boxNo: boxNo ?? this.boxNo, - quantity: quantity ?? this.quantity, - totalQuantity: totalQuantity ?? this.totalQuantity, - ); - } -} - // === 主页面 === class BoxingPage extends StatefulWidget { @@ -114,7 +82,7 @@ class _BoxingPageState extends State with WidgetsBindingObserver { final _quantityFocusNode = FocusNode(); // 多码凑箱:本次操作已确认装入当前箱的明细 - final _manyToOnePackedItems = <_ManyToOnePackedItem>[]; + final _manyToOnePackedItems = []; int? _editingPackedItemId; String _editingPackedQuantity = ''; int? _deletingPackedItemId; @@ -349,7 +317,8 @@ class _BoxingPageState extends State with WidgetsBindingObserver { (sum, item) => sum + item.quantity, ); final totalQuantity = result.quantity ?? 0; - final alreadyCompleted = totalQuantity > 0 && packedQuantity >= totalQuantity; + final alreadyCompleted = + totalQuantity > 0 && packedQuantity >= totalQuantity; _feedbackService.trigger( alreadyCompleted @@ -655,8 +624,9 @@ class _BoxingPageState extends State with WidgetsBindingObserver { child: ElevatedButton( onPressed: onTap, style: ElevatedButton.styleFrom( - backgroundColor: - selected ? Colors.blue.shade700 : Colors.grey.shade200, + backgroundColor: selected + ? Colors.blue.shade700 + : Colors.grey.shade200, foregroundColor: selected ? Colors.white : Colors.black87, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), @@ -745,15 +715,15 @@ class _BoxingPageState extends State with WidgetsBindingObserver { return boxNo; } - List<_ManyToOnePackedItem> get _visibleManyToOnePackedItems { + List get _visibleManyToOnePackedItems { final boxNo = _currentManyToOneBoxNo; if (boxNo == null) return const []; - final byItemId = {}; + final byItemId = {}; for (final box in _existingBoxes.where((box) => box.boxNo == boxNo)) { for (final item in box.items) { final boxItemId = item.boxItemId; if (boxItemId == null) continue; - byItemId[boxItemId] = _ManyToOnePackedItem( + byItemId[boxItemId] = ManyToOnePackedItem( boxItemId: boxItemId, zongpaiNo: item.zongpaiNo, paichanNo: _paichanNo, @@ -891,7 +861,7 @@ class _BoxingPageState extends State with WidgetsBindingObserver { setState(() { if (boxItemId != null) { _manyToOnePackedItems.add( - _ManyToOnePackedItem( + ManyToOnePackedItem( boxItemId: boxItemId, zongpaiNo: _zongpaiNo!, paichanNo: _paichanNo, @@ -1191,7 +1161,7 @@ class _BoxingPageState extends State with WidgetsBindingObserver { } } - void _startEditPackedItem(_ManyToOnePackedItem item) { + void _startEditPackedItem(ManyToOnePackedItem item) { setState(() { _editingPackedItemId = item.boxItemId; _editingPackedQuantity = item.quantity.toString(); @@ -1206,7 +1176,7 @@ class _BoxingPageState extends State with WidgetsBindingObserver { }); } - Future _savePackedItem(_ManyToOnePackedItem item) async { + Future _savePackedItem(ManyToOnePackedItem item) async { final quantity = int.tryParse(_editingPackedQuantity); final boxNo = int.tryParse(_boxNoController.text.trim()); if (quantity == null || quantity <= 0 || boxNo == null || boxNo <= 0) { @@ -1272,7 +1242,7 @@ class _BoxingPageState extends State with WidgetsBindingObserver { } void _replaceExistingPackedItem( - _ManyToOnePackedItem item, + ManyToOnePackedItem item, int boxNo, int quantity, ) { @@ -1311,7 +1281,7 @@ class _BoxingPageState extends State with WidgetsBindingObserver { ); } - Future _deletePackedItem(_ManyToOnePackedItem item) async { + Future _deletePackedItem(ManyToOnePackedItem item) async { final configService = AppConfigService(); final baseUrl = await configService.getString('api_url') ?? ''; if (baseUrl.isEmpty) { @@ -1491,8 +1461,6 @@ class _BoxingPageState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - _updateBaseStatus(); final effectiveDot = _statusOverrideDot ?? _statusDot; final effectiveText = _statusOverrideText ?? _statusText; @@ -1506,7 +1474,12 @@ class _BoxingPageState extends State with WidgetsBindingObserver { children: [ const Text('装箱编号'), const SizedBox(width: 8), - _buildModePill(), + BoxingModePill( + isSingle: _mode == BoxingMode.singleCode, + label: _modeLabel, + enabled: !_isAutoProcessing, + onTap: _cycleMode, + ), ], ), toolbarHeight: 44, @@ -1518,8 +1491,92 @@ class _BoxingPageState extends State with WidgetsBindingObserver { Expanded( child: _mode == BoxingMode.multiCode - ? _buildManyToOneBody(colorScheme) - : _buildSingleCodeBody(colorScheme), + ? MultiCodeBody( + hasScan: _zongpaiNo != null && _phase == _Phase.scanned, + zongpaiNo: _zongpaiNo, + paichanNo: _paichanNo, + workOrderNo: _workOrderNo, + erpQuantity: _erpQuantity, + maxBoxNo: _maxBoxNo, + existingBoxes: _existingBoxes, + visibleItems: _visibleManyToOnePackedItems, + boxNoController: _boxNoController, + quantityController: _quantityController, + boxNoFocusNode: _boxNoFocusNode, + quantityFocusNode: _quantityFocusNode, + isSubmitting: _isSubmitting, + quantityTooHigh: _quantityTooHigh, + canSubmit: _canSubmit, + editingPackedItemId: _editingPackedItemId, + editingPackedQuantity: _editingPackedQuantity, + deletingPackedItemId: _deletingPackedItemId, + onOpenDetail: _openDetail, + onSubmitFromKeyboard: _submitFromKeyboard, + onSubmit: _submit, + onQuantityChanged: () => setState(() {}), + onFinishBox: _finishManyToOneBox, + onEditingPackedQuantityChanged: (value) => + _editingPackedQuantity = value, + onSavePackedItem: _savePackedItem, + onCancelEditPackedItem: _cancelEditPackedItem, + onStartEditPackedItem: _startEditPackedItem, + onStartDeletePackedItem: (item) { + setState(() { + _deletingPackedItemId = item.boxItemId; + _editingPackedItemId = null; + _editingPackedQuantity = ''; + }); + }, + onDeletePackedItem: _deletePackedItem, + onCancelDeletePackedItem: () => + setState(() => _deletingPackedItemId = null), + ) + : SingleCodeBody( + isWaiting: _phase == _Phase.waiting && _zongpaiNo == null, + isFinished: + _phase == _Phase.scanned && + _zongpaiNo != null && + _remainingQuantity <= 0, + zongpaiNo: _zongpaiNo, + paichanNo: _paichanNo, + workOrderNo: _workOrderNo, + erpQuantity: _erpQuantity, + packedQuantity: _packedQuantity, + maxBoxNo: _maxBoxNo, + assignedItems: _currentZongpaiBoxes, + existingBoxes: _existingBoxes, + boxNoController: _boxNoController, + quantityController: _quantityController, + boxNoFocusNode: _boxNoFocusNode, + quantityFocusNode: _quantityFocusNode, + isSubmitting: _isSubmitting, + isDuplicateBoxNo: _isDuplicateBoxNo, + quantityTooHigh: _quantityTooHigh, + canSubmit: _canSubmit, + editingAssignedItemId: _editingAssignedItemId, + editingAssignedQuantity: _editingAssignedQuantity, + deletingAssignedItemId: _deletingAssignedItemId, + onOpenDetail: _openDetail, + onCheckDuplicateBoxNo: _checkDuplicateBoxNo, + onSubmitFromKeyboard: _submitFromKeyboard, + onSubmit: _submit, + onQuantityChanged: () => setState(() {}), + onEditingAssignedQuantityChanged: (value) => + _editingAssignedQuantity = value, + onSaveAssignedItem: _saveAssignedItem, + onCancelEditAssigned: _cancelEditAssigned, + onStartEditAssigned: _startEditAssigned, + onStartDeleteAssigned: (item) { + setState(() { + _deletingAssignedItemId = item.boxItemId; + _editingAssignedItemId = null; + _editingAssignedQuantity = ''; + }); + }, + onDeleteAssignedItem: _deleteAssignedItem, + onCancelDeleteAssigned: () => + setState(() => _deletingAssignedItemId = null), + ), ), // 底部状态栏 @@ -1664,1244 +1721,4 @@ class _BoxingPageState extends State with WidgetsBindingObserver { ), ); } - - Widget _buildSingleScanBar(bool isWaiting) { - if (isWaiting) { - return Container( - height: 50, - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: Colors.grey.shade100, - border: Border(bottom: BorderSide(color: Colors.grey.shade300)), - ), - child: const Row( - children: [ - Icon(Icons.qr_code_scanner, color: Colors.grey, size: 16), - SizedBox(width: 8), - Text( - '请扫描执行卡二维码', - style: TextStyle(color: Colors.grey, fontSize: 13), - ), - ], - ), - ); - } - - return Container( - height: 50, - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: Colors.green.shade50, - border: Border(bottom: BorderSide(color: Colors.green.shade200)), - ), - child: Row( - children: [ - const Icon(Icons.check_circle, color: Colors.green, size: 18), - const SizedBox(width: 8), - Expanded( - child: Text( - _zongpaiNo ?? '', - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700), - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 8), - Text( - '${_workOrderNo ?? "--"} / ${_erpQuantity ?? 0}件 / 已装$_packedQuantity', - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: Colors.black54, - ), - ), - ], - ), - ); - } - - Widget _buildSingleCodeBody(ColorScheme colorScheme) { - final isWaiting = _phase == _Phase.waiting && _zongpaiNo == null; - final isFinished = - _phase == _Phase.scanned && - _zongpaiNo != null && - _remainingQuantity <= 0; - - return Column( - children: [ - _buildSingleScanBar(isWaiting), - _buildSingleInfoBar(isWaiting, colorScheme), - _buildSingleAssignedList(isWaiting, isFinished), - _buildSingleBottomArea(isWaiting || isFinished), - ], - ); - } - - Widget _buildSingleInfoBar(bool isWaiting, ColorScheme colorScheme) { - final total = _erpQuantity ?? 0; - final packed = isWaiting ? 0 : _packedQuantity; - final progress = total <= 0 ? 0.0 : (packed / total).clamp(0.0, 1.0); - final finished = !isWaiting && total > 0 && packed >= total; - final detailEnabled = !isWaiting && _existingBoxes.isNotEmpty; - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.grey.shade50, - border: Border(bottom: BorderSide(color: Colors.grey.shade300)), - ), - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Text( - isWaiting ? '排产号 --' : (_paichanNo ?? '--'), - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: isWaiting ? Colors.grey.shade400 : Colors.black87, - ), - overflow: TextOverflow.ellipsis, - ), - const SizedBox(width: 10), - Expanded( - child: Text( - isWaiting - ? '已有 -- 箱 · 最大箱号 --' - : '已有 ${_existingBoxes.length} 箱 · 最大箱号 $_maxBoxNo', - style: TextStyle( - fontSize: 10, - color: isWaiting - ? Colors.grey.shade400 - : Colors.grey.shade600, - ), - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.right, - ), - ), - ], - ), - const SizedBox(height: 4), - ClipRRect( - borderRadius: BorderRadius.circular(2), - child: LinearProgressIndicator( - value: progress, - minHeight: 3, - backgroundColor: Colors.grey.shade300, - valueColor: AlwaysStoppedAnimation( - finished ? Colors.green.shade700 : colorScheme.primary, - ), - ), - ), - ], - ), - ), - const SizedBox(width: 10), - _buildDetailButton(detailEnabled, colorScheme), - ], - ), - ), - ); - } - - Widget _buildDetailButton(bool enabled, ColorScheme colorScheme) { - final foreground = enabled ? colorScheme.primary : Colors.grey.shade400; - final background = enabled ? Colors.blue.shade50 : Colors.grey.shade100; - final border = enabled ? colorScheme.primary : Colors.grey.shade300; - - return SizedBox( - height: 34, - child: OutlinedButton.icon( - onPressed: enabled ? _openDetail : null, - icon: Icon(Icons.receipt_long, size: 15, color: foreground), - label: Text( - '详情', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w700, - color: foreground, - ), - ), - style: OutlinedButton.styleFrom( - backgroundColor: background, - side: BorderSide(color: border), - padding: const EdgeInsets.symmetric(horizontal: 10), - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), - ), - ), - ); - } - - Widget _buildSingleAssignedList(bool isWaiting, bool isFinished) { - final items = _currentZongpaiBoxes; - return Expanded( - child: Column( - children: [ - _buildSingleAssignedHeader(items.length), - Expanded( - child: items.isEmpty - ? Center( - child: Text( - isFinished - ? '该总排号已全部装箱完毕' - : (isWaiting ? '扫码后显示已分配箱号记录' : '本次扫码尚未分配箱号'), - style: TextStyle( - fontSize: 13, - color: isFinished - ? Colors.green.shade700 - : Colors.grey.shade500, - fontStyle: FontStyle.italic, - ), - ), - ) - : ListView.builder( - itemCount: items.length, - itemBuilder: (context, index) { - return _buildSingleAssignedRow(items[index], index + 1); - }, - ), - ), - ], - ), - ); - } - - Widget _buildSingleAssignedHeader(int count) { - return Container( - height: 28, - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: Colors.grey.shade200, - border: Border(bottom: BorderSide(color: Colors.grey.shade400)), - ), - child: Row( - children: [ - const Expanded(flex: 22, child: Text('箱号', textAlign: TextAlign.center, style: _headerStyle)), - const Expanded(flex: 30, child: Text('工令号', textAlign: TextAlign.center, style: _headerStyle)), - const Expanded( - flex: 23, - child: Text('数量', textAlign: TextAlign.center, style: _headerStyle), - ), - const SizedBox( - width: 54, - child: Text('操作', textAlign: TextAlign.center, style: _headerStyle), - ), - ], - ), - ); - } - - Widget _buildSingleAssignedRow(CurrentZongpaiBoxData item, int index) { - final editing = _editingAssignedItemId == item.boxItemId; - final deleting = _deletingAssignedItemId == item.boxItemId; - final barColor = editing - ? Colors.amber - : (deleting ? Colors.red : Colors.green); - - final Color bgColor; - if (editing) { - bgColor = Colors.yellow.shade50; - } else if (deleting) { - bgColor = Colors.red.shade50; - } else { - bgColor = Colors.transparent; - } - - final rowStyle = TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: Colors.black87, - ); - - return Column( - children: [ - Container( - constraints: const BoxConstraints(minHeight: 36), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: bgColor, - border: Border( - left: BorderSide(color: barColor, width: 3), - bottom: BorderSide(color: Colors.grey.shade200), - ), - ), - child: Row( - children: [ - Expanded( - flex: 22, - child: Text( - '${item.boxNo} 号箱', - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: rowStyle.copyWith(fontWeight: FontWeight.w700), - ), - ), - Expanded( - flex: 30, - child: Text( - _workOrderNo ?? '--', - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: rowStyle, - ), - ), - Expanded( - flex: 23, - child: editing - ? SizedBox( - width: 52, - height: 28, - child: TextField( - keyboardType: TextInputType.none, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], - textAlign: TextAlign.center, - controller: TextEditingController( - text: _editingAssignedQuantity, - )..selection = TextSelection.collapsed( - offset: _editingAssignedQuantity.length, - ), - onChanged: (v) => _editingAssignedQuantity = v, - onSubmitted: (_) => _saveAssignedItem(item), - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric( - horizontal: 4, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(5), - borderSide: const BorderSide( - color: Colors.amber, - width: 1.5, - ), - ), - ), - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w700, - ), - ), - ) - : Text( - '${item.quantity} 件', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: deleting ? Colors.red : Colors.black54, - ), - ), - ), - SizedBox( - width: 54, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (editing) ...[ - _iconBtn( - Icons.check, - color: Colors.green, - onTap: _isSubmitting - ? null - : () => _saveAssignedItem(item), - ), - _iconBtn( - Icons.close, - color: Colors.grey, - onTap: _isSubmitting ? null : _cancelEditAssigned, - ), - ] else ...[ - _iconBtn( - Icons.edit, - color: Colors.grey.shade700, - onTap: _isSubmitting - ? null - : () => _startEditAssigned(item), - ), - _iconBtn( - Icons.delete_outline, - color: Colors.red, - onTap: _isSubmitting - ? null - : () { - setState(() { - _deletingAssignedItemId = item.boxItemId; - _editingAssignedItemId = null; - _editingAssignedQuantity = ''; - }); - }, - ), - ], - ], - ), - ), - ], - ), - ), - if (deleting) - Container( - height: 28, - padding: const EdgeInsets.only(left: 15, right: 12), - decoration: BoxDecoration( - color: Colors.red.shade50, - border: Border(bottom: BorderSide(color: Colors.red.shade200)), - ), - child: Row( - children: [ - Expanded( - child: Text( - '确认删除 ${item.boxNo} 号箱记录?', - style: const TextStyle(fontSize: 12, color: Colors.red), - overflow: TextOverflow.ellipsis, - ), - ), - TextButton( - onPressed: _isSubmitting - ? null - : () => _deleteAssignedItem(item), - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 2, - ), - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - backgroundColor: Colors.red, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), - ), - child: const Text( - '删除', - style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700), - ), - ), - const SizedBox(width: 6), - TextButton( - onPressed: _isSubmitting - ? null - : () => setState(() => _deletingAssignedItemId = null), - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 2, - ), - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: const Text( - '取消', - style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700), - ), - ), - ], - ), - ), - ], - ); - } - - Widget _buildSingleBottomArea(bool disabled) { - return Container( - decoration: BoxDecoration( - color: Colors.white, - border: Border(top: BorderSide(color: Colors.grey.shade300)), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: Row( - children: [ - Text( - '箱号', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w700, - color: disabled ? Colors.grey.shade400 : Colors.black54, - ), - ), - const SizedBox(width: 4), - SizedBox( - width: 52, - height: 34, - child: TextField( - controller: _boxNoController, - focusNode: _boxNoFocusNode, - enabled: !disabled && !_isSubmitting, - keyboardType: TextInputType.none, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - textAlign: TextAlign.center, - onChanged: (_) => _checkDuplicateBoxNo(), - onSubmitted: (_) => _submitFromKeyboard(), - decoration: _compactInputDecoration( - disabled: disabled || _isSubmitting, - borderColor: _isDuplicateBoxNo - ? Colors.amber - : Colors.blue.shade700, - ), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w800, - ), - ), - ), - const SizedBox(width: 8), - Text( - '数量', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w700, - color: disabled ? Colors.grey.shade400 : Colors.black54, - ), - ), - const SizedBox(width: 4), - SizedBox( - width: 52, - height: 34, - child: TextField( - controller: _quantityController, - focusNode: _quantityFocusNode, - enabled: !disabled && !_isSubmitting, - keyboardType: TextInputType.none, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - textAlign: TextAlign.center, - onChanged: (_) => setState(() {}), - onSubmitted: (_) => _submitFromKeyboard(), - decoration: _compactInputDecoration( - disabled: disabled || _isSubmitting, - borderColor: _quantityTooHigh - ? Colors.red - : Colors.blue.shade700, - ), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w800, - ), - ), - ), - const Spacer(), - SizedBox( - height: 34, - child: ElevatedButton( - onPressed: _canSubmit ? _submit : null, - style: ElevatedButton.styleFrom( - backgroundColor: _canSubmit - ? Theme.of(context).colorScheme.primary - : Colors.grey.shade300, - foregroundColor: _canSubmit - ? Colors.white - : Colors.grey.shade600, - padding: const EdgeInsets.symmetric(horizontal: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), - ), - child: _isSubmitting - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : const Text( - '确认', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - ), - ), - ), - ), - ], - ), - ), - ); - } - - InputDecoration _compactInputDecoration({ - required bool disabled, - required Color borderColor, - }) { - final disabledBorder = OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: BorderSide(color: Colors.grey.shade300), - ); - final activeBorder = OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: BorderSide(color: borderColor, width: 1.5), - ); - return InputDecoration( - contentPadding: EdgeInsets.zero, - border: activeBorder, - enabledBorder: activeBorder, - focusedBorder: activeBorder, - disabledBorder: disabledBorder, - filled: disabled, - fillColor: Colors.grey.shade100, - ); - } - - Widget _buildModePill() { - final isSingle = _mode == BoxingMode.singleCode; - final bgColor = isSingle ? Colors.blue.shade700 : Colors.orange.shade700; - - return Material( - borderRadius: BorderRadius.circular(8), - color: bgColor, - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: _isAutoProcessing ? null : _cycleMode, - child: Container( - height: 36, - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.swap_horiz, color: Colors.white, size: 18), - const SizedBox(width: 6), - Flexible( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - _modeLabel, - maxLines: 1, - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.w800, - ), - ), - ), - ), - const SizedBox(width: 8), - Text( - 'P2', - style: TextStyle( - color: Colors.white.withValues(alpha: 0.88), - fontSize: 12, - fontWeight: FontWeight.w700, - ), - ), - ], - ), - ), - ), - ); - } - - Widget _buildManyToOneBody(ColorScheme colorScheme) { - final hasScan = _zongpaiNo != null && _phase == _Phase.scanned; - final visibleItems = _visibleManyToOnePackedItems; - - return Column( - children: [ - // Header bar: box number + paichan info merged - _buildManyToOneHeaderBar(), - // List header (sticky above scroll area) - _buildManyToOneListHeader(visibleItems.length), - // Scrollable list - Expanded( - child: visibleItems.isEmpty - ? Center( - child: Text( - '尚未装入任何物料', - style: TextStyle( - fontSize: 13, - color: Colors.grey.shade500, - fontStyle: FontStyle.italic, - ), - ), - ) - : ListView.builder( - itemCount: visibleItems.length, - itemBuilder: (context, index) { - return _buildManyToOnePackedRow( - visibleItems[index], - index + 1, - ); - }, - ), - ), - // Fixed bottom area: scan bar + submit + finish - _buildManyToOneBottomArea(hasScan), - ], - ); - } - - // ── Header bar: box number + paichan info ── - - Widget _buildManyToOneHeaderBar() { - final hasDetail = _existingBoxes.isNotEmpty; - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: Colors.grey.shade50, - border: Border(bottom: BorderSide(color: Colors.grey.shade300)), - ), - child: Row( - children: [ - // Box number - const Text( - '箱号', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: Colors.black54, - ), - ), - const SizedBox(width: 6), - SizedBox( - width: 52, - height: 32, - child: TextField( - controller: _boxNoController, - focusNode: _boxNoFocusNode, - enabled: !_isSubmitting, - keyboardType: TextInputType.none, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - textAlign: TextAlign.center, - onEditingComplete: () {}, - onSubmitted: (_) => _submitFromKeyboard(), - decoration: InputDecoration( - contentPadding: EdgeInsets.zero, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: BorderSide(color: Colors.grey.shade400), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: const BorderSide(color: Colors.blue, width: 1.5), - ), - ), - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w800), - ), - ), - // Divider - Container( - width: 1, - height: 28, - margin: const EdgeInsets.symmetric(horizontal: 10), - color: Colors.grey.shade300, - ), - // Paichan info - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _paichanNo ?? '--', - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w700, - ), - overflow: TextOverflow.ellipsis, - ), - Text( - '已有 ${_existingBoxes.length} 箱 · 最大箱号 $_maxBoxNo', - style: TextStyle(fontSize: 11, color: Colors.grey.shade500), - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - const SizedBox(width: 10), - _buildDetailButton(hasDetail, Theme.of(context).colorScheme), - ], - ), - ); - } - - // ── List header ── - - Widget _buildManyToOneListHeader(int count) { - return Container( - height: 28, - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: Colors.grey.shade200, - border: Border(bottom: BorderSide(color: Colors.grey.shade400)), - ), - child: Row( - children: [ - const Expanded(flex: 26, child: Text('总排号', textAlign: TextAlign.center, style: _headerStyle)), - const Expanded(flex: 30, child: Text('工令号', textAlign: TextAlign.center, style: _headerStyle)), - const Expanded( - flex: 23, - child: Text('数量', textAlign: TextAlign.center, style: _headerStyle), - ), - const SizedBox( - width: 54, - child: Text('操作', textAlign: TextAlign.center, style: _headerStyle), - ), - ], - ), - ); - } - - // ── Packed item row ── - - Widget _buildManyToOnePackedRow(_ManyToOnePackedItem item, int index) { - final editing = _editingPackedItemId == item.boxItemId; - final deleting = _deletingPackedItemId == item.boxItemId; - final totalText = item.totalQuantity?.toString() ?? '--'; - - final barColor = editing - ? Colors.amber - : (deleting ? Colors.red : Colors.green); - - final Color bgColor; - if (editing) { - bgColor = Colors.yellow.shade50; - } else if (deleting) { - bgColor = Colors.red.shade50; - } else { - bgColor = Colors.transparent; - } - - final rowStyle = TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: Colors.black87, - ); - - return Column( - children: [ - Container( - constraints: const BoxConstraints(minHeight: 36), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: bgColor, - border: Border( - left: BorderSide(color: barColor, width: 3), - bottom: BorderSide(color: Colors.grey.shade200), - ), - ), - child: Row( - children: [ - Expanded( - flex: 26, - child: Text( - item.zongpaiNo, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: rowStyle.copyWith(fontWeight: FontWeight.w700), - ), - ), - Expanded( - flex: 30, - child: Text( - item.workOrderNo ?? '--', - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: rowStyle, - ), - ), - Expanded( - flex: 23, - child: editing - ? SizedBox( - width: 52, - height: 26, - child: TextField( - keyboardType: TextInputType.none, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], - textAlign: TextAlign.center, - controller: TextEditingController( - text: _editingPackedQuantity, - )..selection = TextSelection.collapsed( - offset: _editingPackedQuantity.length, - ), - onChanged: (v) => _editingPackedQuantity = v, - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric( - horizontal: 4, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(4), - borderSide: const BorderSide( - color: Colors.amber, - width: 1.5, - ), - ), - ), - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - ), - ), - ) - : Text( - '${item.quantity} / $totalText', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: deleting ? Colors.red : Colors.green.shade700, - ), - ), - ), - SizedBox( - width: 54, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (editing) ...[ - _iconBtn( - Icons.check, - color: Colors.green, - onTap: _isSubmitting - ? null - : () => _savePackedItem(item), - ), - _iconBtn( - Icons.close, - color: Colors.grey, - onTap: _isSubmitting ? null : _cancelEditPackedItem, - ), - ] else ...[ - _iconBtn( - Icons.edit, - color: Colors.grey.shade700, - onTap: _isSubmitting - ? null - : () => _startEditPackedItem(item), - ), - _iconBtn( - Icons.delete_outline, - color: Colors.red, - onTap: _isSubmitting - ? null - : () { - setState(() { - _deletingPackedItemId = item.boxItemId; - _editingPackedItemId = null; - _editingPackedQuantity = ''; - }); - }, - ), - ], - ], - ), - ), - ], - ), - ), - if (deleting) - Container( - height: 28, - padding: const EdgeInsets.only(left: 15, right: 12), - decoration: BoxDecoration( - color: Colors.red.shade50, - border: Border(bottom: BorderSide(color: Colors.red.shade200)), - ), - child: Row( - children: [ - Expanded( - child: Text( - '确认删除?', - style: const TextStyle( - fontSize: 12, - color: Colors.red, - fontWeight: FontWeight.w500, - ), - ), - ), - TextButton( - onPressed: _isSubmitting - ? null - : () => _deletePackedItem(item), - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 3, - ), - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - backgroundColor: Colors.red.shade100, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), - ), - child: const Text( - '删除', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w700, - color: Colors.red, - ), - ), - ), - const SizedBox(width: 4), - TextButton( - onPressed: _isSubmitting - ? null - : () => setState(() => _deletingPackedItemId = null), - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 3, - ), - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: const Text( - '取消', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w700, - color: Colors.grey, - ), - ), - ), - ], - ), - ), - ], - ); - } - - // ── Compact icon button helper ── - - Widget _iconBtn(IconData icon, {Color? color, VoidCallback? onTap}) { - return SizedBox( - width: 28, - height: 28, - child: IconButton( - icon: Icon(icon, size: 16), - color: color, - onPressed: onTap, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 28, minHeight: 28), - ), - ); - } - - // ── Fixed bottom area ── - - Widget _buildManyToOneBottomArea(bool hasScan) { - final hasItems = _visibleManyToOnePackedItems.isNotEmpty; - return Container( - decoration: BoxDecoration( - color: Colors.white, - border: Border(top: BorderSide(color: Colors.grey.shade300)), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _buildManyToOneScanBar(hasScan), - _buildManyToOneSubmitRow(hasScan, hasItems), - ], - ), - ); - } - - Widget _buildManyToOneScanBar(bool hasScan) { - if (!hasScan) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), - color: Colors.grey.shade50, - child: const Row( - children: [ - Icon(Icons.qr_code_scanner, color: Colors.grey, size: 16), - SizedBox(width: 8), - Text( - '请扫描执行卡二维码', - style: TextStyle( - color: Colors.grey, - fontSize: 13, - fontWeight: FontWeight.w400, - ), - ), - ], - ), - ); - } - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), - decoration: BoxDecoration( - color: Colors.green.shade50, - border: Border(bottom: BorderSide(color: Colors.green.shade200)), - ), - child: Row( - children: [ - const Icon(Icons.check_circle, color: Colors.green, size: 15), - const SizedBox(width: 6), - Expanded( - child: Text( - '${_zongpaiNo ?? ""} · ${_workOrderNo ?? "--"} · ${_erpQuantity ?? "--"}件', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Colors.green.shade700, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - } - - Widget _buildManyToOneSubmitRow(bool hasScan, bool hasItems) { - return Padding( - padding: const EdgeInsets.fromLTRB(12, 7, 12, 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - '数量', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: hasScan ? Colors.black54 : Colors.grey.shade400, - ), - ), - const SizedBox(width: 6), - SizedBox( - width: 52, - height: 34, - child: TextField( - controller: _quantityController, - focusNode: _quantityFocusNode, - enabled: hasScan && !_isSubmitting, - keyboardType: TextInputType.none, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - textAlign: TextAlign.center, - onChanged: (_) => setState(() {}), - onEditingComplete: () {}, - onSubmitted: (_) => _submitFromKeyboard(), - decoration: InputDecoration( - contentPadding: EdgeInsets.zero, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: BorderSide( - color: _quantityTooHigh - ? Colors.red - : Colors.grey.shade300, - ), - ), - disabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: BorderSide(color: Colors.grey.shade200), - ), - filled: !hasScan, - fillColor: Colors.grey.shade100, - ), - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - ), - ), - ), - const SizedBox(width: 8), - SizedBox( - height: 34, - child: ElevatedButton( - onPressed: _canSubmit ? _submit : null, - style: ElevatedButton.styleFrom( - backgroundColor: _canSubmit - ? Theme.of(context).colorScheme.primary - : Colors.grey.shade200, - foregroundColor: _canSubmit - ? Colors.white - : Colors.grey.shade400, - padding: const EdgeInsets.symmetric(horizontal: 18), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), - ), - child: _isSubmitting - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : const Text( - '确认', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - ), - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: SizedBox( - height: 34, - child: OutlinedButton( - onPressed: hasItems ? _finishManyToOneBox : null, - style: OutlinedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), - side: BorderSide( - color: hasItems - ? Colors.grey.shade600 - : Colors.grey.shade300, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '完成本箱', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: hasItems ? Colors.black87 : Colors.grey.shade400, - ), - ), - const SizedBox(width: 6), - Container( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), - decoration: BoxDecoration( - color: hasItems - ? Colors.grey.shade200 - : Colors.grey.shade100, - borderRadius: BorderRadius.circular(3), - ), - child: Text( - 'P3', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w700, - color: hasItems ? Colors.black54 : Colors.grey.shade400, - ), - ), - ), - ], - ), - ), - ), - ), - ], - ), - ); - } } - -const _headerStyle = TextStyle(fontSize: 10, fontWeight: FontWeight.w700);