feat(boxing): update many-to-one packing flow
This commit is contained in:
@@ -24,6 +24,42 @@ enum _Phase {
|
|||||||
submitted, // 已提交成功
|
submitted, // 已提交成功
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ManyToOnePackedItem {
|
||||||
|
final int boxItemId;
|
||||||
|
final String zongpaiNo;
|
||||||
|
final String? paichanNo;
|
||||||
|
final String? workOrderNo;
|
||||||
|
final int boxNo;
|
||||||
|
final int quantity;
|
||||||
|
final int? totalQuantity;
|
||||||
|
|
||||||
|
const _ManyToOnePackedItem({
|
||||||
|
required this.boxItemId,
|
||||||
|
required this.zongpaiNo,
|
||||||
|
this.paichanNo,
|
||||||
|
required this.workOrderNo,
|
||||||
|
required this.boxNo,
|
||||||
|
required this.quantity,
|
||||||
|
this.totalQuantity,
|
||||||
|
});
|
||||||
|
|
||||||
|
_ManyToOnePackedItem copyWith({
|
||||||
|
int? boxNo,
|
||||||
|
int? quantity,
|
||||||
|
int? totalQuantity,
|
||||||
|
}) {
|
||||||
|
return _ManyToOnePackedItem(
|
||||||
|
boxItemId: boxItemId,
|
||||||
|
zongpaiNo: zongpaiNo,
|
||||||
|
paichanNo: paichanNo,
|
||||||
|
workOrderNo: workOrderNo,
|
||||||
|
boxNo: boxNo ?? this.boxNo,
|
||||||
|
quantity: quantity ?? this.quantity,
|
||||||
|
totalQuantity: totalQuantity ?? this.totalQuantity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// === 主页面 ===
|
// === 主页面 ===
|
||||||
|
|
||||||
class BoxingPage extends StatefulWidget {
|
class BoxingPage extends StatefulWidget {
|
||||||
@@ -61,11 +97,11 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
final _boxNoFocusNode = FocusNode();
|
final _boxNoFocusNode = FocusNode();
|
||||||
final _quantityFocusNode = FocusNode();
|
final _quantityFocusNode = FocusNode();
|
||||||
|
|
||||||
// 多码一箱:已扫描过的总排号列表(用于显示)
|
// 多码一箱:本次操作已确认装入当前箱的明细
|
||||||
final _scannedZongpais = <String>[];
|
final _manyToOnePackedItems = <_ManyToOnePackedItem>[];
|
||||||
|
int? _editingPackedItemId;
|
||||||
// 箱号锁定(多码一箱模式)
|
String _editingPackedQuantity = '';
|
||||||
bool _boxNoLocked = false;
|
int? _deletingPackedItemId;
|
||||||
|
|
||||||
// 提交中
|
// 提交中
|
||||||
bool _isSubmitting = false;
|
bool _isSubmitting = false;
|
||||||
@@ -86,11 +122,13 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_boxNoController.addListener(_onBoxNoTextChanged);
|
||||||
_scannerService.scanResults.listen(_onScan);
|
_scannerService.scanResults.listen(_onScan);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_boxNoController.removeListener(_onBoxNoTextChanged);
|
||||||
_boxNoController.dispose();
|
_boxNoController.dispose();
|
||||||
_quantityController.dispose();
|
_quantityController.dispose();
|
||||||
_boxNoFocusNode.dispose();
|
_boxNoFocusNode.dispose();
|
||||||
@@ -99,6 +137,15 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onBoxNoTextChanged() {
|
||||||
|
if (!mounted || _mode != BoxingMode.many2one) return;
|
||||||
|
setState(() {
|
||||||
|
_editingPackedItemId = null;
|
||||||
|
_editingPackedQuantity = '';
|
||||||
|
_deletingPackedItemId = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// === 模式切换 ===
|
// === 模式切换 ===
|
||||||
|
|
||||||
String get _modeLabel {
|
String get _modeLabel {
|
||||||
@@ -138,8 +185,10 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
_maxBoxNo = 0;
|
_maxBoxNo = 0;
|
||||||
_boxNoController.clear();
|
_boxNoController.clear();
|
||||||
_quantityController.clear();
|
_quantityController.clear();
|
||||||
_scannedZongpais.clear();
|
_manyToOnePackedItems.clear();
|
||||||
_boxNoLocked = false;
|
_editingPackedItemId = null;
|
||||||
|
_editingPackedQuantity = '';
|
||||||
|
_deletingPackedItemId = null;
|
||||||
_isSubmitting = false;
|
_isSubmitting = false;
|
||||||
_lastBoxNo = null;
|
_lastBoxNo = null;
|
||||||
_editingBox = null;
|
_editingBox = null;
|
||||||
@@ -165,26 +214,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
|
|
||||||
final zongpai = parsed.value;
|
final zongpai = parsed.value;
|
||||||
|
|
||||||
// 多码一箱已提交后:等待下一个扫码
|
// 三种模式都允许后扫入的总排号覆盖当前扫码区;未确认数据不会入库。
|
||||||
if (_mode == BoxingMode.many2one && _phase == _Phase.submitted) {
|
|
||||||
_handleNextScanMany2One(zongpai);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 一码一箱和一码多箱:允许覆盖,直接查询新二维码(不提前清空状态,避免闪烁)
|
|
||||||
if (_mode == BoxingMode.many2one && _phase != _Phase.waiting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_queryBoxInfo(zongpai);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleNextScanMany2One(String zongpai) {
|
|
||||||
setState(() {
|
|
||||||
_phase = _Phase.waiting;
|
|
||||||
_zongpaiNo = null;
|
|
||||||
_statusOverrideText = null;
|
|
||||||
});
|
|
||||||
_queryBoxInfo(zongpai);
|
_queryBoxInfo(zongpai);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,11 +266,6 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
_editingBox = null;
|
_editingBox = null;
|
||||||
_statusOverrideText = null;
|
_statusOverrideText = null;
|
||||||
|
|
||||||
// 多码一箱:记录已扫描列表
|
|
||||||
if (_mode == BoxingMode.many2one && !_scannedZongpais.contains(zongpai)) {
|
|
||||||
_scannedZongpais.add(zongpai);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据模式自动填充
|
// 根据模式自动填充
|
||||||
_applyAutoFill();
|
_applyAutoFill();
|
||||||
});
|
});
|
||||||
@@ -260,11 +285,19 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
_quantityController.clear();
|
_quantityController.clear();
|
||||||
}
|
}
|
||||||
case BoxingMode.many2one:
|
case BoxingMode.many2one:
|
||||||
if (!_boxNoLocked) {
|
if (_boxNoController.text.trim().isEmpty) {
|
||||||
_boxNoController.text = (_maxBoxNo + 1).toString();
|
_boxNoController.text = (_maxBoxNo + 1).toString();
|
||||||
_boxNoLocked = true;
|
|
||||||
}
|
}
|
||||||
_quantityController.text = (_erpQuantity ?? 0).toString();
|
_quantityController.text = (_erpQuantity ?? 0).toString();
|
||||||
|
_quantityController.selection = TextSelection(
|
||||||
|
baseOffset: 0,
|
||||||
|
extentOffset: _quantityController.text.length,
|
||||||
|
);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted && _mode == BoxingMode.many2one) {
|
||||||
|
_quantityFocusNode.requestFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
_checkDuplicateBoxNo();
|
_checkDuplicateBoxNo();
|
||||||
}
|
}
|
||||||
@@ -300,6 +333,39 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int? get _currentManyToOneBoxNo {
|
||||||
|
final boxNo = int.tryParse(_boxNoController.text.trim());
|
||||||
|
if (boxNo == null || boxNo <= 0) return null;
|
||||||
|
return boxNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<_ManyToOnePackedItem> get _visibleManyToOnePackedItems {
|
||||||
|
final boxNo = _currentManyToOneBoxNo;
|
||||||
|
if (boxNo == null) return const [];
|
||||||
|
final byItemId = <int, _ManyToOnePackedItem>{};
|
||||||
|
for (final box in _existingBoxes.where((box) => box.boxNo == boxNo)) {
|
||||||
|
for (final item in box.items) {
|
||||||
|
final boxItemId = item.boxItemId;
|
||||||
|
if (boxItemId == null) continue;
|
||||||
|
byItemId[boxItemId] = _ManyToOnePackedItem(
|
||||||
|
boxItemId: boxItemId,
|
||||||
|
zongpaiNo: item.zongpaiNo,
|
||||||
|
paichanNo: _paichanNo,
|
||||||
|
workOrderNo: item.workOrderNo,
|
||||||
|
boxNo: boxNo,
|
||||||
|
quantity: item.quantity,
|
||||||
|
totalQuantity: item.totalQuantity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final item in _manyToOnePackedItems.where(
|
||||||
|
(item) => item.boxNo == boxNo,
|
||||||
|
)) {
|
||||||
|
byItemId[item.boxItemId] = item;
|
||||||
|
}
|
||||||
|
return byItemId.values.toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _submit() async {
|
Future<void> _submit() async {
|
||||||
if (!_canSubmit) return;
|
if (!_canSubmit) return;
|
||||||
|
|
||||||
@@ -339,6 +405,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
boxItemId: editing.boxItemId,
|
boxItemId: editing.boxItemId,
|
||||||
boxNo: boxNo,
|
boxNo: boxNo,
|
||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
|
boxMode: modeStr,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -353,7 +420,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
}
|
}
|
||||||
} else if (result.isDuplicate) {
|
} else if (result.isDuplicate) {
|
||||||
_feedbackService.trigger(FeedbackEvent.duplicateBoxNo);
|
_feedbackService.trigger(FeedbackEvent.duplicateBoxNo);
|
||||||
if (_mode == BoxingMode.one2one) {
|
if (result.errorCode == 'DUPLICATE_ZONGPAI_BIND') {
|
||||||
_showStatusOverride(
|
_showStatusOverride(
|
||||||
'该总排号已绑定箱号 ${result.boxNo ?? "?"},请勿重复装箱',
|
'该总排号已绑定箱号 ${result.boxNo ?? "?"},请勿重复装箱',
|
||||||
StatusDotColor.red,
|
StatusDotColor.red,
|
||||||
@@ -424,11 +491,27 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
|
|
||||||
case BoxingMode.many2one:
|
case BoxingMode.many2one:
|
||||||
setState(() {
|
setState(() {
|
||||||
_phase = _Phase.submitted;
|
if (boxItemId != null) {
|
||||||
|
_manyToOnePackedItems.add(
|
||||||
|
_ManyToOnePackedItem(
|
||||||
|
boxItemId: boxItemId,
|
||||||
|
zongpaiNo: _zongpaiNo!,
|
||||||
|
paichanNo: _paichanNo,
|
||||||
|
workOrderNo: _workOrderNo,
|
||||||
|
boxNo: boxNo,
|
||||||
|
quantity: quantity,
|
||||||
|
totalQuantity: _erpQuantity,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_phase = _Phase.waiting;
|
||||||
_zongpaiNo = null;
|
_zongpaiNo = null;
|
||||||
|
_editingPackedItemId = null;
|
||||||
|
_editingPackedQuantity = '';
|
||||||
|
_deletingPackedItemId = null;
|
||||||
});
|
});
|
||||||
_showStatusOverride(
|
_showStatusOverride(
|
||||||
'装箱成功,请扫描下一个总排号',
|
'装箱成功,请继续扫码',
|
||||||
StatusDotColor.green,
|
StatusDotColor.green,
|
||||||
const Duration(milliseconds: 1500),
|
const Duration(milliseconds: 1500),
|
||||||
);
|
);
|
||||||
@@ -486,6 +569,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
zongpaiNo: _zongpaiNo!,
|
zongpaiNo: _zongpaiNo!,
|
||||||
workOrderNo: _workOrderNo,
|
workOrderNo: _workOrderNo,
|
||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
|
totalQuantity: _erpQuantity,
|
||||||
);
|
);
|
||||||
if (targetIndex >= 0) {
|
if (targetIndex >= 0) {
|
||||||
final target = nextBoxes[targetIndex];
|
final target = nextBoxes[targetIndex];
|
||||||
@@ -510,6 +594,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
zongpaiNo: _zongpaiNo!,
|
zongpaiNo: _zongpaiNo!,
|
||||||
workOrderNo: _workOrderNo,
|
workOrderNo: _workOrderNo,
|
||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
|
totalQuantity: _erpQuantity,
|
||||||
);
|
);
|
||||||
final index = _existingBoxes.indexWhere((box) => box.boxNo == boxNo);
|
final index = _existingBoxes.indexWhere((box) => box.boxNo == boxNo);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
@@ -532,6 +617,23 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
setState(() => _resetState());
|
setState(() => _resetState());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _finishManyToOneBox() {
|
||||||
|
final currentBoxNo = int.tryParse(_boxNoController.text.trim());
|
||||||
|
setState(() {
|
||||||
|
_editingPackedItemId = null;
|
||||||
|
_editingPackedQuantity = '';
|
||||||
|
_deletingPackedItemId = null;
|
||||||
|
_zongpaiNo = null;
|
||||||
|
_phase = _Phase.waiting;
|
||||||
|
_quantityController.clear();
|
||||||
|
if (currentBoxNo != null && currentBoxNo > 0) {
|
||||||
|
_boxNoController.text = (currentBoxNo + 1).toString();
|
||||||
|
}
|
||||||
|
_statusOverrideText = null;
|
||||||
|
_statusOverrideDot = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _startEditAssigned(CurrentZongpaiBoxData item) {
|
void _startEditAssigned(CurrentZongpaiBoxData item) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_editingBox = item;
|
_editingBox = item;
|
||||||
@@ -553,7 +655,128 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _deleteAssigned(CurrentZongpaiBoxData item) async {
|
void _startEditPackedItem(_ManyToOnePackedItem item) {
|
||||||
|
setState(() {
|
||||||
|
_editingPackedItemId = item.boxItemId;
|
||||||
|
_editingPackedQuantity = item.quantity.toString();
|
||||||
|
_deletingPackedItemId = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cancelEditPackedItem() {
|
||||||
|
setState(() {
|
||||||
|
_editingPackedItemId = null;
|
||||||
|
_editingPackedQuantity = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _savePackedItem(_ManyToOnePackedItem item) async {
|
||||||
|
final quantity = int.tryParse(_editingPackedQuantity);
|
||||||
|
final boxNo = int.tryParse(_boxNoController.text.trim());
|
||||||
|
if (quantity == null || quantity <= 0 || boxNo == null || boxNo <= 0) {
|
||||||
|
_showStatusOverride(
|
||||||
|
'请输入有效箱号和数量',
|
||||||
|
StatusDotColor.red,
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final configService = AppConfigService();
|
||||||
|
final baseUrl = await configService.getString('api_url') ?? '';
|
||||||
|
if (baseUrl.isEmpty) {
|
||||||
|
_showStatusOverride(
|
||||||
|
'未配置 API 地址,请前往设置',
|
||||||
|
StatusDotColor.red,
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _isSubmitting = true);
|
||||||
|
final result = await _apiService.updateBoxRecord(
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
boxItemId: item.boxItemId,
|
||||||
|
boxNo: boxNo,
|
||||||
|
quantity: quantity,
|
||||||
|
boxMode: 'many-to-one',
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() => _isSubmitting = false);
|
||||||
|
if (result.success) {
|
||||||
|
setState(() {
|
||||||
|
final index = _manyToOnePackedItems.indexWhere(
|
||||||
|
(packed) => packed.boxItemId == item.boxItemId,
|
||||||
|
);
|
||||||
|
if (index >= 0) {
|
||||||
|
_manyToOnePackedItems[index] = _manyToOnePackedItems[index].copyWith(
|
||||||
|
boxNo: boxNo,
|
||||||
|
quantity: quantity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_replaceExistingPackedItem(item, boxNo, quantity);
|
||||||
|
_editingPackedItemId = null;
|
||||||
|
_editingPackedQuantity = '';
|
||||||
|
});
|
||||||
|
_showStatusOverride(
|
||||||
|
'修改成功',
|
||||||
|
StatusDotColor.green,
|
||||||
|
const Duration(milliseconds: 1500),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final dot = result.isDuplicate
|
||||||
|
? StatusDotColor.amber
|
||||||
|
: StatusDotColor.red;
|
||||||
|
_showStatusOverride(
|
||||||
|
result.errorMessage ?? '修改失败',
|
||||||
|
dot,
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _replaceExistingPackedItem(
|
||||||
|
_ManyToOnePackedItem item,
|
||||||
|
int boxNo,
|
||||||
|
int quantity,
|
||||||
|
) {
|
||||||
|
final nextBoxes = <BoxDetailData>[];
|
||||||
|
for (final box in _existingBoxes) {
|
||||||
|
final items = box.items.where((boxItem) {
|
||||||
|
return boxItem.boxItemId != item.boxItemId;
|
||||||
|
}).toList();
|
||||||
|
if (items.isNotEmpty) {
|
||||||
|
nextBoxes.add(BoxDetailData(boxNo: box.boxNo, items: items));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final updatedItem = BoxItemData(
|
||||||
|
boxItemId: item.boxItemId,
|
||||||
|
zongpaiNo: item.zongpaiNo,
|
||||||
|
workOrderNo: item.workOrderNo,
|
||||||
|
quantity: quantity,
|
||||||
|
totalQuantity: item.totalQuantity,
|
||||||
|
);
|
||||||
|
final targetIndex = nextBoxes.indexWhere((box) => box.boxNo == boxNo);
|
||||||
|
if (targetIndex >= 0) {
|
||||||
|
final target = nextBoxes[targetIndex];
|
||||||
|
nextBoxes[targetIndex] = BoxDetailData(
|
||||||
|
boxNo: target.boxNo,
|
||||||
|
items: [...target.items, updatedItem],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
nextBoxes.add(BoxDetailData(boxNo: boxNo, items: [updatedItem]));
|
||||||
|
}
|
||||||
|
nextBoxes.sort((a, b) => a.boxNo.compareTo(b.boxNo));
|
||||||
|
_existingBoxes = nextBoxes;
|
||||||
|
_maxBoxNo = nextBoxes.fold<int>(
|
||||||
|
0,
|
||||||
|
(max, box) => box.boxNo > max ? box.boxNo : max,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deletePackedItem(_ManyToOnePackedItem item) async {
|
||||||
final configService = AppConfigService();
|
final configService = AppConfigService();
|
||||||
final baseUrl = await configService.getString('api_url') ?? '';
|
final baseUrl = await configService.getString('api_url') ?? '';
|
||||||
if (baseUrl.isEmpty) {
|
if (baseUrl.isEmpty) {
|
||||||
@@ -575,17 +798,15 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_isSubmitting = false;
|
_isSubmitting = false;
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
_currentZongpaiBoxes = _currentZongpaiBoxes
|
_manyToOnePackedItems.removeWhere(
|
||||||
.where((box) => box.boxItemId != item.boxItemId)
|
(packed) => packed.boxItemId == item.boxItemId,
|
||||||
.toList();
|
);
|
||||||
_removeExistingBoxItem(item.boxItemId);
|
_removeExistingBoxItem(item.boxItemId);
|
||||||
if (_editingBox?.boxItemId == item.boxItemId) {
|
if (_editingPackedItemId == item.boxItemId) {
|
||||||
_editingBox = null;
|
_editingPackedItemId = null;
|
||||||
|
_editingPackedQuantity = '';
|
||||||
}
|
}
|
||||||
_lastBoxNo = _maxBoxNo;
|
_deletingPackedItemId = null;
|
||||||
_boxNoController.text = (_maxBoxNo + 1).toString();
|
|
||||||
_quantityController.clear();
|
|
||||||
_isDuplicateBoxNo = false;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -668,14 +889,17 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
switch (_phase) {
|
switch (_phase) {
|
||||||
case _Phase.waiting:
|
case _Phase.waiting:
|
||||||
_statusDot = StatusDotColor.blue;
|
_statusDot = StatusDotColor.blue;
|
||||||
if (_mode == BoxingMode.many2one && _boxNoLocked) {
|
if (_mode == BoxingMode.many2one &&
|
||||||
_statusText = '请扫描下一个总排号';
|
_visibleManyToOnePackedItems.isNotEmpty) {
|
||||||
|
_statusText = '请继续扫码或完成本箱';
|
||||||
} else {
|
} else {
|
||||||
_statusText = '等待扫码';
|
_statusText = '等待扫码';
|
||||||
}
|
}
|
||||||
case _Phase.scanned:
|
case _Phase.scanned:
|
||||||
_statusDot = StatusDotColor.blue;
|
_statusDot = StatusDotColor.blue;
|
||||||
_statusText = '请确认信息并提交';
|
_statusText = _mode == BoxingMode.many2one
|
||||||
|
? '数量已填入,请确认或修改'
|
||||||
|
: '请确认信息并提交';
|
||||||
case _Phase.submitted:
|
case _Phase.submitted:
|
||||||
_statusDot = StatusDotColor.green;
|
_statusDot = StatusDotColor.green;
|
||||||
switch (_mode) {
|
switch (_mode) {
|
||||||
@@ -696,8 +920,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final isWaiting = _phase == _Phase.waiting && _zongpaiNo == null;
|
final isWaiting = _phase == _Phase.waiting && _zongpaiNo == null;
|
||||||
final showActionButtons =
|
final showActionButtons =
|
||||||
(_phase == _Phase.submitted && _mode == BoxingMode.many2one) ||
|
_phase == _Phase.scanned && _mode == BoxingMode.one2many;
|
||||||
(_phase == _Phase.scanned && _mode == BoxingMode.one2many);
|
|
||||||
|
|
||||||
_updateBaseStatus();
|
_updateBaseStatus();
|
||||||
final effectiveDot = _statusOverrideDot ?? _statusDot;
|
final effectiveDot = _statusOverrideDot ?? _statusDot;
|
||||||
@@ -736,35 +959,28 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 主内容区
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: _mode == BoxingMode.many2one
|
||||||
padding: const EdgeInsets.all(12),
|
? _buildManyToOneBody(colorScheme)
|
||||||
child: Column(
|
: SingleChildScrollView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: const EdgeInsets.all(12),
|
||||||
children: [
|
child: Column(
|
||||||
// === 扫码区 ===
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
_buildScanArea(isWaiting),
|
children: [
|
||||||
|
_buildScanArea(isWaiting),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
_buildInfoArea(isWaiting, colorScheme),
|
||||||
// === 信息区 ===
|
const Divider(height: 24),
|
||||||
_buildInfoArea(isWaiting, colorScheme),
|
if (_mode == BoxingMode.one2many &&
|
||||||
|
!isWaiting &&
|
||||||
const Divider(height: 24),
|
_currentZongpaiBoxes.isNotEmpty) ...[
|
||||||
|
_buildAssignedBoxes(),
|
||||||
if (_mode == BoxingMode.one2many &&
|
const Divider(height: 24),
|
||||||
!isWaiting &&
|
],
|
||||||
_currentZongpaiBoxes.isNotEmpty) ...[
|
_buildInputArea(isWaiting),
|
||||||
_buildAssignedBoxes(),
|
],
|
||||||
const Divider(height: 24),
|
),
|
||||||
],
|
),
|
||||||
|
|
||||||
// === 输入区 ===
|
|
||||||
_buildInputArea(isWaiting),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// 操作按钮(一码多箱 / 多码一箱)
|
// 操作按钮(一码多箱 / 多码一箱)
|
||||||
@@ -802,6 +1018,20 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
if (_mode == BoxingMode.many2one)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: _visibleManyToOnePackedItems.isEmpty
|
||||||
|
? null
|
||||||
|
: _finishManyToOneBox,
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
minimumSize: const Size.fromHeight(40),
|
||||||
|
),
|
||||||
|
child: const Text('完成本箱'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// 底部状态栏
|
// 底部状态栏
|
||||||
StatusBar(dotColor: effectiveDot, text: effectiveText),
|
StatusBar(dotColor: effectiveDot, text: effectiveText),
|
||||||
],
|
],
|
||||||
@@ -849,14 +1079,6 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
icon: const Icon(Icons.edit, size: 18),
|
icon: const Icon(Icons.edit, size: 18),
|
||||||
onPressed: () => _startEditAssigned(item),
|
onPressed: () => _startEditAssigned(item),
|
||||||
),
|
),
|
||||||
IconButton(
|
|
||||||
tooltip: '删除',
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
icon: const Icon(Icons.delete_outline, size: 18),
|
|
||||||
onPressed: _isSubmitting
|
|
||||||
? null
|
|
||||||
: () => _deleteAssigned(item),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -866,6 +1088,353 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildManyToOneBody(ColorScheme colorScheme) {
|
||||||
|
final hasScan = _zongpaiNo != null && _phase == _Phase.scanned;
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildManyToOneBoxNoPanel(),
|
||||||
|
if (_paichanNo != null) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildManyToOneInfoRow(colorScheme),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildManyToOnePackedList(),
|
||||||
|
const Divider(height: 24),
|
||||||
|
_buildManyToOneScanArea(hasScan),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_buildManyToOneSubmitArea(hasScan),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildManyToOneBoxNoPanel() {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'当前箱号',
|
||||||
|
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
SizedBox(
|
||||||
|
width: 96,
|
||||||
|
height: 44,
|
||||||
|
child: TextField(
|
||||||
|
controller: _boxNoController,
|
||||||
|
focusNode: _boxNoFocusNode,
|
||||||
|
enabled: !_isSubmitting,
|
||||||
|
keyboardType: TextInputType.none,
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Divider(height: 1, color: Colors.grey.shade300),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildManyToOneInfoRow(ColorScheme colorScheme) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'${_paichanNo ?? "--"} 已有${_existingBoxes.length}箱 最大箱号$_maxBoxNo',
|
||||||
|
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _existingBoxes.isEmpty ? null : _openDetail,
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
minimumSize: Size.zero,
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'详情 →',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: _existingBoxes.isEmpty
|
||||||
|
? Colors.grey.shade400
|
||||||
|
: colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildManyToOnePackedList() {
|
||||||
|
final visibleItems = _visibleManyToOnePackedItems;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'已装入本箱(${visibleItems.length}项)',
|
||||||
|
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
if (visibleItems.isEmpty)
|
||||||
|
Text(
|
||||||
|
'(空)',
|
||||||
|
style: TextStyle(fontSize: 13, color: Colors.grey.shade600),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
...visibleItems.map(_buildManyToOnePackedRow),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildManyToOnePackedRow(_ManyToOnePackedItem item) {
|
||||||
|
final editing = _editingPackedItemId == item.boxItemId;
|
||||||
|
final deleting = _deletingPackedItemId == item.boxItemId;
|
||||||
|
final totalText = item.totalQuantity?.toString() ?? '--';
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 6),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: editing ? Colors.orange.shade50 : Colors.grey.shade50,
|
||||||
|
border: Border.all(
|
||||||
|
color: editing ? Colors.orange : Colors.grey.shade300,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'${item.zongpaiNo} ${item.workOrderNo ?? "--"}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (editing)
|
||||||
|
SizedBox(
|
||||||
|
width: 56,
|
||||||
|
height: 32,
|
||||||
|
child: TextField(
|
||||||
|
keyboardType: TextInputType.none,
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
controller:
|
||||||
|
TextEditingController(text: _editingPackedQuantity)
|
||||||
|
..selection = TextSelection.collapsed(
|
||||||
|
offset: _editingPackedQuantity.length,
|
||||||
|
),
|
||||||
|
onChanged: (value) => _editingPackedQuantity = value,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Text(
|
||||||
|
'${item.quantity}/$totalText',
|
||||||
|
style: const TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
if (editing) ...[
|
||||||
|
IconButton(
|
||||||
|
tooltip: '保存',
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
icon: const Icon(Icons.check, size: 18),
|
||||||
|
onPressed: _isSubmitting ? null : () => _savePackedItem(item),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: '取消',
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
icon: const Icon(Icons.close, size: 18),
|
||||||
|
onPressed: _isSubmitting ? null : _cancelEditPackedItem,
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
IconButton(
|
||||||
|
tooltip: '修改',
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
icon: const Icon(Icons.edit, size: 18),
|
||||||
|
onPressed: _isSubmitting
|
||||||
|
? null
|
||||||
|
: () => _startEditPackedItem(item),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: '删除',
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
icon: const Icon(Icons.delete_outline, size: 18),
|
||||||
|
onPressed: _isSubmitting
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
setState(() {
|
||||||
|
_deletingPackedItemId = item.boxItemId;
|
||||||
|
_editingPackedItemId = null;
|
||||||
|
_editingPackedQuantity = '';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (deleting)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(
|
||||||
|
child: Text('确认删除此条记录?', style: TextStyle(fontSize: 13)),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isSubmitting
|
||||||
|
? null
|
||||||
|
: () => _deletePackedItem(item),
|
||||||
|
child: const Text('是'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isSubmitting
|
||||||
|
? null
|
||||||
|
: () => setState(() => _deletingPackedItemId = null),
|
||||||
|
child: const Text('否'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildManyToOneScanArea(bool hasScan) {
|
||||||
|
if (!hasScan) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.qr_code_scanner, color: Colors.grey, size: 20),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'请扫描执行卡二维码',
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 14),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: Colors.green, width: 1.5),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.check_circle, color: Colors.green, size: 18),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'${_zongpaiNo ?? ""} ${_workOrderNo ?? "--"} ${_erpQuantity ?? "--"}件',
|
||||||
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildManyToOneSubmitArea(bool hasScan) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'数量',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: hasScan ? Colors.black54 : Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
SizedBox(
|
||||||
|
height: 40,
|
||||||
|
child: TextField(
|
||||||
|
controller: _quantityController,
|
||||||
|
focusNode: _quantityFocusNode,
|
||||||
|
enabled: hasScan && !_isSubmitting,
|
||||||
|
keyboardType: TextInputType.none,
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
|
onChanged: (_) => setState(() {}),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
SizedBox(
|
||||||
|
height: 40,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _canSubmit ? _submit : null,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: _canSubmit
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Colors.grey.shade300,
|
||||||
|
foregroundColor: _canSubmit ? Colors.white : Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
child: _isSubmitting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Text('确认', style: TextStyle(fontSize: 15)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// === 扫码区 Widget ===
|
// === 扫码区 Widget ===
|
||||||
|
|
||||||
Widget _buildScanArea(bool isWaiting) {
|
Widget _buildScanArea(bool isWaiting) {
|
||||||
@@ -891,25 +1460,6 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_mode == BoxingMode.many2one && _scannedZongpais.isNotEmpty) {
|
|
||||||
return Wrap(
|
|
||||||
spacing: 6,
|
|
||||||
runSpacing: 4,
|
|
||||||
children: _scannedZongpais.map((zp) {
|
|
||||||
final isCurrent = zp == _zongpaiNo;
|
|
||||||
return Chip(
|
|
||||||
avatar: Icon(
|
|
||||||
Icons.check_circle,
|
|
||||||
size: 16,
|
|
||||||
color: isCurrent ? Colors.green : Colors.grey,
|
|
||||||
),
|
|
||||||
label: Text(zp, style: const TextStyle(fontSize: 13)),
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||||
@@ -1086,7 +1636,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _boxNoController,
|
controller: _boxNoController,
|
||||||
focusNode: _boxNoFocusNode,
|
focusNode: _boxNoFocusNode,
|
||||||
enabled: enabled && !_boxNoLocked,
|
enabled: enabled,
|
||||||
keyboardType: TextInputType.none,
|
keyboardType: TextInputType.none,
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
onChanged: (_) => _checkDuplicateBoxNo(),
|
onChanged: (_) => _checkDuplicateBoxNo(),
|
||||||
@@ -1100,9 +1650,6 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
: Colors.grey.shade400,
|
: Colors.grey.shade400,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
suffixIcon: _boxNoLocked
|
|
||||||
? const Icon(Icons.lock, size: 18, color: Colors.orange)
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
style: const TextStyle(fontSize: 16),
|
style: const TextStyle(fontSize: 16),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -54,12 +54,14 @@ class BoxItemData {
|
|||||||
final String zongpaiNo;
|
final String zongpaiNo;
|
||||||
final String? workOrderNo;
|
final String? workOrderNo;
|
||||||
final int quantity;
|
final int quantity;
|
||||||
|
final int? totalQuantity;
|
||||||
|
|
||||||
BoxItemData({
|
BoxItemData({
|
||||||
this.boxItemId,
|
this.boxItemId,
|
||||||
required this.zongpaiNo,
|
required this.zongpaiNo,
|
||||||
this.workOrderNo,
|
this.workOrderNo,
|
||||||
required this.quantity,
|
required this.quantity,
|
||||||
|
this.totalQuantity,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory BoxItemData.fromJson(Map<String, dynamic> json) {
|
factory BoxItemData.fromJson(Map<String, dynamic> json) {
|
||||||
@@ -68,6 +70,7 @@ class BoxItemData {
|
|||||||
zongpaiNo: json['zongpai_no'] as String,
|
zongpaiNo: json['zongpai_no'] as String,
|
||||||
workOrderNo: json['work_order_no']?.toString(),
|
workOrderNo: json['work_order_no']?.toString(),
|
||||||
quantity: json['quantity'] as int,
|
quantity: json['quantity'] as int,
|
||||||
|
totalQuantity: json['total_quantity'] as int?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,8 +175,10 @@ class BoxSaveResult {
|
|||||||
final bool success;
|
final bool success;
|
||||||
final bool isDuplicate;
|
final bool isDuplicate;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
final String? errorCode;
|
||||||
final int? boxItemId;
|
final int? boxItemId;
|
||||||
final String? paichanNo;
|
final String? paichanNo;
|
||||||
|
final String? zongpaiNo;
|
||||||
final int? boxNo;
|
final int? boxNo;
|
||||||
final int? quantity;
|
final int? quantity;
|
||||||
|
|
||||||
@@ -181,8 +186,10 @@ class BoxSaveResult {
|
|||||||
required this.success,
|
required this.success,
|
||||||
this.isDuplicate = false,
|
this.isDuplicate = false,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
|
this.errorCode,
|
||||||
this.boxItemId,
|
this.boxItemId,
|
||||||
this.paichanNo,
|
this.paichanNo,
|
||||||
|
this.zongpaiNo,
|
||||||
this.boxNo,
|
this.boxNo,
|
||||||
this.quantity,
|
this.quantity,
|
||||||
});
|
});
|
||||||
@@ -192,6 +199,7 @@ class BoxSaveResult {
|
|||||||
success: true,
|
success: true,
|
||||||
boxItemId: json['box_item_id'] as int?,
|
boxItemId: json['box_item_id'] as int?,
|
||||||
paichanNo: json['paichan_no'] as String?,
|
paichanNo: json['paichan_no'] as String?,
|
||||||
|
zongpaiNo: json['zongpai_no'] as String?,
|
||||||
boxNo: json['box_no'] as int?,
|
boxNo: json['box_no'] as int?,
|
||||||
quantity: json['quantity'] as int?,
|
quantity: json['quantity'] as int?,
|
||||||
);
|
);
|
||||||
@@ -201,7 +209,10 @@ class BoxSaveResult {
|
|||||||
return BoxSaveResult(
|
return BoxSaveResult(
|
||||||
success: false,
|
success: false,
|
||||||
isDuplicate: true,
|
isDuplicate: true,
|
||||||
|
errorCode: json['error_code']?.toString(),
|
||||||
|
errorMessage: json['message']?.toString(),
|
||||||
paichanNo: json['paichan_no'] as String?,
|
paichanNo: json['paichan_no'] as String?,
|
||||||
|
zongpaiNo: json['zongpai_no'] as String?,
|
||||||
boxNo: json['box_no'] as int?,
|
boxNo: json['box_no'] as int?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -358,6 +369,7 @@ class ApiService {
|
|||||||
required int boxItemId,
|
required int boxItemId,
|
||||||
required int boxNo,
|
required int boxNo,
|
||||||
required int quantity,
|
required int quantity,
|
||||||
|
String boxMode = 'one-to-many',
|
||||||
}) async {
|
}) async {
|
||||||
final uri = Uri.parse('$baseUrl/CargoTrace/box/$boxItemId');
|
final uri = Uri.parse('$baseUrl/CargoTrace/box/$boxItemId');
|
||||||
try {
|
try {
|
||||||
@@ -368,7 +380,7 @@ class ApiService {
|
|||||||
body: jsonEncode({
|
body: jsonEncode({
|
||||||
'box_no': boxNo,
|
'box_no': boxNo,
|
||||||
'quantity': quantity,
|
'quantity': quantity,
|
||||||
'box_mode': 'one-to-many',
|
'box_mode': boxMode,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.timeout(timeout);
|
.timeout(timeout);
|
||||||
|
|||||||
@@ -142,6 +142,152 @@ void main() {
|
|||||||
expect(result.errorMessage, '网络异常,请检查网络连接');
|
expect(result.errorMessage, '网络异常,请检查网络连接');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('ApiService boxing APIs', () {
|
||||||
|
test('fetchBoxInfo parses current boxes and box item ids', () async {
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'zongpai_no': '26BW0011',
|
||||||
|
'paichan_no': 'W00009',
|
||||||
|
'work_order_no': '6-1(7)',
|
||||||
|
'quantity': 80,
|
||||||
|
'current_zongpai_boxes': [
|
||||||
|
{'box_item_id': 101, 'box_no': 3, 'quantity': 30},
|
||||||
|
],
|
||||||
|
'existing_boxes': [
|
||||||
|
{
|
||||||
|
'box_no': 3,
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'box_item_id': 101,
|
||||||
|
'zongpai_no': '26BW0011',
|
||||||
|
'work_order_no': '6-1(7)',
|
||||||
|
'quantity': 30,
|
||||||
|
'total_quantity': 80,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'max_box_no': 3,
|
||||||
|
'suggested_box_no': 4,
|
||||||
|
}),
|
||||||
|
200,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.fetchBoxInfo(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
zongpaiNo: '26BW0011',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success, isTrue);
|
||||||
|
expect(result.currentZongpaiBoxes.single.boxItemId, 101);
|
||||||
|
expect(result.existingBoxes.single.items.single.boxItemId, 101);
|
||||||
|
expect(result.existingBoxes.single.items.single.totalQuantity, 80);
|
||||||
|
expect(result.suggestedBoxNo, 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('saveBoxRecord parses zongpai number from success response', () async {
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'box_item_id': 102,
|
||||||
|
'paichan_no': 'W00009',
|
||||||
|
'box_no': 4,
|
||||||
|
'zongpai_no': '26BW0012',
|
||||||
|
'quantity': 34,
|
||||||
|
'created_at': '2026-05-13T10:00:00',
|
||||||
|
}),
|
||||||
|
200,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.saveBoxRecord(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
zongpaiNo: '26BW0012',
|
||||||
|
boxNo: 4,
|
||||||
|
quantity: 34,
|
||||||
|
boxMode: 'many-to-one',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success, isTrue);
|
||||||
|
expect(result.boxItemId, 102);
|
||||||
|
expect(result.zongpaiNo, '26BW0012');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updateBoxRecord sends many-to-one box mode', () async {
|
||||||
|
late Map<String, dynamic> body;
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
final req = request as http.Request;
|
||||||
|
body = jsonDecode(req.body) as Map<String, dynamic>;
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'box_item_id': 102,
|
||||||
|
'paichan_no': 'W00009',
|
||||||
|
'box_no': 4,
|
||||||
|
'zongpai_no': '26BW0012',
|
||||||
|
'quantity': 20,
|
||||||
|
'updated_at': '2026-05-13T10:00:00',
|
||||||
|
}),
|
||||||
|
200,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.updateBoxRecord(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
boxItemId: 102,
|
||||||
|
boxNo: 4,
|
||||||
|
quantity: 20,
|
||||||
|
boxMode: 'many-to-one',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success, isTrue);
|
||||||
|
expect(body['box_mode'], 'many-to-one');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleteBoxRecord handles success and not found', () async {
|
||||||
|
var calls = 0;
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
calls += 1;
|
||||||
|
if (calls == 1) {
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({'box_item_id': 102, 'deleted': true}),
|
||||||
|
200,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'error_code': 'BOX_ITEM_NOT_FOUND',
|
||||||
|
'message': '指定装箱明细不存在',
|
||||||
|
}),
|
||||||
|
404,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final ok = await svc.deleteBoxRecord(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
boxItemId: 102,
|
||||||
|
);
|
||||||
|
final missing = await svc.deleteBoxRecord(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
boxItemId: 999,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ok.success, isTrue);
|
||||||
|
expect(missing.success, isFalse);
|
||||||
|
expect(missing.errorMessage, '指定装箱明细不存在');
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MockClient extends http.BaseClient {
|
class _MockClient extends http.BaseClient {
|
||||||
|
|||||||
Reference in New Issue
Block a user