refactor: unify registration mode concept to single-code/multi-code shelving

Replace the locked/unlocked terminology with RegistrationMode enum
(singleCode/multiCode) to align with boxing module's naming convention.
Multi-code mode now allows switching shelf locations with a confirmation
dialog when pending data exists. Use shared BoxingModePill widget for
consistent UI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-05-21 17:30:23 +08:00
parent 9ab7a51dfb
commit a601858225
2 changed files with 106 additions and 85 deletions

View File

@@ -11,6 +11,12 @@ import 'package:pad_scanner/services/registration_linking.dart';
import 'package:vibration/vibration.dart'; import 'package:vibration/vibration.dart';
import 'package:pad_scanner/pages/boxing_page.dart'; import 'package:pad_scanner/pages/boxing_page.dart';
import 'package:pad_scanner/widgets/status_bar.dart'; import 'package:pad_scanner/widgets/status_bar.dart';
import 'package:pad_scanner/pages/boxing/widgets/boxing_shared_widgets.dart';
enum RegistrationMode {
singleCode, // 单码上架
multiCode, // 多码上架
}
class RegistrationPage extends StatefulWidget { class RegistrationPage extends StatefulWidget {
const RegistrationPage({super.key}); const RegistrationPage({super.key});
@@ -29,7 +35,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
final _zongpaiNos = <String>[]; final _zongpaiNos = <String>[];
String? _locationCode; String? _locationCode;
CodeType? _locationType; CodeType? _locationType;
bool _isLocked = false; RegistrationMode _mode = RegistrationMode.singleCode;
bool _isSubmitting = false; bool _isSubmitting = false;
StatusDotColor _statusDot = StatusDotColor.blue; StatusDotColor _statusDot = StatusDotColor.blue;
@@ -88,7 +94,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
void _onKeyEvent(KeyEvent event) { void _onKeyEvent(KeyEvent event) {
if (event is! KeyDownEvent) return; if (event is! KeyDownEvent) return;
if (_isLockToggleKey(event)) { if (_isLockToggleKey(event)) {
_toggleLock(!_isLocked); _cycleMode();
return; return;
} }
if (event.logicalKey == LogicalKeyboardKey.enter) { if (event.logicalKey == LogicalKeyboardKey.enter) {
@@ -109,14 +115,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
_handleZongpaiScan(parsed.value); _handleZongpaiScan(parsed.value);
case CodeType.locationNormal: case CodeType.locationNormal:
case CodeType.locationTransit: case CodeType.locationTransit:
if (!_isLocked) { _handleLocationScan(parsed.value, parsed.type);
_feedbackService.trigger(FeedbackEvent.scanValid);
setState(() {
_locationCode = parsed.value;
_locationType = parsed.type;
_clearStatusOverride();
});
} else {}
case CodeType.invalid: case CodeType.invalid:
_feedbackService.trigger(FeedbackEvent.scanInvalid); _feedbackService.trigger(FeedbackEvent.scanInvalid);
_showStatusOverride( _showStatusOverride(
@@ -127,6 +126,71 @@ class _RegistrationPageState extends State<RegistrationPage> {
} }
} }
void _handleLocationScan(String locationCode, CodeType locationType) {
// In singleCode mode or no existing data, switch directly
if (_mode == RegistrationMode.singleCode || _zongpaiNos.isEmpty) {
_feedbackService.trigger(FeedbackEvent.scanValid);
setState(() {
_locationCode = locationCode;
_locationType = locationType;
_clearStatusOverride();
});
return;
}
// In multiCode mode with existing data, confirm before switching
_showLocationSwitchDialog(locationCode, locationType);
}
void _showLocationSwitchDialog(String newLocationCode, CodeType newLocationType) {
final count = _zongpaiNos.length;
showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => 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: Text(
'当前已有 $count 条待上架数据,切换货架号不会清除已扫描的总排号。',
style: const TextStyle(fontSize: 14),
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(ctx);
},
child: const Text('取消'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(ctx);
_feedbackService.trigger(FeedbackEvent.scanValid);
setState(() {
_locationCode = newLocationCode;
_locationType = newLocationType;
_clearStatusOverride();
});
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange.shade700,
foregroundColor: Colors.white,
),
child: const Text('确认切换'),
),
],
),
);
}
void _handleZongpaiScan(String zongpaiNo) { void _handleZongpaiScan(String zongpaiNo) {
// If cross-paicha dialog is showing, just vibrate and discard // If cross-paicha dialog is showing, just vibrate and discard
if (_crossPaichaPending) { if (_crossPaichaPending) {
@@ -134,8 +198,8 @@ class _RegistrationPageState extends State<RegistrationPage> {
return; return;
} }
// In locked mode with existing data, check for cross-paicha scan // In multiCode mode with existing data, check for cross-paicha scan
if (_isLocked && if (_mode == RegistrationMode.multiCode &&
_zongpaiNos.isNotEmpty && _zongpaiNos.isNotEmpty &&
_overview?.success == true && _overview?.success == true &&
!_overview!.items.any((item) => item.zongpaiNo == zongpaiNo)) { !_overview!.items.any((item) => item.zongpaiNo == zongpaiNo)) {
@@ -148,7 +212,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
final shouldRefresh = _overviewZongpaiNo != zongpaiNo; final shouldRefresh = _overviewZongpaiNo != zongpaiNo;
_feedbackService.trigger(FeedbackEvent.scanValid); _feedbackService.trigger(FeedbackEvent.scanValid);
setState(() { setState(() {
if (_isLocked) { if (_mode == RegistrationMode.multiCode) {
if (!_zongpaiNos.contains(zongpaiNo)) { if (!_zongpaiNos.contains(zongpaiNo)) {
_zongpaiNos.add(zongpaiNo); _zongpaiNos.add(zongpaiNo);
} }
@@ -197,9 +261,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
_statusText = '正在提交…'; _statusText = '正在提交…';
return; return;
} }
if (_isLocked && _locationCode != null && _zongpaiNos.isEmpty) { if (_mode == RegistrationMode.multiCode && _locationCode != null && _zongpaiNos.isEmpty) {
_statusDot = StatusDotColor.blue; _statusDot = StatusDotColor.blue;
_statusText = '货位已锁定,请扫描下一张执行卡'; _statusText = '多码上架模式,请扫描下一张执行卡';
return; return;
} }
final hasZongpai = _zongpaiNos.isNotEmpty; final hasZongpai = _zongpaiNos.isNotEmpty;
@@ -343,7 +407,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
} }
} }
if (_isLocked && _zongpaiNos.length > 1) { if (_mode == RegistrationMode.multiCode && _zongpaiNos.length > 1) {
if (_isTransitTarget && !await _ensureBatchSamePaicha(baseUrl)) { if (_isTransitTarget && !await _ensureBatchSamePaicha(baseUrl)) {
if (!mounted) return; if (!mounted) return;
setState(() => _isSubmitting = false); setState(() => _isSubmitting = false);
@@ -420,20 +484,20 @@ class _RegistrationPageState extends State<RegistrationPage> {
mode: BoxingMode.singleCode, mode: BoxingMode.singleCode,
codes: [zongpaiNo], codes: [zongpaiNo],
codesToClear: [zongpaiNo], codesToClear: [zongpaiNo],
clearLocation: !_isLocked, clearLocation: _mode == RegistrationMode.singleCode,
); );
return; return;
} }
setState(() { setState(() {
_zongpaiNos.remove(zongpaiNo); _zongpaiNos.remove(zongpaiNo);
if (!_isLocked) { if (_mode == RegistrationMode.singleCode) {
_locationCode = null; _locationCode = null;
_locationType = null; _locationType = null;
} }
}); });
final msg = result.isOffShelfSuccess final msg = result.isOffShelfSuccess
? '下架成功' ? '下架成功'
: (_isLocked ? '货位已锁定,请扫描下一张执行卡' : '上架成功'); : (_mode == RegistrationMode.multiCode ? '多码上架模式,请扫描下一张执行卡' : '上架成功');
_showStatusOverride( _showStatusOverride(
msg, msg,
StatusDotColor.green, StatusDotColor.green,
@@ -886,20 +950,18 @@ class _RegistrationPageState extends State<RegistrationPage> {
_loadOverview(newZongpaiNo); _loadOverview(newZongpaiNo);
} }
void _toggleLock(bool value) { void _cycleMode() {
if (value && _locationCode == null) { _feedbackService.trigger(FeedbackEvent.modeSwitch);
_feedbackService.trigger(FeedbackEvent.submitFailure);
_showStatusOverride(
'请先扫描货位号',
StatusDotColor.red,
const Duration(seconds: 2),
);
return;
}
setState(() { setState(() {
_isLocked = value; switch (_mode) {
if (!value && _zongpaiNos.length > 1) { case RegistrationMode.singleCode:
_zongpaiNos.removeRange(0, _zongpaiNos.length - 1); _mode = RegistrationMode.multiCode;
case RegistrationMode.multiCode:
_mode = RegistrationMode.singleCode;
// 切换到单码时,只保留最后一个总排号
if (_zongpaiNos.length > 1) {
_zongpaiNos.removeRange(0, _zongpaiNos.length - 1);
}
} }
}); });
} }
@@ -930,7 +992,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
children: [ children: [
const Text('上架登记'), const Text('上架登记'),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded(child: _buildLockHeader()), Expanded(child: _buildModePill()),
], ],
), ),
titleSpacing: 12, titleSpacing: 12,
@@ -948,53 +1010,12 @@ class _RegistrationPageState extends State<RegistrationPage> {
); );
} }
Widget _buildLockHeader() { Widget _buildModePill() {
final backgroundColor = return BoxingModePill(
_isLocked ? Colors.orange.shade700 : Colors.blue.shade700; isSingle: _mode == RegistrationMode.singleCode,
final label = _isLocked ? '货位锁定' : '未锁定'; label: _mode == RegistrationMode.singleCode ? '单码上架' : '多码上架',
enabled: !_isSubmitting,
return Material( onTap: _cycleMode,
borderRadius: BorderRadius.circular(8),
color: backgroundColor,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => _toggleLock(!_isLocked),
child: Container(
width: double.infinity,
height: 36,
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.swap_horiz, color: Colors.white, size: 18),
const SizedBox(width: 6),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
label,
maxLines: 1,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w800,
),
),
),
),
const SizedBox(width: 8),
Text(
'P2',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.88),
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
],
),
),
),
); );
} }
@@ -1336,7 +1357,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
style: rowStyle, style: rowStyle,
), ),
), ),
if (isScanned && _isLocked) ...[ if (isScanned && _mode == RegistrationMode.multiCode) ...[
const SizedBox(width: 6), const SizedBox(width: 6),
SizedBox( SizedBox(
width: 28, width: 28,
@@ -1447,7 +1468,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
: Text( : Text(
registrationSubmitLabel( registrationSubmitLabel(
isTransitTarget: _isTransitTarget, isTransitTarget: _isTransitTarget,
isLocked: _isLocked, isMultiCode: _mode == RegistrationMode.multiCode,
zongpaiCount: _zongpaiNos.length, zongpaiCount: _zongpaiNos.length,
), ),
style: const TextStyle( style: const TextStyle(

View File

@@ -10,13 +10,13 @@ bool isTransitTarget(CodeType? locationType, String? locationCode) {
String registrationSubmitLabel({ String registrationSubmitLabel({
required bool isTransitTarget, required bool isTransitTarget,
required bool isLocked, required bool isMultiCode,
required int zongpaiCount, required int zongpaiCount,
}) { }) {
if (!isTransitTarget) { if (!isTransitTarget) {
return isLocked && zongpaiCount > 1 ? '批量上架($zongpaiCount 条)' : '确 认 上 架'; return isMultiCode && zongpaiCount > 1 ? '批量上架($zongpaiCount 条)' : '确 认 上 架';
} }
return isLocked && zongpaiCount > 1 ? '批量转运并凑箱($zongpaiCount 条)' : '转运并装箱'; return isMultiCode && zongpaiCount > 1 ? '批量转运并凑箱($zongpaiCount 条)' : '转运并装箱';
} }
Color registrationSubmitColor({ Color registrationSubmitColor({