refactor: split boxing page into part files for scan, submit, ops and status

Extract large methods from _BoxingPageState into 6 part files:
- boxing_scan_part.dart: scan handling and auto-scan processing
- boxing_submit_part.dart: submit/update/delete API calls
- boxing_box_mutation_part.dart: box item mutations
- boxing_single_ops_part.dart: single-code boxing operations
- boxing_multi_ops_part.dart: multi-code boxing operations
- boxing_status_part.dart: status bar management

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-05-21 10:44:22 +08:00
parent b9f17d5d49
commit e87d29e696
7 changed files with 859 additions and 824 deletions

View File

@@ -0,0 +1,59 @@
part of '../../boxing_page.dart';
extension _BoxingBoxMutationPart on _BoxingPageState {
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 _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;
}
void _removeExistingBoxItem(int boxItemId) {
final result = box_mutations.removeExistingBoxItem(
existingBoxes: _existingBoxes,
boxItemId: boxItemId,
);
_existingBoxes = result.boxes;
_maxBoxNo = result.maxBoxNo;
}
}

View File

@@ -0,0 +1,112 @@
// ignore_for_file: invalid_use_of_protected_member, unnecessary_this
part of '../../boxing_page.dart';
extension _BoxingMultiOpsPart on _BoxingPageState {
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 _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) {
this._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,
);
}
this._replaceExistingPackedItem(item, boxNo, quantity);
_editingPackedItemId = null;
_editingPackedQuantity = '';
});
this._showStatusOverride(
'修改成功',
StatusDotColor.green,
const Duration(milliseconds: 1500),
);
} else {
this._showActionError(action, fallback: '修改失败');
}
}
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,
);
this._removeExistingBoxItem(item.boxItemId);
if (_editingPackedItemId == item.boxItemId) {
_editingPackedItemId = null;
_editingPackedQuantity = '';
}
_deletingPackedItemId = null;
}
});
if (action.success) {
this._showStatusOverride(
'删除成功',
StatusDotColor.green,
const Duration(milliseconds: 1500),
);
} else {
this._showActionError(action, fallback: '删除失败');
}
}
}

View File

@@ -0,0 +1,271 @@
// ignore_for_file: invalid_use_of_protected_member, unnecessary_this
part of '../../boxing_page.dart';
extension _BoxingScanPart on _BoxingPageState {
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);
this._showStatusOverride(
'无效码,请重新扫描',
StatusDotColor.red,
const Duration(seconds: 2),
);
return;
}
final zongpai = parsed.value;
// 三种模式都允许后扫入的总排号覆盖当前扫码区;未确认数据不会入库。
this._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,
});
this._showStatusOverride(
action.errorMessage ?? '查询失败',
this._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;
}
// 根据模式自动填充
this._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 this._queryBoxInfo(code);
if (!ok && mounted) {
setState(() => _autoWarnings.add('总排号 $code 未找到排产号信息,已忽略'));
}
} else {
for (final code in codes) {
if (!mounted) return;
final queried = await this._queryBoxInfo(code);
if (!queried) {
if (mounted) {
setState(() => _autoWarnings.add('总排号 $code 未找到排产号信息,已忽略'));
}
continue;
}
if (!_canSubmit) {
if (mounted) {
setState(() => _autoWarnings.add('总排号 $code 暂不能装箱,已忽略'));
}
continue;
}
final saved = await this._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);
this._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 ? this._confirmPaichanSwitch() : this._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;
});
}
}

View File

@@ -0,0 +1,133 @@
// ignore_for_file: invalid_use_of_protected_member, unnecessary_this
part of '../../boxing_page.dart';
extension _BoxingSingleOpsPart on _BoxingPageState {
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) {
this._showStatusOverride(
'请输入有效数量',
StatusDotColor.red,
const Duration(seconds: 2),
);
return;
}
if (quantity > this._maxAssignedQuantity(item)) {
this._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();
this._replaceExistingBoxItem(item, item.boxNo, quantity);
_editingAssignedItemId = null;
_editingAssignedQuantity = '';
this._refreshSingleCodeNewInput();
});
this._showStatusOverride(
'修改成功',
StatusDotColor.green,
const Duration(milliseconds: 1500),
);
} else {
this._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();
this._removeExistingBoxItem(item.boxItemId);
if (_editingAssignedItemId == item.boxItemId) {
_editingAssignedItemId = null;
_editingAssignedQuantity = '';
}
_deletingAssignedItemId = null;
this._refreshSingleCodeNewInput();
}
});
if (action.success) {
this._showStatusOverride(
'删除成功',
StatusDotColor.green,
const Duration(milliseconds: 1500),
);
} else {
this._showActionError(action, fallback: '删除失败');
}
}
}

View File

@@ -0,0 +1,86 @@
// ignore_for_file: invalid_use_of_protected_member, unnecessary_this
part of '../../boxing_page.dart';
extension _BoxingStatusPart on _BoxingPageState {
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,
});
this._showStatusOverride(
action.errorMessage ?? fallback,
this._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;
}
}

View File

@@ -0,0 +1,161 @@
// ignore_for_file: invalid_use_of_protected_member, unnecessary_this
part of '../../boxing_page.dart';
extension _BoxingSubmitPart on _BoxingPageState {
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) {
this._onSubmitSuccess(boxNo, quantity, action.data!.boxItemId);
} else {
this._onUpdateSuccess(editing, boxNo, quantity);
}
return true;
} else if (action.errorKind == BoxingActionErrorKind.duplicate) {
_feedbackService.trigger(FeedbackEvent.duplicateBoxNo);
this._showStatusOverride(
'该总排号在箱号 ${action.boxNo ?? ""} 已存在,请重新输入',
StatusDotColor.amber,
const Duration(seconds: 2),
);
return false;
} else {
this._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,
),
);
}
this._addExistingBoxItem(boxNo, quantity, boxItemId);
_maxBoxNo = _maxBoxNo > boxNo ? _maxBoxNo : boxNo;
});
switch (_mode) {
case BoxingMode.singleCode:
final remaining = _remainingQuantity;
if (remaining <= 0) {
this._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();
this._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;
});
this._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();
this._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;
});
this._showStatusOverride(
'修改成功',
StatusDotColor.green,
const Duration(milliseconds: 1500),
);
}
}