From 99ed3bb9ecf7a121d504748de139d2a2f2b2a4a2 Mon Sep 17 00:00:00 2001 From: Misaka Date: Mon, 25 May 2026 22:48:29 +0800 Subject: [PATCH] feat: add temp storage shelf (B prefix) UI support Recognize B-prefix location codes as temp storage shelves. Add purple color scheme, conflict detection for temp_stored status, and updated submit/dialog logic for temp storage target handling. Co-Authored-By: Claude Opus 4.6 --- .../dialogs/registration_dialogs_part.dart | 24 ++++++++++++++ .../parts/registration_overview_part.dart | 8 +++++ .../parts/registration_scan_part.dart | 1 + .../parts/registration_status_part.dart | 3 ++ .../parts/registration_submit_part.dart | 23 +++++++++++-- .../registration_calculations.dart | 32 ++++++++++++++++++- .../widgets/registration_widgets_part.dart | 10 ++++++ lib/services/code_parser.dart | 30 ++++++++++------- lib/services/registration_linking.dart | 9 +++++- 9 files changed, 125 insertions(+), 15 deletions(-) diff --git a/lib/pages/registration/dialogs/registration_dialogs_part.dart b/lib/pages/registration/dialogs/registration_dialogs_part.dart index fc780e7..5b1550d 100644 --- a/lib/pages/registration/dialogs/registration_dialogs_part.dart +++ b/lib/pages/registration/dialogs/registration_dialogs_part.dart @@ -97,6 +97,7 @@ extension _RegistrationDialogsPart on _RegistrationPageState { void _showLocationConflictDialog({ required List onShelfItems, required List transferredItems, + List tempStoredItems = const [], }) { final contentParts = []; @@ -142,6 +143,29 @@ extension _RegistrationDialogsPart on _RegistrationPageState { } } + if (tempStoredItems.isNotEmpty) { + if (onShelfItems.isNotEmpty || transferredItems.isNotEmpty) { + contentParts.add(const Divider()); + contentParts.add(const SizedBox(height: 4)); + } + contentParts.add( + Text( + '暂存中', + style: TextStyle( + fontWeight: FontWeight.bold, + color: const Color(0xFF7B1FA2), + ), + ), + ); + for (final item in tempStoredItems) { + contentParts.addAll([ + Text('总排号:${item.zongpaiNo}'), + Text('暂存货位:${item.locationCode ?? "未知"}'), + const SizedBox(height: 6), + ]); + } + } + contentParts.add(const SizedBox(height: 4)); contentParts.add( const Text( diff --git a/lib/pages/registration/parts/registration_overview_part.dart b/lib/pages/registration/parts/registration_overview_part.dart index b209e88..f915e86 100644 --- a/lib/pages/registration/parts/registration_overview_part.dart +++ b/lib/pages/registration/parts/registration_overview_part.dart @@ -19,6 +19,14 @@ extension _RegistrationOverviewPart on _RegistrationPageState { ); } + /// Find all scanned items that are in temp storage. + List _findTempStoredItemsInScanned() { + return registration_calculations.findTempStoredItemsInScanned( + overview: _overview, + zongpaiNos: _zongpaiNos, + ); + } + Future _baseUrl() async { final configService = AppConfigService(); final baseUrl = await configService.getString('api_url') ?? ''; diff --git a/lib/pages/registration/parts/registration_scan_part.dart b/lib/pages/registration/parts/registration_scan_part.dart index dd057e9..a57863a 100644 --- a/lib/pages/registration/parts/registration_scan_part.dart +++ b/lib/pages/registration/parts/registration_scan_part.dart @@ -48,6 +48,7 @@ extension _RegistrationScanPart on _RegistrationPageState { _handleZongpaiScan(parsed.value); case CodeType.locationNormal: case CodeType.locationTransit: + case CodeType.locationTempStorage: _handleLocationScan(parsed.value, parsed.type); case CodeType.invalid: _feedbackService.trigger(FeedbackEvent.scanInvalid); diff --git a/lib/pages/registration/parts/registration_status_part.dart b/lib/pages/registration/parts/registration_status_part.dart index a13cb5e..6e25673 100644 --- a/lib/pages/registration/parts/registration_status_part.dart +++ b/lib/pages/registration/parts/registration_status_part.dart @@ -52,4 +52,7 @@ extension _RegistrationStatusPart on _RegistrationPageState { _zongpaiNos.isNotEmpty && _locationCode != null && !_isSubmitting; bool get _isTransitTarget => isTransitTarget(_locationType, _locationCode); + + bool get _isTempStorageTarget => + isTempStorageTarget(_locationType, _locationCode); } diff --git a/lib/pages/registration/parts/registration_submit_part.dart b/lib/pages/registration/parts/registration_submit_part.dart index 7068fe7..bf59476 100644 --- a/lib/pages/registration/parts/registration_submit_part.dart +++ b/lib/pages/registration/parts/registration_submit_part.dart @@ -34,6 +34,23 @@ extension _RegistrationSubmitPart on _RegistrationPageState { } } + // For temp storage target: block if any scanned item is transferred or temp_stored + // (on_shelf items are allowed — temp storage acts as shelf change) + if (_isTempStorageTarget) { + final transferredItems = _findTransferredItemsInScanned(); + final tempStoredItems = _findTempStoredItemsInScanned(); + if (transferredItems.isNotEmpty || tempStoredItems.isNotEmpty) { + setState(() => _isSubmitting = false); + _feedbackService.trigger(FeedbackEvent.submitFailure); + _showLocationConflictDialog( + onShelfItems: [], + transferredItems: transferredItems, + tempStoredItems: tempStoredItems, + ); + return; + } + } + if (_mode == RegistrationMode.multiCode && _zongpaiNos.length > 1) { if (_isTransitTarget && !await _ensureBatchSamePaicha(baseUrl)) { if (!mounted) return; @@ -47,7 +64,7 @@ extension _RegistrationSubmitPart on _RegistrationPageState { return; } // Pre-check: block entire batch if any item already has a location binding - if (!_isTransitTarget) { + if (!_isTransitTarget && !_isTempStorageTarget) { final onShelfItems = _findOnShelfItemsInScanned(); final transferredItems = _findTransferredItemsInScanned(); if (onShelfItems.isNotEmpty || transferredItems.isNotEmpty) { @@ -117,13 +134,15 @@ extension _RegistrationSubmitPart on _RegistrationPageState { } setState(() { _zongpaiNos.remove(zongpaiNo); - if (_mode == RegistrationMode.singleCode) { + if (_mode == RegistrationMode.singleCode && !_isTempStorageTarget) { _locationCode = null; _locationType = null; } }); final msg = result.isOffShelfSuccess ? '下架成功' + : _isTempStorageTarget + ? '变更货架成功' : (_mode == RegistrationMode.multiCode ? '多码上架模式,请扫描下一张执行卡' : '上架成功'); _showStatusOverride( msg, diff --git a/lib/pages/registration/registration_calculations.dart b/lib/pages/registration/registration_calculations.dart index f7476e2..023948a 100644 --- a/lib/pages/registration/registration_calculations.dart +++ b/lib/pages/registration/registration_calculations.dart @@ -5,11 +5,13 @@ class RegistrationOverviewStats { final int totalCount; final int shelvedCount; final int transferredCount; + final int tempStoredCount; const RegistrationOverviewStats({ required this.totalCount, required this.shelvedCount, required this.transferredCount, + required this.tempStoredCount, }); } @@ -31,17 +33,21 @@ RegistrationOverviewStats overviewStats(PaichaOverviewResult? overview) { totalCount: 0, shelvedCount: 0, transferredCount: 0, + tempStoredCount: 0, ); } var shelvedCount = 0; var transferredCount = 0; + var tempStoredCount = 0; for (final item in overview!.items) { switch (item.status) { case 'on_shelf': shelvedCount++; case 'transferred': transferredCount++; + case 'temp_stored': + tempStoredCount++; } } @@ -49,6 +55,7 @@ RegistrationOverviewStats overviewStats(PaichaOverviewResult? overview) { totalCount: overview.totalCount, shelvedCount: shelvedCount, transferredCount: transferredCount, + tempStoredCount: tempStoredCount, ); } @@ -91,6 +98,17 @@ List findTransferredItemsInScanned({ ); } +List findTempStoredItemsInScanned({ + required PaichaOverviewResult? overview, + required Iterable zongpaiNos, +}) { + return _findScannedItemsByStatus( + overview: overview, + zongpaiNos: zongpaiNos, + status: 'temp_stored', + ); +} + bool hasAnyLocatedInScanned({ required PaichaOverviewResult? overview, required Iterable zongpaiNos, @@ -100,7 +118,9 @@ bool hasAnyLocatedInScanned({ return overview!.items.any( (item) => scannedSet.contains(item.zongpaiNo) && - (item.status == 'on_shelf' || item.status == 'transferred'), + (item.status == 'on_shelf' || + item.status == 'transferred' || + item.status == 'temp_stored'), ); } @@ -108,10 +128,13 @@ Color registrationBarColor({ required String status, required bool isScanned, required bool isTransitTarget, + bool isTempStorageTarget = false, }) { if (isScanned) { final isConflict = isTransitTarget ? status == 'transferred' + : isTempStorageTarget + ? (status == 'transferred' || status == 'temp_stored') : (status == 'on_shelf' || status == 'transferred'); return isConflict ? Colors.red : const Color(0xFF43A047); } @@ -119,6 +142,8 @@ Color registrationBarColor({ switch (status) { case 'on_shelf': return const Color(0xFF2196F3); + case 'temp_stored': + return const Color(0xFF7B1FA2); case 'transferred': return const Color(0xFFFF9800); default: @@ -130,10 +155,13 @@ Color registrationRowBgColor({ required String status, required bool isScanned, required bool isTransitTarget, + bool isTempStorageTarget = false, }) { if (isScanned) { final isConflict = isTransitTarget ? status == 'transferred' + : isTempStorageTarget + ? (status == 'transferred' || status == 'temp_stored') : (status == 'on_shelf' || status == 'transferred'); return isConflict ? Colors.red.shade50 : const Color(0xFFF1F8E9); } @@ -141,6 +169,8 @@ Color registrationRowBgColor({ switch (status) { case 'on_shelf': return const Color(0xFFE3F2FD); + case 'temp_stored': + return const Color(0xFFF3E5F5); case 'transferred': return const Color(0xFFFFF3E0); default: diff --git a/lib/pages/registration/widgets/registration_widgets_part.dart b/lib/pages/registration/widgets/registration_widgets_part.dart index 2e3411e..6612f51 100644 --- a/lib/pages/registration/widgets/registration_widgets_part.dart +++ b/lib/pages/registration/widgets/registration_widgets_part.dart @@ -5,11 +5,13 @@ part of '../../registration_page.dart'; extension _RegistrationWidgetsPart on _RegistrationPageState { String _locationLabel(CodeType? type) { if (type == CodeType.locationTransit) return '转运区域'; + if (type == CodeType.locationTempStorage) return '暂存货架'; return '普通货架'; } Color _locationLabelColor(CodeType? type) { if (type == CodeType.locationTransit) return Colors.orange; + if (type == CodeType.locationTempStorage) return const Color(0xFF7B1FA2); return Colors.blue; } @@ -108,6 +110,12 @@ extension _RegistrationWidgetsPart on _RegistrationPageState { const Color(0xFFE65100), hasData, ), + _buildStatTag( + '暂存 ${stats.tempStoredCount}', + const Color(0xFFF3E5F5), + const Color(0xFF7B1FA2), + hasData, + ), ], ), ], @@ -293,11 +301,13 @@ extension _RegistrationWidgetsPart on _RegistrationPageState { status: item.status, isScanned: isScanned, isTransitTarget: _isTransitTarget, + isTempStorageTarget: _isTempStorageTarget, ); final bgColor = registration_calculations.registrationRowBgColor( status: item.status, isScanned: isScanned, isTransitTarget: _isTransitTarget, + isTempStorageTarget: _isTempStorageTarget, ); return Container( diff --git a/lib/services/code_parser.dart b/lib/services/code_parser.dart index c4dc651..3f63a9a 100644 --- a/lib/services/code_parser.dart +++ b/lib/services/code_parser.dart @@ -1,14 +1,8 @@ // lib/services/code_parser.dart class CodeParser { - static final _zongpaiRegex = RegExp( - r'^\d{2}(B|C|T)\d+$|^\d{2}(BW|CW)\d{4}$', - ); - static final _locationRegex = RegExp( - r'^[A-Z0-9]+-[A-Z0-9]+-[A-Z0-9]+$', - ); - static final _transitRegex = RegExp( - r'^TRANS-', - ); + static final _zongpaiRegex = RegExp(r'^\d{2}(B|C|T)\d+$|^\d{2}(BW|CW)\d{4}$'); + static final _locationRegex = RegExp(r'^[A-Z0-9]+-[A-Z0-9]+-[A-Z0-9]+$'); + static final _transitRegex = RegExp(r'^TRANS-'); static ParseResult parse(String code) { final trimmed = code.trim(); @@ -23,16 +17,30 @@ class CodeParser { return ParseResult(type: CodeType.zongpaiNo, value: normalized); } if (_locationRegex.hasMatch(normalized)) { + if (normalized.startsWith('B')) { + return ParseResult( + type: CodeType.locationTempStorage, + value: normalized, + ); + } return ParseResult(type: CodeType.locationNormal, value: normalized); } return ParseResult(type: CodeType.invalid, value: code); } static bool isLocation(CodeType type) => - type == CodeType.locationNormal || type == CodeType.locationTransit; + type == CodeType.locationNormal || + type == CodeType.locationTransit || + type == CodeType.locationTempStorage; } -enum CodeType { zongpaiNo, locationNormal, locationTransit, invalid } +enum CodeType { + zongpaiNo, + locationNormal, + locationTransit, + locationTempStorage, + invalid, +} class ParseResult { final CodeType type; diff --git a/lib/services/registration_linking.dart b/lib/services/registration_linking.dart index 7d2455d..77f15c7 100644 --- a/lib/services/registration_linking.dart +++ b/lib/services/registration_linking.dart @@ -8,13 +8,20 @@ bool isTransitTarget(CodeType? locationType, String? locationCode) { (locationCode?.startsWith('TRANS-') ?? false); } +bool isTempStorageTarget(CodeType? locationType, String? locationCode) { + return locationType == CodeType.locationTempStorage || + (locationCode?.startsWith('B') ?? false); +} + String registrationSubmitLabel({ required bool isTransitTarget, required bool isMultiCode, required int zongpaiCount, }) { if (!isTransitTarget) { - return isMultiCode && zongpaiCount > 1 ? '批量上架($zongpaiCount 条)' : '确 认 上 架'; + return isMultiCode && zongpaiCount > 1 + ? '批量上架($zongpaiCount 条)' + : '确 认 上 架'; } return isMultiCode && zongpaiCount > 1 ? '批量转运并凑箱($zongpaiCount 条)' : '转运并装箱'; }