feat: improve one-to-many boxing workflow
This commit is contained in:
@@ -51,6 +51,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
String? _paichanNo;
|
String? _paichanNo;
|
||||||
String? _workOrderNo;
|
String? _workOrderNo;
|
||||||
int? _erpQuantity;
|
int? _erpQuantity;
|
||||||
|
List<CurrentZongpaiBoxData> _currentZongpaiBoxes = [];
|
||||||
List<BoxDetailData> _existingBoxes = [];
|
List<BoxDetailData> _existingBoxes = [];
|
||||||
int _maxBoxNo = 0;
|
int _maxBoxNo = 0;
|
||||||
|
|
||||||
@@ -69,9 +70,9 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
// 提交中
|
// 提交中
|
||||||
bool _isSubmitting = false;
|
bool _isSubmitting = false;
|
||||||
|
|
||||||
// 一码多箱:上次提交的值(用于继续添加预填)
|
// 一码多箱:上次提交的箱号(用于继续添加预填)
|
||||||
int? _lastBoxNo;
|
int? _lastBoxNo;
|
||||||
int? _lastQuantity;
|
CurrentZongpaiBoxData? _editingBox;
|
||||||
|
|
||||||
// 重复箱号
|
// 重复箱号
|
||||||
bool _isDuplicateBoxNo = false;
|
bool _isDuplicateBoxNo = false;
|
||||||
@@ -132,6 +133,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
_paichanNo = null;
|
_paichanNo = null;
|
||||||
_workOrderNo = null;
|
_workOrderNo = null;
|
||||||
_erpQuantity = null;
|
_erpQuantity = null;
|
||||||
|
_currentZongpaiBoxes = [];
|
||||||
_existingBoxes = [];
|
_existingBoxes = [];
|
||||||
_maxBoxNo = 0;
|
_maxBoxNo = 0;
|
||||||
_boxNoController.clear();
|
_boxNoController.clear();
|
||||||
@@ -140,7 +142,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
_boxNoLocked = false;
|
_boxNoLocked = false;
|
||||||
_isSubmitting = false;
|
_isSubmitting = false;
|
||||||
_lastBoxNo = null;
|
_lastBoxNo = null;
|
||||||
_lastQuantity = null;
|
_editingBox = null;
|
||||||
_isDuplicateBoxNo = false;
|
_isDuplicateBoxNo = false;
|
||||||
_statusOverrideText = null;
|
_statusOverrideText = null;
|
||||||
_statusOverrideDot = null;
|
_statusOverrideDot = null;
|
||||||
@@ -169,8 +171,8 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 一码一箱:允许覆盖,直接查询新二维码(不提前清空状态,避免闪烁)
|
// 一码一箱和一码多箱:允许覆盖,直接查询新二维码(不提前清空状态,避免闪烁)
|
||||||
if (_mode != BoxingMode.one2one && _phase != _Phase.waiting) {
|
if (_mode == BoxingMode.many2one && _phase != _Phase.waiting) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,10 +228,12 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
_paichanNo = result.paichanNo;
|
_paichanNo = result.paichanNo;
|
||||||
_workOrderNo = result.workOrderNo;
|
_workOrderNo = result.workOrderNo;
|
||||||
_erpQuantity = result.quantity;
|
_erpQuantity = result.quantity;
|
||||||
|
_currentZongpaiBoxes = result.currentZongpaiBoxes;
|
||||||
_existingBoxes = result.existingBoxes;
|
_existingBoxes = result.existingBoxes;
|
||||||
_maxBoxNo = result.maxBoxNo;
|
_maxBoxNo = result.maxBoxNo;
|
||||||
_phase = _Phase.scanned;
|
_phase = _Phase.scanned;
|
||||||
_isDuplicateBoxNo = false;
|
_isDuplicateBoxNo = false;
|
||||||
|
_editingBox = null;
|
||||||
_statusOverrideText = null;
|
_statusOverrideText = null;
|
||||||
|
|
||||||
// 多码一箱:记录已扫描列表
|
// 多码一箱:记录已扫描列表
|
||||||
@@ -250,7 +254,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
case BoxingMode.one2many:
|
case BoxingMode.one2many:
|
||||||
if (_lastBoxNo != null) {
|
if (_lastBoxNo != null) {
|
||||||
_boxNoController.text = (_lastBoxNo! + 1).toString();
|
_boxNoController.text = (_lastBoxNo! + 1).toString();
|
||||||
_quantityController.text = _lastQuantity?.toString() ?? '';
|
_quantityController.clear();
|
||||||
} else {
|
} else {
|
||||||
_boxNoController.text = (_maxBoxNo + 1).toString();
|
_boxNoController.text = (_maxBoxNo + 1).toString();
|
||||||
_quantityController.clear();
|
_quantityController.clear();
|
||||||
@@ -273,7 +277,14 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
setState(() => _isDuplicateBoxNo = false);
|
setState(() => _isDuplicateBoxNo = false);
|
||||||
return;
|
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);
|
setState(() => _isDuplicateBoxNo = exists);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,20 +325,32 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
BoxingMode.one2many => 'one-to-many',
|
BoxingMode.one2many => 'one-to-many',
|
||||||
BoxingMode.many2one => 'many-to-one',
|
BoxingMode.many2one => 'many-to-one',
|
||||||
};
|
};
|
||||||
final result = await _apiService.saveBoxRecord(
|
final editing = _editingBox;
|
||||||
baseUrl: baseUrl,
|
final result = editing == null
|
||||||
zongpaiNo: _zongpaiNo!,
|
? await _apiService.saveBoxRecord(
|
||||||
boxNo: boxNo,
|
baseUrl: baseUrl,
|
||||||
quantity: quantity,
|
zongpaiNo: _zongpaiNo!,
|
||||||
boxMode: modeStr,
|
boxNo: boxNo,
|
||||||
);
|
quantity: quantity,
|
||||||
|
boxMode: modeStr,
|
||||||
|
)
|
||||||
|
: await _apiService.updateBoxRecord(
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
boxItemId: editing.boxItemId,
|
||||||
|
boxNo: boxNo,
|
||||||
|
quantity: quantity,
|
||||||
|
);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
setState(() => _isSubmitting = false);
|
setState(() => _isSubmitting = false);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
_onSubmitSuccess(boxNo, quantity);
|
if (editing == null) {
|
||||||
|
_onSubmitSuccess(boxNo, quantity, result.boxItemId);
|
||||||
|
} else {
|
||||||
|
_onUpdateSuccess(editing, boxNo, quantity);
|
||||||
|
}
|
||||||
} else if (result.isDuplicate) {
|
} else if (result.isDuplicate) {
|
||||||
_feedbackService.trigger(FeedbackEvent.duplicateBoxNo);
|
_feedbackService.trigger(FeedbackEvent.duplicateBoxNo);
|
||||||
if (_mode == BoxingMode.one2one) {
|
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);
|
_feedbackService.trigger(FeedbackEvent.submitSuccess);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_lastBoxNo = boxNo;
|
_lastBoxNo = boxNo;
|
||||||
_lastQuantity = quantity;
|
if (_mode == BoxingMode.one2many && boxItemId != null) {
|
||||||
_existingBoxes = List.from(_existingBoxes)
|
_currentZongpaiBoxes = List.from(_currentZongpaiBoxes)
|
||||||
..add(
|
..add(
|
||||||
BoxDetailData(
|
CurrentZongpaiBoxData(
|
||||||
boxNo: boxNo,
|
boxItemId: boxItemId,
|
||||||
items: [
|
boxNo: boxNo,
|
||||||
BoxItemData(
|
quantity: quantity,
|
||||||
zongpaiNo: _zongpaiNo!,
|
),
|
||||||
workOrderNo: _workOrderNo,
|
);
|
||||||
quantity: quantity,
|
}
|
||||||
),
|
_addExistingBoxItem(boxNo, quantity, boxItemId);
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_maxBoxNo = _maxBoxNo > boxNo ? _maxBoxNo : boxNo;
|
_maxBoxNo = _maxBoxNo > boxNo ? _maxBoxNo : boxNo;
|
||||||
});
|
});
|
||||||
|
|
||||||
switch (_mode) {
|
switch (_mode) {
|
||||||
case BoxingMode.one2one:
|
case BoxingMode.one2one:
|
||||||
_showStatusOverride('装箱成功', StatusDotColor.green, const Duration(milliseconds: 1500));
|
_showStatusOverride(
|
||||||
|
'装箱成功',
|
||||||
|
StatusDotColor.green,
|
||||||
|
const Duration(milliseconds: 1500),
|
||||||
|
);
|
||||||
Future.delayed(const Duration(milliseconds: 1500), () {
|
Future.delayed(const Duration(milliseconds: 1500), () {
|
||||||
if (mounted) setState(() => _resetState());
|
if (mounted) setState(() => _resetState());
|
||||||
});
|
});
|
||||||
|
|
||||||
case BoxingMode.one2many:
|
case BoxingMode.one2many:
|
||||||
setState(() {
|
setState(() {
|
||||||
_phase = _Phase.submitted;
|
_phase = _Phase.scanned;
|
||||||
_zongpaiNo = null;
|
_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:
|
case BoxingMode.many2one:
|
||||||
setState(() {
|
setState(() {
|
||||||
_phase = _Phase.submitted;
|
_phase = _Phase.submitted;
|
||||||
_zongpaiNo = null;
|
_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(() {
|
setState(() {
|
||||||
_phase = _Phase.waiting;
|
_editingBox = item;
|
||||||
|
_phase = _Phase.scanned;
|
||||||
|
_boxNoController.text = item.boxNo.toString();
|
||||||
|
_quantityController.text = item.quantity.toString();
|
||||||
|
_isDuplicateBoxNo = false;
|
||||||
_statusOverrideText = null;
|
_statusOverrideText = null;
|
||||||
|
_statusOverrideDot = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onGoBack() {
|
void _cancelEditAssigned() {
|
||||||
setState(() => _resetState());
|
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 ===
|
// === Status bar management ===
|
||||||
@@ -489,7 +696,8 @@ 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.one2one;
|
(_phase == _Phase.submitted && _mode == BoxingMode.many2one) ||
|
||||||
|
(_phase == _Phase.scanned && _mode == BoxingMode.one2many);
|
||||||
|
|
||||||
_updateBaseStatus();
|
_updateBaseStatus();
|
||||||
final effectiveDot = _statusOverrideDot ?? _statusDot;
|
final effectiveDot = _statusOverrideDot ?? _statusDot;
|
||||||
@@ -545,6 +753,13 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
|
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
|
|
||||||
|
if (_mode == BoxingMode.one2many &&
|
||||||
|
!isWaiting &&
|
||||||
|
_currentZongpaiBoxes.isNotEmpty) ...[
|
||||||
|
_buildAssignedBoxes(),
|
||||||
|
const Divider(height: 24),
|
||||||
|
],
|
||||||
|
|
||||||
// === 输入区 ===
|
// === 输入区 ===
|
||||||
_buildInputArea(isWaiting),
|
_buildInputArea(isWaiting),
|
||||||
],
|
],
|
||||||
@@ -567,15 +782,19 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
child: const Text('返回'),
|
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),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _onContinueAdding,
|
onPressed: _cancelEditAssigned,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
minimumSize: const Size.fromHeight(40),
|
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 ===
|
||||||
|
|
||||||
Widget _buildScanArea(bool isWaiting) {
|
Widget _buildScanArea(bool isWaiting) {
|
||||||
@@ -669,6 +945,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
final grey = isWaiting;
|
final grey = isWaiting;
|
||||||
final paichanText = grey ? '--' : (_paichanNo ?? '--');
|
final paichanText = grey ? '--' : (_paichanNo ?? '--');
|
||||||
final workOrderText = grey ? '--' : (_workOrderNo ?? '--');
|
final workOrderText = grey ? '--' : (_workOrderNo ?? '--');
|
||||||
|
final quantityText = grey ? '--' : (_erpQuantity?.toString() ?? '--');
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -676,6 +953,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
flex: 5,
|
||||||
child: Text(
|
child: Text(
|
||||||
paichanText,
|
paichanText,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -686,10 +964,25 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
flex: 4,
|
||||||
child: Text(
|
child: Text(
|
||||||
workOrderText,
|
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,
|
textAlign: TextAlign.right,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
@@ -781,7 +1074,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'箱号',
|
_editingBox == null ? '箱号' : '修改箱号',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: enabled ? Colors.black54 : Colors.grey,
|
color: enabled ? Colors.black54 : Colors.grey,
|
||||||
@@ -794,7 +1087,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
controller: _boxNoController,
|
controller: _boxNoController,
|
||||||
focusNode: _boxNoFocusNode,
|
focusNode: _boxNoFocusNode,
|
||||||
enabled: enabled && !_boxNoLocked,
|
enabled: enabled && !_boxNoLocked,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.none,
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
onChanged: (_) => _checkDuplicateBoxNo(),
|
onChanged: (_) => _checkDuplicateBoxNo(),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@@ -825,7 +1118,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'数量',
|
_editingBox == null ? '数量' : '修改数量',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: enabled ? Colors.black54 : Colors.grey,
|
color: enabled ? Colors.black54 : Colors.grey,
|
||||||
@@ -838,8 +1131,9 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
controller: _quantityController,
|
controller: _quantityController,
|
||||||
focusNode: _quantityFocusNode,
|
focusNode: _quantityFocusNode,
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.none,
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
|
onChanged: (_) => setState(() {}),
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
@@ -872,7 +1166,10 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
color: Colors.white,
|
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 {
|
class BoxItemData {
|
||||||
|
final int? boxItemId;
|
||||||
final String zongpaiNo;
|
final String zongpaiNo;
|
||||||
final String? workOrderNo;
|
final String? workOrderNo;
|
||||||
final int quantity;
|
final int quantity;
|
||||||
|
|
||||||
BoxItemData({
|
BoxItemData({
|
||||||
|
this.boxItemId,
|
||||||
required this.zongpaiNo,
|
required this.zongpaiNo,
|
||||||
this.workOrderNo,
|
this.workOrderNo,
|
||||||
required this.quantity,
|
required this.quantity,
|
||||||
@@ -44,6 +46,7 @@ class BoxItemData {
|
|||||||
|
|
||||||
factory BoxItemData.fromJson(Map<String, dynamic> json) {
|
factory BoxItemData.fromJson(Map<String, dynamic> json) {
|
||||||
return BoxItemData(
|
return BoxItemData(
|
||||||
|
boxItemId: json['box_item_id'] as int?,
|
||||||
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,
|
||||||
@@ -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 {
|
class BoxInfoResult {
|
||||||
final bool success;
|
final bool success;
|
||||||
@@ -76,6 +100,7 @@ class BoxInfoResult {
|
|||||||
final String? paichanNo;
|
final String? paichanNo;
|
||||||
final String? workOrderNo;
|
final String? workOrderNo;
|
||||||
final int? quantity;
|
final int? quantity;
|
||||||
|
final List<CurrentZongpaiBoxData> currentZongpaiBoxes;
|
||||||
final List<BoxDetailData> existingBoxes;
|
final List<BoxDetailData> existingBoxes;
|
||||||
final int maxBoxNo;
|
final int maxBoxNo;
|
||||||
final int suggestedBoxNo;
|
final int suggestedBoxNo;
|
||||||
@@ -87,6 +112,7 @@ class BoxInfoResult {
|
|||||||
this.paichanNo,
|
this.paichanNo,
|
||||||
this.workOrderNo,
|
this.workOrderNo,
|
||||||
this.quantity,
|
this.quantity,
|
||||||
|
this.currentZongpaiBoxes = const [],
|
||||||
this.existingBoxes = const [],
|
this.existingBoxes = const [],
|
||||||
this.maxBoxNo = 0,
|
this.maxBoxNo = 0,
|
||||||
this.suggestedBoxNo = 1,
|
this.suggestedBoxNo = 1,
|
||||||
@@ -98,12 +124,20 @@ class BoxInfoResult {
|
|||||||
?.map((b) => BoxDetailData.fromJson(b as Map<String, dynamic>))
|
?.map((b) => BoxDetailData.fromJson(b as Map<String, dynamic>))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[];
|
[];
|
||||||
|
final currentBoxes =
|
||||||
|
(json['current_zongpai_boxes'] as List<dynamic>?)
|
||||||
|
?.map(
|
||||||
|
(b) => CurrentZongpaiBoxData.fromJson(b as Map<String, dynamic>),
|
||||||
|
)
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
return BoxInfoResult(
|
return BoxInfoResult(
|
||||||
success: true,
|
success: true,
|
||||||
zongpaiNo: json['zongpai_no'] as String?,
|
zongpaiNo: json['zongpai_no'] as String?,
|
||||||
paichanNo: json['paichan_no'] as String?,
|
paichanNo: json['paichan_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?,
|
||||||
|
currentZongpaiBoxes: currentBoxes,
|
||||||
existingBoxes: boxes,
|
existingBoxes: boxes,
|
||||||
maxBoxNo: json['max_box_no'] as int? ?? 0,
|
maxBoxNo: json['max_box_no'] as int? ?? 0,
|
||||||
suggestedBoxNo: json['suggested_box_no'] as int? ?? 1,
|
suggestedBoxNo: json['suggested_box_no'] as int? ?? 1,
|
||||||
@@ -120,22 +154,28 @@ class BoxSaveResult {
|
|||||||
final bool success;
|
final bool success;
|
||||||
final bool isDuplicate;
|
final bool isDuplicate;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
final int? boxItemId;
|
||||||
final String? paichanNo;
|
final String? paichanNo;
|
||||||
final int? boxNo;
|
final int? boxNo;
|
||||||
|
final int? quantity;
|
||||||
|
|
||||||
BoxSaveResult({
|
BoxSaveResult({
|
||||||
required this.success,
|
required this.success,
|
||||||
this.isDuplicate = false,
|
this.isDuplicate = false,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
|
this.boxItemId,
|
||||||
this.paichanNo,
|
this.paichanNo,
|
||||||
this.boxNo,
|
this.boxNo,
|
||||||
|
this.quantity,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory BoxSaveResult.ok(Map<String, dynamic> json) {
|
factory BoxSaveResult.ok(Map<String, dynamic> json) {
|
||||||
return BoxSaveResult(
|
return BoxSaveResult(
|
||||||
success: true,
|
success: true,
|
||||||
|
boxItemId: json['box_item_id'] as int?,
|
||||||
paichanNo: json['paichan_no'] as String?,
|
paichanNo: json['paichan_no'] as String?,
|
||||||
boxNo: json['box_no'] as int?,
|
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 {
|
class ApiService {
|
||||||
final http.Client _client;
|
final http.Client _client;
|
||||||
final Duration timeout;
|
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.
|
/// Test connectivity by making a HEAD request to the base URL.
|
||||||
Future<bool> testConnection(String baseUrl) async {
|
Future<bool> testConnection(String baseUrl) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user