feat: add cross-paichan warning dialog to multi-code boxing & fix box number calculation

Multi-code boxing mode now shows a confirmation dialog when scanning a
code from a different paichan, matching the registration page pattern.
Scans are blocked while the dialog is visible.

Also removes the redundant _lastBoxNo variable. Box numbers now always
use _maxBoxNo + 1 from the backend, fixing a bug where switching to a
new paichan incorrectly carried over the previous paichan's box number.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-05-20 18:15:42 +08:00
parent 3fd47f4d7b
commit b17e0e7e62

View File

@@ -123,12 +123,11 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
int? _deletingAssignedItemId;
String? _lastScannedPaichanNo;
String? _paichanSwitchNotice;
bool _crossPaichaPending = false;
// 提交中
bool _isSubmitting = false;
// 单码装箱:上次提交的箱号(用于继续添加预填)
int? _lastBoxNo;
CurrentZongpaiBoxData? _editingBox;
// 重复箱号
@@ -273,8 +272,8 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
_deletingAssignedItemId = null;
_lastScannedPaichanNo = null;
_paichanSwitchNotice = null;
_crossPaichaPending = false;
_isSubmitting = false;
_lastBoxNo = null;
_editingBox = null;
_isDuplicateBoxNo = false;
_statusOverrideText = null;
@@ -288,6 +287,11 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
return;
}
if (_crossPaichaPending) {
_feedbackService.trigger(FeedbackEvent.alreadyCompleted);
return;
}
final parsed = CodeParser.parse(result.barcode);
if (parsed.type != CodeType.zongpaiNo) {
@@ -438,11 +442,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
_statusOverrideDot = StatusDotColor.red;
return;
}
if (_lastBoxNo != null) {
_boxNoController.text = (_lastBoxNo! + 1).toString();
} else {
_boxNoController.text = (_maxBoxNo + 1).toString();
}
_quantityController.text = remaining.toString();
_quantityController.selection = TextSelection(
baseOffset: 0,
@@ -461,12 +461,10 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
maxBoxNo: _maxBoxNo,
);
if (decision.isSwitched) {
_manyToOnePackedItems.clear();
_editingPackedItemId = null;
_editingPackedQuantity = '';
_deletingPackedItemId = null;
_paichanSwitchNotice = '排产号已切换:${_paichanNo ?? "--"}';
_feedbackService.trigger(FeedbackEvent.paichanSwitch);
_crossPaichaPending = true;
_feedbackService.trigger(FeedbackEvent.alreadyCompleted);
_showCrossPaichaDialog();
return;
} else {
_paichanSwitchNotice = null;
}
@@ -500,6 +498,178 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
_isDuplicateBoxNo = _boxNoIsDuplicate();
}
// === 跨排产号确认对话框 ===
void _showCrossPaichaDialog() {
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(
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,
),
],
),
),
),
);
},
),
);
WidgetsBinding.instance.addPostFrameCallback((_) {
dialogFocus.requestFocus();
});
}
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 = _Phase.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() {
@@ -674,7 +844,6 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
_feedbackService.trigger(FeedbackEvent.submitSuccess);
setState(() {
_lastBoxNo = boxNo;
if (_mode == BoxingMode.singleCode && boxItemId != null) {
_currentZongpaiBoxes = List.from(_currentZongpaiBoxes)
..add(
@@ -765,7 +934,6 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
}).toList();
_replaceExistingBoxItem(editing, boxNo, quantity);
_editingBox = null;
_lastBoxNo = _maxBoxNo;
_boxNoController.text = (_maxBoxNo + 1).toString();
final remaining = _remainingQuantity;
if (remaining > 0) {
@@ -884,7 +1052,6 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
}
void _refreshSingleCodeNewInput({bool focusQuantity = true}) {
_lastBoxNo = _maxBoxNo;
_boxNoController.text = (_maxBoxNo + 1).toString();
final remaining = _remainingQuantity;
if (remaining > 0) {