feat(boxing): support v2.5 dual modes
This commit is contained in:
@@ -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),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user