Files
pad_scanner/lib/pages/boxing/widgets/single_code_body.dart
Misaka 9dc714dbeb feat: extend boxing page for accessory boxing support
Add pending accessories section to both single-code and multi-code boxing
modes. Accessories display with orange styling and a "加入本箱" button that
calls saveBoxRecord with item_type=accessory. Extend API service with
PendingAccessory model, itemType fields on data classes, and optional
accessory_id parameter on saveBoxRecord. Show accessories with visual
indicator (wrench icon + orange background) in boxing detail page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-24 18:01:01 +08:00

700 lines
22 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pad_scanner/pages/boxing/widgets/boxing_shared_widgets.dart';
import 'package:pad_scanner/pages/boxing/widgets/single_assigned_row.dart';
import 'package:pad_scanner/services/api_service.dart';
class SingleCodeBody extends StatelessWidget {
final bool isWaiting;
final bool isFinished;
final String? zongpaiNo;
final String? paichanNo;
final String? workOrderNo;
final int? erpQuantity;
final int packedQuantity;
final int maxBoxNo;
final List<CurrentZongpaiBoxData> assignedItems;
final List<BoxDetailData> existingBoxes;
final TextEditingController boxNoController;
final TextEditingController quantityController;
final FocusNode boxNoFocusNode;
final FocusNode quantityFocusNode;
final bool isSubmitting;
final bool isDuplicateBoxNo;
final bool quantityTooHigh;
final bool canSubmit;
final int? editingAssignedItemId;
final String editingAssignedQuantity;
final int? deletingAssignedItemId;
final VoidCallback onOpenDetail;
final VoidCallback onCheckDuplicateBoxNo;
final VoidCallback onSubmitFromKeyboard;
final VoidCallback onSubmit;
final VoidCallback onQuantityChanged;
final ValueChanged<String> onEditingAssignedQuantityChanged;
final ValueChanged<CurrentZongpaiBoxData> onSaveAssignedItem;
final VoidCallback onCancelEditAssigned;
final ValueChanged<CurrentZongpaiBoxData> onStartEditAssigned;
final ValueChanged<CurrentZongpaiBoxData> onStartDeleteAssigned;
final ValueChanged<CurrentZongpaiBoxData> onDeleteAssignedItem;
final VoidCallback onCancelDeleteAssigned;
final List<PendingAccessory> pendingAccessories;
final ValueChanged<PendingAccessory> onSubmitAccessory;
const SingleCodeBody({
super.key,
required this.isWaiting,
required this.isFinished,
required this.zongpaiNo,
required this.paichanNo,
required this.workOrderNo,
required this.erpQuantity,
required this.packedQuantity,
required this.maxBoxNo,
required this.assignedItems,
required this.existingBoxes,
required this.boxNoController,
required this.quantityController,
required this.boxNoFocusNode,
required this.quantityFocusNode,
required this.isSubmitting,
required this.isDuplicateBoxNo,
required this.quantityTooHigh,
required this.canSubmit,
required this.editingAssignedItemId,
required this.editingAssignedQuantity,
required this.deletingAssignedItemId,
required this.onOpenDetail,
required this.onCheckDuplicateBoxNo,
required this.onSubmitFromKeyboard,
required this.onSubmit,
required this.onQuantityChanged,
required this.onEditingAssignedQuantityChanged,
required this.onSaveAssignedItem,
required this.onCancelEditAssigned,
required this.onStartEditAssigned,
required this.onStartDeleteAssigned,
required this.onDeleteAssignedItem,
required this.onCancelDeleteAssigned,
required this.pendingAccessories,
required this.onSubmitAccessory,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Column(
children: [
_SingleInfoBar(
isWaiting: isWaiting,
paichanNo: paichanNo,
existingBoxes: existingBoxes,
maxBoxNo: maxBoxNo,
colorScheme: colorScheme,
onOpenDetail: onOpenDetail,
),
_SingleScanBar(
isWaiting: isWaiting,
zongpaiNo: zongpaiNo,
workOrderNo: workOrderNo,
erpQuantity: erpQuantity,
packedQuantity: packedQuantity,
),
_SingleAssignedList(
isWaiting: isWaiting,
isFinished: isFinished,
items: assignedItems,
workOrderNo: workOrderNo,
isSubmitting: isSubmitting,
editingAssignedItemId: editingAssignedItemId,
editingAssignedQuantity: editingAssignedQuantity,
deletingAssignedItemId: deletingAssignedItemId,
onEditingAssignedQuantityChanged: onEditingAssignedQuantityChanged,
onSaveAssignedItem: onSaveAssignedItem,
onCancelEditAssigned: onCancelEditAssigned,
onStartEditAssigned: onStartEditAssigned,
onStartDeleteAssigned: onStartDeleteAssigned,
onDeleteAssignedItem: onDeleteAssignedItem,
onCancelDeleteAssigned: onCancelDeleteAssigned,
),
if (pendingAccessories.isNotEmpty && !isWaiting)
_PendingAccessoriesSection(
pendingAccessories: pendingAccessories,
isSubmitting: isSubmitting,
onSubmitAccessory: onSubmitAccessory,
),
_SingleBottomArea(
disabled: isWaiting || isFinished,
boxNoController: boxNoController,
quantityController: quantityController,
boxNoFocusNode: boxNoFocusNode,
quantityFocusNode: quantityFocusNode,
isSubmitting: isSubmitting,
isDuplicateBoxNo: isDuplicateBoxNo,
quantityTooHigh: quantityTooHigh,
canSubmit: canSubmit,
onCheckDuplicateBoxNo: onCheckDuplicateBoxNo,
onSubmitFromKeyboard: onSubmitFromKeyboard,
onSubmit: onSubmit,
onQuantityChanged: onQuantityChanged,
),
],
);
}
}
class _SingleScanBar extends StatelessWidget {
final bool isWaiting;
final String? zongpaiNo;
final String? workOrderNo;
final int? erpQuantity;
final int packedQuantity;
const _SingleScanBar({
required this.isWaiting,
required this.zongpaiNo,
required this.workOrderNo,
required this.erpQuantity,
required this.packedQuantity,
});
@override
Widget build(BuildContext context) {
if (isWaiting) {
return Container(
height: 35,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
border: Border(bottom: BorderSide(color: Colors.grey.shade300)),
),
child: const Row(
children: [
Icon(Icons.qr_code_scanner, color: Colors.grey, size: 16),
SizedBox(width: 8),
Text(
'请扫描执行卡二维码',
style: TextStyle(color: Colors.grey, fontSize: 13),
),
],
),
);
}
return Container(
height: 35,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Colors.green.shade50,
border: Border(bottom: BorderSide(color: Colors.green.shade200)),
),
child: Row(
children: [
const Icon(Icons.check_circle, color: Colors.green, size: 18),
const SizedBox(width: 8),
Expanded(
child: Text(
zongpaiNo ?? '',
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Text(
'${workOrderNo ?? "--"} / ${erpQuantity ?? 0}件 / 已装$packedQuantity',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Colors.black54,
),
),
],
),
);
}
}
class _SingleInfoBar extends StatelessWidget {
final bool isWaiting;
final String? paichanNo;
final List<BoxDetailData> existingBoxes;
final int maxBoxNo;
final ColorScheme colorScheme;
final VoidCallback onOpenDetail;
const _SingleInfoBar({
required this.isWaiting,
required this.paichanNo,
required this.existingBoxes,
required this.maxBoxNo,
required this.colorScheme,
required this.onOpenDetail,
});
@override
Widget build(BuildContext context) {
final detailEnabled = !isWaiting && existingBoxes.isNotEmpty;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.grey.shade50,
border: Border(bottom: BorderSide(color: Colors.grey.shade300)),
),
child: Row(
children: [
Text(
isWaiting ? '排产号 --' : (paichanNo ?? '--'),
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
color: isWaiting ? Colors.grey.shade400 : Colors.black87,
),
overflow: TextOverflow.ellipsis,
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Text(
isWaiting ? '已有 -- 箱' : '已有 ${existingBoxes.length}',
style: TextStyle(
fontSize: 12,
color: isWaiting
? Colors.grey.shade400
: Colors.grey.shade600,
),
),
Text(
isWaiting ? '最大箱号 --' : '最大箱号 $maxBoxNo',
style: TextStyle(
fontSize: 12,
color: isWaiting
? Colors.grey.shade400
: Colors.grey.shade600,
),
),
],
),
),
const SizedBox(width: 10),
BoxingDetailButton(
enabled: detailEnabled,
colorScheme: colorScheme,
onPressed: onOpenDetail,
),
],
),
);
}
}
class _SingleAssignedList extends StatelessWidget {
final bool isWaiting;
final bool isFinished;
final List<CurrentZongpaiBoxData> items;
final String? workOrderNo;
final bool isSubmitting;
final int? editingAssignedItemId;
final String editingAssignedQuantity;
final int? deletingAssignedItemId;
final ValueChanged<String> onEditingAssignedQuantityChanged;
final ValueChanged<CurrentZongpaiBoxData> onSaveAssignedItem;
final VoidCallback onCancelEditAssigned;
final ValueChanged<CurrentZongpaiBoxData> onStartEditAssigned;
final ValueChanged<CurrentZongpaiBoxData> onStartDeleteAssigned;
final ValueChanged<CurrentZongpaiBoxData> onDeleteAssignedItem;
final VoidCallback onCancelDeleteAssigned;
const _SingleAssignedList({
required this.isWaiting,
required this.isFinished,
required this.items,
required this.workOrderNo,
required this.isSubmitting,
required this.editingAssignedItemId,
required this.editingAssignedQuantity,
required this.deletingAssignedItemId,
required this.onEditingAssignedQuantityChanged,
required this.onSaveAssignedItem,
required this.onCancelEditAssigned,
required this.onStartEditAssigned,
required this.onStartDeleteAssigned,
required this.onDeleteAssignedItem,
required this.onCancelDeleteAssigned,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Column(
children: [
const _SingleAssignedHeader(),
Expanded(
child: items.isEmpty
? Center(
child: Text(
isFinished
? '该总排号已全部装箱完毕'
: (isWaiting ? '扫码后显示已分配箱号记录' : '本次扫码尚未分配箱号'),
style: TextStyle(
fontSize: 13,
color: isFinished
? Colors.green.shade700
: Colors.grey.shade500,
fontStyle: FontStyle.italic,
),
),
)
: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return SingleAssignedRow(
item: item,
workOrderNo: workOrderNo,
editing: editingAssignedItemId == item.boxItemId,
deleting: deletingAssignedItemId == item.boxItemId,
isSubmitting: isSubmitting,
editingQuantity: editingAssignedQuantity,
onEditingQuantityChanged:
onEditingAssignedQuantityChanged,
onSave: () => onSaveAssignedItem(item),
onCancelEdit: onCancelEditAssigned,
onStartEdit: () => onStartEditAssigned(item),
onStartDelete: () => onStartDeleteAssigned(item),
onDelete: () => onDeleteAssignedItem(item),
onCancelDelete: onCancelDeleteAssigned,
);
},
),
),
],
),
);
}
}
class _SingleAssignedHeader extends StatelessWidget {
const _SingleAssignedHeader();
@override
Widget build(BuildContext context) {
return Container(
height: 28,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Colors.grey.shade200,
border: Border(bottom: BorderSide(color: Colors.grey.shade400)),
),
child: const Row(
children: [
Expanded(
flex: 22,
child: Text(
'箱号',
textAlign: TextAlign.center,
style: boxingHeaderStyle,
),
),
Expanded(
flex: 30,
child: Text(
'工令号',
textAlign: TextAlign.center,
style: boxingHeaderStyle,
),
),
Expanded(
flex: 23,
child: Text(
'数量',
textAlign: TextAlign.center,
style: boxingHeaderStyle,
),
),
SizedBox(
width: 54,
child: Text(
'操作',
textAlign: TextAlign.center,
style: boxingHeaderStyle,
),
),
],
),
);
}
}
class _SingleBottomArea extends StatelessWidget {
final bool disabled;
final TextEditingController boxNoController;
final TextEditingController quantityController;
final FocusNode boxNoFocusNode;
final FocusNode quantityFocusNode;
final bool isSubmitting;
final bool isDuplicateBoxNo;
final bool quantityTooHigh;
final bool canSubmit;
final VoidCallback onCheckDuplicateBoxNo;
final VoidCallback onSubmitFromKeyboard;
final VoidCallback onSubmit;
final VoidCallback onQuantityChanged;
const _SingleBottomArea({
required this.disabled,
required this.boxNoController,
required this.quantityController,
required this.boxNoFocusNode,
required this.quantityFocusNode,
required this.isSubmitting,
required this.isDuplicateBoxNo,
required this.quantityTooHigh,
required this.canSubmit,
required this.onCheckDuplicateBoxNo,
required this.onSubmitFromKeyboard,
required this.onSubmit,
required this.onQuantityChanged,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.grey.shade300)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
Text(
'箱号',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: disabled ? Colors.grey.shade400 : Colors.black54,
),
),
const SizedBox(width: 4),
SizedBox(
width: 52,
height: 34,
child: TextField(
controller: boxNoController,
focusNode: boxNoFocusNode,
enabled: !disabled && !isSubmitting,
keyboardType: TextInputType.none,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
textAlign: TextAlign.center,
onChanged: (_) => onCheckDuplicateBoxNo(),
onSubmitted: (_) => onSubmitFromKeyboard(),
decoration: compactInputDecoration(
disabled: disabled || isSubmitting,
borderColor: isDuplicateBoxNo
? Colors.amber
: Colors.blue.shade700,
),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
),
),
),
const SizedBox(width: 8),
Text(
'数量',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: disabled ? Colors.grey.shade400 : Colors.black54,
),
),
const SizedBox(width: 4),
SizedBox(
width: 52,
height: 34,
child: TextField(
controller: quantityController,
focusNode: quantityFocusNode,
enabled: !disabled && !isSubmitting,
keyboardType: TextInputType.none,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
textAlign: TextAlign.center,
onChanged: (_) => onQuantityChanged(),
onSubmitted: (_) => onSubmitFromKeyboard(),
decoration: compactInputDecoration(
disabled: disabled || isSubmitting,
borderColor: quantityTooHigh
? Colors.red
: Colors.blue.shade700,
),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
),
),
),
const Spacer(),
SizedBox(
height: 34,
child: ElevatedButton(
onPressed: canSubmit ? onSubmit : null,
style: ElevatedButton.styleFrom(
backgroundColor: canSubmit
? Theme.of(context).colorScheme.primary
: Colors.grey.shade300,
foregroundColor: canSubmit
? Colors.white
: Colors.grey.shade600,
padding: const EdgeInsets.symmetric(horizontal: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
child: isSubmitting
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text(
'确认',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
),
),
],
),
),
);
}
}
class _PendingAccessoriesSection extends StatelessWidget {
final List<PendingAccessory> pendingAccessories;
final bool isSubmitting;
final ValueChanged<PendingAccessory> onSubmitAccessory;
const _PendingAccessoriesSection({
required this.pendingAccessories,
required this.isSubmitting,
required this.onSubmitAccessory,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 28,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Colors.orange.shade50,
border: Border(bottom: BorderSide(color: Colors.orange.shade200)),
),
child: Row(
children: [
Icon(Icons.build, size: 14, color: Colors.orange.shade700),
const SizedBox(width: 6),
Text(
'待装箱附件(${pendingAccessories.length} 项)',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: Colors.orange.shade800,
),
),
],
),
),
...pendingAccessories.map(
(acc) => _AccessoryRow(
accessory: acc,
isSubmitting: isSubmitting,
onSubmit: () => onSubmitAccessory(acc),
),
),
],
);
}
}
class _AccessoryRow extends StatelessWidget {
final PendingAccessory accessory;
final bool isSubmitting;
final VoidCallback onSubmit;
const _AccessoryRow({
required this.accessory,
required this.isSubmitting,
required this.onSubmit,
});
@override
Widget build(BuildContext context) {
return Container(
height: 36,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Colors.orange.shade50,
border: Border(bottom: BorderSide(color: Colors.grey.shade200)),
),
child: Row(
children: [
Expanded(
flex: 40,
child: Text(
accessory.accessoryType,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
overflow: TextOverflow.ellipsis,
),
),
Expanded(
flex: 20,
child: Text(
'\u00d7${accessory.quantity}',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
),
SizedBox(
height: 28,
child: ElevatedButton(
onPressed: isSubmitting ? null : onSubmit,
style: ElevatedButton.styleFrom(
backgroundColor: isSubmitting
? Colors.grey.shade300
: Colors.orange.shade600,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
),
child: const Text(
'加入本箱',
style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700),
),
),
),
],
),
);
}
}