From a165bfeabeded739095f31022ee71a4633a5b7b6 Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Thu, 14 May 2026 14:04:25 +0800 Subject: [PATCH] feat(boxing): handle multi-code paichan switches --- lib/pages/boxing_page.dart | 47 +++++++++++++++++++- lib/services/boxing_context.dart | 38 ++++++++++++++++ lib/services/feedback_service.dart | 5 +++ test/services/boxing_context_test.dart | 60 ++++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 lib/services/boxing_context.dart create mode 100644 test/services/boxing_context_test.dart diff --git a/lib/pages/boxing_page.dart b/lib/pages/boxing_page.dart index 6e212e2..5b075a3 100644 --- a/lib/pages/boxing_page.dart +++ b/lib/pages/boxing_page.dart @@ -4,6 +4,7 @@ import 'package:pad_scanner/services/app_config_service.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_detail_page.dart'; import 'package:pad_scanner/widgets/status_bar.dart'; @@ -101,6 +102,8 @@ class _BoxingPageState extends State { int? _editingPackedItemId; String _editingPackedQuantity = ''; int? _deletingPackedItemId; + String? _lastScannedPaichanNo; + String? _paichanSwitchNotice; // 提交中 bool _isSubmitting = false; @@ -184,6 +187,8 @@ class _BoxingPageState extends State { _editingPackedItemId = null; _editingPackedQuantity = ''; _deletingPackedItemId = null; + _lastScannedPaichanNo = null; + _paichanSwitchNotice = null; _isSubmitting = false; _lastBoxNo = null; _editingBox = null; @@ -260,6 +265,9 @@ class _BoxingPageState extends State { _isDuplicateBoxNo = false; _editingBox = null; _statusOverrideText = null; + if (_mode == BoxingMode.singleCode) { + _paichanSwitchNotice = null; + } // 根据模式自动填充 _applyAutoFill(); @@ -293,9 +301,27 @@ class _BoxingPageState extends State { } }); case BoxingMode.multiCode: - if (_boxNoController.text.trim().isEmpty) { - _boxNoController.text = (_maxBoxNo + 1).toString(); + final decision = decideMultiCodePaichanContext( + lastPaichanNo: _lastScannedPaichanNo, + currentPaichanNo: _paichanNo, + currentBoxNoText: _boxNoController.text, + maxBoxNo: _maxBoxNo, + ); + if (decision.isSwitched) { + _manyToOnePackedItems.clear(); + _editingPackedItemId = null; + _editingPackedQuantity = ''; + _deletingPackedItemId = null; + _paichanSwitchNotice = '排产号已切换:${_paichanNo ?? "--"}'; + _feedbackService.trigger(FeedbackEvent.paichanSwitch); + } else { + _paichanSwitchNotice = null; } + final boxNoToApply = decision.boxNoToApply; + if (boxNoToApply != null) { + _boxNoController.text = boxNoToApply.toString(); + } + _lastScannedPaichanNo = _paichanNo; _quantityController.text = (_erpQuantity ?? 0).toString(); _quantityController.selection = TextSelection( baseOffset: 0, @@ -508,6 +534,7 @@ class _BoxingPageState extends State { } case BoxingMode.multiCode: + _lastScannedPaichanNo = _paichanNo; setState(() { if (boxItemId != null) { _manyToOnePackedItems.add( @@ -643,6 +670,7 @@ class _BoxingPageState extends State { void _finishManyToOneBox() { final currentBoxNo = int.tryParse(_boxNoController.text.trim()); setState(() { + _manyToOnePackedItems.clear(); _editingPackedItemId = null; _editingPackedQuantity = ''; _deletingPackedItemId = null; @@ -913,6 +941,11 @@ class _BoxingPageState extends State { _statusText = '超出可装数量上限'; return; } + if (_paichanSwitchNotice != null) { + _statusDot = StatusDotColor.blue; + _statusText = '排产号已切换,箱号已重置'; + return; + } switch (_phase) { case _Phase.waiting: _statusDot = StatusDotColor.blue; @@ -978,6 +1011,16 @@ class _BoxingPageState extends State { body: Column( children: [ // 重复箱号警告条(保留,因为这是输入区关联的即时反馈) + if (_paichanSwitchNotice != null) + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + color: Colors.blue.shade50, + child: Text( + _paichanSwitchNotice!, + style: TextStyle(color: Colors.blue.shade900, fontSize: 13), + ), + ), if (_isDuplicateBoxNo && _phase == _Phase.scanned) Container( width: double.infinity, diff --git a/lib/services/boxing_context.dart b/lib/services/boxing_context.dart new file mode 100644 index 0000000..ae4c2f2 --- /dev/null +++ b/lib/services/boxing_context.dart @@ -0,0 +1,38 @@ +enum PaichanContextChange { firstScan, samePaichan, switchedPaichan } + +class PaichanContextDecision { + final PaichanContextChange change; + final int? boxNoToApply; + + const PaichanContextDecision({required this.change, this.boxNoToApply}); + + bool get isSwitched => change == PaichanContextChange.switchedPaichan; +} + +PaichanContextDecision decideMultiCodePaichanContext({ + required String? lastPaichanNo, + required String? currentPaichanNo, + required String currentBoxNoText, + required int maxBoxNo, +}) { + final recommendedBoxNo = maxBoxNo + 1; + if (currentPaichanNo == null || currentPaichanNo.isEmpty) { + return const PaichanContextDecision(change: PaichanContextChange.firstScan); + } + if (lastPaichanNo == null || lastPaichanNo.isEmpty) { + return PaichanContextDecision( + change: PaichanContextChange.firstScan, + boxNoToApply: currentBoxNoText.trim().isEmpty ? recommendedBoxNo : null, + ); + } + if (lastPaichanNo == currentPaichanNo) { + return PaichanContextDecision( + change: PaichanContextChange.samePaichan, + boxNoToApply: currentBoxNoText.trim().isEmpty ? recommendedBoxNo : null, + ); + } + return PaichanContextDecision( + change: PaichanContextChange.switchedPaichan, + boxNoToApply: recommendedBoxNo, + ); +} diff --git a/lib/services/feedback_service.dart b/lib/services/feedback_service.dart index db5ec18..1ab8770 100644 --- a/lib/services/feedback_service.dart +++ b/lib/services/feedback_service.dart @@ -11,6 +11,7 @@ enum FeedbackEvent { networkError, // Network error → failure sound + short vibrate duplicateBoxNo, // Duplicate box no → failure sound + short vibrate modeSwitch, // Mode toggle → beep only + paichanSwitch, // Paichan changed → beep + short vibrate } /// Centralized feedback dispatcher. @@ -56,6 +57,10 @@ class FeedbackService { case FeedbackEvent.modeSwitch: _soundService.playBeep(); + + case FeedbackEvent.paichanSwitch: + _soundService.playBeep(); + _vibrateShort(); } } diff --git a/test/services/boxing_context_test.dart b/test/services/boxing_context_test.dart new file mode 100644 index 0000000..944def5 --- /dev/null +++ b/test/services/boxing_context_test.dart @@ -0,0 +1,60 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:pad_scanner/services/boxing_context.dart'; + +void main() { + group('decideMultiCodePaichanContext', () { + test('first scan records context and fills recommended box if empty', () { + final decision = decideMultiCodePaichanContext( + lastPaichanNo: null, + currentPaichanNo: 'W00009', + currentBoxNoText: '', + maxBoxNo: 3, + ); + + expect(decision.change, PaichanContextChange.firstScan); + expect(decision.boxNoToApply, 4); + expect(decision.isSwitched, isFalse); + }); + + test('same paichan keeps current box number', () { + final decision = decideMultiCodePaichanContext( + lastPaichanNo: 'W00009', + currentPaichanNo: 'W00009', + currentBoxNoText: '8', + maxBoxNo: 3, + ); + + expect(decision.change, PaichanContextChange.samePaichan); + expect(decision.boxNoToApply, isNull); + expect(decision.isSwitched, isFalse); + }); + + test('switched paichan resets to new paichan max box plus one', () { + final decision = decideMultiCodePaichanContext( + lastPaichanNo: 'W00009', + currentPaichanNo: 'W00010', + currentBoxNoText: '8', + maxBoxNo: 1, + ); + + expect(decision.change, PaichanContextChange.switchedPaichan); + expect(decision.boxNoToApply, 2); + expect(decision.isSwitched, isTrue); + }); + + test( + 'same paichan defensively fills recommended box when box is empty', + () { + final decision = decideMultiCodePaichanContext( + lastPaichanNo: 'W00009', + currentPaichanNo: 'W00009', + currentBoxNoText: '', + maxBoxNo: 4, + ); + + expect(decision.change, PaichanContextChange.samePaichan); + expect(decision.boxNoToApply, 5); + }, + ); + }); +}