refactor: extract boxing logic into dedicated modules with tests
- boxing_models.dart: add BoxingMode, BoxingPhase, BoxingPageArguments - boxing_box_mutations.dart: box item add/replace/remove operations - boxing_calculations.dart: pure calculation helpers (quantities, validation) - boxing_status_presenter.dart: status bar text/dot determination - dialogs/cross_paichan_dialog.dart: cross-paichan confirmation dialog - widgets/boxing_notice_banners.dart: notice banner widgets - Add unit tests for mutations, calculations, and status presenter Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,37 +8,22 @@ 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_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';
|
||||
|
||||
// === 装箱模式 ===
|
||||
|
||||
enum BoxingMode {
|
||||
singleCode, // 单码装箱
|
||||
multiCode, // 多码凑箱
|
||||
}
|
||||
|
||||
class BoxingPageArguments {
|
||||
final BoxingMode initialMode;
|
||||
final List<String> autoScanCodes;
|
||||
|
||||
const BoxingPageArguments({
|
||||
required this.initialMode,
|
||||
required this.autoScanCodes,
|
||||
});
|
||||
}
|
||||
|
||||
// === 页面阶段 ===
|
||||
|
||||
enum _Phase {
|
||||
waiting, // 等待扫码
|
||||
scanned, // 已扫码,显示信息
|
||||
submitted, // 已提交成功
|
||||
}
|
||||
export 'package:pad_scanner/pages/boxing/boxing_models.dart'
|
||||
show BoxingMode, BoxingPageArguments;
|
||||
|
||||
// === 主页面 ===
|
||||
|
||||
@@ -62,7 +47,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
BoxingMode _mode = BoxingMode.singleCode;
|
||||
|
||||
// 阶段
|
||||
_Phase _phase = _Phase.waiting;
|
||||
BoxingPhase _phase = BoxingPhase.waiting;
|
||||
|
||||
// 当前总排号
|
||||
String? _zongpaiNo;
|
||||
@@ -221,7 +206,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
void _resetState() {
|
||||
_phase = _Phase.waiting;
|
||||
_phase = BoxingPhase.waiting;
|
||||
_zongpaiNo = null;
|
||||
_paichanNo = null;
|
||||
_workOrderNo = null;
|
||||
@@ -334,7 +319,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
_currentZongpaiBoxes = result.currentZongpaiBoxes;
|
||||
_existingBoxes = result.existingBoxes;
|
||||
_maxBoxNo = result.maxBoxNo;
|
||||
_phase = _Phase.scanned;
|
||||
_phase = BoxingPhase.scanned;
|
||||
_isDuplicateBoxNo = false;
|
||||
_editingBox = null;
|
||||
_editingAssignedItemId = null;
|
||||
@@ -469,98 +454,16 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
|
||||
// === 跨排产号确认对话框 ===
|
||||
|
||||
void _showCrossPaichaDialog() {
|
||||
Future<void> _showCrossPaichaDialog() async {
|
||||
final currentPaicha = _lastScannedPaichanNo ?? '--';
|
||||
final newPaicha = _paichanNo ?? '--';
|
||||
final dialogFocus = FocusNode();
|
||||
var dismissed = false;
|
||||
var selectedIndex = 0; // 0 = "否" (default), 1 = "是"
|
||||
|
||||
void dismissAsNo() {
|
||||
if (dismissed) return;
|
||||
dismissed = true;
|
||||
Navigator.of(context).pop();
|
||||
_cancelPaichanSwitch();
|
||||
}
|
||||
|
||||
void confirmSwitch() {
|
||||
if (dismissed) return;
|
||||
dismissed = true;
|
||||
Navigator.of(context).pop();
|
||||
_confirmPaichanSwitch();
|
||||
}
|
||||
|
||||
showDialog(
|
||||
final confirmed = await showCrossPaichanDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, setDialogState) {
|
||||
return KeyboardListener(
|
||||
focusNode: dialogFocus,
|
||||
onKeyEvent: (event) {
|
||||
if (event is! KeyDownEvent) return;
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||
setDialogState(() => selectedIndex = 0);
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
setDialogState(() => selectedIndex = 1);
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||||
selectedIndex == 0 ? dismissAsNo() : confirmSwitch();
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
dismissAsNo();
|
||||
}
|
||||
},
|
||||
child: PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop) dismissAsNo();
|
||||
},
|
||||
child: AlertDialog(
|
||||
backgroundColor: Colors.orange.shade50,
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber, color: Colors.orange.shade800),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'跨排产号扫描',
|
||||
style: TextStyle(color: Colors.orange.shade900),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'当前排产号:$currentPaicha\n'
|
||||
'新排产号:$newPaicha\n\n'
|
||||
'是否切换到新的排产号?\n\n'
|
||||
'选择「是」将放弃当前已扫描的所有数据。',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_dialogOptionBtn(
|
||||
label: '否',
|
||||
selected: selectedIndex == 0,
|
||||
onTap: dismissAsNo,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_dialogOptionBtn(
|
||||
label: '是,切换排产号',
|
||||
selected: selectedIndex == 1,
|
||||
onTap: confirmSwitch,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
currentPaicha: currentPaicha,
|
||||
newPaicha: newPaicha,
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
dialogFocus.requestFocus();
|
||||
});
|
||||
if (!mounted) return;
|
||||
confirmed ? _confirmPaichanSwitch() : _cancelPaichanSwitch();
|
||||
}
|
||||
|
||||
void _confirmPaichanSwitch() {
|
||||
@@ -609,50 +512,20 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
_crossPaichaPending = false;
|
||||
_zongpaiNo = null;
|
||||
_paichanNo = _lastScannedPaichanNo;
|
||||
_phase = _Phase.waiting;
|
||||
_phase = BoxingPhase.waiting;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _dialogOptionBtn({
|
||||
required String label,
|
||||
required bool selected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 40,
|
||||
child: ElevatedButton(
|
||||
onPressed: onTap,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: selected
|
||||
? Colors.blue.shade700
|
||||
: Colors.grey.shade200,
|
||||
foregroundColor: selected ? Colors.white : Colors.black87,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontWeight: selected ? FontWeight.w800 : FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// === 重复箱号检测 ===
|
||||
|
||||
bool _boxNoIsDuplicate() {
|
||||
final boxNo = int.tryParse(_boxNoController.text);
|
||||
if (boxNo == null) {
|
||||
return false;
|
||||
}
|
||||
return switch (_mode) {
|
||||
BoxingMode.multiCode => false,
|
||||
BoxingMode.singleCode => _currentZongpaiBoxes.any((b) {
|
||||
if (b.boxNo != boxNo) return false;
|
||||
return _editingBox == null || b.boxItemId != _editingBox!.boxItemId;
|
||||
}),
|
||||
BoxingMode.singleCode => boxing_calculations.singleCodeBoxNoIsDuplicate(
|
||||
boxNoText: _boxNoController.text,
|
||||
currentBoxes: _currentZongpaiBoxes,
|
||||
editingBoxItemId: _editingBox?.boxItemId,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -663,33 +536,34 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
// === 提交 ===
|
||||
|
||||
int get _packedQuantity {
|
||||
return _currentZongpaiBoxes.fold<int>(
|
||||
0,
|
||||
(sum, item) => sum + item.quantity,
|
||||
);
|
||||
return boxing_calculations.packedQuantity(_currentZongpaiBoxes);
|
||||
}
|
||||
|
||||
int get _remainingQuantity {
|
||||
final total = _erpQuantity ?? 0;
|
||||
return total - _packedQuantity;
|
||||
return boxing_calculations.remainingQuantity(
|
||||
totalQuantity: _erpQuantity,
|
||||
currentBoxes: _currentZongpaiBoxes,
|
||||
);
|
||||
}
|
||||
|
||||
bool get _quantityTooHigh {
|
||||
if (_remainingQuantity <= 0) return false;
|
||||
final qty = int.tryParse(_quantityController.text);
|
||||
return qty != null && qty > _remainingQuantity;
|
||||
return boxing_calculations.quantityTooHigh(
|
||||
remainingQuantity: _remainingQuantity,
|
||||
quantityText: _quantityController.text,
|
||||
);
|
||||
}
|
||||
|
||||
bool get _canSubmit {
|
||||
if (_isSubmitting || _phase != _Phase.scanned) return false;
|
||||
if (_zongpaiNo == null) return false;
|
||||
final boxNo = int.tryParse(_boxNoController.text);
|
||||
final qty = int.tryParse(_quantityController.text);
|
||||
if (boxNo == null || boxNo <= 0 || qty == null || qty <= 0) return false;
|
||||
if (_remainingQuantity <= 0) return false;
|
||||
if (_quantityTooHigh) return false;
|
||||
if (_isDuplicateBoxNo) return false;
|
||||
return true;
|
||||
return boxing_calculations.canSubmitBoxing(
|
||||
isSubmitting: _isSubmitting,
|
||||
phase: _phase,
|
||||
zongpaiNo: _zongpaiNo,
|
||||
boxNoText: _boxNoController.text,
|
||||
quantityText: _quantityController.text,
|
||||
remainingQuantity: _remainingQuantity,
|
||||
quantityTooHigh: _quantityTooHigh,
|
||||
isDuplicateBoxNo: _isDuplicateBoxNo,
|
||||
);
|
||||
}
|
||||
|
||||
void _submitFromKeyboard() {
|
||||
@@ -710,36 +584,16 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
int? get _currentManyToOneBoxNo {
|
||||
final boxNo = int.tryParse(_boxNoController.text.trim());
|
||||
if (boxNo == null || boxNo <= 0) return null;
|
||||
return boxNo;
|
||||
return boxing_calculations.currentManyToOneBoxNo(_boxNoController.text);
|
||||
}
|
||||
|
||||
List<ManyToOnePackedItem> get _visibleManyToOnePackedItems {
|
||||
final boxNo = _currentManyToOneBoxNo;
|
||||
if (boxNo == null) return const [];
|
||||
final byItemId = <int, ManyToOnePackedItem>{};
|
||||
for (final box in _existingBoxes.where((box) => box.boxNo == boxNo)) {
|
||||
for (final item in box.items) {
|
||||
final boxItemId = item.boxItemId;
|
||||
if (boxItemId == null) continue;
|
||||
byItemId[boxItemId] = ManyToOnePackedItem(
|
||||
boxItemId: boxItemId,
|
||||
zongpaiNo: item.zongpaiNo,
|
||||
paichanNo: _paichanNo,
|
||||
workOrderNo: item.workOrderNo,
|
||||
boxNo: boxNo,
|
||||
quantity: item.quantity,
|
||||
totalQuantity: item.totalQuantity,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (final item in _manyToOnePackedItems.where(
|
||||
(item) => item.boxNo == boxNo,
|
||||
)) {
|
||||
byItemId[item.boxItemId] = item;
|
||||
}
|
||||
return byItemId.values.toList(growable: false);
|
||||
return boxing_calculations.visibleManyToOnePackedItems(
|
||||
boxNo: _currentManyToOneBoxNo,
|
||||
existingBoxes: _existingBoxes,
|
||||
packedItems: _manyToOnePackedItems,
|
||||
paichanNo: _paichanNo,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _submit() async {
|
||||
@@ -839,7 +693,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
);
|
||||
} else {
|
||||
setState(() {
|
||||
_phase = _Phase.scanned;
|
||||
_phase = BoxingPhase.scanned;
|
||||
_boxNoController.text = (_maxBoxNo + 1).toString();
|
||||
_quantityController.text = remaining.toString();
|
||||
_quantityController.selection = TextSelection(
|
||||
@@ -872,7 +726,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
),
|
||||
);
|
||||
}
|
||||
_phase = _Phase.waiting;
|
||||
_phase = BoxingPhase.waiting;
|
||||
_zongpaiNo = null;
|
||||
_editingPackedItemId = null;
|
||||
_editingPackedQuantity = '';
|
||||
@@ -925,62 +779,31 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
int boxNo,
|
||||
int quantity,
|
||||
) {
|
||||
final nextBoxes = <BoxDetailData>[];
|
||||
for (final box in _existingBoxes) {
|
||||
final items = box.items.where((item) {
|
||||
return item.boxItemId != editing.boxItemId;
|
||||
}).toList();
|
||||
if (items.isNotEmpty) {
|
||||
nextBoxes.add(BoxDetailData(boxNo: box.boxNo, items: items));
|
||||
}
|
||||
}
|
||||
|
||||
final targetIndex = nextBoxes.indexWhere((box) => box.boxNo == boxNo);
|
||||
final updatedItem = BoxItemData(
|
||||
boxItemId: editing.boxItemId,
|
||||
final result = box_mutations.replaceExistingBoxItem(
|
||||
existingBoxes: _existingBoxes,
|
||||
editing: editing,
|
||||
boxNo: boxNo,
|
||||
quantity: quantity,
|
||||
zongpaiNo: _zongpaiNo!,
|
||||
workOrderNo: _workOrderNo,
|
||||
quantity: quantity,
|
||||
totalQuantity: _erpQuantity,
|
||||
);
|
||||
if (targetIndex >= 0) {
|
||||
final target = nextBoxes[targetIndex];
|
||||
nextBoxes[targetIndex] = BoxDetailData(
|
||||
boxNo: target.boxNo,
|
||||
items: [...target.items, updatedItem],
|
||||
);
|
||||
} else {
|
||||
nextBoxes.add(BoxDetailData(boxNo: boxNo, items: [updatedItem]));
|
||||
}
|
||||
nextBoxes.sort((a, b) => a.boxNo.compareTo(b.boxNo));
|
||||
_existingBoxes = nextBoxes;
|
||||
_maxBoxNo = nextBoxes.fold<int>(
|
||||
0,
|
||||
(max, box) => box.boxNo > max ? box.boxNo : max,
|
||||
);
|
||||
_existingBoxes = result.boxes;
|
||||
_maxBoxNo = result.maxBoxNo;
|
||||
}
|
||||
|
||||
void _addExistingBoxItem(int boxNo, int quantity, int? boxItemId) {
|
||||
final item = BoxItemData(
|
||||
final result = box_mutations.addExistingBoxItem(
|
||||
existingBoxes: _existingBoxes,
|
||||
boxNo: boxNo,
|
||||
quantity: quantity,
|
||||
boxItemId: boxItemId,
|
||||
zongpaiNo: _zongpaiNo!,
|
||||
workOrderNo: _workOrderNo,
|
||||
quantity: quantity,
|
||||
totalQuantity: _erpQuantity,
|
||||
);
|
||||
final index = _existingBoxes.indexWhere((box) => box.boxNo == boxNo);
|
||||
if (index < 0) {
|
||||
_existingBoxes = [
|
||||
..._existingBoxes,
|
||||
BoxDetailData(boxNo: boxNo, items: [item]),
|
||||
]..sort((a, b) => a.boxNo.compareTo(b.boxNo));
|
||||
return;
|
||||
}
|
||||
|
||||
final next = List<BoxDetailData>.from(_existingBoxes);
|
||||
final box = next[index];
|
||||
next[index] = BoxDetailData(boxNo: box.boxNo, items: [...box.items, item]);
|
||||
_existingBoxes = next;
|
||||
_existingBoxes = result.boxes;
|
||||
_maxBoxNo = result.maxBoxNo;
|
||||
}
|
||||
|
||||
void _finishManyToOneBox() {
|
||||
@@ -990,7 +813,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
_editingPackedQuantity = '';
|
||||
_deletingPackedItemId = null;
|
||||
_zongpaiNo = null;
|
||||
_phase = _Phase.waiting;
|
||||
_phase = BoxingPhase.waiting;
|
||||
_quantityController.clear();
|
||||
_boxNoController.text = (_maxBoxNo + 1).toString();
|
||||
_statusOverrideText = null;
|
||||
@@ -1016,9 +839,11 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
int _maxAssignedQuantity(CurrentZongpaiBoxData item) {
|
||||
final total = _erpQuantity ?? 0;
|
||||
final packedWithoutItem = _packedQuantity - item.quantity;
|
||||
return total - packedWithoutItem;
|
||||
return boxing_calculations.maxAssignedQuantity(
|
||||
totalQuantity: _erpQuantity,
|
||||
packedQuantity: _packedQuantity,
|
||||
itemQuantity: item.quantity,
|
||||
);
|
||||
}
|
||||
|
||||
void _refreshSingleCodeNewInput({bool focusQuantity = true}) {
|
||||
@@ -1246,39 +1071,14 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
int boxNo,
|
||||
int quantity,
|
||||
) {
|
||||
final nextBoxes = <BoxDetailData>[];
|
||||
for (final box in _existingBoxes) {
|
||||
final items = box.items.where((boxItem) {
|
||||
return boxItem.boxItemId != item.boxItemId;
|
||||
}).toList();
|
||||
if (items.isNotEmpty) {
|
||||
nextBoxes.add(BoxDetailData(boxNo: box.boxNo, items: items));
|
||||
}
|
||||
}
|
||||
|
||||
final updatedItem = BoxItemData(
|
||||
boxItemId: item.boxItemId,
|
||||
zongpaiNo: item.zongpaiNo,
|
||||
workOrderNo: item.workOrderNo,
|
||||
final result = box_mutations.replaceExistingPackedItem(
|
||||
existingBoxes: _existingBoxes,
|
||||
item: item,
|
||||
boxNo: boxNo,
|
||||
quantity: quantity,
|
||||
totalQuantity: item.totalQuantity,
|
||||
);
|
||||
final targetIndex = nextBoxes.indexWhere((box) => box.boxNo == boxNo);
|
||||
if (targetIndex >= 0) {
|
||||
final target = nextBoxes[targetIndex];
|
||||
nextBoxes[targetIndex] = BoxDetailData(
|
||||
boxNo: target.boxNo,
|
||||
items: [...target.items, updatedItem],
|
||||
);
|
||||
} else {
|
||||
nextBoxes.add(BoxDetailData(boxNo: boxNo, items: [updatedItem]));
|
||||
}
|
||||
nextBoxes.sort((a, b) => a.boxNo.compareTo(b.boxNo));
|
||||
_existingBoxes = nextBoxes;
|
||||
_maxBoxNo = nextBoxes.fold<int>(
|
||||
0,
|
||||
(max, box) => box.boxNo > max ? box.boxNo : max,
|
||||
);
|
||||
_existingBoxes = result.boxes;
|
||||
_maxBoxNo = result.maxBoxNo;
|
||||
}
|
||||
|
||||
Future<void> _deletePackedItem(ManyToOnePackedItem item) async {
|
||||
@@ -1331,20 +1131,12 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
void _removeExistingBoxItem(int boxItemId) {
|
||||
final nextBoxes = <BoxDetailData>[];
|
||||
for (final box in _existingBoxes) {
|
||||
final items = box.items
|
||||
.where((item) => item.boxItemId != boxItemId)
|
||||
.toList();
|
||||
if (items.isNotEmpty) {
|
||||
nextBoxes.add(BoxDetailData(boxNo: box.boxNo, items: items));
|
||||
}
|
||||
}
|
||||
_existingBoxes = nextBoxes;
|
||||
_maxBoxNo = nextBoxes.fold<int>(
|
||||
0,
|
||||
(max, box) => box.boxNo > max ? box.boxNo : max,
|
||||
final result = box_mutations.removeExistingBoxItem(
|
||||
existingBoxes: _existingBoxes,
|
||||
boxItemId: boxItemId,
|
||||
);
|
||||
_existingBoxes = result.boxes;
|
||||
_maxBoxNo = result.maxBoxNo;
|
||||
}
|
||||
|
||||
// === Status bar management ===
|
||||
@@ -1392,69 +1184,22 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
// === 状态文字 ===
|
||||
|
||||
void _updateBaseStatus() {
|
||||
if (_isAutoProcessing) {
|
||||
_statusDot = StatusDotColor.orange;
|
||||
_statusText = '正在同步转运数据...';
|
||||
return;
|
||||
}
|
||||
if (_isSubmitting) {
|
||||
_statusDot = StatusDotColor.orange;
|
||||
_statusText = '正在提交…';
|
||||
return;
|
||||
}
|
||||
if (_isDuplicateBoxNo && _phase == _Phase.scanned) {
|
||||
_statusDot = StatusDotColor.amber;
|
||||
_statusText = '箱号 ${_boxNoController.text} 已存在,请重新输入';
|
||||
return;
|
||||
}
|
||||
if (_quantityTooHigh && _phase == _Phase.scanned) {
|
||||
_statusDot = StatusDotColor.red;
|
||||
_statusText = '超出可装数量上限';
|
||||
return;
|
||||
}
|
||||
if (_editingAssignedItemId != null) {
|
||||
_statusDot = StatusDotColor.amber;
|
||||
_statusText = '正在编辑已分配记录';
|
||||
return;
|
||||
}
|
||||
if (_deletingAssignedItemId != null) {
|
||||
_statusDot = StatusDotColor.red;
|
||||
_statusText = '请确认是否删除该箱记录';
|
||||
return;
|
||||
}
|
||||
if (_paichanSwitchNotice != null) {
|
||||
_statusDot = StatusDotColor.blue;
|
||||
_statusText = '排产号已切换,箱号已重置';
|
||||
return;
|
||||
}
|
||||
switch (_phase) {
|
||||
case _Phase.waiting:
|
||||
_statusDot = StatusDotColor.blue;
|
||||
if (_mode == BoxingMode.multiCode &&
|
||||
_visibleManyToOnePackedItems.isNotEmpty) {
|
||||
_statusText = '请继续扫码或完成本箱';
|
||||
} else {
|
||||
_statusText = '等待扫码';
|
||||
}
|
||||
case _Phase.scanned:
|
||||
_statusDot = StatusDotColor.blue;
|
||||
if (_remainingQuantity <= 0) {
|
||||
_statusDot = StatusDotColor.red;
|
||||
_statusText = '该总排号已全部装箱完毕';
|
||||
return;
|
||||
}
|
||||
_statusText = _mode == BoxingMode.multiCode
|
||||
? '数量已填入,请确认或修改'
|
||||
: '数量已填入,请确认或修改';
|
||||
case _Phase.submitted:
|
||||
_statusDot = StatusDotColor.green;
|
||||
switch (_mode) {
|
||||
case BoxingMode.singleCode:
|
||||
_statusText = '装箱成功,可继续扫码';
|
||||
case BoxingMode.multiCode:
|
||||
_statusText = '请扫描下一个总排号';
|
||||
}
|
||||
}
|
||||
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 ===
|
||||
@@ -1487,12 +1232,25 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
..._buildNoticeBanners(),
|
||||
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 == _Phase.scanned,
|
||||
hasScan:
|
||||
_zongpaiNo != null && _phase == BoxingPhase.scanned,
|
||||
zongpaiNo: _zongpaiNo,
|
||||
paichanNo: _paichanNo,
|
||||
workOrderNo: _workOrderNo,
|
||||
@@ -1532,9 +1290,10 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
setState(() => _deletingPackedItemId = null),
|
||||
)
|
||||
: SingleCodeBody(
|
||||
isWaiting: _phase == _Phase.waiting && _zongpaiNo == null,
|
||||
isWaiting:
|
||||
_phase == BoxingPhase.waiting && _zongpaiNo == null,
|
||||
isFinished:
|
||||
_phase == _Phase.scanned &&
|
||||
_phase == BoxingPhase.scanned &&
|
||||
_zongpaiNo != null &&
|
||||
_remainingQuantity <= 0,
|
||||
zongpaiNo: _zongpaiNo,
|
||||
@@ -1586,139 +1345,4 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildNoticeBanners() {
|
||||
final banners = <Widget>[];
|
||||
if (_isAutoProcessing) {
|
||||
banners.add(
|
||||
_noticeBanner(
|
||||
icon: SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1.5,
|
||||
color: Colors.orange.shade700,
|
||||
),
|
||||
),
|
||||
text: '正在同步转运数据...',
|
||||
background: Colors.orange.shade50,
|
||||
foreground: Colors.orange.shade900,
|
||||
border: Colors.orange.shade100,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_autoWarnings.isNotEmpty) {
|
||||
banners.add(
|
||||
_noticeBanner(
|
||||
icon: Icon(
|
||||
Icons.warning_amber,
|
||||
size: 14,
|
||||
color: Colors.amber.shade900,
|
||||
),
|
||||
text: _autoWarnings.join(';'),
|
||||
background: Colors.amber.shade100,
|
||||
foreground: Colors.amber.shade900,
|
||||
border: Colors.amber.shade200,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_paichanSwitchNotice != null) {
|
||||
banners.add(
|
||||
_noticeBanner(
|
||||
icon: Icon(Icons.info_outline, size: 14, color: Colors.blue.shade900),
|
||||
text: _paichanSwitchNotice!,
|
||||
background: Colors.blue.shade50,
|
||||
foreground: Colors.blue.shade900,
|
||||
border: Colors.blue.shade100,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_isDuplicateBoxNo && _phase == _Phase.scanned) {
|
||||
banners.add(
|
||||
_noticeBanner(
|
||||
icon: Icon(
|
||||
Icons.warning_amber,
|
||||
size: 14,
|
||||
color: Colors.amber.shade900,
|
||||
),
|
||||
text: '箱号 ${_boxNoController.text} 已存在,请重新输入',
|
||||
background: Colors.amber.shade100,
|
||||
foreground: Colors.amber.shade900,
|
||||
border: Colors.amber.shade200,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_quantityTooHigh && _phase == _Phase.scanned) {
|
||||
banners.add(
|
||||
_noticeBanner(
|
||||
icon: Icon(Icons.close, size: 14, color: Colors.red.shade800),
|
||||
text: '超出可装数量上限(最多可装 $_remainingQuantity 件)',
|
||||
background: Colors.red.shade50,
|
||||
foreground: Colors.red.shade800,
|
||||
border: Colors.red.shade100,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_phase == _Phase.scanned &&
|
||||
_zongpaiNo != null &&
|
||||
_remainingQuantity <= 0) {
|
||||
final jumpBoxNo = _completedJumpBoxNo;
|
||||
if (jumpBoxNo != null) {
|
||||
banners.add(
|
||||
GestureDetector(
|
||||
onTap: _jumpToCompletedBox,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: _noticeBanner(
|
||||
icon: Icon(Icons.warning, size: 14, color: Colors.amber.shade800),
|
||||
text: '${_zongpaiNo!} 已完成装箱。(P4跳转所在箱号)',
|
||||
background: Colors.amber.shade50,
|
||||
foreground: Colors.amber.shade800,
|
||||
border: Colors.amber.shade200,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
banners.add(
|
||||
_noticeBanner(
|
||||
icon: Icon(Icons.warning, size: 14, color: Colors.red.shade800),
|
||||
text: '${_zongpaiNo!} 已全部装箱完毕,无需操作',
|
||||
background: Colors.red.shade50,
|
||||
foreground: Colors.red.shade800,
|
||||
border: Colors.red.shade100,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return banners;
|
||||
}
|
||||
|
||||
Widget _noticeBanner({
|
||||
required Widget icon,
|
||||
required String text,
|
||||
required Color background,
|
||||
required Color foreground,
|
||||
required Color border,
|
||||
}) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: background,
|
||||
border: Border(bottom: BorderSide(color: border)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(fontSize: 12, color: foreground),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user