feat(boxing): support v2.5 dual modes

This commit is contained in:
Misaka_Company
2026-05-14 12:29:33 +08:00
parent 077a1ab1b9
commit 28871eb21f
3 changed files with 161 additions and 118 deletions

View File

@@ -11,9 +11,8 @@ import 'package:pad_scanner/widgets/status_bar.dart';
// === 装箱模式 ===
enum BoxingMode {
one2one, // 一码一
one2many, // 一码多箱
many2one, // 多码一箱
singleCode, // 单码装
multiCode, // 多码凑
}
// === 页面阶段 ===
@@ -75,7 +74,7 @@ class _BoxingPageState extends State<BoxingPage> {
final _feedbackService = FeedbackService();
// 模式
BoxingMode _mode = BoxingMode.one2one;
BoxingMode _mode = BoxingMode.singleCode;
// 阶段
_Phase _phase = _Phase.waiting;
@@ -97,7 +96,7 @@ class _BoxingPageState extends State<BoxingPage> {
final _boxNoFocusNode = FocusNode();
final _quantityFocusNode = FocusNode();
// 多码箱:本次操作已确认装入当前箱的明细
// 多码箱:本次操作已确认装入当前箱的明细
final _manyToOnePackedItems = <_ManyToOnePackedItem>[];
int? _editingPackedItemId;
String _editingPackedQuantity = '';
@@ -106,7 +105,7 @@ class _BoxingPageState extends State<BoxingPage> {
// 提交中
bool _isSubmitting = false;
// 一码多箱:上次提交的箱号(用于继续添加预填)
// 单码装箱:上次提交的箱号(用于继续添加预填)
int? _lastBoxNo;
CurrentZongpaiBoxData? _editingBox;
@@ -138,7 +137,7 @@ class _BoxingPageState extends State<BoxingPage> {
}
void _onBoxNoTextChanged() {
if (!mounted || _mode != BoxingMode.many2one) return;
if (!mounted || _mode != BoxingMode.multiCode) return;
setState(() {
_editingPackedItemId = null;
_editingPackedQuantity = '';
@@ -150,12 +149,10 @@ class _BoxingPageState extends State<BoxingPage> {
String get _modeLabel {
switch (_mode) {
case BoxingMode.one2one:
return '一码一';
case BoxingMode.one2many:
return '一码多箱';
case BoxingMode.many2one:
return '多码一箱';
case BoxingMode.singleCode:
return '单码装';
case BoxingMode.multiCode:
return '码凑';
}
}
@@ -163,12 +160,10 @@ class _BoxingPageState extends State<BoxingPage> {
_feedbackService.trigger(FeedbackEvent.modeSwitch);
setState(() {
switch (_mode) {
case BoxingMode.one2one:
_mode = BoxingMode.one2many;
case BoxingMode.one2many:
_mode = BoxingMode.many2one;
case BoxingMode.many2one:
_mode = BoxingMode.one2one;
case BoxingMode.singleCode:
_mode = BoxingMode.multiCode;
case BoxingMode.multiCode:
_mode = BoxingMode.singleCode;
}
_resetState();
});
@@ -273,18 +268,31 @@ class _BoxingPageState extends State<BoxingPage> {
void _applyAutoFill() {
switch (_mode) {
case BoxingMode.one2one:
_boxNoController.text = (_maxBoxNo + 1).toString();
_quantityController.text = (_erpQuantity ?? 0).toString();
case BoxingMode.one2many:
case BoxingMode.singleCode:
final remaining = _remainingQuantity;
if (remaining <= 0) {
_boxNoController.clear();
_quantityController.clear();
_statusOverrideText = '该总排号已全部装箱完毕';
_statusOverrideDot = StatusDotColor.red;
return;
}
if (_lastBoxNo != null) {
_boxNoController.text = (_lastBoxNo! + 1).toString();
_quantityController.clear();
} else {
_boxNoController.text = (_maxBoxNo + 1).toString();
_quantityController.clear();
}
case BoxingMode.many2one:
_quantityController.text = remaining.toString();
_quantityController.selection = TextSelection(
baseOffset: 0,
extentOffset: _quantityController.text.length,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && _mode == BoxingMode.singleCode) {
_quantityFocusNode.requestFocus();
}
});
case BoxingMode.multiCode:
if (_boxNoController.text.trim().isEmpty) {
_boxNoController.text = (_maxBoxNo + 1).toString();
}
@@ -294,41 +302,60 @@ class _BoxingPageState extends State<BoxingPage> {
extentOffset: _quantityController.text.length,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && _mode == BoxingMode.many2one) {
if (mounted && _mode == BoxingMode.multiCode) {
_quantityFocusNode.requestFocus();
}
});
}
_checkDuplicateBoxNo();
_isDuplicateBoxNo = _boxNoIsDuplicate();
}
// === 重复箱号检测 ===
void _checkDuplicateBoxNo() {
bool _boxNoIsDuplicate() {
final boxNo = int.tryParse(_boxNoController.text);
if (boxNo == null) {
setState(() => _isDuplicateBoxNo = false);
return;
return false;
}
final exists = switch (_mode) {
BoxingMode.many2one => false,
BoxingMode.one2one => _existingBoxes.any((b) => b.boxNo == boxNo),
BoxingMode.one2many => _existingBoxes.any((b) {
return switch (_mode) {
BoxingMode.multiCode => false,
BoxingMode.singleCode => _currentZongpaiBoxes.any((b) {
if (b.boxNo != boxNo) return false;
return _editingBox == null || b.boxNo != _editingBox!.boxNo;
return _editingBox == null || b.boxItemId != _editingBox!.boxItemId;
}),
};
setState(() => _isDuplicateBoxNo = exists);
}
void _checkDuplicateBoxNo() {
setState(() => _isDuplicateBoxNo = _boxNoIsDuplicate());
}
// === 提交 ===
int get _packedQuantity {
return _currentZongpaiBoxes.fold<int>(
0,
(sum, item) => sum + item.quantity,
);
}
int get _remainingQuantity {
final total = _erpQuantity ?? 0;
return total - _packedQuantity;
}
bool get _quantityTooHigh {
final qty = int.tryParse(_quantityController.text);
return qty != null && qty > _remainingQuantity;
}
bool get _canSubmit {
if (_isSubmitting || _phase != _Phase.scanned) return false;
if (_zongpaiNo == null) return false;
final boxNo = int.tryParse(_boxNoController.text);
final qty = int.tryParse(_quantityController.text);
if (boxNo == null || qty == null || qty <= 0) return false;
if (boxNo == null || boxNo <= 0 || qty == null || qty <= 0) return false;
if (_quantityTooHigh) return false;
if (_isDuplicateBoxNo) return false;
return true;
}
@@ -386,11 +413,6 @@ class _BoxingPageState extends State<BoxingPage> {
setState(() => _isSubmitting = true);
final modeStr = switch (_mode) {
BoxingMode.one2one => 'one-to-one',
BoxingMode.one2many => 'one-to-many',
BoxingMode.many2one => 'many-to-one',
};
final editing = _editingBox;
final result = editing == null
? await _apiService.saveBoxRecord(
@@ -398,14 +420,12 @@ class _BoxingPageState extends State<BoxingPage> {
zongpaiNo: _zongpaiNo!,
boxNo: boxNo,
quantity: quantity,
boxMode: modeStr,
)
: await _apiService.updateBoxRecord(
baseUrl: baseUrl,
boxItemId: editing.boxItemId,
boxNo: boxNo,
quantity: quantity,
boxMode: modeStr,
);
if (!mounted) return;
@@ -420,19 +440,11 @@ class _BoxingPageState extends State<BoxingPage> {
}
} else if (result.isDuplicate) {
_feedbackService.trigger(FeedbackEvent.duplicateBoxNo);
if (result.errorCode == 'DUPLICATE_ZONGPAI_BIND') {
_showStatusOverride(
'该总排号已绑定箱号 ${result.boxNo ?? "?"},请勿重复装箱',
StatusDotColor.red,
const Duration(seconds: 2),
);
} else {
_showStatusOverride(
'排产号 ${result.paichanNo ?? ""} 下箱号 ${result.boxNo ?? ""} 已存在',
StatusDotColor.amber,
const Duration(seconds: 2),
);
}
_showStatusOverride(
'该总排号在箱号 ${result.boxNo ?? ""} 已存在,请重新输入',
StatusDotColor.amber,
const Duration(seconds: 2),
);
} else {
final isNetwork = result.errorMessage == '网络异常,请检查网络连接';
_feedbackService.trigger(
@@ -451,7 +463,7 @@ class _BoxingPageState extends State<BoxingPage> {
setState(() {
_lastBoxNo = boxNo;
if (_mode == BoxingMode.one2many && boxItemId != null) {
if (_mode == BoxingMode.singleCode && boxItemId != null) {
_currentZongpaiBoxes = List.from(_currentZongpaiBoxes)
..add(
CurrentZongpaiBoxData(
@@ -466,30 +478,36 @@ class _BoxingPageState extends State<BoxingPage> {
});
switch (_mode) {
case BoxingMode.one2one:
_showStatusOverride(
'装箱成功',
StatusDotColor.green,
const Duration(milliseconds: 1500),
);
Future.delayed(const Duration(milliseconds: 1500), () {
if (mounted) setState(() => _resetState());
});
case BoxingMode.singleCode:
final remaining = _remainingQuantity;
if (remaining <= 0) {
_showStatusOverride(
'装箱成功',
StatusDotColor.green,
const Duration(milliseconds: 1500),
);
Future.delayed(const Duration(milliseconds: 1500), () {
if (mounted) setState(() => _resetState());
});
} else {
setState(() {
_phase = _Phase.scanned;
_boxNoController.text = (boxNo + 1).toString();
_quantityController.text = remaining.toString();
_quantityController.selection = TextSelection(
baseOffset: 0,
extentOffset: _quantityController.text.length,
);
_isDuplicateBoxNo = false;
});
_showStatusOverride(
'请继续完成剩余数量装箱',
StatusDotColor.green,
const Duration(milliseconds: 1500),
);
}
case BoxingMode.one2many:
setState(() {
_phase = _Phase.scanned;
_boxNoController.text = (boxNo + 1).toString();
_quantityController.clear();
_isDuplicateBoxNo = false;
});
_showStatusOverride(
'装箱成功,可继续添加',
StatusDotColor.green,
const Duration(milliseconds: 1500),
);
case BoxingMode.many2one:
case BoxingMode.multiCode:
setState(() {
if (boxItemId != null) {
_manyToOnePackedItems.add(
@@ -538,7 +556,12 @@ class _BoxingPageState extends State<BoxingPage> {
_editingBox = null;
_lastBoxNo = _maxBoxNo;
_boxNoController.text = (_maxBoxNo + 1).toString();
_quantityController.clear();
final remaining = _remainingQuantity;
if (remaining > 0) {
_quantityController.text = remaining.toString();
} else {
_quantityController.clear();
}
_isDuplicateBoxNo = false;
});
_showStatusOverride(
@@ -699,7 +722,6 @@ class _BoxingPageState extends State<BoxingPage> {
boxItemId: item.boxItemId,
boxNo: boxNo,
quantity: quantity,
boxMode: 'many-to-one',
);
if (!mounted) return;
@@ -886,10 +908,15 @@ class _BoxingPageState extends State<BoxingPage> {
_statusText = '箱号 ${_boxNoController.text} 已存在,请重新输入';
return;
}
if (_quantityTooHigh && _phase == _Phase.scanned) {
_statusDot = StatusDotColor.red;
_statusText = '超出可装数量上限';
return;
}
switch (_phase) {
case _Phase.waiting:
_statusDot = StatusDotColor.blue;
if (_mode == BoxingMode.many2one &&
if (_mode == BoxingMode.multiCode &&
_visibleManyToOnePackedItems.isNotEmpty) {
_statusText = '请继续扫码或完成本箱';
} else {
@@ -897,18 +924,21 @@ class _BoxingPageState extends State<BoxingPage> {
}
case _Phase.scanned:
_statusDot = StatusDotColor.blue;
_statusText = _mode == BoxingMode.many2one
if (_remainingQuantity <= 0) {
_statusDot = StatusDotColor.red;
_statusText = '该总排号已全部装箱完毕';
return;
}
_statusText = _mode == BoxingMode.multiCode
? '数量已填入,请确认或修改'
: '请确认信息并提交';
: '数量已填入,请确认或修改';
case _Phase.submitted:
_statusDot = StatusDotColor.green;
switch (_mode) {
case BoxingMode.one2many:
case BoxingMode.singleCode:
_statusText = '装箱成功,可继续添加或返回';
case BoxingMode.many2one:
case BoxingMode.multiCode:
_statusText = '请扫描下一个总排号';
case BoxingMode.one2one:
_statusText = '';
}
}
}
@@ -920,7 +950,7 @@ class _BoxingPageState extends State<BoxingPage> {
final colorScheme = Theme.of(context).colorScheme;
final isWaiting = _phase == _Phase.waiting && _zongpaiNo == null;
final showActionButtons =
_phase == _Phase.scanned && _mode == BoxingMode.one2many;
_phase == _Phase.scanned && _mode == BoxingMode.singleCode;
_updateBaseStatus();
final effectiveDot = _statusOverrideDot ?? _statusDot;
@@ -937,7 +967,7 @@ class _BoxingPageState extends State<BoxingPage> {
icon: const Icon(Icons.swap_horiz, size: 18),
label: Text(_modeLabel, style: const TextStyle(fontSize: 13)),
style: TextButton.styleFrom(
foregroundColor: _mode == BoxingMode.many2one
foregroundColor: _mode == BoxingMode.multiCode
? Colors.orange
: colorScheme.primary,
),
@@ -960,7 +990,7 @@ class _BoxingPageState extends State<BoxingPage> {
),
Expanded(
child: _mode == BoxingMode.many2one
child: _mode == BoxingMode.multiCode
? _buildManyToOneBody(colorScheme)
: SingleChildScrollView(
padding: const EdgeInsets.all(12),
@@ -971,7 +1001,7 @@ class _BoxingPageState extends State<BoxingPage> {
const SizedBox(height: 12),
_buildInfoArea(isWaiting, colorScheme),
const Divider(height: 24),
if (_mode == BoxingMode.one2many &&
if (_mode == BoxingMode.singleCode &&
!isWaiting &&
_currentZongpaiBoxes.isNotEmpty) ...[
_buildAssignedBoxes(),
@@ -983,7 +1013,7 @@ class _BoxingPageState extends State<BoxingPage> {
),
),
// 操作按钮(一码多箱 / 多码箱)
// 操作按钮(单码装箱 / 多码箱)
if (showActionButtons)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
@@ -998,11 +1028,13 @@ class _BoxingPageState extends State<BoxingPage> {
child: const Text('返回'),
),
),
if (_mode == BoxingMode.one2many && _editingBox == null) ...[
if (_mode == BoxingMode.singleCode &&
_editingBox == null) ...[
const SizedBox(width: 12),
const Expanded(child: SizedBox.shrink()),
],
if (_mode == BoxingMode.one2many && _editingBox != null) ...[
if (_mode == BoxingMode.singleCode &&
_editingBox != null) ...[
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
@@ -1018,7 +1050,7 @@ class _BoxingPageState extends State<BoxingPage> {
),
),
if (_mode == BoxingMode.many2one)
if (_mode == BoxingMode.multiCode)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: OutlinedButton(
@@ -1399,9 +1431,16 @@ class _BoxingPageState extends State<BoxingPage> {
keyboardType: TextInputType.none,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (_) => setState(() {}),
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 10),
border: OutlineInputBorder(),
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
border: const OutlineInputBorder(),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: _quantityTooHigh
? Colors.red
: Colors.grey.shade400,
),
),
),
),
),
@@ -1681,9 +1720,16 @@ class _BoxingPageState extends State<BoxingPage> {
keyboardType: TextInputType.none,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (_) => setState(() {}),
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 10),
border: OutlineInputBorder(),
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
border: const OutlineInputBorder(),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: _quantityTooHigh
? Colors.red
: Colors.grey.shade400,
),
),
),
style: const TextStyle(fontSize: 16),
),