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; // === 主页面 === 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(_onScan); if (arguments != null && arguments.autoScanCodes.isNotEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) { _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) { _finishManyToOneBox(); } } if (_completedJumpBoxNo != null && _isJumpToBoxKey(event)) { _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; } // === 扫码处理 === void _onScan(ScanResult result) { if (_isAutoProcessing) { return; } if (_crossPaichaPending) { _feedbackService.trigger(FeedbackEvent.alreadyCompleted); return; } final parsed = CodeParser.parse(result.barcode); if (parsed.type != CodeType.zongpaiNo) { _feedbackService.trigger(FeedbackEvent.scanInvalid); _showStatusOverride( '无效码,请重新扫描', StatusDotColor.red, const Duration(seconds: 2), ); return; } final zongpai = parsed.value; // 三种模式都允许后扫入的总排号覆盖当前扫码区;未确认数据不会入库。 _queryBoxInfo(zongpai); } Future _queryBoxInfo(String zongpai) async { final action = await _apiActions.fetchBoxInfo(zongpai); if (!mounted) return false; if (!action.success) { _feedbackService.trigger(switch (action.errorKind) { BoxingActionErrorKind.network => FeedbackEvent.networkError, BoxingActionErrorKind.missingApiUrl => FeedbackEvent.submitFailure, _ => FeedbackEvent.scanInvalid, }); _showStatusOverride( action.errorMessage ?? '查询失败', _dotForActionError(action.errorKind), const Duration(seconds: 2), ); return false; } final result = action.data!; // 判断该总牌号是否已全部装箱完毕 final packedQuantity = result.currentZongpaiBoxes.fold( 0, (sum, item) => sum + item.quantity, ); final totalQuantity = result.quantity ?? 0; final alreadyCompleted = totalQuantity > 0 && packedQuantity >= totalQuantity; _feedbackService.trigger( alreadyCompleted ? FeedbackEvent.alreadyCompleted : FeedbackEvent.scanValid, ); setState(() { _zongpaiNo = zongpai; _paichanNo = result.paichanNo; _workOrderNo = result.workOrderNo; _erpQuantity = result.quantity; _currentZongpaiBoxes = result.currentZongpaiBoxes; _existingBoxes = result.existingBoxes; _maxBoxNo = result.maxBoxNo; _phase = BoxingPhase.scanned; _isDuplicateBoxNo = false; _editingBox = null; _editingAssignedItemId = null; _editingAssignedQuantity = ''; _deletingAssignedItemId = null; _completedJumpBoxNo = null; _statusOverrideText = null; if (_mode == BoxingMode.singleCode) { _paichanSwitchNotice = null; } // 根据模式自动填充 _applyAutoFill(); }); return true; } Future _processAutoScanCodes(List codes) async { if (!mounted) return; setState(() { _isAutoProcessing = true; _autoWarnings.clear(); _statusOverrideText = '正在同步转运数据...'; _statusOverrideDot = StatusDotColor.orange; }); if (_mode == BoxingMode.singleCode) { final code = codes.first; final ok = await _queryBoxInfo(code); if (!ok && mounted) { setState(() => _autoWarnings.add('总排号 $code 未找到排产号信息,已忽略')); } } else { for (final code in codes) { if (!mounted) return; final queried = await _queryBoxInfo(code); if (!queried) { if (mounted) { setState(() => _autoWarnings.add('总排号 $code 未找到排产号信息,已忽略')); } continue; } if (!_canSubmit) { if (mounted) { setState(() => _autoWarnings.add('总排号 $code 暂不能装箱,已忽略')); } continue; } final saved = await _submit(); if (!saved && mounted) { setState(() => _autoWarnings.add('总排号 $code 自动装箱失败,请手动处理')); } } } if (!mounted) return; setState(() { _isAutoProcessing = false; _statusOverrideText = null; _statusOverrideDot = null; }); _focusQuantityInput(); _requestFocus(); } void _applyAutoFill() { switch (_mode) { case BoxingMode.singleCode: final remaining = _remainingQuantity; if (remaining <= 0) { _boxNoController.clear(); _quantityController.clear(); _statusOverrideText = '该总排号已全部装箱完毕'; _statusOverrideDot = StatusDotColor.red; return; } _boxNoController.text = (_maxBoxNo + 1).toString(); _quantityController.text = remaining.toString(); _quantityController.selection = TextSelection( baseOffset: 0, extentOffset: _quantityController.text.length, ); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && _mode == BoxingMode.singleCode) { _quantityFocusNode.requestFocus(); } }); case BoxingMode.multiCode: final decision = decideMultiCodePaichanContext( lastPaichanNo: _lastScannedPaichanNo, currentPaichanNo: _paichanNo, currentBoxNoText: _boxNoController.text, maxBoxNo: _maxBoxNo, ); if (decision.isSwitched) { _crossPaichaPending = true; _feedbackService.trigger(FeedbackEvent.alreadyCompleted); _showCrossPaichaDialog(); return; } else { _paichanSwitchNotice = null; } final boxNoToApply = decision.boxNoToApply; if (boxNoToApply != null) { _boxNoController.text = boxNoToApply.toString(); } _lastScannedPaichanNo = _paichanNo; final remaining = _remainingQuantity; if (remaining <= 0) { _quantityController.clear(); if (_manyToOnePackedItems.isEmpty && _currentZongpaiBoxes.isNotEmpty) { _completedJumpBoxNo = _currentZongpaiBoxes.first.boxNo; } _statusOverrideText = '该总排号已全部装箱完毕'; _statusOverrideDot = StatusDotColor.red; return; } _quantityController.text = remaining.toString(); _quantityController.selection = TextSelection( baseOffset: 0, extentOffset: _quantityController.text.length, ); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && _mode == BoxingMode.multiCode) { _quantityFocusNode.requestFocus(); } }); } _isDuplicateBoxNo = _boxNoIsDuplicate(); } // === 跨排产号确认对话框 === Future _showCrossPaichaDialog() async { final currentPaicha = _lastScannedPaichanNo ?? '--'; final newPaicha = _paichanNo ?? '--'; final confirmed = await showCrossPaichanDialog( context: context, currentPaicha: currentPaicha, newPaicha: newPaicha, ); if (!mounted) return; confirmed ? _confirmPaichanSwitch() : _cancelPaichanSwitch(); } void _confirmPaichanSwitch() { setState(() { _crossPaichaPending = false; _manyToOnePackedItems.clear(); _editingPackedItemId = null; _editingPackedQuantity = ''; _deletingPackedItemId = null; _paichanSwitchNotice = '排产号已切换:${_paichanNo ?? "--"}'; _boxNoController.text = (_maxBoxNo + 1).toString(); _lastScannedPaichanNo = _paichanNo; }); _feedbackService.trigger(FeedbackEvent.paichanSwitch); // Continue with remaining quantity flow final remaining = _remainingQuantity; if (remaining <= 0) { _quantityController.clear(); if (_manyToOnePackedItems.isEmpty && _currentZongpaiBoxes.isNotEmpty) { _completedJumpBoxNo = _currentZongpaiBoxes.first.boxNo; } setState(() { _statusOverrideText = '该总排号已全部装箱完毕'; _statusOverrideDot = StatusDotColor.red; }); return; } setState(() { _quantityController.text = remaining.toString(); _quantityController.selection = TextSelection( baseOffset: 0, extentOffset: _quantityController.text.length, ); _isDuplicateBoxNo = _boxNoIsDuplicate(); }); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && _mode == BoxingMode.multiCode) { _quantityFocusNode.requestFocus(); } }); } void _cancelPaichanSwitch() { setState(() { _crossPaichaPending = false; _zongpaiNo = null; _paichanNo = _lastScannedPaichanNo; _phase = BoxingPhase.waiting; }); } // === 重复箱号检测 === 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) { _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, ); } Future _submit() async { if (!_canSubmit) return false; final boxNo = int.parse(_boxNoController.text); final quantity = int.parse(_quantityController.text); setState(() => _isSubmitting = true); final editing = _editingBox; final action = editing == null ? await _apiActions.saveBoxRecord( zongpaiNo: _zongpaiNo!, boxNo: boxNo, quantity: quantity, ) : await _apiActions.updateBoxRecord( boxItemId: editing.boxItemId, boxNo: boxNo, quantity: quantity, ); if (!mounted) return false; setState(() => _isSubmitting = false); if (action.success) { if (editing == null) { _onSubmitSuccess(boxNo, quantity, action.data!.boxItemId); } else { _onUpdateSuccess(editing, boxNo, quantity); } return true; } else if (action.errorKind == BoxingActionErrorKind.duplicate) { _feedbackService.trigger(FeedbackEvent.duplicateBoxNo); _showStatusOverride( '该总排号在箱号 ${action.boxNo ?? ""} 已存在,请重新输入', StatusDotColor.amber, const Duration(seconds: 2), ); return false; } else { _showActionError(action, fallback: '提交失败'); return false; } } void _onSubmitSuccess(int boxNo, int quantity, int? boxItemId) { _feedbackService.trigger(FeedbackEvent.submitSuccess); setState(() { if (_mode == BoxingMode.singleCode && boxItemId != null) { _currentZongpaiBoxes = List.from(_currentZongpaiBoxes) ..add( CurrentZongpaiBoxData( boxItemId: boxItemId, boxNo: boxNo, quantity: quantity, ), ); } _addExistingBoxItem(boxNo, quantity, boxItemId); _maxBoxNo = _maxBoxNo > boxNo ? _maxBoxNo : boxNo; }); switch (_mode) { case BoxingMode.singleCode: final remaining = _remainingQuantity; if (remaining <= 0) { _showStatusOverride( '装箱成功', StatusDotColor.green, const Duration(milliseconds: 1500), ); } else { setState(() { _phase = BoxingPhase.scanned; _boxNoController.text = (_maxBoxNo + 1).toString(); _quantityController.text = remaining.toString(); _quantityController.selection = TextSelection( baseOffset: 0, extentOffset: _quantityController.text.length, ); _isDuplicateBoxNo = false; }); _focusQuantityInput(); _showStatusOverride( '请继续完成剩余数量装箱', StatusDotColor.green, const Duration(milliseconds: 1500), ); } case BoxingMode.multiCode: _lastScannedPaichanNo = _paichanNo; setState(() { if (boxItemId != null) { _manyToOnePackedItems.add( ManyToOnePackedItem( boxItemId: boxItemId, zongpaiNo: _zongpaiNo!, paichanNo: _paichanNo, workOrderNo: _workOrderNo, boxNo: boxNo, quantity: quantity, totalQuantity: _erpQuantity, ), ); } _phase = BoxingPhase.waiting; _zongpaiNo = null; _editingPackedItemId = null; _editingPackedQuantity = ''; _deletingPackedItemId = null; }); _showStatusOverride( '装箱成功,请继续扫码', StatusDotColor.green, const Duration(milliseconds: 1500), ); } } void _onUpdateSuccess( CurrentZongpaiBoxData editing, int boxNo, int quantity, ) { _feedbackService.trigger(FeedbackEvent.submitSuccess); setState(() { _currentZongpaiBoxes = _currentZongpaiBoxes.map((item) { if (item.boxItemId != editing.boxItemId) return item; return CurrentZongpaiBoxData( boxItemId: item.boxItemId, boxNo: boxNo, quantity: quantity, ); }).toList(); _replaceExistingBoxItem(editing, boxNo, quantity); _editingBox = null; _boxNoController.text = (_maxBoxNo + 1).toString(); final remaining = _remainingQuantity; if (remaining > 0) { _quantityController.text = remaining.toString(); } else { _quantityController.clear(); } _isDuplicateBoxNo = false; }); _showStatusOverride( '修改成功', StatusDotColor.green, const Duration(milliseconds: 1500), ); } void _replaceExistingBoxItem( CurrentZongpaiBoxData editing, int boxNo, int quantity, ) { final result = box_mutations.replaceExistingBoxItem( existingBoxes: _existingBoxes, editing: editing, boxNo: boxNo, quantity: quantity, zongpaiNo: _zongpaiNo!, workOrderNo: _workOrderNo, totalQuantity: _erpQuantity, ); _existingBoxes = result.boxes; _maxBoxNo = result.maxBoxNo; } void _addExistingBoxItem(int boxNo, int quantity, int? boxItemId) { final result = box_mutations.addExistingBoxItem( existingBoxes: _existingBoxes, boxNo: boxNo, quantity: quantity, boxItemId: boxItemId, zongpaiNo: _zongpaiNo!, workOrderNo: _workOrderNo, totalQuantity: _erpQuantity, ); _existingBoxes = result.boxes; _maxBoxNo = result.maxBoxNo; } void _finishManyToOneBox() { setState(() { _manyToOnePackedItems.clear(); _editingPackedItemId = null; _editingPackedQuantity = ''; _deletingPackedItemId = null; _zongpaiNo = null; _phase = BoxingPhase.waiting; _quantityController.clear(); _boxNoController.text = (_maxBoxNo + 1).toString(); _statusOverrideText = null; _statusOverrideDot = null; }); } void _startEditAssigned(CurrentZongpaiBoxData item) { setState(() { _editingAssignedItemId = item.boxItemId; _editingAssignedQuantity = item.quantity.toString(); _deletingAssignedItemId = null; _statusOverrideText = null; _statusOverrideDot = null; }); } void _cancelEditAssigned() { setState(() { _editingAssignedItemId = null; _editingAssignedQuantity = ''; }); } int _maxAssignedQuantity(CurrentZongpaiBoxData item) { return boxing_calculations.maxAssignedQuantity( totalQuantity: _erpQuantity, packedQuantity: _packedQuantity, itemQuantity: item.quantity, ); } void _refreshSingleCodeNewInput({bool focusQuantity = true}) { _boxNoController.text = (_maxBoxNo + 1).toString(); final remaining = _remainingQuantity; if (remaining > 0) { _quantityController.text = remaining.toString(); _quantityController.selection = TextSelection( baseOffset: 0, extentOffset: _quantityController.text.length, ); if (focusQuantity) { _focusQuantityInput(); } } else { _quantityController.clear(); } _isDuplicateBoxNo = false; } Future _saveAssignedItem(CurrentZongpaiBoxData item) async { final quantity = int.tryParse(_editingAssignedQuantity); if (quantity == null || quantity <= 0) { _showStatusOverride( '请输入有效数量', StatusDotColor.red, const Duration(seconds: 2), ); return; } if (quantity > _maxAssignedQuantity(item)) { _showStatusOverride( '超出可装数量上限', StatusDotColor.red, const Duration(seconds: 2), ); return; } setState(() => _isSubmitting = true); final action = await _apiActions.updateBoxRecord( boxItemId: item.boxItemId, boxNo: item.boxNo, quantity: quantity, ); if (!mounted) return; setState(() => _isSubmitting = false); if (action.success) { setState(() { _currentZongpaiBoxes = _currentZongpaiBoxes.map((box) { if (box.boxItemId != item.boxItemId) return box; return CurrentZongpaiBoxData( boxItemId: box.boxItemId, boxNo: box.boxNo, quantity: quantity, ); }).toList(); _replaceExistingBoxItem(item, item.boxNo, quantity); _editingAssignedItemId = null; _editingAssignedQuantity = ''; _refreshSingleCodeNewInput(); }); _showStatusOverride( '修改成功', StatusDotColor.green, const Duration(milliseconds: 1500), ); } else { _showActionError(action, fallback: '修改失败'); } } Future _deleteAssignedItem(CurrentZongpaiBoxData item) async { setState(() => _isSubmitting = true); final action = await _apiActions.deleteBoxRecord(boxItemId: item.boxItemId); if (!mounted) return; setState(() { _isSubmitting = false; if (action.success) { _currentZongpaiBoxes = _currentZongpaiBoxes .where((box) => box.boxItemId != item.boxItemId) .toList(); _removeExistingBoxItem(item.boxItemId); if (_editingAssignedItemId == item.boxItemId) { _editingAssignedItemId = null; _editingAssignedQuantity = ''; } _deletingAssignedItemId = null; _refreshSingleCodeNewInput(); } }); if (action.success) { _showStatusOverride( '删除成功', StatusDotColor.green, const Duration(milliseconds: 1500), ); } else { _showActionError(action, fallback: '删除失败'); } } void _startEditPackedItem(ManyToOnePackedItem item) { setState(() { _editingPackedItemId = item.boxItemId; _editingPackedQuantity = item.quantity.toString(); _deletingPackedItemId = null; }); } void _cancelEditPackedItem() { setState(() { _editingPackedItemId = null; _editingPackedQuantity = ''; }); } 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) { _showStatusOverride( '请输入有效箱号和数量', StatusDotColor.red, const Duration(seconds: 2), ); return; } setState(() => _isSubmitting = true); final action = await _apiActions.updateBoxRecord( boxItemId: item.boxItemId, boxNo: boxNo, quantity: quantity, ); if (!mounted) return; setState(() => _isSubmitting = false); if (action.success) { setState(() { final index = _manyToOnePackedItems.indexWhere( (packed) => packed.boxItemId == item.boxItemId, ); if (index >= 0) { _manyToOnePackedItems[index] = _manyToOnePackedItems[index].copyWith( boxNo: boxNo, quantity: quantity, ); } _replaceExistingPackedItem(item, boxNo, quantity); _editingPackedItemId = null; _editingPackedQuantity = ''; }); _showStatusOverride( '修改成功', StatusDotColor.green, const Duration(milliseconds: 1500), ); } else { _showActionError(action, fallback: '修改失败'); } } void _replaceExistingPackedItem( ManyToOnePackedItem item, int boxNo, int quantity, ) { final result = box_mutations.replaceExistingPackedItem( existingBoxes: _existingBoxes, item: item, boxNo: boxNo, quantity: quantity, ); _existingBoxes = result.boxes; _maxBoxNo = result.maxBoxNo; } Future _deletePackedItem(ManyToOnePackedItem item) async { setState(() => _isSubmitting = true); final action = await _apiActions.deleteBoxRecord(boxItemId: item.boxItemId); if (!mounted) return; setState(() { _isSubmitting = false; if (action.success) { _manyToOnePackedItems.removeWhere( (packed) => packed.boxItemId == item.boxItemId, ); _removeExistingBoxItem(item.boxItemId); if (_editingPackedItemId == item.boxItemId) { _editingPackedItemId = null; _editingPackedQuantity = ''; } _deletingPackedItemId = null; } }); if (action.success) { _showStatusOverride( '删除成功', StatusDotColor.green, const Duration(milliseconds: 1500), ); } else { _showActionError(action, fallback: '删除失败'); } } void _removeExistingBoxItem(int boxItemId) { final result = box_mutations.removeExistingBoxItem( existingBoxes: _existingBoxes, boxItemId: boxItemId, ); _existingBoxes = result.boxes; _maxBoxNo = result.maxBoxNo; } // === Status bar management === StatusDotColor _dotForActionError(BoxingActionErrorKind? kind) { return switch (kind) { BoxingActionErrorKind.network => StatusDotColor.yellow, BoxingActionErrorKind.duplicate => StatusDotColor.amber, _ => StatusDotColor.red, }; } void _showActionError( BoxingActionResult action, { required String fallback, }) { _feedbackService.trigger(switch (action.errorKind) { BoxingActionErrorKind.network => FeedbackEvent.networkError, BoxingActionErrorKind.duplicate => FeedbackEvent.duplicateBoxNo, _ => FeedbackEvent.submitFailure, }); _showStatusOverride( action.errorMessage ?? fallback, _dotForActionError(action.errorKind), const Duration(seconds: 2), ); } void _showStatusOverride(String text, StatusDotColor dot, Duration duration) { setState(() { _statusOverrideText = text; _statusOverrideDot = dot; }); Future.delayed(duration, () { if (mounted) { setState(() { _statusOverrideText = null; _statusOverrideDot = null; }); } }); } void _jumpToCompletedBox() { final boxNo = _completedJumpBoxNo; if (boxNo == null) return; setState(() { _boxNoController.text = boxNo.toString(); _completedJumpBoxNo = null; _statusOverrideText = null; _statusOverrideDot = null; }); } // === 导航到详情页 === void _openDetail() { Navigator.push( context, MaterialPageRoute( builder: (_) => BoxingDetailPage( paichanNo: _paichanNo ?? '', existingBoxes: _existingBoxes, ), ), ); } // === 状态文字 === void _updateBaseStatus() { final status = boxingStatusFor( isAutoProcessing: _isAutoProcessing, isSubmitting: _isSubmitting, isDuplicateBoxNo: _isDuplicateBoxNo, quantityTooHigh: _quantityTooHigh, isEditingAssigned: _editingAssignedItemId != null, isDeletingAssigned: _deletingAssignedItemId != null, hasPaichanSwitchNotice: _paichanSwitchNotice != null, phase: _phase, isMultiCode: _mode == BoxingMode.multiCode, hasVisibleManyToOneItems: _visibleManyToOnePackedItems.isNotEmpty, remainingQuantity: _remainingQuantity, boxNoText: _boxNoController.text, ); _statusDot = status.dot; _statusText = status.text; } // === Build === @override Widget build(BuildContext context) { _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: _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: _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 == 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: _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), ), ), // 底部状态栏 StatusBar(dotColor: effectiveDot, text: effectiveText), ], ), ), ); } }