refactor: extract boxing logic into dedicated modules with tests
- boxing_models.dart: add BoxingMode, BoxingPhase, BoxingPageArguments - boxing_box_mutations.dart: box item add/replace/remove operations - boxing_calculations.dart: pure calculation helpers (quantities, validation) - boxing_status_presenter.dart: status bar text/dot determination - dialogs/cross_paichan_dialog.dart: cross-paichan confirmation dialog - widgets/boxing_notice_banners.dart: notice banner widgets - Add unit tests for mutations, calculations, and status presenter Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
135
lib/pages/boxing/boxing_box_mutations.dart
Normal file
135
lib/pages/boxing/boxing_box_mutations.dart
Normal file
@@ -0,0 +1,135 @@
|
||||
import 'package:pad_scanner/pages/boxing/boxing_models.dart';
|
||||
import 'package:pad_scanner/services/api_service.dart';
|
||||
|
||||
class BoxMutationResult {
|
||||
final List<BoxDetailData> boxes;
|
||||
final int maxBoxNo;
|
||||
|
||||
const BoxMutationResult({required this.boxes, required this.maxBoxNo});
|
||||
}
|
||||
|
||||
int maxBoxNoFor(List<BoxDetailData> boxes) {
|
||||
return boxes.fold<int>(0, (max, box) => box.boxNo > max ? box.boxNo : max);
|
||||
}
|
||||
|
||||
BoxMutationResult addExistingBoxItem({
|
||||
required List<BoxDetailData> existingBoxes,
|
||||
required int boxNo,
|
||||
required int quantity,
|
||||
required int? boxItemId,
|
||||
required String zongpaiNo,
|
||||
required String? workOrderNo,
|
||||
required int? totalQuantity,
|
||||
}) {
|
||||
final item = BoxItemData(
|
||||
boxItemId: boxItemId,
|
||||
zongpaiNo: zongpaiNo,
|
||||
workOrderNo: workOrderNo,
|
||||
quantity: quantity,
|
||||
totalQuantity: totalQuantity,
|
||||
);
|
||||
final index = existingBoxes.indexWhere((box) => box.boxNo == boxNo);
|
||||
if (index < 0) {
|
||||
final boxes = [
|
||||
...existingBoxes,
|
||||
BoxDetailData(boxNo: boxNo, items: [item]),
|
||||
]..sort((a, b) => a.boxNo.compareTo(b.boxNo));
|
||||
return BoxMutationResult(boxes: boxes, maxBoxNo: maxBoxNoFor(boxes));
|
||||
}
|
||||
|
||||
final boxes = List<BoxDetailData>.from(existingBoxes);
|
||||
final box = boxes[index];
|
||||
boxes[index] = BoxDetailData(boxNo: box.boxNo, items: [...box.items, item]);
|
||||
return BoxMutationResult(boxes: boxes, maxBoxNo: maxBoxNoFor(boxes));
|
||||
}
|
||||
|
||||
BoxMutationResult replaceExistingBoxItem({
|
||||
required List<BoxDetailData> existingBoxes,
|
||||
required CurrentZongpaiBoxData editing,
|
||||
required int boxNo,
|
||||
required int quantity,
|
||||
required String zongpaiNo,
|
||||
required String? workOrderNo,
|
||||
required int? totalQuantity,
|
||||
}) {
|
||||
final updatedItem = BoxItemData(
|
||||
boxItemId: editing.boxItemId,
|
||||
zongpaiNo: zongpaiNo,
|
||||
workOrderNo: workOrderNo,
|
||||
quantity: quantity,
|
||||
totalQuantity: totalQuantity,
|
||||
);
|
||||
return _replaceItemById(
|
||||
existingBoxes: existingBoxes,
|
||||
boxItemId: editing.boxItemId,
|
||||
targetBoxNo: boxNo,
|
||||
updatedItem: updatedItem,
|
||||
);
|
||||
}
|
||||
|
||||
BoxMutationResult replaceExistingPackedItem({
|
||||
required List<BoxDetailData> existingBoxes,
|
||||
required ManyToOnePackedItem item,
|
||||
required int boxNo,
|
||||
required int quantity,
|
||||
}) {
|
||||
final updatedItem = BoxItemData(
|
||||
boxItemId: item.boxItemId,
|
||||
zongpaiNo: item.zongpaiNo,
|
||||
workOrderNo: item.workOrderNo,
|
||||
quantity: quantity,
|
||||
totalQuantity: item.totalQuantity,
|
||||
);
|
||||
return _replaceItemById(
|
||||
existingBoxes: existingBoxes,
|
||||
boxItemId: item.boxItemId,
|
||||
targetBoxNo: boxNo,
|
||||
updatedItem: updatedItem,
|
||||
);
|
||||
}
|
||||
|
||||
BoxMutationResult removeExistingBoxItem({
|
||||
required List<BoxDetailData> existingBoxes,
|
||||
required int boxItemId,
|
||||
}) {
|
||||
final boxes = <BoxDetailData>[];
|
||||
for (final box in existingBoxes) {
|
||||
final items = box.items
|
||||
.where((item) => item.boxItemId != boxItemId)
|
||||
.toList();
|
||||
if (items.isNotEmpty) {
|
||||
boxes.add(BoxDetailData(boxNo: box.boxNo, items: items));
|
||||
}
|
||||
}
|
||||
return BoxMutationResult(boxes: boxes, maxBoxNo: maxBoxNoFor(boxes));
|
||||
}
|
||||
|
||||
BoxMutationResult _replaceItemById({
|
||||
required List<BoxDetailData> existingBoxes,
|
||||
required int boxItemId,
|
||||
required int targetBoxNo,
|
||||
required BoxItemData updatedItem,
|
||||
}) {
|
||||
final boxes = <BoxDetailData>[];
|
||||
for (final box in existingBoxes) {
|
||||
final items = box.items.where((item) {
|
||||
return item.boxItemId != boxItemId;
|
||||
}).toList();
|
||||
if (items.isNotEmpty) {
|
||||
boxes.add(BoxDetailData(boxNo: box.boxNo, items: items));
|
||||
}
|
||||
}
|
||||
|
||||
final targetIndex = boxes.indexWhere((box) => box.boxNo == targetBoxNo);
|
||||
if (targetIndex >= 0) {
|
||||
final target = boxes[targetIndex];
|
||||
boxes[targetIndex] = BoxDetailData(
|
||||
boxNo: target.boxNo,
|
||||
items: [...target.items, updatedItem],
|
||||
);
|
||||
} else {
|
||||
boxes.add(BoxDetailData(boxNo: targetBoxNo, items: [updatedItem]));
|
||||
}
|
||||
boxes.sort((a, b) => a.boxNo.compareTo(b.boxNo));
|
||||
return BoxMutationResult(boxes: boxes, maxBoxNo: maxBoxNoFor(boxes));
|
||||
}
|
||||
101
lib/pages/boxing/boxing_calculations.dart
Normal file
101
lib/pages/boxing/boxing_calculations.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
import 'package:pad_scanner/pages/boxing/boxing_models.dart';
|
||||
import 'package:pad_scanner/services/api_service.dart';
|
||||
|
||||
int packedQuantity(List<CurrentZongpaiBoxData> items) {
|
||||
return items.fold<int>(0, (sum, item) => sum + item.quantity);
|
||||
}
|
||||
|
||||
int remainingQuantity({
|
||||
required int? totalQuantity,
|
||||
required List<CurrentZongpaiBoxData> currentBoxes,
|
||||
}) {
|
||||
return (totalQuantity ?? 0) - packedQuantity(currentBoxes);
|
||||
}
|
||||
|
||||
bool quantityTooHigh({
|
||||
required int remainingQuantity,
|
||||
required String quantityText,
|
||||
}) {
|
||||
if (remainingQuantity <= 0) return false;
|
||||
final quantity = int.tryParse(quantityText);
|
||||
return quantity != null && quantity > remainingQuantity;
|
||||
}
|
||||
|
||||
bool canSubmitBoxing({
|
||||
required bool isSubmitting,
|
||||
required BoxingPhase phase,
|
||||
required String? zongpaiNo,
|
||||
required String boxNoText,
|
||||
required String quantityText,
|
||||
required int remainingQuantity,
|
||||
required bool quantityTooHigh,
|
||||
required bool isDuplicateBoxNo,
|
||||
}) {
|
||||
if (isSubmitting || phase != BoxingPhase.scanned) return false;
|
||||
if (zongpaiNo == null) return false;
|
||||
final boxNo = int.tryParse(boxNoText);
|
||||
final quantity = int.tryParse(quantityText);
|
||||
if (boxNo == null || boxNo <= 0 || quantity == null || quantity <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (remainingQuantity <= 0) return false;
|
||||
if (quantityTooHigh) return false;
|
||||
if (isDuplicateBoxNo) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
int? currentManyToOneBoxNo(String boxNoText) {
|
||||
final boxNo = int.tryParse(boxNoText.trim());
|
||||
if (boxNo == null || boxNo <= 0) return null;
|
||||
return boxNo;
|
||||
}
|
||||
|
||||
bool singleCodeBoxNoIsDuplicate({
|
||||
required String boxNoText,
|
||||
required List<CurrentZongpaiBoxData> currentBoxes,
|
||||
required int? editingBoxItemId,
|
||||
}) {
|
||||
final boxNo = int.tryParse(boxNoText);
|
||||
if (boxNo == null) return false;
|
||||
return currentBoxes.any((box) {
|
||||
if (box.boxNo != boxNo) return false;
|
||||
return editingBoxItemId == null || box.boxItemId != editingBoxItemId;
|
||||
});
|
||||
}
|
||||
|
||||
List<ManyToOnePackedItem> visibleManyToOnePackedItems({
|
||||
required int? boxNo,
|
||||
required List<BoxDetailData> existingBoxes,
|
||||
required List<ManyToOnePackedItem> packedItems,
|
||||
required String? paichanNo,
|
||||
}) {
|
||||
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 packedItems.where((item) => item.boxNo == boxNo)) {
|
||||
byItemId[item.boxItemId] = item;
|
||||
}
|
||||
return byItemId.values.toList(growable: false);
|
||||
}
|
||||
|
||||
int maxAssignedQuantity({
|
||||
required int? totalQuantity,
|
||||
required int packedQuantity,
|
||||
required int itemQuantity,
|
||||
}) {
|
||||
return (totalQuantity ?? 0) - (packedQuantity - itemQuantity);
|
||||
}
|
||||
@@ -1,3 +1,24 @@
|
||||
enum BoxingMode {
|
||||
singleCode, // 单码装箱
|
||||
multiCode, // 多码凑箱
|
||||
}
|
||||
|
||||
enum BoxingPhase {
|
||||
waiting, // 等待扫码
|
||||
scanned, // 已扫码,显示信息
|
||||
submitted, // 已提交成功
|
||||
}
|
||||
|
||||
class BoxingPageArguments {
|
||||
final BoxingMode initialMode;
|
||||
final List<String> autoScanCodes;
|
||||
|
||||
const BoxingPageArguments({
|
||||
required this.initialMode,
|
||||
required this.autoScanCodes,
|
||||
});
|
||||
}
|
||||
|
||||
class ManyToOnePackedItem {
|
||||
final int boxItemId;
|
||||
final String zongpaiNo;
|
||||
|
||||
97
lib/pages/boxing/boxing_status_presenter.dart
Normal file
97
lib/pages/boxing/boxing_status_presenter.dart
Normal file
@@ -0,0 +1,97 @@
|
||||
import 'package:pad_scanner/pages/boxing/boxing_models.dart';
|
||||
import 'package:pad_scanner/widgets/status_bar.dart';
|
||||
|
||||
class BoxingStatusPresentation {
|
||||
final StatusDotColor dot;
|
||||
final String text;
|
||||
|
||||
const BoxingStatusPresentation({required this.dot, required this.text});
|
||||
}
|
||||
|
||||
BoxingStatusPresentation boxingStatusFor({
|
||||
required bool isAutoProcessing,
|
||||
required bool isSubmitting,
|
||||
required bool isDuplicateBoxNo,
|
||||
required bool quantityTooHigh,
|
||||
required bool isEditingAssigned,
|
||||
required bool isDeletingAssigned,
|
||||
required bool hasPaichanSwitchNotice,
|
||||
required BoxingPhase phase,
|
||||
required bool isMultiCode,
|
||||
required bool hasVisibleManyToOneItems,
|
||||
required int remainingQuantity,
|
||||
required String boxNoText,
|
||||
}) {
|
||||
if (isAutoProcessing) {
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.orange,
|
||||
text: '正在同步转运数据...',
|
||||
);
|
||||
}
|
||||
if (isSubmitting) {
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.orange,
|
||||
text: '正在提交…',
|
||||
);
|
||||
}
|
||||
if (isDuplicateBoxNo && phase == BoxingPhase.scanned) {
|
||||
return BoxingStatusPresentation(
|
||||
dot: StatusDotColor.amber,
|
||||
text: '箱号 $boxNoText 已存在,请重新输入',
|
||||
);
|
||||
}
|
||||
if (quantityTooHigh && phase == BoxingPhase.scanned) {
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.red,
|
||||
text: '超出可装数量上限',
|
||||
);
|
||||
}
|
||||
if (isEditingAssigned) {
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.amber,
|
||||
text: '正在编辑已分配记录',
|
||||
);
|
||||
}
|
||||
if (isDeletingAssigned) {
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.red,
|
||||
text: '请确认是否删除该箱记录',
|
||||
);
|
||||
}
|
||||
if (hasPaichanSwitchNotice) {
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.blue,
|
||||
text: '排产号已切换,箱号已重置',
|
||||
);
|
||||
}
|
||||
|
||||
switch (phase) {
|
||||
case BoxingPhase.waiting:
|
||||
if (isMultiCode && hasVisibleManyToOneItems) {
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.blue,
|
||||
text: '请继续扫码或完成本箱',
|
||||
);
|
||||
}
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.blue,
|
||||
text: '等待扫码',
|
||||
);
|
||||
case BoxingPhase.scanned:
|
||||
if (remainingQuantity <= 0) {
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.red,
|
||||
text: '该总排号已全部装箱完毕',
|
||||
);
|
||||
}
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.blue,
|
||||
text: '数量已填入,请确认或修改',
|
||||
);
|
||||
case BoxingPhase.submitted:
|
||||
return BoxingStatusPresentation(
|
||||
dot: StatusDotColor.green,
|
||||
text: isMultiCode ? '请扫描下一个总排号' : '装箱成功,可继续扫码',
|
||||
);
|
||||
}
|
||||
}
|
||||
126
lib/pages/boxing/dialogs/cross_paichan_dialog.dart
Normal file
126
lib/pages/boxing/dialogs/cross_paichan_dialog.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
Future<bool> showCrossPaichanDialog({
|
||||
required BuildContext context,
|
||||
required String currentPaicha,
|
||||
required String newPaicha,
|
||||
}) async {
|
||||
final dialogFocus = FocusNode();
|
||||
var selectedIndex = 0; // 0 = "否" (default), 1 = "是"
|
||||
|
||||
final dialogFuture = showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, setDialogState) {
|
||||
void dismissAsNo() => Navigator.of(ctx).pop(false);
|
||||
void confirmSwitch() => Navigator.of(ctx).pop(true);
|
||||
|
||||
return KeyboardListener(
|
||||
focusNode: dialogFocus,
|
||||
onKeyEvent: (event) {
|
||||
if (event is! KeyDownEvent) return;
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||
setDialogState(() => selectedIndex = 0);
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
setDialogState(() => selectedIndex = 1);
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||||
selectedIndex == 0 ? dismissAsNo() : confirmSwitch();
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
dismissAsNo();
|
||||
}
|
||||
},
|
||||
child: PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop) dismissAsNo();
|
||||
},
|
||||
child: AlertDialog(
|
||||
backgroundColor: Colors.orange.shade50,
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber, color: Colors.orange.shade800),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'跨排产号扫描',
|
||||
style: TextStyle(color: Colors.orange.shade900),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'当前排产号:$currentPaicha\n'
|
||||
'新排产号:$newPaicha\n\n'
|
||||
'是否切换到新的排产号?\n\n'
|
||||
'选择「是」将放弃当前已扫描的所有数据。',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_DialogOptionButton(
|
||||
label: '否',
|
||||
selected: selectedIndex == 0,
|
||||
onTap: dismissAsNo,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_DialogOptionButton(
|
||||
label: '是,切换排产号',
|
||||
selected: selectedIndex == 1,
|
||||
onTap: confirmSwitch,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
dialogFocus.requestFocus();
|
||||
});
|
||||
|
||||
final result = await dialogFuture;
|
||||
dialogFocus.dispose();
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
class _DialogOptionButton extends StatelessWidget {
|
||||
final String label;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _DialogOptionButton({
|
||||
required this.label,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 40,
|
||||
child: ElevatedButton(
|
||||
onPressed: onTap,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: selected
|
||||
? Colors.blue.shade700
|
||||
: Colors.grey.shade200,
|
||||
foregroundColor: selected ? Colors.white : Colors.black87,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontWeight: selected ? FontWeight.w800 : FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
178
lib/pages/boxing/widgets/boxing_notice_banners.dart
Normal file
178
lib/pages/boxing/widgets/boxing_notice_banners.dart
Normal file
@@ -0,0 +1,178 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pad_scanner/pages/boxing/boxing_models.dart';
|
||||
|
||||
class BoxingNoticeBanners extends StatelessWidget {
|
||||
final bool isAutoProcessing;
|
||||
final List<String> autoWarnings;
|
||||
final String? paichanSwitchNotice;
|
||||
final bool isDuplicateBoxNo;
|
||||
final bool quantityTooHigh;
|
||||
final BoxingPhase phase;
|
||||
final String boxNoText;
|
||||
final int remainingQuantity;
|
||||
final String? zongpaiNo;
|
||||
final int? completedJumpBoxNo;
|
||||
final VoidCallback onJumpToCompletedBox;
|
||||
|
||||
const BoxingNoticeBanners({
|
||||
super.key,
|
||||
required this.isAutoProcessing,
|
||||
required this.autoWarnings,
|
||||
required this.paichanSwitchNotice,
|
||||
required this.isDuplicateBoxNo,
|
||||
required this.quantityTooHigh,
|
||||
required this.phase,
|
||||
required this.boxNoText,
|
||||
required this.remainingQuantity,
|
||||
required this.zongpaiNo,
|
||||
required this.completedJumpBoxNo,
|
||||
required this.onJumpToCompletedBox,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final banners = <Widget>[];
|
||||
if (isAutoProcessing) {
|
||||
banners.add(
|
||||
_NoticeBanner(
|
||||
icon: SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1.5,
|
||||
color: Colors.orange.shade700,
|
||||
),
|
||||
),
|
||||
text: '正在同步转运数据...',
|
||||
background: Colors.orange.shade50,
|
||||
foreground: Colors.orange.shade900,
|
||||
border: Colors.orange.shade100,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (autoWarnings.isNotEmpty) {
|
||||
banners.add(
|
||||
_NoticeBanner(
|
||||
icon: Icon(
|
||||
Icons.warning_amber,
|
||||
size: 14,
|
||||
color: Colors.amber.shade900,
|
||||
),
|
||||
text: autoWarnings.join(';'),
|
||||
background: Colors.amber.shade100,
|
||||
foreground: Colors.amber.shade900,
|
||||
border: Colors.amber.shade200,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (paichanSwitchNotice != null) {
|
||||
banners.add(
|
||||
_NoticeBanner(
|
||||
icon: Icon(Icons.info_outline, size: 14, color: Colors.blue.shade900),
|
||||
text: paichanSwitchNotice!,
|
||||
background: Colors.blue.shade50,
|
||||
foreground: Colors.blue.shade900,
|
||||
border: Colors.blue.shade100,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (isDuplicateBoxNo && phase == BoxingPhase.scanned) {
|
||||
banners.add(
|
||||
_NoticeBanner(
|
||||
icon: Icon(
|
||||
Icons.warning_amber,
|
||||
size: 14,
|
||||
color: Colors.amber.shade900,
|
||||
),
|
||||
text: '箱号 $boxNoText 已存在,请重新输入',
|
||||
background: Colors.amber.shade100,
|
||||
foreground: Colors.amber.shade900,
|
||||
border: Colors.amber.shade200,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (quantityTooHigh && phase == BoxingPhase.scanned) {
|
||||
banners.add(
|
||||
_NoticeBanner(
|
||||
icon: Icon(Icons.close, size: 14, color: Colors.red.shade800),
|
||||
text: '超出可装数量上限(最多可装 $remainingQuantity 件)',
|
||||
background: Colors.red.shade50,
|
||||
foreground: Colors.red.shade800,
|
||||
border: Colors.red.shade100,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (phase == BoxingPhase.scanned &&
|
||||
zongpaiNo != null &&
|
||||
remainingQuantity <= 0) {
|
||||
final jumpBoxNo = completedJumpBoxNo;
|
||||
if (jumpBoxNo != null) {
|
||||
banners.add(
|
||||
GestureDetector(
|
||||
onTap: onJumpToCompletedBox,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: _NoticeBanner(
|
||||
icon: Icon(Icons.warning, size: 14, color: Colors.amber.shade800),
|
||||
text: '$zongpaiNo 已完成装箱。(P4跳转所在箱号)',
|
||||
background: Colors.amber.shade50,
|
||||
foreground: Colors.amber.shade800,
|
||||
border: Colors.amber.shade200,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
banners.add(
|
||||
_NoticeBanner(
|
||||
icon: Icon(Icons.warning, size: 14, color: Colors.red.shade800),
|
||||
text: '$zongpaiNo 已全部装箱完毕,无需操作',
|
||||
background: Colors.red.shade50,
|
||||
foreground: Colors.red.shade800,
|
||||
border: Colors.red.shade100,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: banners);
|
||||
}
|
||||
}
|
||||
|
||||
class _NoticeBanner extends StatelessWidget {
|
||||
final Widget icon;
|
||||
final String text;
|
||||
final Color background;
|
||||
final Color foreground;
|
||||
final Color border;
|
||||
|
||||
const _NoticeBanner({
|
||||
required this.icon,
|
||||
required this.text,
|
||||
required this.background,
|
||||
required this.foreground,
|
||||
required this.border,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: background,
|
||||
border: Border(bottom: BorderSide(color: border)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(fontSize: 12, color: foreground),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user