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

@@ -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,
),
),
],
),
);
}
}