Files
pad_scanner/lib/pages/boxing_page.dart
Misaka_Company b9f17d5d49 refactor: extract API actions into BoxingApiActions with error handling
Centralize all API call logic (fetchBoxInfo, saveBoxRecord,
updateBoxRecord, deleteBoxRecord) into BoxingApiActions class with
unified error categorization (network, duplicate, missingApiUrl).
Add helper methods to reduce repetitive error handling in boxing_page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-21 09:54:16 +08:00

1266 lines
39 KiB
Dart

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<BoxingPage> createState() => _BoxingPageState();
}
class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
final _scannerService = ScannerService();
final _apiService = ApiService();
late final BoxingApiActions _apiActions;
final _feedbackService = FeedbackService();
final _focusNode = FocusNode();
StreamSubscription<ScanResult>? _scanSubscription;
// 模式
BoxingMode _mode = BoxingMode.singleCode;
// 阶段
BoxingPhase _phase = BoxingPhase.waiting;
// 当前总排号
String? _zongpaiNo;
// 排产号信息(来自后端查询)
String? _paichanNo;
String? _workOrderNo;
int? _erpQuantity;
List<CurrentZongpaiBoxData> _currentZongpaiBoxes = [];
List<BoxDetailData> _existingBoxes = [];
int _maxBoxNo = 0;
// 输入
final _boxNoController = TextEditingController();
final _quantityController = TextEditingController();
final _boxNoFocusNode = FocusNode();
final _quantityFocusNode = FocusNode();
// 多码凑箱:本次操作已确认装入当前箱的明细
final _manyToOnePackedItems = <ManyToOnePackedItem>[];
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 = <String>[];
// 多码凑箱:已完成装箱时可跳转的箱号
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<bool> _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<int>(
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<void> _processAutoScanCodes(List<String> 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<void> _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<ManyToOnePackedItem> get _visibleManyToOnePackedItems {
return boxing_calculations.visibleManyToOnePackedItems(
boxNo: _currentManyToOneBoxNo,
existingBoxes: _existingBoxes,
packedItems: _manyToOnePackedItems,
paichanNo: _paichanNo,
);
}
Future<bool> _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<void> _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<void> _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<void> _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<void> _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<T>(
BoxingActionResult<T> 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),
],
),
),
);
}
}