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:pad_scanner/pages/boxing_page.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 {
const RegistrationPage({super.key});
@@ -29,7 +35,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
final _zongpaiNos = <String>[];
String? _locationCode;
CodeType? _locationType;
bool _isLocked = false;
RegistrationMode _mode = RegistrationMode.singleCode;
bool _isSubmitting = false;
StatusDotColor _statusDot = StatusDotColor.blue;
@@ -88,7 +94,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
void _onKeyEvent(KeyEvent event) {
if (event is! KeyDownEvent) return;
if (_isLockToggleKey(event)) {
_toggleLock(!_isLocked);
_cycleMode();
return;
}
if (event.logicalKey == LogicalKeyboardKey.enter) {
@@ -109,14 +115,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
_handleZongpaiScan(parsed.value);
case CodeType.locationNormal:
case CodeType.locationTransit:
if (!_isLocked) {
_feedbackService.trigger(FeedbackEvent.scanValid);
setState(() {
_locationCode = parsed.value;
_locationType = parsed.type;
_clearStatusOverride();
});
} else {}
_handleLocationScan(parsed.value, parsed.type);
case CodeType.invalid:
_feedbackService.trigger(FeedbackEvent.scanInvalid);
_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) {
// If cross-paicha dialog is showing, just vibrate and discard
if (_crossPaichaPending) {
@@ -134,8 +198,8 @@ class _RegistrationPageState extends State<RegistrationPage> {
return;
}
// In locked mode with existing data, check for cross-paicha scan
if (_isLocked &&
// In multiCode mode with existing data, check for cross-paicha scan
if (_mode == RegistrationMode.multiCode &&
_zongpaiNos.isNotEmpty &&
_overview?.success == true &&
!_overview!.items.any((item) => item.zongpaiNo == zongpaiNo)) {
@@ -148,7 +212,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
final shouldRefresh = _overviewZongpaiNo != zongpaiNo;
_feedbackService.trigger(FeedbackEvent.scanValid);
setState(() {
if (_isLocked) {
if (_mode == RegistrationMode.multiCode) {
if (!_zongpaiNos.contains(zongpaiNo)) {
_zongpaiNos.add(zongpaiNo);
}
@@ -197,9 +261,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
_statusText = '正在提交…';
return;
}
if (_isLocked && _locationCode != null && _zongpaiNos.isEmpty) {
if (_mode == RegistrationMode.multiCode && _locationCode != null && _zongpaiNos.isEmpty) {
_statusDot = StatusDotColor.blue;
_statusText = '货位已锁定,请扫描下一张执行卡';
_statusText = '多码上架模式,请扫描下一张执行卡';
return;
}
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 (!mounted) return;
setState(() => _isSubmitting = false);
@@ -420,20 +484,20 @@ class _RegistrationPageState extends State<RegistrationPage> {
mode: BoxingMode.singleCode,
codes: [zongpaiNo],
codesToClear: [zongpaiNo],
clearLocation: !_isLocked,
clearLocation: _mode == RegistrationMode.singleCode,
);
return;
}
setState(() {
_zongpaiNos.remove(zongpaiNo);
if (!_isLocked) {
if (_mode == RegistrationMode.singleCode) {
_locationCode = null;
_locationType = null;
}
});
final msg = result.isOffShelfSuccess
? '下架成功'
: (_isLocked ? '货位已锁定,请扫描下一张执行卡' : '上架成功');
: (_mode == RegistrationMode.multiCode ? '多码上架模式,请扫描下一张执行卡' : '上架成功');
_showStatusOverride(
msg,
StatusDotColor.green,
@@ -886,20 +950,18 @@ class _RegistrationPageState extends State<RegistrationPage> {
_loadOverview(newZongpaiNo);
}
void _toggleLock(bool value) {
if (value && _locationCode == null) {
_feedbackService.trigger(FeedbackEvent.submitFailure);
_showStatusOverride(
'请先扫描货位号',
StatusDotColor.red,
const Duration(seconds: 2),
);
return;
}
void _cycleMode() {
_feedbackService.trigger(FeedbackEvent.modeSwitch);
setState(() {
_isLocked = value;
if (!value && _zongpaiNos.length > 1) {
_zongpaiNos.removeRange(0, _zongpaiNos.length - 1);
switch (_mode) {
case RegistrationMode.singleCode:
_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: [
const Text('上架登记'),
const SizedBox(width: 12),
Expanded(child: _buildLockHeader()),
Expanded(child: _buildModePill()),
],
),
titleSpacing: 12,
@@ -948,53 +1010,12 @@ class _RegistrationPageState extends State<RegistrationPage> {
);
}
Widget _buildLockHeader() {
final backgroundColor =
_isLocked ? Colors.orange.shade700 : Colors.blue.shade700;
final label = _isLocked ? '货位锁定' : '未锁定';
return Material(
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,
),
),
],
),
),
),
Widget _buildModePill() {
return BoxingModePill(
isSingle: _mode == RegistrationMode.singleCode,
label: _mode == RegistrationMode.singleCode ? '单码上架' : '多码上架',
enabled: !_isSubmitting,
onTap: _cycleMode,
);
}
@@ -1336,7 +1357,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
style: rowStyle,
),
),
if (isScanned && _isLocked) ...[
if (isScanned && _mode == RegistrationMode.multiCode) ...[
const SizedBox(width: 6),
SizedBox(
width: 28,
@@ -1447,7 +1468,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
: Text(
registrationSubmitLabel(
isTransitTarget: _isTransitTarget,
isLocked: _isLocked,
isMultiCode: _mode == RegistrationMode.multiCode,
zongpaiCount: _zongpaiNos.length,
),
style: const TextStyle(

View File

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