// ignore_for_file: unnecessary_this import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pad_scanner/services/scanner_service.dart'; 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_api_actions.dart'; import 'package:pad_scanner/pages/boxing/boxing_box_mutations.dart' as box_mutations; import 'package:pad_scanner/pages/boxing/boxing_calculations.dart' as boxing_calculations; import 'package:pad_scanner/pages/boxing/boxing_models.dart'; import 'package:pad_scanner/pages/boxing/boxing_status_presenter.dart'; import 'package:pad_scanner/pages/boxing/dialogs/cross_paichan_dialog.dart'; import 'package:pad_scanner/pages/boxing/widgets/boxing_shared_widgets.dart'; import 'package:pad_scanner/pages/boxing/widgets/boxing_notice_banners.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'; export 'package:pad_scanner/pages/boxing/boxing_models.dart' show BoxingMode, BoxingPageArguments; part 'boxing/parts/boxing_box_mutation_part.dart'; part 'boxing/parts/boxing_multi_ops_part.dart'; part 'boxing/parts/boxing_scan_part.dart'; part 'boxing/parts/boxing_single_ops_part.dart'; part 'boxing/parts/boxing_status_part.dart'; part 'boxing/parts/boxing_submit_part.dart'; // === 主页面 === class BoxingPage extends StatefulWidget { final BoxingPageArguments? arguments; const BoxingPage({super.key, this.arguments}); @override State createState() => _BoxingPageState(); } class _BoxingPageState extends State with WidgetsBindingObserver { final _scannerService = ScannerService(); final _apiService = ApiService(); late final BoxingApiActions _apiActions; final _feedbackService = FeedbackService(); final _focusNode = FocusNode(); StreamSubscription? _scanSubscription; // 模式 BoxingMode _mode = BoxingMode.singleCode; // 阶段 BoxingPhase _phase = BoxingPhase.waiting; // 当前总排号 String? _zongpaiNo; // 排产号信息(来自后端查询) String? _paichanNo; String? _workOrderNo; int? _erpQuantity; List _currentZongpaiBoxes = []; List _existingBoxes = []; int _maxBoxNo = 0; // 输入 final _boxNoController = TextEditingController(); final _quantityController = TextEditingController(); final _boxNoFocusNode = FocusNode(); final _quantityFocusNode = FocusNode(); // 多码凑箱:本次操作已确认装入当前箱的明细 final _manyToOnePackedItems = []; int? _editingPackedItemId; String _editingPackedQuantity = ''; int? _deletingPackedItemId; int? _editingAssignedItemId; String _editingAssignedQuantity = ''; int? _deletingAssignedItemId; String? _lastScannedPaichanNo; String? _paichanSwitchNotice; bool _crossPaichaPending = false; // 提交中 bool _isSubmitting = false; CurrentZongpaiBoxData? _editingBox; // 重复箱号 bool _isDuplicateBoxNo = false; // Status bar state StatusDotColor _statusDot = StatusDotColor.blue; String _statusText = '等待扫码'; String? _statusOverrideText; StatusDotColor? _statusOverrideDot; bool _isAutoProcessing = false; final _autoWarnings = []; // 多码凑箱:已完成装箱时可跳转的箱号 int? _completedJumpBoxNo; @override void initState() { super.initState(); _apiActions = BoxingApiActions(apiService: _apiService); WidgetsBinding.instance.addObserver(this); final arguments = widget.arguments; if (arguments != null) { _mode = arguments.initialMode; } _boxNoController.addListener(_onBoxNoTextChanged); _scanSubscription = _scannerService.scanResults.listen( (result) => this._onScan(result), ); if (arguments != null && arguments.autoScanCodes.isNotEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) { this._processAutoScanCodes(arguments.autoScanCodes); }); } WidgetsBinding.instance.addPostFrameCallback((_) => _requestFocus()); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _boxNoController.removeListener(_onBoxNoTextChanged); _scanSubscription?.cancel(); _focusNode.dispose(); _boxNoController.dispose(); _quantityController.dispose(); _boxNoFocusNode.dispose(); _quantityFocusNode.dispose(); _feedbackService.dispose(); super.dispose(); } void _requestFocus() { if (mounted) { _focusNode.requestFocus(); } } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { _requestFocus(); } } void _onKeyEvent(KeyEvent event) { if (event is! KeyDownEvent || _isAutoProcessing) return; if (_isModeSwitchKey(event)) { _cycleMode(); } if (_isFinishBoxKey(event)) { final hasItems = _visibleManyToOnePackedItems.isNotEmpty; if (hasItems && _mode == BoxingMode.multiCode) { this._finishManyToOneBox(); } } if (_completedJumpBoxNo != null && _isJumpToBoxKey(event)) { this._jumpToCompletedBox(); } } bool _isModeSwitchKey(KeyEvent event) { final key = event.logicalKey; return key == LogicalKeyboardKey.select || key == LogicalKeyboardKey.gameButtonRight1; } bool _isFinishBoxKey(KeyEvent event) { return event.logicalKey == LogicalKeyboardKey.gameButtonLeft2; } bool _isJumpToBoxKey(KeyEvent event) { return event.logicalKey == LogicalKeyboardKey.gameButtonRight2; } void _onBoxNoTextChanged() { if (!mounted || _mode != BoxingMode.multiCode) return; setState(() { _editingPackedItemId = null; _editingPackedQuantity = ''; _deletingPackedItemId = null; }); } // === 模式切换 === String get _modeLabel { switch (_mode) { case BoxingMode.singleCode: return '单码装箱'; case BoxingMode.multiCode: return '多码凑箱'; } } void _cycleMode() { _feedbackService.trigger(FeedbackEvent.modeSwitch); setState(() { switch (_mode) { case BoxingMode.singleCode: _mode = BoxingMode.multiCode; case BoxingMode.multiCode: _mode = BoxingMode.singleCode; } _resetState(); }); } void _resetState() { _phase = BoxingPhase.waiting; _zongpaiNo = null; _paichanNo = null; _workOrderNo = null; _erpQuantity = null; _currentZongpaiBoxes = []; _existingBoxes = []; _maxBoxNo = 0; _boxNoController.clear(); _quantityController.clear(); _manyToOnePackedItems.clear(); _editingPackedItemId = null; _editingPackedQuantity = ''; _deletingPackedItemId = null; _editingAssignedItemId = null; _editingAssignedQuantity = ''; _deletingAssignedItemId = null; _lastScannedPaichanNo = null; _paichanSwitchNotice = null; _crossPaichaPending = false; _isSubmitting = false; _editingBox = null; _isDuplicateBoxNo = false; _statusOverrideText = null; _statusOverrideDot = null; } // === 重复箱号检测 === bool _boxNoIsDuplicate() { return switch (_mode) { BoxingMode.multiCode => false, BoxingMode.singleCode => boxing_calculations.singleCodeBoxNoIsDuplicate( boxNoText: _boxNoController.text, currentBoxes: _currentZongpaiBoxes, editingBoxItemId: _editingBox?.boxItemId, ), }; } void _checkDuplicateBoxNo() { setState(() => _isDuplicateBoxNo = _boxNoIsDuplicate()); } // === 提交 === int get _packedQuantity { return boxing_calculations.packedQuantity(_currentZongpaiBoxes); } int get _remainingQuantity { return boxing_calculations.remainingQuantity( totalQuantity: _erpQuantity, currentBoxes: _currentZongpaiBoxes, ); } bool get _quantityTooHigh { return boxing_calculations.quantityTooHigh( remainingQuantity: _remainingQuantity, quantityText: _quantityController.text, ); } bool get _canSubmit { return boxing_calculations.canSubmitBoxing( isSubmitting: _isSubmitting, phase: _phase, zongpaiNo: _zongpaiNo, boxNoText: _boxNoController.text, quantityText: _quantityController.text, remainingQuantity: _remainingQuantity, quantityTooHigh: _quantityTooHigh, isDuplicateBoxNo: _isDuplicateBoxNo, ); } void _submitFromKeyboard() { if (_canSubmit) { this._submit(); } } void _focusQuantityInput() { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; _quantityFocusNode.requestFocus(); _quantityController.selection = TextSelection( baseOffset: 0, extentOffset: _quantityController.text.length, ); }); } int? get _currentManyToOneBoxNo { return boxing_calculations.currentManyToOneBoxNo(_boxNoController.text); } List get _visibleManyToOnePackedItems { return boxing_calculations.visibleManyToOnePackedItems( boxNo: _currentManyToOneBoxNo, existingBoxes: _existingBoxes, packedItems: _manyToOnePackedItems, paichanNo: _paichanNo, ); } // === Build === @override Widget build(BuildContext context) { this._updateBaseStatus(); final effectiveDot = _statusOverrideDot ?? _statusDot; final effectiveText = _statusOverrideText ?? _statusText; return KeyboardListener( focusNode: _focusNode, onKeyEvent: _onKeyEvent, child: Scaffold( appBar: AppBar( title: Row( children: [ const Text('装箱编号'), const SizedBox(width: 8), BoxingModePill( isSingle: _mode == BoxingMode.singleCode, label: _modeLabel, enabled: !_isAutoProcessing, onTap: _cycleMode, ), ], ), toolbarHeight: 44, titleSpacing: 12, ), body: Column( children: [ BoxingNoticeBanners( isAutoProcessing: _isAutoProcessing, autoWarnings: _autoWarnings, paichanSwitchNotice: _paichanSwitchNotice, isDuplicateBoxNo: _isDuplicateBoxNo, quantityTooHigh: _quantityTooHigh, phase: _phase, boxNoText: _boxNoController.text, remainingQuantity: _remainingQuantity, zongpaiNo: _zongpaiNo, completedJumpBoxNo: _completedJumpBoxNo, onJumpToCompletedBox: () => this._jumpToCompletedBox(), ), Expanded( child: _mode == BoxingMode.multiCode ? MultiCodeBody( hasScan: _zongpaiNo != null && _phase == BoxingPhase.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: () => this._openDetail(), onSubmitFromKeyboard: _submitFromKeyboard, onSubmit: () => this._submit(), onQuantityChanged: () => setState(() {}), onFinishBox: () => this._finishManyToOneBox(), onEditingPackedQuantityChanged: (value) => _editingPackedQuantity = value, onSavePackedItem: (item) => this._savePackedItem(item), onCancelEditPackedItem: () => this._cancelEditPackedItem(), onStartEditPackedItem: (item) => this._startEditPackedItem(item), onStartDeletePackedItem: (item) { setState(() { _deletingPackedItemId = item.boxItemId; _editingPackedItemId = null; _editingPackedQuantity = ''; }); }, onDeletePackedItem: (item) => this._deletePackedItem(item), onCancelDeletePackedItem: () => setState(() => _deletingPackedItemId = null), ) : SingleCodeBody( isWaiting: _phase == BoxingPhase.waiting && _zongpaiNo == null, isFinished: _phase == BoxingPhase.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: () => this._openDetail(), onCheckDuplicateBoxNo: _checkDuplicateBoxNo, onSubmitFromKeyboard: _submitFromKeyboard, onSubmit: () => this._submit(), onQuantityChanged: () => setState(() {}), onEditingAssignedQuantityChanged: (value) => _editingAssignedQuantity = value, onSaveAssignedItem: (item) => this._saveAssignedItem(item), onCancelEditAssigned: () => this._cancelEditAssigned(), onStartEditAssigned: (item) => this._startEditAssigned(item), onStartDeleteAssigned: (item) { setState(() { _deletingAssignedItemId = item.boxItemId; _editingAssignedItemId = null; _editingAssignedQuantity = ''; }); }, onDeleteAssignedItem: (item) => this._deleteAssignedItem(item), onCancelDeleteAssigned: () => setState(() => _deletingAssignedItemId = null), ), ), // 底部状态栏 StatusBar(dotColor: effectiveDot, text: effectiveText), ], ), ), ); } }