feat: improve one-to-many boxing workflow
This commit is contained in:
@@ -51,6 +51,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
String? _paichanNo;
|
||||
String? _workOrderNo;
|
||||
int? _erpQuantity;
|
||||
List<CurrentZongpaiBoxData> _currentZongpaiBoxes = [];
|
||||
List<BoxDetailData> _existingBoxes = [];
|
||||
int _maxBoxNo = 0;
|
||||
|
||||
@@ -69,9 +70,9 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
// 提交中
|
||||
bool _isSubmitting = false;
|
||||
|
||||
// 一码多箱:上次提交的值(用于继续添加预填)
|
||||
// 一码多箱:上次提交的箱号(用于继续添加预填)
|
||||
int? _lastBoxNo;
|
||||
int? _lastQuantity;
|
||||
CurrentZongpaiBoxData? _editingBox;
|
||||
|
||||
// 重复箱号
|
||||
bool _isDuplicateBoxNo = false;
|
||||
@@ -132,6 +133,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
_paichanNo = null;
|
||||
_workOrderNo = null;
|
||||
_erpQuantity = null;
|
||||
_currentZongpaiBoxes = [];
|
||||
_existingBoxes = [];
|
||||
_maxBoxNo = 0;
|
||||
_boxNoController.clear();
|
||||
@@ -140,7 +142,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
_boxNoLocked = false;
|
||||
_isSubmitting = false;
|
||||
_lastBoxNo = null;
|
||||
_lastQuantity = null;
|
||||
_editingBox = null;
|
||||
_isDuplicateBoxNo = false;
|
||||
_statusOverrideText = null;
|
||||
_statusOverrideDot = null;
|
||||
@@ -169,8 +171,8 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
// 一码一箱:允许覆盖,直接查询新二维码(不提前清空状态,避免闪烁)
|
||||
if (_mode != BoxingMode.one2one && _phase != _Phase.waiting) {
|
||||
// 一码一箱和一码多箱:允许覆盖,直接查询新二维码(不提前清空状态,避免闪烁)
|
||||
if (_mode == BoxingMode.many2one && _phase != _Phase.waiting) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -226,10 +228,12 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
_paichanNo = result.paichanNo;
|
||||
_workOrderNo = result.workOrderNo;
|
||||
_erpQuantity = result.quantity;
|
||||
_currentZongpaiBoxes = result.currentZongpaiBoxes;
|
||||
_existingBoxes = result.existingBoxes;
|
||||
_maxBoxNo = result.maxBoxNo;
|
||||
_phase = _Phase.scanned;
|
||||
_isDuplicateBoxNo = false;
|
||||
_editingBox = null;
|
||||
_statusOverrideText = null;
|
||||
|
||||
// 多码一箱:记录已扫描列表
|
||||
@@ -250,7 +254,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
case BoxingMode.one2many:
|
||||
if (_lastBoxNo != null) {
|
||||
_boxNoController.text = (_lastBoxNo! + 1).toString();
|
||||
_quantityController.text = _lastQuantity?.toString() ?? '';
|
||||
_quantityController.clear();
|
||||
} else {
|
||||
_boxNoController.text = (_maxBoxNo + 1).toString();
|
||||
_quantityController.clear();
|
||||
@@ -273,7 +277,14 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
setState(() => _isDuplicateBoxNo = false);
|
||||
return;
|
||||
}
|
||||
final exists = _existingBoxes.any((b) => b.boxNo == boxNo);
|
||||
final exists = switch (_mode) {
|
||||
BoxingMode.many2one => false,
|
||||
BoxingMode.one2one => _existingBoxes.any((b) => b.boxNo == boxNo),
|
||||
BoxingMode.one2many => _existingBoxes.any((b) {
|
||||
if (b.boxNo != boxNo) return false;
|
||||
return _editingBox == null || b.boxNo != _editingBox!.boxNo;
|
||||
}),
|
||||
};
|
||||
setState(() => _isDuplicateBoxNo = exists);
|
||||
}
|
||||
|
||||
@@ -314,20 +325,32 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
BoxingMode.one2many => 'one-to-many',
|
||||
BoxingMode.many2one => 'many-to-one',
|
||||
};
|
||||
final result = await _apiService.saveBoxRecord(
|
||||
baseUrl: baseUrl,
|
||||
zongpaiNo: _zongpaiNo!,
|
||||
boxNo: boxNo,
|
||||
quantity: quantity,
|
||||
boxMode: modeStr,
|
||||
);
|
||||
final editing = _editingBox;
|
||||
final result = editing == null
|
||||
? await _apiService.saveBoxRecord(
|
||||
baseUrl: baseUrl,
|
||||
zongpaiNo: _zongpaiNo!,
|
||||
boxNo: boxNo,
|
||||
quantity: quantity,
|
||||
boxMode: modeStr,
|
||||
)
|
||||
: await _apiService.updateBoxRecord(
|
||||
baseUrl: baseUrl,
|
||||
boxItemId: editing.boxItemId,
|
||||
boxNo: boxNo,
|
||||
quantity: quantity,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() => _isSubmitting = false);
|
||||
|
||||
if (result.success) {
|
||||
_onSubmitSuccess(boxNo, quantity);
|
||||
if (editing == null) {
|
||||
_onSubmitSuccess(boxNo, quantity, result.boxItemId);
|
||||
} else {
|
||||
_onUpdateSuccess(editing, boxNo, quantity);
|
||||
}
|
||||
} else if (result.isDuplicate) {
|
||||
_feedbackService.trigger(FeedbackEvent.duplicateBoxNo);
|
||||
if (_mode == BoxingMode.one2one) {
|
||||
@@ -356,62 +379,246 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _onSubmitSuccess(int boxNo, int quantity) {
|
||||
void _onSubmitSuccess(int boxNo, int quantity, int? boxItemId) {
|
||||
_feedbackService.trigger(FeedbackEvent.submitSuccess);
|
||||
|
||||
setState(() {
|
||||
_lastBoxNo = boxNo;
|
||||
_lastQuantity = quantity;
|
||||
_existingBoxes = List.from(_existingBoxes)
|
||||
..add(
|
||||
BoxDetailData(
|
||||
boxNo: boxNo,
|
||||
items: [
|
||||
BoxItemData(
|
||||
zongpaiNo: _zongpaiNo!,
|
||||
workOrderNo: _workOrderNo,
|
||||
quantity: quantity,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (_mode == BoxingMode.one2many && boxItemId != null) {
|
||||
_currentZongpaiBoxes = List.from(_currentZongpaiBoxes)
|
||||
..add(
|
||||
CurrentZongpaiBoxData(
|
||||
boxItemId: boxItemId,
|
||||
boxNo: boxNo,
|
||||
quantity: quantity,
|
||||
),
|
||||
);
|
||||
}
|
||||
_addExistingBoxItem(boxNo, quantity, boxItemId);
|
||||
_maxBoxNo = _maxBoxNo > boxNo ? _maxBoxNo : boxNo;
|
||||
});
|
||||
|
||||
switch (_mode) {
|
||||
case BoxingMode.one2one:
|
||||
_showStatusOverride('装箱成功', StatusDotColor.green, const Duration(milliseconds: 1500));
|
||||
_showStatusOverride(
|
||||
'装箱成功',
|
||||
StatusDotColor.green,
|
||||
const Duration(milliseconds: 1500),
|
||||
);
|
||||
Future.delayed(const Duration(milliseconds: 1500), () {
|
||||
if (mounted) setState(() => _resetState());
|
||||
});
|
||||
|
||||
case BoxingMode.one2many:
|
||||
setState(() {
|
||||
_phase = _Phase.submitted;
|
||||
_zongpaiNo = null;
|
||||
_phase = _Phase.scanned;
|
||||
_boxNoController.text = (boxNo + 1).toString();
|
||||
_quantityController.clear();
|
||||
_isDuplicateBoxNo = false;
|
||||
});
|
||||
_showStatusOverride('装箱成功,可继续添加或返回', StatusDotColor.green, const Duration(milliseconds: 1500));
|
||||
_showStatusOverride(
|
||||
'装箱成功,可继续添加',
|
||||
StatusDotColor.green,
|
||||
const Duration(milliseconds: 1500),
|
||||
);
|
||||
|
||||
case BoxingMode.many2one:
|
||||
setState(() {
|
||||
_phase = _Phase.submitted;
|
||||
_zongpaiNo = null;
|
||||
});
|
||||
_showStatusOverride('装箱成功,请扫描下一个总排号', StatusDotColor.green, const Duration(milliseconds: 1500));
|
||||
_showStatusOverride(
|
||||
'装箱成功,请扫描下一个总排号',
|
||||
StatusDotColor.green,
|
||||
const Duration(milliseconds: 1500),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onUpdateSuccess(
|
||||
CurrentZongpaiBoxData editing,
|
||||
int boxNo,
|
||||
int quantity,
|
||||
) {
|
||||
_feedbackService.trigger(FeedbackEvent.submitSuccess);
|
||||
|
||||
setState(() {
|
||||
_currentZongpaiBoxes = _currentZongpaiBoxes.map((item) {
|
||||
if (item.boxItemId != editing.boxItemId) return item;
|
||||
return CurrentZongpaiBoxData(
|
||||
boxItemId: item.boxItemId,
|
||||
boxNo: boxNo,
|
||||
quantity: quantity,
|
||||
);
|
||||
}).toList();
|
||||
_replaceExistingBoxItem(editing, boxNo, quantity);
|
||||
_editingBox = null;
|
||||
_lastBoxNo = _maxBoxNo;
|
||||
_boxNoController.text = (_maxBoxNo + 1).toString();
|
||||
_quantityController.clear();
|
||||
_isDuplicateBoxNo = false;
|
||||
});
|
||||
_showStatusOverride(
|
||||
'修改成功',
|
||||
StatusDotColor.green,
|
||||
const Duration(milliseconds: 1500),
|
||||
);
|
||||
}
|
||||
|
||||
void _replaceExistingBoxItem(
|
||||
CurrentZongpaiBoxData editing,
|
||||
int boxNo,
|
||||
int quantity,
|
||||
) {
|
||||
final nextBoxes = <BoxDetailData>[];
|
||||
for (final box in _existingBoxes) {
|
||||
final items = box.items.where((item) {
|
||||
return item.boxItemId != editing.boxItemId;
|
||||
}).toList();
|
||||
if (items.isNotEmpty) {
|
||||
nextBoxes.add(BoxDetailData(boxNo: box.boxNo, items: items));
|
||||
}
|
||||
}
|
||||
|
||||
final targetIndex = nextBoxes.indexWhere((box) => box.boxNo == boxNo);
|
||||
final updatedItem = BoxItemData(
|
||||
boxItemId: editing.boxItemId,
|
||||
zongpaiNo: _zongpaiNo!,
|
||||
workOrderNo: _workOrderNo,
|
||||
quantity: quantity,
|
||||
);
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
void _addExistingBoxItem(int boxNo, int quantity, int? boxItemId) {
|
||||
final item = BoxItemData(
|
||||
boxItemId: boxItemId,
|
||||
zongpaiNo: _zongpaiNo!,
|
||||
workOrderNo: _workOrderNo,
|
||||
quantity: quantity,
|
||||
);
|
||||
final index = _existingBoxes.indexWhere((box) => box.boxNo == boxNo);
|
||||
if (index < 0) {
|
||||
_existingBoxes = [
|
||||
..._existingBoxes,
|
||||
BoxDetailData(boxNo: boxNo, items: [item]),
|
||||
]..sort((a, b) => a.boxNo.compareTo(b.boxNo));
|
||||
return;
|
||||
}
|
||||
|
||||
final next = List<BoxDetailData>.from(_existingBoxes);
|
||||
final box = next[index];
|
||||
next[index] = BoxDetailData(boxNo: box.boxNo, items: [...box.items, item]);
|
||||
_existingBoxes = next;
|
||||
}
|
||||
|
||||
// === 操作按钮 ===
|
||||
|
||||
void _onContinueAdding() {
|
||||
void _onGoBack() {
|
||||
setState(() => _resetState());
|
||||
}
|
||||
|
||||
void _startEditAssigned(CurrentZongpaiBoxData item) {
|
||||
setState(() {
|
||||
_phase = _Phase.waiting;
|
||||
_editingBox = item;
|
||||
_phase = _Phase.scanned;
|
||||
_boxNoController.text = item.boxNo.toString();
|
||||
_quantityController.text = item.quantity.toString();
|
||||
_isDuplicateBoxNo = false;
|
||||
_statusOverrideText = null;
|
||||
_statusOverrideDot = null;
|
||||
});
|
||||
}
|
||||
|
||||
void _onGoBack() {
|
||||
setState(() => _resetState());
|
||||
void _cancelEditAssigned() {
|
||||
setState(() {
|
||||
_editingBox = null;
|
||||
_boxNoController.text = ((_lastBoxNo ?? _maxBoxNo) + 1).toString();
|
||||
_quantityController.clear();
|
||||
_isDuplicateBoxNo = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _deleteAssigned(CurrentZongpaiBoxData item) async {
|
||||
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.deleteBoxRecord(
|
||||
baseUrl: baseUrl,
|
||||
boxItemId: item.boxItemId,
|
||||
);
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
if (result.success) {
|
||||
_currentZongpaiBoxes = _currentZongpaiBoxes
|
||||
.where((box) => box.boxItemId != item.boxItemId)
|
||||
.toList();
|
||||
_removeExistingBoxItem(item.boxItemId);
|
||||
if (_editingBox?.boxItemId == item.boxItemId) {
|
||||
_editingBox = null;
|
||||
}
|
||||
_lastBoxNo = _maxBoxNo;
|
||||
_boxNoController.text = (_maxBoxNo + 1).toString();
|
||||
_quantityController.clear();
|
||||
_isDuplicateBoxNo = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
_showStatusOverride(
|
||||
'删除成功',
|
||||
StatusDotColor.green,
|
||||
const Duration(milliseconds: 1500),
|
||||
);
|
||||
} else {
|
||||
_showStatusOverride(
|
||||
result.errorMessage ?? '删除失败',
|
||||
StatusDotColor.red,
|
||||
const Duration(seconds: 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _removeExistingBoxItem(int boxItemId) {
|
||||
final nextBoxes = <BoxDetailData>[];
|
||||
for (final box in _existingBoxes) {
|
||||
final items = box.items
|
||||
.where((item) => item.boxItemId != boxItemId)
|
||||
.toList();
|
||||
if (items.isNotEmpty) {
|
||||
nextBoxes.add(BoxDetailData(boxNo: box.boxNo, items: items));
|
||||
}
|
||||
}
|
||||
_existingBoxes = nextBoxes;
|
||||
_maxBoxNo = nextBoxes.fold<int>(
|
||||
0,
|
||||
(max, box) => box.boxNo > max ? box.boxNo : max,
|
||||
);
|
||||
}
|
||||
|
||||
// === Status bar management ===
|
||||
@@ -489,7 +696,8 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isWaiting = _phase == _Phase.waiting && _zongpaiNo == null;
|
||||
final showActionButtons =
|
||||
_phase == _Phase.submitted && _mode != BoxingMode.one2one;
|
||||
(_phase == _Phase.submitted && _mode == BoxingMode.many2one) ||
|
||||
(_phase == _Phase.scanned && _mode == BoxingMode.one2many);
|
||||
|
||||
_updateBaseStatus();
|
||||
final effectiveDot = _statusOverrideDot ?? _statusDot;
|
||||
@@ -545,6 +753,13 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
|
||||
const Divider(height: 24),
|
||||
|
||||
if (_mode == BoxingMode.one2many &&
|
||||
!isWaiting &&
|
||||
_currentZongpaiBoxes.isNotEmpty) ...[
|
||||
_buildAssignedBoxes(),
|
||||
const Divider(height: 24),
|
||||
],
|
||||
|
||||
// === 输入区 ===
|
||||
_buildInputArea(isWaiting),
|
||||
],
|
||||
@@ -567,15 +782,19 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
child: const Text('返回'),
|
||||
),
|
||||
),
|
||||
if (_mode == BoxingMode.one2many) ...[
|
||||
if (_mode == BoxingMode.one2many && _editingBox == null) ...[
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(child: SizedBox.shrink()),
|
||||
],
|
||||
if (_mode == BoxingMode.one2many && _editingBox != null) ...[
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _onContinueAdding,
|
||||
onPressed: _cancelEditAssigned,
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(40),
|
||||
),
|
||||
child: const Text('继续添加'),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -590,6 +809,63 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAssignedBoxes() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'已分配',
|
||||
style: TextStyle(fontSize: 13, color: Colors.black54),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
..._currentZongpaiBoxes.map((item) {
|
||||
final editing = _editingBox?.boxItemId == item.boxItemId;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: editing ? Colors.blue.shade50 : Colors.grey.shade50,
|
||||
border: Border.all(
|
||||
color: editing ? Colors.blue : Colors.grey.shade300,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.only(left: 10, right: 4),
|
||||
onTap: () => _startEditAssigned(item),
|
||||
title: Text(
|
||||
'${item.boxNo}号箱 / 数量 ${item.quantity}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: editing ? FontWeight.w700 : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: '修改',
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(Icons.edit, size: 18),
|
||||
onPressed: () => _startEditAssigned(item),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: '删除',
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(Icons.delete_outline, size: 18),
|
||||
onPressed: _isSubmitting
|
||||
? null
|
||||
: () => _deleteAssigned(item),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// === 扫码区 Widget ===
|
||||
|
||||
Widget _buildScanArea(bool isWaiting) {
|
||||
@@ -669,6 +945,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
final grey = isWaiting;
|
||||
final paichanText = grey ? '--' : (_paichanNo ?? '--');
|
||||
final workOrderText = grey ? '--' : (_workOrderNo ?? '--');
|
||||
final quantityText = grey ? '--' : (_erpQuantity?.toString() ?? '--');
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -676,6 +953,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Text(
|
||||
paichanText,
|
||||
style: TextStyle(
|
||||
@@ -686,10 +964,25 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Text(
|
||||
workOrderText,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: grey ? Colors.grey : Colors.black87,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
quantityText,
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
@@ -781,7 +1074,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'箱号',
|
||||
_editingBox == null ? '箱号' : '修改箱号',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: enabled ? Colors.black54 : Colors.grey,
|
||||
@@ -794,7 +1087,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
controller: _boxNoController,
|
||||
focusNode: _boxNoFocusNode,
|
||||
enabled: enabled && !_boxNoLocked,
|
||||
keyboardType: TextInputType.number,
|
||||
keyboardType: TextInputType.none,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
onChanged: (_) => _checkDuplicateBoxNo(),
|
||||
decoration: InputDecoration(
|
||||
@@ -825,7 +1118,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'数量',
|
||||
_editingBox == null ? '数量' : '修改数量',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: enabled ? Colors.black54 : Colors.grey,
|
||||
@@ -838,8 +1131,9 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
controller: _quantityController,
|
||||
focusNode: _quantityFocusNode,
|
||||
enabled: enabled,
|
||||
keyboardType: TextInputType.number,
|
||||
keyboardType: TextInputType.none,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
onChanged: (_) => setState(() {}),
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
border: OutlineInputBorder(),
|
||||
@@ -872,7 +1166,10 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Text('确认', style: TextStyle(fontSize: 15)),
|
||||
: Text(
|
||||
_editingBox == null ? '确认' : '保存',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -32,11 +32,13 @@ class RegistrationResult {
|
||||
|
||||
/// 箱号内单个总排号明细
|
||||
class BoxItemData {
|
||||
final int? boxItemId;
|
||||
final String zongpaiNo;
|
||||
final String? workOrderNo;
|
||||
final int quantity;
|
||||
|
||||
BoxItemData({
|
||||
this.boxItemId,
|
||||
required this.zongpaiNo,
|
||||
this.workOrderNo,
|
||||
required this.quantity,
|
||||
@@ -44,6 +46,7 @@ class BoxItemData {
|
||||
|
||||
factory BoxItemData.fromJson(Map<String, dynamic> json) {
|
||||
return BoxItemData(
|
||||
boxItemId: json['box_item_id'] as int?,
|
||||
zongpaiNo: json['zongpai_no'] as String,
|
||||
workOrderNo: json['work_order_no']?.toString(),
|
||||
quantity: json['quantity'] as int,
|
||||
@@ -68,6 +71,27 @@ class BoxDetailData {
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前总排号已分配的装箱明细
|
||||
class CurrentZongpaiBoxData {
|
||||
final int boxItemId;
|
||||
final int boxNo;
|
||||
final int quantity;
|
||||
|
||||
CurrentZongpaiBoxData({
|
||||
required this.boxItemId,
|
||||
required this.boxNo,
|
||||
required this.quantity,
|
||||
});
|
||||
|
||||
factory CurrentZongpaiBoxData.fromJson(Map<String, dynamic> json) {
|
||||
return CurrentZongpaiBoxData(
|
||||
boxItemId: json['box_item_id'] as int,
|
||||
boxNo: json['box_no'] as int,
|
||||
quantity: json['quantity'] as int,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 装箱信息查询结果
|
||||
class BoxInfoResult {
|
||||
final bool success;
|
||||
@@ -76,6 +100,7 @@ class BoxInfoResult {
|
||||
final String? paichanNo;
|
||||
final String? workOrderNo;
|
||||
final int? quantity;
|
||||
final List<CurrentZongpaiBoxData> currentZongpaiBoxes;
|
||||
final List<BoxDetailData> existingBoxes;
|
||||
final int maxBoxNo;
|
||||
final int suggestedBoxNo;
|
||||
@@ -87,6 +112,7 @@ class BoxInfoResult {
|
||||
this.paichanNo,
|
||||
this.workOrderNo,
|
||||
this.quantity,
|
||||
this.currentZongpaiBoxes = const [],
|
||||
this.existingBoxes = const [],
|
||||
this.maxBoxNo = 0,
|
||||
this.suggestedBoxNo = 1,
|
||||
@@ -98,12 +124,20 @@ class BoxInfoResult {
|
||||
?.map((b) => BoxDetailData.fromJson(b as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[];
|
||||
final currentBoxes =
|
||||
(json['current_zongpai_boxes'] as List<dynamic>?)
|
||||
?.map(
|
||||
(b) => CurrentZongpaiBoxData.fromJson(b as Map<String, dynamic>),
|
||||
)
|
||||
.toList() ??
|
||||
[];
|
||||
return BoxInfoResult(
|
||||
success: true,
|
||||
zongpaiNo: json['zongpai_no'] as String?,
|
||||
paichanNo: json['paichan_no'] as String?,
|
||||
workOrderNo: json['work_order_no']?.toString(),
|
||||
quantity: json['quantity'] as int?,
|
||||
currentZongpaiBoxes: currentBoxes,
|
||||
existingBoxes: boxes,
|
||||
maxBoxNo: json['max_box_no'] as int? ?? 0,
|
||||
suggestedBoxNo: json['suggested_box_no'] as int? ?? 1,
|
||||
@@ -120,22 +154,28 @@ class BoxSaveResult {
|
||||
final bool success;
|
||||
final bool isDuplicate;
|
||||
final String? errorMessage;
|
||||
final int? boxItemId;
|
||||
final String? paichanNo;
|
||||
final int? boxNo;
|
||||
final int? quantity;
|
||||
|
||||
BoxSaveResult({
|
||||
required this.success,
|
||||
this.isDuplicate = false,
|
||||
this.errorMessage,
|
||||
this.boxItemId,
|
||||
this.paichanNo,
|
||||
this.boxNo,
|
||||
this.quantity,
|
||||
});
|
||||
|
||||
factory BoxSaveResult.ok(Map<String, dynamic> json) {
|
||||
return BoxSaveResult(
|
||||
success: true,
|
||||
boxItemId: json['box_item_id'] as int?,
|
||||
paichanNo: json['paichan_no'] as String?,
|
||||
boxNo: json['box_no'] as int?,
|
||||
quantity: json['quantity'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,6 +193,13 @@ class BoxSaveResult {
|
||||
}
|
||||
}
|
||||
|
||||
class BoxDeleteResult {
|
||||
final bool success;
|
||||
final String? errorMessage;
|
||||
|
||||
BoxDeleteResult({required this.success, this.errorMessage});
|
||||
}
|
||||
|
||||
class ApiService {
|
||||
final http.Client _client;
|
||||
final Duration timeout;
|
||||
@@ -275,6 +322,75 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新装箱明细 — PATCH /CargoTrace/box/{boxItemId}
|
||||
Future<BoxSaveResult> updateBoxRecord({
|
||||
required String baseUrl,
|
||||
required int boxItemId,
|
||||
required int boxNo,
|
||||
required int quantity,
|
||||
}) async {
|
||||
final uri = Uri.parse('$baseUrl/CargoTrace/box/$boxItemId');
|
||||
try {
|
||||
final response = await _client
|
||||
.patch(
|
||||
uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'box_no': boxNo,
|
||||
'quantity': quantity,
|
||||
'box_mode': 'one-to-many',
|
||||
}),
|
||||
)
|
||||
.timeout(timeout);
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
return BoxSaveResult.ok(body);
|
||||
case 400:
|
||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final msg = body['message']?.toString() ?? '请求参数错误';
|
||||
return BoxSaveResult.error(msg);
|
||||
case 409:
|
||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
return BoxSaveResult.duplicate(body);
|
||||
case 404:
|
||||
return BoxSaveResult.error('指定装箱明细不存在');
|
||||
default:
|
||||
return BoxSaveResult.error('修改失败 (${response.statusCode})');
|
||||
}
|
||||
} catch (e) {
|
||||
return BoxSaveResult.error('网络异常,请检查网络连接');
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除装箱明细 — DELETE /CargoTrace/box/{boxItemId}
|
||||
Future<BoxDeleteResult> deleteBoxRecord({
|
||||
required String baseUrl,
|
||||
required int boxItemId,
|
||||
}) async {
|
||||
final uri = Uri.parse('$baseUrl/CargoTrace/box/$boxItemId');
|
||||
try {
|
||||
final response = await _client
|
||||
.delete(uri, headers: {'Content-Type': 'application/json'})
|
||||
.timeout(timeout);
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
return BoxDeleteResult(success: true);
|
||||
case 404:
|
||||
return BoxDeleteResult(success: false, errorMessage: '指定装箱明细不存在');
|
||||
default:
|
||||
return BoxDeleteResult(
|
||||
success: false,
|
||||
errorMessage: '删除失败 (${response.statusCode})',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
return BoxDeleteResult(success: false, errorMessage: '网络异常,请检查网络连接');
|
||||
}
|
||||
}
|
||||
|
||||
/// Test connectivity by making a HEAD request to the base URL.
|
||||
Future<bool> testConnection(String baseUrl) async {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user