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:
Misaka_Company
2026-05-21 09:02:56 +08:00
parent f2b2f41929
commit c2d9f5fd56
10 changed files with 1066 additions and 492 deletions

View File

@@ -8,37 +8,22 @@ import 'package:pad_scanner/services/code_parser.dart';
import 'package:pad_scanner/services/api_service.dart';
import 'package:pad_scanner/services/boxing_context.dart';
import 'package:pad_scanner/services/feedback_service.dart';
import 'package:pad_scanner/pages/boxing/boxing_box_mutations.dart'
as box_mutations;
import 'package:pad_scanner/pages/boxing/boxing_calculations.dart'
as boxing_calculations;
import 'package:pad_scanner/pages/boxing/boxing_models.dart';
import 'package:pad_scanner/pages/boxing/boxing_status_presenter.dart';
import 'package:pad_scanner/pages/boxing/dialogs/cross_paichan_dialog.dart';
import 'package:pad_scanner/pages/boxing/widgets/boxing_shared_widgets.dart';
import 'package:pad_scanner/pages/boxing/widgets/boxing_notice_banners.dart';
import 'package:pad_scanner/pages/boxing/widgets/multi_code_body.dart';
import 'package:pad_scanner/pages/boxing/widgets/single_code_body.dart';
import 'package:pad_scanner/pages/boxing_detail_page.dart';
import 'package:pad_scanner/widgets/status_bar.dart';
// === 装箱模式 ===
enum BoxingMode {
singleCode, // 单码装箱
multiCode, // 多码凑箱
}
class BoxingPageArguments {
final BoxingMode initialMode;
final List<String> autoScanCodes;
const BoxingPageArguments({
required this.initialMode,
required this.autoScanCodes,
});
}
// === 页面阶段 ===
enum _Phase {
waiting, // 等待扫码
scanned, // 已扫码,显示信息
submitted, // 已提交成功
}
export 'package:pad_scanner/pages/boxing/boxing_models.dart'
show BoxingMode, BoxingPageArguments;
// === 主页面 ===
@@ -62,7 +47,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
BoxingMode _mode = BoxingMode.singleCode;
// 阶段
_Phase _phase = _Phase.waiting;
BoxingPhase _phase = BoxingPhase.waiting;
// 当前总排号
String? _zongpaiNo;
@@ -221,7 +206,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
}
void _resetState() {
_phase = _Phase.waiting;
_phase = BoxingPhase.waiting;
_zongpaiNo = null;
_paichanNo = null;
_workOrderNo = null;
@@ -334,7 +319,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
_currentZongpaiBoxes = result.currentZongpaiBoxes;
_existingBoxes = result.existingBoxes;
_maxBoxNo = result.maxBoxNo;
_phase = _Phase.scanned;
_phase = BoxingPhase.scanned;
_isDuplicateBoxNo = false;
_editingBox = null;
_editingAssignedItemId = null;
@@ -469,98 +454,16 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
// === 跨排产号确认对话框 ===
void _showCrossPaichaDialog() {
Future<void> _showCrossPaichaDialog() async {
final currentPaicha = _lastScannedPaichanNo ?? '--';
final newPaicha = _paichanNo ?? '--';
final dialogFocus = FocusNode();
var dismissed = false;
var selectedIndex = 0; // 0 = "否" (default), 1 = "是"
void dismissAsNo() {
if (dismissed) return;
dismissed = true;
Navigator.of(context).pop();
_cancelPaichanSwitch();
}
void confirmSwitch() {
if (dismissed) return;
dismissed = true;
Navigator.of(context).pop();
_confirmPaichanSwitch();
}
showDialog(
final confirmed = await showCrossPaichanDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => StatefulBuilder(
builder: (ctx, setDialogState) {
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),
_dialogOptionBtn(
label: '',
selected: selectedIndex == 0,
onTap: dismissAsNo,
),
const SizedBox(height: 8),
_dialogOptionBtn(
label: '是,切换排产号',
selected: selectedIndex == 1,
onTap: confirmSwitch,
),
],
),
),
),
);
},
),
currentPaicha: currentPaicha,
newPaicha: newPaicha,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
dialogFocus.requestFocus();
});
if (!mounted) return;
confirmed ? _confirmPaichanSwitch() : _cancelPaichanSwitch();
}
void _confirmPaichanSwitch() {
@@ -609,50 +512,20 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
_crossPaichaPending = false;
_zongpaiNo = null;
_paichanNo = _lastScannedPaichanNo;
_phase = _Phase.waiting;
_phase = BoxingPhase.waiting;
});
}
Widget _dialogOptionBtn({
required String label,
required bool selected,
required VoidCallback onTap,
}) {
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,
),
),
),
);
}
// === 重复箱号检测 ===
bool _boxNoIsDuplicate() {
final boxNo = int.tryParse(_boxNoController.text);
if (boxNo == null) {
return false;
}
return switch (_mode) {
BoxingMode.multiCode => false,
BoxingMode.singleCode => _currentZongpaiBoxes.any((b) {
if (b.boxNo != boxNo) return false;
return _editingBox == null || b.boxItemId != _editingBox!.boxItemId;
}),
BoxingMode.singleCode => boxing_calculations.singleCodeBoxNoIsDuplicate(
boxNoText: _boxNoController.text,
currentBoxes: _currentZongpaiBoxes,
editingBoxItemId: _editingBox?.boxItemId,
),
};
}
@@ -663,33 +536,34 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
// === 提交 ===
int get _packedQuantity {
return _currentZongpaiBoxes.fold<int>(
0,
(sum, item) => sum + item.quantity,
);
return boxing_calculations.packedQuantity(_currentZongpaiBoxes);
}
int get _remainingQuantity {
final total = _erpQuantity ?? 0;
return total - _packedQuantity;
return boxing_calculations.remainingQuantity(
totalQuantity: _erpQuantity,
currentBoxes: _currentZongpaiBoxes,
);
}
bool get _quantityTooHigh {
if (_remainingQuantity <= 0) return false;
final qty = int.tryParse(_quantityController.text);
return qty != null && qty > _remainingQuantity;
return boxing_calculations.quantityTooHigh(
remainingQuantity: _remainingQuantity,
quantityText: _quantityController.text,
);
}
bool get _canSubmit {
if (_isSubmitting || _phase != _Phase.scanned) return false;
if (_zongpaiNo == null) return false;
final boxNo = int.tryParse(_boxNoController.text);
final qty = int.tryParse(_quantityController.text);
if (boxNo == null || boxNo <= 0 || qty == null || qty <= 0) return false;
if (_remainingQuantity <= 0) return false;
if (_quantityTooHigh) return false;
if (_isDuplicateBoxNo) return false;
return true;
return boxing_calculations.canSubmitBoxing(
isSubmitting: _isSubmitting,
phase: _phase,
zongpaiNo: _zongpaiNo,
boxNoText: _boxNoController.text,
quantityText: _quantityController.text,
remainingQuantity: _remainingQuantity,
quantityTooHigh: _quantityTooHigh,
isDuplicateBoxNo: _isDuplicateBoxNo,
);
}
void _submitFromKeyboard() {
@@ -710,36 +584,16 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
}
int? get _currentManyToOneBoxNo {
final boxNo = int.tryParse(_boxNoController.text.trim());
if (boxNo == null || boxNo <= 0) return null;
return boxNo;
return boxing_calculations.currentManyToOneBoxNo(_boxNoController.text);
}
List<ManyToOnePackedItem> get _visibleManyToOnePackedItems {
final boxNo = _currentManyToOneBoxNo;
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 _manyToOnePackedItems.where(
(item) => item.boxNo == boxNo,
)) {
byItemId[item.boxItemId] = item;
}
return byItemId.values.toList(growable: false);
return boxing_calculations.visibleManyToOnePackedItems(
boxNo: _currentManyToOneBoxNo,
existingBoxes: _existingBoxes,
packedItems: _manyToOnePackedItems,
paichanNo: _paichanNo,
);
}
Future<bool> _submit() async {
@@ -839,7 +693,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
);
} else {
setState(() {
_phase = _Phase.scanned;
_phase = BoxingPhase.scanned;
_boxNoController.text = (_maxBoxNo + 1).toString();
_quantityController.text = remaining.toString();
_quantityController.selection = TextSelection(
@@ -872,7 +726,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
),
);
}
_phase = _Phase.waiting;
_phase = BoxingPhase.waiting;
_zongpaiNo = null;
_editingPackedItemId = null;
_editingPackedQuantity = '';
@@ -925,62 +779,31 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
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,
final result = box_mutations.replaceExistingBoxItem(
existingBoxes: _existingBoxes,
editing: editing,
boxNo: boxNo,
quantity: quantity,
zongpaiNo: _zongpaiNo!,
workOrderNo: _workOrderNo,
quantity: quantity,
totalQuantity: _erpQuantity,
);
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,
);
_existingBoxes = result.boxes;
_maxBoxNo = result.maxBoxNo;
}
void _addExistingBoxItem(int boxNo, int quantity, int? boxItemId) {
final item = BoxItemData(
final result = box_mutations.addExistingBoxItem(
existingBoxes: _existingBoxes,
boxNo: boxNo,
quantity: quantity,
boxItemId: boxItemId,
zongpaiNo: _zongpaiNo!,
workOrderNo: _workOrderNo,
quantity: quantity,
totalQuantity: _erpQuantity,
);
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;
_existingBoxes = result.boxes;
_maxBoxNo = result.maxBoxNo;
}
void _finishManyToOneBox() {
@@ -990,7 +813,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
_editingPackedQuantity = '';
_deletingPackedItemId = null;
_zongpaiNo = null;
_phase = _Phase.waiting;
_phase = BoxingPhase.waiting;
_quantityController.clear();
_boxNoController.text = (_maxBoxNo + 1).toString();
_statusOverrideText = null;
@@ -1016,9 +839,11 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
}
int _maxAssignedQuantity(CurrentZongpaiBoxData item) {
final total = _erpQuantity ?? 0;
final packedWithoutItem = _packedQuantity - item.quantity;
return total - packedWithoutItem;
return boxing_calculations.maxAssignedQuantity(
totalQuantity: _erpQuantity,
packedQuantity: _packedQuantity,
itemQuantity: item.quantity,
);
}
void _refreshSingleCodeNewInput({bool focusQuantity = true}) {
@@ -1246,39 +1071,14 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
int boxNo,
int quantity,
) {
final nextBoxes = <BoxDetailData>[];
for (final box in _existingBoxes) {
final items = box.items.where((boxItem) {
return boxItem.boxItemId != item.boxItemId;
}).toList();
if (items.isNotEmpty) {
nextBoxes.add(BoxDetailData(boxNo: box.boxNo, items: items));
}
}
final updatedItem = BoxItemData(
boxItemId: item.boxItemId,
zongpaiNo: item.zongpaiNo,
workOrderNo: item.workOrderNo,
final result = box_mutations.replaceExistingPackedItem(
existingBoxes: _existingBoxes,
item: item,
boxNo: boxNo,
quantity: quantity,
totalQuantity: item.totalQuantity,
);
final targetIndex = nextBoxes.indexWhere((box) => box.boxNo == boxNo);
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,
);
_existingBoxes = result.boxes;
_maxBoxNo = result.maxBoxNo;
}
Future<void> _deletePackedItem(ManyToOnePackedItem item) async {
@@ -1331,20 +1131,12 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
}
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,
final result = box_mutations.removeExistingBoxItem(
existingBoxes: _existingBoxes,
boxItemId: boxItemId,
);
_existingBoxes = result.boxes;
_maxBoxNo = result.maxBoxNo;
}
// === Status bar management ===
@@ -1392,69 +1184,22 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
// === 状态文字 ===
void _updateBaseStatus() {
if (_isAutoProcessing) {
_statusDot = StatusDotColor.orange;
_statusText = '正在同步转运数据...';
return;
}
if (_isSubmitting) {
_statusDot = StatusDotColor.orange;
_statusText = '正在提交…';
return;
}
if (_isDuplicateBoxNo && _phase == _Phase.scanned) {
_statusDot = StatusDotColor.amber;
_statusText = '箱号 ${_boxNoController.text} 已存在,请重新输入';
return;
}
if (_quantityTooHigh && _phase == _Phase.scanned) {
_statusDot = StatusDotColor.red;
_statusText = '超出可装数量上限';
return;
}
if (_editingAssignedItemId != null) {
_statusDot = StatusDotColor.amber;
_statusText = '正在编辑已分配记录';
return;
}
if (_deletingAssignedItemId != null) {
_statusDot = StatusDotColor.red;
_statusText = '请确认是否删除该箱记录';
return;
}
if (_paichanSwitchNotice != null) {
_statusDot = StatusDotColor.blue;
_statusText = '排产号已切换,箱号已重置';
return;
}
switch (_phase) {
case _Phase.waiting:
_statusDot = StatusDotColor.blue;
if (_mode == BoxingMode.multiCode &&
_visibleManyToOnePackedItems.isNotEmpty) {
_statusText = '请继续扫码或完成本箱';
} else {
_statusText = '等待扫码';
}
case _Phase.scanned:
_statusDot = StatusDotColor.blue;
if (_remainingQuantity <= 0) {
_statusDot = StatusDotColor.red;
_statusText = '该总排号已全部装箱完毕';
return;
}
_statusText = _mode == BoxingMode.multiCode
? '数量已填入,请确认或修改'
: '数量已填入,请确认或修改';
case _Phase.submitted:
_statusDot = StatusDotColor.green;
switch (_mode) {
case BoxingMode.singleCode:
_statusText = '装箱成功,可继续扫码';
case BoxingMode.multiCode:
_statusText = '请扫描下一个总排号';
}
}
final status = boxingStatusFor(
isAutoProcessing: _isAutoProcessing,
isSubmitting: _isSubmitting,
isDuplicateBoxNo: _isDuplicateBoxNo,
quantityTooHigh: _quantityTooHigh,
isEditingAssigned: _editingAssignedItemId != null,
isDeletingAssigned: _deletingAssignedItemId != null,
hasPaichanSwitchNotice: _paichanSwitchNotice != null,
phase: _phase,
isMultiCode: _mode == BoxingMode.multiCode,
hasVisibleManyToOneItems: _visibleManyToOnePackedItems.isNotEmpty,
remainingQuantity: _remainingQuantity,
boxNoText: _boxNoController.text,
);
_statusDot = status.dot;
_statusText = status.text;
}
// === Build ===
@@ -1487,12 +1232,25 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
),
body: Column(
children: [
..._buildNoticeBanners(),
BoxingNoticeBanners(
isAutoProcessing: _isAutoProcessing,
autoWarnings: _autoWarnings,
paichanSwitchNotice: _paichanSwitchNotice,
isDuplicateBoxNo: _isDuplicateBoxNo,
quantityTooHigh: _quantityTooHigh,
phase: _phase,
boxNoText: _boxNoController.text,
remainingQuantity: _remainingQuantity,
zongpaiNo: _zongpaiNo,
completedJumpBoxNo: _completedJumpBoxNo,
onJumpToCompletedBox: _jumpToCompletedBox,
),
Expanded(
child: _mode == BoxingMode.multiCode
? MultiCodeBody(
hasScan: _zongpaiNo != null && _phase == _Phase.scanned,
hasScan:
_zongpaiNo != null && _phase == BoxingPhase.scanned,
zongpaiNo: _zongpaiNo,
paichanNo: _paichanNo,
workOrderNo: _workOrderNo,
@@ -1532,9 +1290,10 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
setState(() => _deletingPackedItemId = null),
)
: SingleCodeBody(
isWaiting: _phase == _Phase.waiting && _zongpaiNo == null,
isWaiting:
_phase == BoxingPhase.waiting && _zongpaiNo == null,
isFinished:
_phase == _Phase.scanned &&
_phase == BoxingPhase.scanned &&
_zongpaiNo != null &&
_remainingQuantity <= 0,
zongpaiNo: _zongpaiNo,
@@ -1586,139 +1345,4 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
),
);
}
List<Widget> _buildNoticeBanners() {
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 == _Phase.scanned) {
banners.add(
_noticeBanner(
icon: Icon(
Icons.warning_amber,
size: 14,
color: Colors.amber.shade900,
),
text: '箱号 ${_boxNoController.text} 已存在,请重新输入',
background: Colors.amber.shade100,
foreground: Colors.amber.shade900,
border: Colors.amber.shade200,
),
);
}
if (_quantityTooHigh && _phase == _Phase.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 == _Phase.scanned &&
_zongpaiNo != null &&
_remainingQuantity <= 0) {
final jumpBoxNo = _completedJumpBoxNo;
if (jumpBoxNo != null) {
banners.add(
GestureDetector(
onTap: _jumpToCompletedBox,
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 banners;
}
Widget _noticeBanner({
required Widget icon,
required String text,
required Color background,
required Color foreground,
required Color border,
}) {
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,
),
),
],
),
);
}
}