feat(boxing): support editing assigned box records

This commit is contained in:
Misaka_Company
2026-05-15 08:19:34 +08:00
parent a165bfeabe
commit ce80e1a1f6
2 changed files with 340 additions and 43 deletions

View File

@@ -61,14 +61,37 @@ class BoxingDetailPage extends StatelessWidget {
}
Widget _buildBoxGroup(BuildContext context, BoxDetailData box) {
final boxTotalQuantity = box.items.fold<int>(
0,
(sum, item) => sum + item.quantity,
);
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
Row(
children: [
Expanded(
child: Text(
'箱号 ${box.boxNo}',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
Text(
'$boxTotalQuantity',
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
const SizedBox(height: 4),
Container(

View File

@@ -102,6 +102,9 @@ class _BoxingPageState extends State<BoxingPage> {
int? _editingPackedItemId;
String _editingPackedQuantity = '';
int? _deletingPackedItemId;
int? _editingAssignedItemId;
String _editingAssignedQuantity = '';
int? _deletingAssignedItemId;
String? _lastScannedPaichanNo;
String? _paichanSwitchNotice;
@@ -187,6 +190,9 @@ class _BoxingPageState extends State<BoxingPage> {
_editingPackedItemId = null;
_editingPackedQuantity = '';
_deletingPackedItemId = null;
_editingAssignedItemId = null;
_editingAssignedQuantity = '';
_deletingAssignedItemId = null;
_lastScannedPaichanNo = null;
_paichanSwitchNotice = null;
_isSubmitting = false;
@@ -264,6 +270,9 @@ class _BoxingPageState extends State<BoxingPage> {
_phase = _Phase.scanned;
_isDuplicateBoxNo = false;
_editingBox = null;
_editingAssignedItemId = null;
_editingAssignedQuantity = '';
_deletingAssignedItemId = null;
_statusOverrideText = null;
if (_mode == BoxingMode.singleCode) {
_paichanSwitchNotice = null;
@@ -322,7 +331,14 @@ class _BoxingPageState extends State<BoxingPage> {
_boxNoController.text = boxNoToApply.toString();
}
_lastScannedPaichanNo = _paichanNo;
_quantityController.text = (_erpQuantity ?? 0).toString();
final remaining = _remainingQuantity;
if (remaining <= 0) {
_quantityController.clear();
_statusOverrideText = '该总排号已全部装箱完毕';
_statusOverrideDot = StatusDotColor.red;
return;
}
_quantityController.text = remaining.toString();
_quantityController.selection = TextSelection(
baseOffset: 0,
extentOffset: _quantityController.text.length,
@@ -386,6 +402,23 @@ class _BoxingPageState extends State<BoxingPage> {
return true;
}
void _submitFromKeyboard() {
if (_canSubmit) {
_submit();
}
}
void _focusQuantityInput() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_quantityFocusNode.requestFocus();
_quantityController.selection = TextSelection(
baseOffset: 0,
extentOffset: _quantityController.text.length,
);
});
}
int? get _currentManyToOneBoxNo {
final boxNo = int.tryParse(_boxNoController.text.trim());
if (boxNo == null || boxNo <= 0) return null;
@@ -526,6 +559,7 @@ class _BoxingPageState extends State<BoxingPage> {
);
_isDuplicateBoxNo = false;
});
_focusQuantityInput();
_showStatusOverride(
'请继续完成剩余数量装箱',
StatusDotColor.green,
@@ -687,11 +721,9 @@ class _BoxingPageState extends State<BoxingPage> {
void _startEditAssigned(CurrentZongpaiBoxData item) {
setState(() {
_editingBox = item;
_phase = _Phase.scanned;
_boxNoController.text = item.boxNo.toString();
_quantityController.text = item.quantity.toString();
_isDuplicateBoxNo = false;
_editingAssignedItemId = item.boxItemId;
_editingAssignedQuantity = item.quantity.toString();
_deletingAssignedItemId = null;
_statusOverrideText = null;
_statusOverrideDot = null;
});
@@ -699,13 +731,158 @@ class _BoxingPageState extends State<BoxingPage> {
void _cancelEditAssigned() {
setState(() {
_editingBox = null;
_boxNoController.text = ((_lastBoxNo ?? _maxBoxNo) + 1).toString();
_quantityController.clear();
_isDuplicateBoxNo = false;
_editingAssignedItemId = null;
_editingAssignedQuantity = '';
});
}
int _maxAssignedQuantity(CurrentZongpaiBoxData item) {
final total = _erpQuantity ?? 0;
final packedWithoutItem = _packedQuantity - item.quantity;
return total - packedWithoutItem;
}
void _refreshSingleCodeNewInput({bool focusQuantity = true}) {
_lastBoxNo = _maxBoxNo;
_boxNoController.text = (_maxBoxNo + 1).toString();
final remaining = _remainingQuantity;
if (remaining > 0) {
_quantityController.text = remaining.toString();
_quantityController.selection = TextSelection(
baseOffset: 0,
extentOffset: _quantityController.text.length,
);
if (focusQuantity) {
_focusQuantityInput();
}
} else {
_quantityController.clear();
}
_isDuplicateBoxNo = false;
}
Future<void> _saveAssignedItem(CurrentZongpaiBoxData item) async {
final quantity = int.tryParse(_editingAssignedQuantity);
if (quantity == null || quantity <= 0) {
_showStatusOverride(
'请输入有效数量',
StatusDotColor.red,
const Duration(seconds: 2),
);
return;
}
if (quantity > _maxAssignedQuantity(item)) {
_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: item.boxNo,
quantity: quantity,
);
if (!mounted) return;
setState(() => _isSubmitting = false);
if (result.success) {
setState(() {
_currentZongpaiBoxes = _currentZongpaiBoxes.map((box) {
if (box.boxItemId != item.boxItemId) return box;
return CurrentZongpaiBoxData(
boxItemId: box.boxItemId,
boxNo: box.boxNo,
quantity: quantity,
);
}).toList();
_replaceExistingBoxItem(item, item.boxNo, quantity);
_editingAssignedItemId = null;
_editingAssignedQuantity = '';
_refreshSingleCodeNewInput();
});
_showStatusOverride(
'修改成功',
StatusDotColor.green,
const Duration(milliseconds: 1500),
);
} else {
final dot = result.isDuplicate
? StatusDotColor.amber
: StatusDotColor.red;
_showStatusOverride(
result.errorMessage ?? '修改失败',
dot,
const Duration(seconds: 2),
);
}
}
Future<void> _deleteAssignedItem(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 (_editingAssignedItemId == item.boxItemId) {
_editingAssignedItemId = null;
_editingAssignedQuantity = '';
}
_deletingAssignedItemId = null;
_refreshSingleCodeNewInput();
}
});
if (result.success) {
_showStatusOverride(
'删除成功',
StatusDotColor.green,
const Duration(milliseconds: 1500),
);
} else {
_showStatusOverride(
result.errorMessage ?? '删除失败',
StatusDotColor.red,
const Duration(seconds: 2),
);
}
}
void _startEditPackedItem(_ManyToOnePackedItem item) {
setState(() {
_editingPackedItemId = item.boxItemId;
@@ -1123,43 +1300,132 @@ class _BoxingPageState extends State<BoxingPage> {
style: TextStyle(fontSize: 13, color: Colors.black54),
),
const SizedBox(height: 6),
..._currentZongpaiBoxes.map((item) {
final editing = _editingBox?.boxItemId == item.boxItemId;
..._currentZongpaiBoxes.map(_buildAssignedBoxRow),
],
);
}
Widget _buildAssignedBoxRow(CurrentZongpaiBoxData item) {
final editing = _editingAssignedItemId == item.boxItemId;
final deleting = _deletingAssignedItemId == item.boxItemId;
return Container(
margin: const EdgeInsets.only(bottom: 6),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: editing ? Colors.blue.shade50 : Colors.grey.shade50,
color: editing ? Colors.orange.shade50 : Colors.grey.shade50,
border: Border.all(
color: editing ? Colors.blue : Colors.grey.shade300,
color: editing ? Colors.orange : 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,
child: Column(
children: [
Row(
children: [
Expanded(
child: Text(
'${item.boxNo}号箱',
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: _editingAssignedQuantity)
..selection = TextSelection.collapsed(
offset: _editingAssignedQuantity.length,
),
onChanged: (value) => _editingAssignedQuantity = value,
onEditingComplete: () {},
onSubmitted: (_) => _saveAssignedItem(item),
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 6),
border: OutlineInputBorder(),
),
),
)
else
Text(
'数量 ${item.quantity}',
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
: () => _saveAssignedItem(item),
),
IconButton(
tooltip: '取消',
visualDensity: VisualDensity.compact,
icon: const Icon(Icons.close, size: 18),
onPressed: _isSubmitting ? null : _cancelEditAssigned,
),
] else ...[
IconButton(
tooltip: '修改',
visualDensity: VisualDensity.compact,
icon: const Icon(Icons.edit, size: 18),
onPressed: () => _startEditAssigned(item),
onPressed: _isSubmitting
? null
: () => _startEditAssigned(item),
),
IconButton(
tooltip: '删除',
visualDensity: VisualDensity.compact,
icon: const Icon(Icons.delete_outline, size: 18),
onPressed: _isSubmitting
? null
: () {
setState(() {
_deletingAssignedItemId = item.boxItemId;
_editingAssignedItemId = null;
_editingAssignedQuantity = '';
});
},
),
],
],
),
if (deleting)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Row(
children: [
const Expanded(
child: Text('确认删除此条记录?', style: TextStyle(fontSize: 13)),
),
TextButton(
onPressed: _isSubmitting
? null
: () => _deleteAssignedItem(item),
child: const Text(''),
),
TextButton(
onPressed: _isSubmitting
? null
: () => setState(() => _deletingAssignedItemId = null),
child: const Text(''),
),
],
),
),
);
}),
],
),
);
}
@@ -1206,6 +1472,8 @@ class _BoxingPageState extends State<BoxingPage> {
keyboardType: TextInputType.none,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
textAlign: TextAlign.center,
onEditingComplete: () {},
onSubmitted: (_) => _submitFromKeyboard(),
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
border: OutlineInputBorder(
@@ -1474,6 +1742,8 @@ class _BoxingPageState extends State<BoxingPage> {
keyboardType: TextInputType.none,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (_) => setState(() {}),
onEditingComplete: () {},
onSubmitted: (_) => _submitFromKeyboard(),
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
border: const OutlineInputBorder(),
@@ -1722,6 +1992,8 @@ class _BoxingPageState extends State<BoxingPage> {
keyboardType: TextInputType.none,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (_) => _checkDuplicateBoxNo(),
onEditingComplete: () {},
onSubmitted: (_) => _submitFromKeyboard(),
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
border: const OutlineInputBorder(),
@@ -1763,6 +2035,8 @@ class _BoxingPageState extends State<BoxingPage> {
keyboardType: TextInputType.none,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (_) => setState(() {}),
onEditingComplete: () {},
onSubmitted: (_) => _submitFromKeyboard(),
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
border: const OutlineInputBorder(),