- In multi-code boxing mode, scanning a completed total-row number shows a clickable warning banner; clicking it or pressing P4 navigates to that box number to view boxing details. - Unify key handling across all pages to LogicalKeyboardKey named constants, removing the _androidKeyCodeFromLogicalKey fallback path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2751 lines
86 KiB
Dart
2751 lines
86 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:pad_scanner/services/app_config_service.dart';
|
||
import 'package:pad_scanner/services/scanner_service.dart';
|
||
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_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, // 已提交成功
|
||
}
|
||
|
||
class _ManyToOnePackedItem {
|
||
final int boxItemId;
|
||
final String zongpaiNo;
|
||
final String? paichanNo;
|
||
final String? workOrderNo;
|
||
final int boxNo;
|
||
final int quantity;
|
||
final int? totalQuantity;
|
||
|
||
const _ManyToOnePackedItem({
|
||
required this.boxItemId,
|
||
required this.zongpaiNo,
|
||
this.paichanNo,
|
||
required this.workOrderNo,
|
||
required this.boxNo,
|
||
required this.quantity,
|
||
this.totalQuantity,
|
||
});
|
||
|
||
_ManyToOnePackedItem copyWith({
|
||
int? boxNo,
|
||
int? quantity,
|
||
int? totalQuantity,
|
||
}) {
|
||
return _ManyToOnePackedItem(
|
||
boxItemId: boxItemId,
|
||
zongpaiNo: zongpaiNo,
|
||
paichanNo: paichanNo,
|
||
workOrderNo: workOrderNo,
|
||
boxNo: boxNo ?? this.boxNo,
|
||
quantity: quantity ?? this.quantity,
|
||
totalQuantity: totalQuantity ?? this.totalQuantity,
|
||
);
|
||
}
|
||
}
|
||
|
||
// === 主页面 ===
|
||
|
||
class BoxingPage extends StatefulWidget {
|
||
final BoxingPageArguments? arguments;
|
||
|
||
const BoxingPage({super.key, this.arguments});
|
||
|
||
@override
|
||
State<BoxingPage> createState() => _BoxingPageState();
|
||
}
|
||
|
||
class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||
final _scannerService = ScannerService();
|
||
final _apiService = ApiService();
|
||
final _feedbackService = FeedbackService();
|
||
final _focusNode = FocusNode();
|
||
StreamSubscription<ScanResult>? _scanSubscription;
|
||
|
||
// 模式
|
||
BoxingMode _mode = BoxingMode.singleCode;
|
||
|
||
// 阶段
|
||
_Phase _phase = _Phase.waiting;
|
||
|
||
// 当前总排号
|
||
String? _zongpaiNo;
|
||
|
||
// 排产号信息(来自后端查询)
|
||
String? _paichanNo;
|
||
String? _workOrderNo;
|
||
int? _erpQuantity;
|
||
List<CurrentZongpaiBoxData> _currentZongpaiBoxes = [];
|
||
List<BoxDetailData> _existingBoxes = [];
|
||
int _maxBoxNo = 0;
|
||
|
||
// 输入
|
||
final _boxNoController = TextEditingController();
|
||
final _quantityController = TextEditingController();
|
||
final _boxNoFocusNode = FocusNode();
|
||
final _quantityFocusNode = FocusNode();
|
||
|
||
// 多码凑箱:本次操作已确认装入当前箱的明细
|
||
final _manyToOnePackedItems = <_ManyToOnePackedItem>[];
|
||
int? _editingPackedItemId;
|
||
String _editingPackedQuantity = '';
|
||
int? _deletingPackedItemId;
|
||
int? _editingAssignedItemId;
|
||
String _editingAssignedQuantity = '';
|
||
int? _deletingAssignedItemId;
|
||
String? _lastScannedPaichanNo;
|
||
String? _paichanSwitchNotice;
|
||
|
||
// 提交中
|
||
bool _isSubmitting = false;
|
||
|
||
// 单码装箱:上次提交的箱号(用于继续添加预填)
|
||
int? _lastBoxNo;
|
||
CurrentZongpaiBoxData? _editingBox;
|
||
|
||
// 重复箱号
|
||
bool _isDuplicateBoxNo = false;
|
||
|
||
// Status bar state
|
||
StatusDotColor _statusDot = StatusDotColor.blue;
|
||
String _statusText = '等待扫码';
|
||
String? _statusOverrideText;
|
||
StatusDotColor? _statusOverrideDot;
|
||
bool _isAutoProcessing = false;
|
||
final _autoWarnings = <String>[];
|
||
|
||
// 多码凑箱:已完成装箱时可跳转的箱号
|
||
int? _completedJumpBoxNo;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
WidgetsBinding.instance.addObserver(this);
|
||
final arguments = widget.arguments;
|
||
if (arguments != null) {
|
||
_mode = arguments.initialMode;
|
||
}
|
||
_boxNoController.addListener(_onBoxNoTextChanged);
|
||
_scanSubscription = _scannerService.scanResults.listen(_onScan);
|
||
if (arguments != null && arguments.autoScanCodes.isNotEmpty) {
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
_processAutoScanCodes(arguments.autoScanCodes);
|
||
});
|
||
}
|
||
WidgetsBinding.instance.addPostFrameCallback((_) => _requestFocus());
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
WidgetsBinding.instance.removeObserver(this);
|
||
_boxNoController.removeListener(_onBoxNoTextChanged);
|
||
_scanSubscription?.cancel();
|
||
_focusNode.dispose();
|
||
_boxNoController.dispose();
|
||
_quantityController.dispose();
|
||
_boxNoFocusNode.dispose();
|
||
_quantityFocusNode.dispose();
|
||
_feedbackService.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
void _requestFocus() {
|
||
if (mounted) {
|
||
_focusNode.requestFocus();
|
||
}
|
||
}
|
||
|
||
@override
|
||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||
if (state == AppLifecycleState.resumed) {
|
||
_requestFocus();
|
||
}
|
||
}
|
||
|
||
void _onKeyEvent(KeyEvent event) {
|
||
if (event is! KeyDownEvent || _isAutoProcessing) return;
|
||
if (_isModeSwitchKey(event)) {
|
||
_cycleMode();
|
||
}
|
||
if (_isFinishBoxKey(event)) {
|
||
final hasItems = _visibleManyToOnePackedItems.isNotEmpty;
|
||
if (hasItems && _mode == BoxingMode.multiCode) {
|
||
_finishManyToOneBox();
|
||
}
|
||
}
|
||
if (_completedJumpBoxNo != null && _isJumpToBoxKey(event)) {
|
||
_jumpToCompletedBox();
|
||
}
|
||
}
|
||
|
||
bool _isModeSwitchKey(KeyEvent event) {
|
||
final key = event.logicalKey;
|
||
return key == LogicalKeyboardKey.select ||
|
||
key == LogicalKeyboardKey.gameButtonRight1;
|
||
}
|
||
|
||
bool _isFinishBoxKey(KeyEvent event) {
|
||
return event.logicalKey == LogicalKeyboardKey.gameButtonLeft2;
|
||
}
|
||
|
||
bool _isJumpToBoxKey(KeyEvent event) {
|
||
return event.logicalKey == LogicalKeyboardKey.gameButtonRight2;
|
||
}
|
||
|
||
void _onBoxNoTextChanged() {
|
||
if (!mounted || _mode != BoxingMode.multiCode) return;
|
||
setState(() {
|
||
_editingPackedItemId = null;
|
||
_editingPackedQuantity = '';
|
||
_deletingPackedItemId = null;
|
||
});
|
||
}
|
||
|
||
// === 模式切换 ===
|
||
|
||
String get _modeLabel {
|
||
switch (_mode) {
|
||
case BoxingMode.singleCode:
|
||
return '单码装箱';
|
||
case BoxingMode.multiCode:
|
||
return '多码凑箱';
|
||
}
|
||
}
|
||
|
||
void _cycleMode() {
|
||
_feedbackService.trigger(FeedbackEvent.modeSwitch);
|
||
setState(() {
|
||
switch (_mode) {
|
||
case BoxingMode.singleCode:
|
||
_mode = BoxingMode.multiCode;
|
||
case BoxingMode.multiCode:
|
||
_mode = BoxingMode.singleCode;
|
||
}
|
||
_resetState();
|
||
});
|
||
}
|
||
|
||
void _resetState() {
|
||
_phase = _Phase.waiting;
|
||
_zongpaiNo = null;
|
||
_paichanNo = null;
|
||
_workOrderNo = null;
|
||
_erpQuantity = null;
|
||
_currentZongpaiBoxes = [];
|
||
_existingBoxes = [];
|
||
_maxBoxNo = 0;
|
||
_boxNoController.clear();
|
||
_quantityController.clear();
|
||
_manyToOnePackedItems.clear();
|
||
_editingPackedItemId = null;
|
||
_editingPackedQuantity = '';
|
||
_deletingPackedItemId = null;
|
||
_editingAssignedItemId = null;
|
||
_editingAssignedQuantity = '';
|
||
_deletingAssignedItemId = null;
|
||
_lastScannedPaichanNo = null;
|
||
_paichanSwitchNotice = null;
|
||
_isSubmitting = false;
|
||
_lastBoxNo = null;
|
||
_editingBox = null;
|
||
_isDuplicateBoxNo = false;
|
||
_statusOverrideText = null;
|
||
_statusOverrideDot = null;
|
||
}
|
||
|
||
// === 扫码处理 ===
|
||
|
||
void _onScan(ScanResult result) {
|
||
if (_isAutoProcessing) {
|
||
return;
|
||
}
|
||
|
||
final parsed = CodeParser.parse(result.barcode);
|
||
|
||
if (parsed.type != CodeType.zongpaiNo) {
|
||
_feedbackService.trigger(FeedbackEvent.scanInvalid);
|
||
_showStatusOverride(
|
||
'无效码,请重新扫描',
|
||
StatusDotColor.red,
|
||
const Duration(seconds: 2),
|
||
);
|
||
return;
|
||
}
|
||
|
||
final zongpai = parsed.value;
|
||
|
||
// 三种模式都允许后扫入的总排号覆盖当前扫码区;未确认数据不会入库。
|
||
_queryBoxInfo(zongpai);
|
||
}
|
||
|
||
Future<bool> _queryBoxInfo(String zongpai) async {
|
||
final configService = AppConfigService();
|
||
final baseUrl = await configService.getString('api_url') ?? '';
|
||
if (baseUrl.isEmpty) {
|
||
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||
_showStatusOverride(
|
||
'未配置 API 地址,请前往设置',
|
||
StatusDotColor.red,
|
||
const Duration(seconds: 2),
|
||
);
|
||
return false;
|
||
}
|
||
|
||
final result = await _apiService.fetchBoxInfo(
|
||
baseUrl: baseUrl,
|
||
zongpaiNo: zongpai,
|
||
);
|
||
|
||
if (!mounted) return false;
|
||
|
||
if (!result.success) {
|
||
final isNetwork = result.errorMessage == '网络异常,请检查网络连接';
|
||
_feedbackService.trigger(
|
||
isNetwork ? FeedbackEvent.networkError : FeedbackEvent.scanInvalid,
|
||
);
|
||
_showStatusOverride(
|
||
result.errorMessage ?? '查询失败',
|
||
isNetwork ? StatusDotColor.yellow : StatusDotColor.red,
|
||
const Duration(seconds: 2),
|
||
);
|
||
return false;
|
||
}
|
||
|
||
// 判断该总牌号是否已全部装箱完毕
|
||
final packedQuantity = result.currentZongpaiBoxes.fold<int>(
|
||
0,
|
||
(sum, item) => sum + item.quantity,
|
||
);
|
||
final totalQuantity = result.quantity ?? 0;
|
||
final alreadyCompleted = totalQuantity > 0 && packedQuantity >= totalQuantity;
|
||
|
||
_feedbackService.trigger(
|
||
alreadyCompleted
|
||
? FeedbackEvent.alreadyCompleted
|
||
: FeedbackEvent.scanValid,
|
||
);
|
||
|
||
setState(() {
|
||
_zongpaiNo = zongpai;
|
||
_paichanNo = result.paichanNo;
|
||
_workOrderNo = result.workOrderNo;
|
||
_erpQuantity = result.quantity;
|
||
_currentZongpaiBoxes = result.currentZongpaiBoxes;
|
||
_existingBoxes = result.existingBoxes;
|
||
_maxBoxNo = result.maxBoxNo;
|
||
_phase = _Phase.scanned;
|
||
_isDuplicateBoxNo = false;
|
||
_editingBox = null;
|
||
_editingAssignedItemId = null;
|
||
_editingAssignedQuantity = '';
|
||
_deletingAssignedItemId = null;
|
||
_completedJumpBoxNo = null;
|
||
_statusOverrideText = null;
|
||
if (_mode == BoxingMode.singleCode) {
|
||
_paichanSwitchNotice = null;
|
||
}
|
||
|
||
// 根据模式自动填充
|
||
_applyAutoFill();
|
||
});
|
||
return true;
|
||
}
|
||
|
||
Future<void> _processAutoScanCodes(List<String> codes) async {
|
||
if (!mounted) return;
|
||
setState(() {
|
||
_isAutoProcessing = true;
|
||
_autoWarnings.clear();
|
||
_statusOverrideText = '正在同步转运数据...';
|
||
_statusOverrideDot = StatusDotColor.orange;
|
||
});
|
||
|
||
if (_mode == BoxingMode.singleCode) {
|
||
final code = codes.first;
|
||
final ok = await _queryBoxInfo(code);
|
||
if (!ok && mounted) {
|
||
setState(() => _autoWarnings.add('总排号 $code 未找到排产号信息,已忽略'));
|
||
}
|
||
} else {
|
||
for (final code in codes) {
|
||
if (!mounted) return;
|
||
final queried = await _queryBoxInfo(code);
|
||
if (!queried) {
|
||
if (mounted) {
|
||
setState(() => _autoWarnings.add('总排号 $code 未找到排产号信息,已忽略'));
|
||
}
|
||
continue;
|
||
}
|
||
if (!_canSubmit) {
|
||
if (mounted) {
|
||
setState(() => _autoWarnings.add('总排号 $code 暂不能装箱,已忽略'));
|
||
}
|
||
continue;
|
||
}
|
||
final saved = await _submit();
|
||
if (!saved && mounted) {
|
||
setState(() => _autoWarnings.add('总排号 $code 自动装箱失败,请手动处理'));
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!mounted) return;
|
||
setState(() {
|
||
_isAutoProcessing = false;
|
||
_statusOverrideText = null;
|
||
_statusOverrideDot = null;
|
||
});
|
||
_focusQuantityInput();
|
||
_requestFocus();
|
||
}
|
||
|
||
void _applyAutoFill() {
|
||
switch (_mode) {
|
||
case BoxingMode.singleCode:
|
||
final remaining = _remainingQuantity;
|
||
if (remaining <= 0) {
|
||
_boxNoController.clear();
|
||
_quantityController.clear();
|
||
_statusOverrideText = '该总排号已全部装箱完毕';
|
||
_statusOverrideDot = StatusDotColor.red;
|
||
return;
|
||
}
|
||
if (_lastBoxNo != null) {
|
||
_boxNoController.text = (_lastBoxNo! + 1).toString();
|
||
} else {
|
||
_boxNoController.text = (_maxBoxNo + 1).toString();
|
||
}
|
||
_quantityController.text = remaining.toString();
|
||
_quantityController.selection = TextSelection(
|
||
baseOffset: 0,
|
||
extentOffset: _quantityController.text.length,
|
||
);
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (mounted && _mode == BoxingMode.singleCode) {
|
||
_quantityFocusNode.requestFocus();
|
||
}
|
||
});
|
||
case BoxingMode.multiCode:
|
||
final decision = decideMultiCodePaichanContext(
|
||
lastPaichanNo: _lastScannedPaichanNo,
|
||
currentPaichanNo: _paichanNo,
|
||
currentBoxNoText: _boxNoController.text,
|
||
maxBoxNo: _maxBoxNo,
|
||
);
|
||
if (decision.isSwitched) {
|
||
_manyToOnePackedItems.clear();
|
||
_editingPackedItemId = null;
|
||
_editingPackedQuantity = '';
|
||
_deletingPackedItemId = null;
|
||
_paichanSwitchNotice = '排产号已切换:${_paichanNo ?? "--"}';
|
||
_feedbackService.trigger(FeedbackEvent.paichanSwitch);
|
||
} else {
|
||
_paichanSwitchNotice = null;
|
||
}
|
||
final boxNoToApply = decision.boxNoToApply;
|
||
if (boxNoToApply != null) {
|
||
_boxNoController.text = boxNoToApply.toString();
|
||
}
|
||
_lastScannedPaichanNo = _paichanNo;
|
||
final remaining = _remainingQuantity;
|
||
if (remaining <= 0) {
|
||
_quantityController.clear();
|
||
if (_manyToOnePackedItems.isEmpty &&
|
||
_currentZongpaiBoxes.isNotEmpty) {
|
||
_completedJumpBoxNo = _currentZongpaiBoxes.first.boxNo;
|
||
}
|
||
_statusOverrideText = '该总排号已全部装箱完毕';
|
||
_statusOverrideDot = StatusDotColor.red;
|
||
return;
|
||
}
|
||
_quantityController.text = remaining.toString();
|
||
_quantityController.selection = TextSelection(
|
||
baseOffset: 0,
|
||
extentOffset: _quantityController.text.length,
|
||
);
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (mounted && _mode == BoxingMode.multiCode) {
|
||
_quantityFocusNode.requestFocus();
|
||
}
|
||
});
|
||
}
|
||
_isDuplicateBoxNo = _boxNoIsDuplicate();
|
||
}
|
||
|
||
// === 重复箱号检测 ===
|
||
|
||
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;
|
||
}),
|
||
};
|
||
}
|
||
|
||
void _checkDuplicateBoxNo() {
|
||
setState(() => _isDuplicateBoxNo = _boxNoIsDuplicate());
|
||
}
|
||
|
||
// === 提交 ===
|
||
|
||
int get _packedQuantity {
|
||
return _currentZongpaiBoxes.fold<int>(
|
||
0,
|
||
(sum, item) => sum + item.quantity,
|
||
);
|
||
}
|
||
|
||
int get _remainingQuantity {
|
||
final total = _erpQuantity ?? 0;
|
||
return total - _packedQuantity;
|
||
}
|
||
|
||
bool get _quantityTooHigh {
|
||
if (_remainingQuantity <= 0) return false;
|
||
final qty = int.tryParse(_quantityController.text);
|
||
return qty != null && qty > _remainingQuantity;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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;
|
||
return boxNo;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
Future<bool> _submit() async {
|
||
if (!_canSubmit) return false;
|
||
|
||
final configService = AppConfigService();
|
||
final baseUrl = await configService.getString('api_url') ?? '';
|
||
if (baseUrl.isEmpty) {
|
||
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||
_showStatusOverride(
|
||
'未配置 API 地址,请前往设置',
|
||
StatusDotColor.red,
|
||
const Duration(seconds: 2),
|
||
);
|
||
return false;
|
||
}
|
||
|
||
final boxNo = int.parse(_boxNoController.text);
|
||
final quantity = int.parse(_quantityController.text);
|
||
|
||
setState(() => _isSubmitting = true);
|
||
|
||
final editing = _editingBox;
|
||
final result = editing == null
|
||
? await _apiService.saveBoxRecord(
|
||
baseUrl: baseUrl,
|
||
zongpaiNo: _zongpaiNo!,
|
||
boxNo: boxNo,
|
||
quantity: quantity,
|
||
)
|
||
: await _apiService.updateBoxRecord(
|
||
baseUrl: baseUrl,
|
||
boxItemId: editing.boxItemId,
|
||
boxNo: boxNo,
|
||
quantity: quantity,
|
||
);
|
||
|
||
if (!mounted) return false;
|
||
|
||
setState(() => _isSubmitting = false);
|
||
|
||
if (result.success) {
|
||
if (editing == null) {
|
||
_onSubmitSuccess(boxNo, quantity, result.boxItemId);
|
||
} else {
|
||
_onUpdateSuccess(editing, boxNo, quantity);
|
||
}
|
||
return true;
|
||
} else if (result.isDuplicate) {
|
||
_feedbackService.trigger(FeedbackEvent.duplicateBoxNo);
|
||
_showStatusOverride(
|
||
'该总排号在箱号 ${result.boxNo ?? ""} 已存在,请重新输入',
|
||
StatusDotColor.amber,
|
||
const Duration(seconds: 2),
|
||
);
|
||
return false;
|
||
} else {
|
||
final isNetwork = result.errorMessage == '网络异常,请检查网络连接';
|
||
_feedbackService.trigger(
|
||
isNetwork ? FeedbackEvent.networkError : FeedbackEvent.submitFailure,
|
||
);
|
||
_showStatusOverride(
|
||
result.errorMessage ?? '提交失败',
|
||
isNetwork ? StatusDotColor.yellow : StatusDotColor.red,
|
||
const Duration(seconds: 2),
|
||
);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
void _onSubmitSuccess(int boxNo, int quantity, int? boxItemId) {
|
||
_feedbackService.trigger(FeedbackEvent.submitSuccess);
|
||
|
||
setState(() {
|
||
_lastBoxNo = boxNo;
|
||
if (_mode == BoxingMode.singleCode && boxItemId != null) {
|
||
_currentZongpaiBoxes = List.from(_currentZongpaiBoxes)
|
||
..add(
|
||
CurrentZongpaiBoxData(
|
||
boxItemId: boxItemId,
|
||
boxNo: boxNo,
|
||
quantity: quantity,
|
||
),
|
||
);
|
||
}
|
||
_addExistingBoxItem(boxNo, quantity, boxItemId);
|
||
_maxBoxNo = _maxBoxNo > boxNo ? _maxBoxNo : boxNo;
|
||
});
|
||
|
||
switch (_mode) {
|
||
case BoxingMode.singleCode:
|
||
final remaining = _remainingQuantity;
|
||
if (remaining <= 0) {
|
||
_showStatusOverride(
|
||
'装箱成功',
|
||
StatusDotColor.green,
|
||
const Duration(milliseconds: 1500),
|
||
);
|
||
} else {
|
||
setState(() {
|
||
_phase = _Phase.scanned;
|
||
_boxNoController.text = (boxNo + 1).toString();
|
||
_quantityController.text = remaining.toString();
|
||
_quantityController.selection = TextSelection(
|
||
baseOffset: 0,
|
||
extentOffset: _quantityController.text.length,
|
||
);
|
||
_isDuplicateBoxNo = false;
|
||
});
|
||
_focusQuantityInput();
|
||
_showStatusOverride(
|
||
'请继续完成剩余数量装箱',
|
||
StatusDotColor.green,
|
||
const Duration(milliseconds: 1500),
|
||
);
|
||
}
|
||
|
||
case BoxingMode.multiCode:
|
||
_lastScannedPaichanNo = _paichanNo;
|
||
setState(() {
|
||
if (boxItemId != null) {
|
||
_manyToOnePackedItems.add(
|
||
_ManyToOnePackedItem(
|
||
boxItemId: boxItemId,
|
||
zongpaiNo: _zongpaiNo!,
|
||
paichanNo: _paichanNo,
|
||
workOrderNo: _workOrderNo,
|
||
boxNo: boxNo,
|
||
quantity: quantity,
|
||
totalQuantity: _erpQuantity,
|
||
),
|
||
);
|
||
}
|
||
_phase = _Phase.waiting;
|
||
_zongpaiNo = null;
|
||
_editingPackedItemId = null;
|
||
_editingPackedQuantity = '';
|
||
_deletingPackedItemId = null;
|
||
});
|
||
_showStatusOverride(
|
||
'装箱成功,请继续扫码',
|
||
StatusDotColor.green,
|
||
const Duration(milliseconds: 1500),
|
||
);
|
||
}
|
||
}
|
||
|
||
void _onUpdateSuccess(
|
||
CurrentZongpaiBoxData editing,
|
||
int boxNo,
|
||
int quantity,
|
||
) {
|
||
_feedbackService.trigger(FeedbackEvent.submitSuccess);
|
||
|
||
setState(() {
|
||
_currentZongpaiBoxes = _currentZongpaiBoxes.map((item) {
|
||
if (item.boxItemId != editing.boxItemId) return item;
|
||
return CurrentZongpaiBoxData(
|
||
boxItemId: item.boxItemId,
|
||
boxNo: boxNo,
|
||
quantity: quantity,
|
||
);
|
||
}).toList();
|
||
_replaceExistingBoxItem(editing, boxNo, quantity);
|
||
_editingBox = null;
|
||
_lastBoxNo = _maxBoxNo;
|
||
_boxNoController.text = (_maxBoxNo + 1).toString();
|
||
final remaining = _remainingQuantity;
|
||
if (remaining > 0) {
|
||
_quantityController.text = remaining.toString();
|
||
} else {
|
||
_quantityController.clear();
|
||
}
|
||
_isDuplicateBoxNo = false;
|
||
});
|
||
_showStatusOverride(
|
||
'修改成功',
|
||
StatusDotColor.green,
|
||
const Duration(milliseconds: 1500),
|
||
);
|
||
}
|
||
|
||
void _replaceExistingBoxItem(
|
||
CurrentZongpaiBoxData editing,
|
||
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,
|
||
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,
|
||
);
|
||
}
|
||
|
||
void _addExistingBoxItem(int boxNo, int quantity, int? boxItemId) {
|
||
final item = BoxItemData(
|
||
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;
|
||
}
|
||
|
||
void _finishManyToOneBox() {
|
||
final currentBoxNo = int.tryParse(_boxNoController.text.trim());
|
||
setState(() {
|
||
_manyToOnePackedItems.clear();
|
||
_editingPackedItemId = null;
|
||
_editingPackedQuantity = '';
|
||
_deletingPackedItemId = null;
|
||
_zongpaiNo = null;
|
||
_phase = _Phase.waiting;
|
||
_quantityController.clear();
|
||
if (currentBoxNo != null && currentBoxNo > 0) {
|
||
_boxNoController.text = (currentBoxNo + 1).toString();
|
||
}
|
||
_statusOverrideText = null;
|
||
_statusOverrideDot = null;
|
||
});
|
||
}
|
||
|
||
void _startEditAssigned(CurrentZongpaiBoxData item) {
|
||
setState(() {
|
||
_editingAssignedItemId = item.boxItemId;
|
||
_editingAssignedQuantity = item.quantity.toString();
|
||
_deletingAssignedItemId = null;
|
||
_statusOverrideText = null;
|
||
_statusOverrideDot = null;
|
||
});
|
||
}
|
||
|
||
void _cancelEditAssigned() {
|
||
setState(() {
|
||
_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;
|
||
_editingPackedQuantity = item.quantity.toString();
|
||
_deletingPackedItemId = null;
|
||
});
|
||
}
|
||
|
||
void _cancelEditPackedItem() {
|
||
setState(() {
|
||
_editingPackedItemId = null;
|
||
_editingPackedQuantity = '';
|
||
});
|
||
}
|
||
|
||
Future<void> _savePackedItem(_ManyToOnePackedItem item) async {
|
||
final quantity = int.tryParse(_editingPackedQuantity);
|
||
final boxNo = int.tryParse(_boxNoController.text.trim());
|
||
if (quantity == null || quantity <= 0 || boxNo == null || boxNo <= 0) {
|
||
_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: boxNo,
|
||
quantity: quantity,
|
||
);
|
||
if (!mounted) return;
|
||
|
||
setState(() => _isSubmitting = false);
|
||
if (result.success) {
|
||
setState(() {
|
||
final index = _manyToOnePackedItems.indexWhere(
|
||
(packed) => packed.boxItemId == item.boxItemId,
|
||
);
|
||
if (index >= 0) {
|
||
_manyToOnePackedItems[index] = _manyToOnePackedItems[index].copyWith(
|
||
boxNo: boxNo,
|
||
quantity: quantity,
|
||
);
|
||
}
|
||
_replaceExistingPackedItem(item, boxNo, quantity);
|
||
_editingPackedItemId = null;
|
||
_editingPackedQuantity = '';
|
||
});
|
||
_showStatusOverride(
|
||
'修改成功',
|
||
StatusDotColor.green,
|
||
const Duration(milliseconds: 1500),
|
||
);
|
||
} else {
|
||
final dot = result.isDuplicate
|
||
? StatusDotColor.amber
|
||
: StatusDotColor.red;
|
||
_showStatusOverride(
|
||
result.errorMessage ?? '修改失败',
|
||
dot,
|
||
const Duration(seconds: 2),
|
||
);
|
||
}
|
||
}
|
||
|
||
void _replaceExistingPackedItem(
|
||
_ManyToOnePackedItem item,
|
||
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,
|
||
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,
|
||
);
|
||
}
|
||
|
||
Future<void> _deletePackedItem(_ManyToOnePackedItem 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) {
|
||
_manyToOnePackedItems.removeWhere(
|
||
(packed) => packed.boxItemId == item.boxItemId,
|
||
);
|
||
_removeExistingBoxItem(item.boxItemId);
|
||
if (_editingPackedItemId == item.boxItemId) {
|
||
_editingPackedItemId = null;
|
||
_editingPackedQuantity = '';
|
||
}
|
||
_deletingPackedItemId = null;
|
||
}
|
||
});
|
||
|
||
if (result.success) {
|
||
_showStatusOverride(
|
||
'删除成功',
|
||
StatusDotColor.green,
|
||
const Duration(milliseconds: 1500),
|
||
);
|
||
} else {
|
||
_showStatusOverride(
|
||
result.errorMessage ?? '删除失败',
|
||
StatusDotColor.red,
|
||
const Duration(seconds: 2),
|
||
);
|
||
}
|
||
}
|
||
|
||
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,
|
||
);
|
||
}
|
||
|
||
// === Status bar management ===
|
||
|
||
void _showStatusOverride(String text, StatusDotColor dot, Duration duration) {
|
||
setState(() {
|
||
_statusOverrideText = text;
|
||
_statusOverrideDot = dot;
|
||
});
|
||
Future.delayed(duration, () {
|
||
if (mounted) {
|
||
setState(() {
|
||
_statusOverrideText = null;
|
||
_statusOverrideDot = null;
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
void _jumpToCompletedBox() {
|
||
final boxNo = _completedJumpBoxNo;
|
||
if (boxNo == null) return;
|
||
setState(() {
|
||
_boxNoController.text = boxNo.toString();
|
||
_completedJumpBoxNo = null;
|
||
_statusOverrideText = null;
|
||
_statusOverrideDot = null;
|
||
});
|
||
}
|
||
|
||
// === 导航到详情页 ===
|
||
|
||
void _openDetail() {
|
||
Navigator.push(
|
||
context,
|
||
MaterialPageRoute(
|
||
builder: (_) => BoxingDetailPage(
|
||
paichanNo: _paichanNo ?? '',
|
||
existingBoxes: _existingBoxes,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// === 状态文字 ===
|
||
|
||
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 = '请扫描下一个总排号';
|
||
}
|
||
}
|
||
}
|
||
|
||
// === Build ===
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final colorScheme = Theme.of(context).colorScheme;
|
||
|
||
_updateBaseStatus();
|
||
final effectiveDot = _statusOverrideDot ?? _statusDot;
|
||
final effectiveText = _statusOverrideText ?? _statusText;
|
||
|
||
return KeyboardListener(
|
||
focusNode: _focusNode,
|
||
onKeyEvent: _onKeyEvent,
|
||
child: Scaffold(
|
||
appBar: AppBar(
|
||
title: Row(
|
||
children: [
|
||
const Text('装箱编号'),
|
||
const SizedBox(width: 8),
|
||
_buildModePill(),
|
||
],
|
||
),
|
||
toolbarHeight: 44,
|
||
titleSpacing: 12,
|
||
),
|
||
body: Column(
|
||
children: [
|
||
..._buildNoticeBanners(),
|
||
|
||
Expanded(
|
||
child: _mode == BoxingMode.multiCode
|
||
? _buildManyToOneBody(colorScheme)
|
||
: _buildSingleCodeBody(colorScheme),
|
||
),
|
||
|
||
// 底部状态栏
|
||
StatusBar(dotColor: effectiveDot, text: effectiveText),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
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,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildSingleScanBar(bool isWaiting) {
|
||
if (isWaiting) {
|
||
return Container(
|
||
height: 50,
|
||
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: 50,
|
||
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: 20, 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,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildSingleCodeBody(ColorScheme colorScheme) {
|
||
final isWaiting = _phase == _Phase.waiting && _zongpaiNo == null;
|
||
final isFinished =
|
||
_phase == _Phase.scanned &&
|
||
_zongpaiNo != null &&
|
||
_remainingQuantity <= 0;
|
||
|
||
return Column(
|
||
children: [
|
||
_buildSingleScanBar(isWaiting),
|
||
_buildSingleInfoBar(isWaiting, colorScheme),
|
||
_buildSingleAssignedList(isWaiting, isFinished),
|
||
_buildSingleBottomArea(isWaiting || isFinished),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildSingleInfoBar(bool isWaiting, ColorScheme colorScheme) {
|
||
final total = _erpQuantity ?? 0;
|
||
final packed = isWaiting ? 0 : _packedQuantity;
|
||
final progress = total <= 0 ? 0.0 : (packed / total).clamp(0.0, 1.0);
|
||
final finished = !isWaiting && total > 0 && packed >= total;
|
||
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: IntrinsicHeight(
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Text(
|
||
isWaiting ? '排产号 --' : (_paichanNo ?? '--'),
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w700,
|
||
color: isWaiting ? Colors.grey.shade400 : Colors.black87,
|
||
),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
const SizedBox(width: 10),
|
||
Expanded(
|
||
child: Text(
|
||
isWaiting
|
||
? '已有 -- 箱 · 最大箱号 --'
|
||
: '已有 ${_existingBoxes.length} 箱 · 最大箱号 $_maxBoxNo',
|
||
style: TextStyle(
|
||
fontSize: 10,
|
||
color: isWaiting
|
||
? Colors.grey.shade400
|
||
: Colors.grey.shade600,
|
||
),
|
||
overflow: TextOverflow.ellipsis,
|
||
textAlign: TextAlign.right,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 4),
|
||
ClipRRect(
|
||
borderRadius: BorderRadius.circular(2),
|
||
child: LinearProgressIndicator(
|
||
value: progress,
|
||
minHeight: 3,
|
||
backgroundColor: Colors.grey.shade300,
|
||
valueColor: AlwaysStoppedAnimation<Color>(
|
||
finished ? Colors.green.shade700 : colorScheme.primary,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(width: 10),
|
||
_buildDetailButton(detailEnabled, colorScheme),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildDetailButton(bool enabled, ColorScheme colorScheme) {
|
||
final foreground = enabled ? colorScheme.primary : Colors.grey.shade400;
|
||
final background = enabled ? Colors.blue.shade50 : Colors.grey.shade100;
|
||
final border = enabled ? colorScheme.primary : Colors.grey.shade300;
|
||
|
||
return SizedBox(
|
||
height: 34,
|
||
child: OutlinedButton.icon(
|
||
onPressed: enabled ? _openDetail : null,
|
||
icon: Icon(Icons.receipt_long, size: 15, color: foreground),
|
||
label: Text(
|
||
'详情',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w700,
|
||
color: foreground,
|
||
),
|
||
),
|
||
style: OutlinedButton.styleFrom(
|
||
backgroundColor: background,
|
||
side: BorderSide(color: border),
|
||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||
minimumSize: Size.zero,
|
||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildSingleAssignedList(bool isWaiting, bool isFinished) {
|
||
final items = _currentZongpaiBoxes;
|
||
return Expanded(
|
||
child: Column(
|
||
children: [
|
||
_buildSingleAssignedHeader(items.length),
|
||
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) {
|
||
return _buildSingleAssignedRow(items[index], index + 1);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildSingleAssignedHeader(int count) {
|
||
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: Row(
|
||
children: [
|
||
const Expanded(flex: 22, child: Text('箱号', style: _headerStyle)),
|
||
const Expanded(flex: 24, child: Text('工令号', style: _headerStyle)),
|
||
const Expanded(
|
||
flex: 14,
|
||
child: Text('数量', textAlign: TextAlign.center, style: _headerStyle),
|
||
),
|
||
const SizedBox(
|
||
width: 64,
|
||
child: Text('操作', textAlign: TextAlign.center, style: _headerStyle),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildSingleAssignedRow(CurrentZongpaiBoxData item, int index) {
|
||
final editing = _editingAssignedItemId == item.boxItemId;
|
||
final deleting = _deletingAssignedItemId == item.boxItemId;
|
||
final barColor = editing
|
||
? Colors.amber
|
||
: (deleting ? Colors.red : Colors.green);
|
||
|
||
final Color bgColor;
|
||
if (editing) {
|
||
bgColor = Colors.yellow.shade50;
|
||
} else if (deleting) {
|
||
bgColor = Colors.red.shade50;
|
||
} else {
|
||
bgColor = Colors.transparent;
|
||
}
|
||
|
||
final rowStyle = TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: Colors.black87,
|
||
);
|
||
|
||
return Column(
|
||
children: [
|
||
Container(
|
||
constraints: const BoxConstraints(minHeight: 36),
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||
decoration: BoxDecoration(
|
||
color: bgColor,
|
||
border: Border(
|
||
left: BorderSide(color: barColor, width: 3),
|
||
bottom: BorderSide(color: Colors.grey.shade200),
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
flex: 22,
|
||
child: Text(
|
||
'${item.boxNo} 号箱',
|
||
overflow: TextOverflow.ellipsis,
|
||
style: rowStyle.copyWith(fontWeight: FontWeight.w700),
|
||
),
|
||
),
|
||
Expanded(
|
||
flex: 24,
|
||
child: Text(
|
||
_workOrderNo ?? '--',
|
||
overflow: TextOverflow.ellipsis,
|
||
style: rowStyle,
|
||
),
|
||
),
|
||
Expanded(
|
||
flex: 14,
|
||
child: editing
|
||
? SizedBox(
|
||
width: 52,
|
||
height: 28,
|
||
child: TextField(
|
||
keyboardType: TextInputType.none,
|
||
inputFormatters: [
|
||
FilteringTextInputFormatter.digitsOnly,
|
||
],
|
||
textAlign: TextAlign.center,
|
||
controller: TextEditingController(
|
||
text: _editingAssignedQuantity,
|
||
)..selection = TextSelection.collapsed(
|
||
offset: _editingAssignedQuantity.length,
|
||
),
|
||
onChanged: (v) => _editingAssignedQuantity = v,
|
||
onSubmitted: (_) => _saveAssignedItem(item),
|
||
decoration: InputDecoration(
|
||
contentPadding: const EdgeInsets.symmetric(
|
||
horizontal: 4,
|
||
),
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(5),
|
||
borderSide: const BorderSide(
|
||
color: Colors.amber,
|
||
width: 1.5,
|
||
),
|
||
),
|
||
),
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
)
|
||
: Text(
|
||
'${item.quantity} 件',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w600,
|
||
color: deleting ? Colors.red : Colors.black54,
|
||
),
|
||
),
|
||
),
|
||
SizedBox(
|
||
width: 64,
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
if (editing) ...[
|
||
_iconBtn(
|
||
Icons.check,
|
||
color: Colors.green,
|
||
onTap: _isSubmitting
|
||
? null
|
||
: () => _saveAssignedItem(item),
|
||
),
|
||
_iconBtn(
|
||
Icons.close,
|
||
color: Colors.grey,
|
||
onTap: _isSubmitting ? null : _cancelEditAssigned,
|
||
),
|
||
] else ...[
|
||
_iconBtn(
|
||
Icons.edit,
|
||
color: Colors.grey.shade700,
|
||
onTap: _isSubmitting
|
||
? null
|
||
: () => _startEditAssigned(item),
|
||
),
|
||
_iconBtn(
|
||
Icons.delete_outline,
|
||
color: Colors.red,
|
||
onTap: _isSubmitting
|
||
? null
|
||
: () {
|
||
setState(() {
|
||
_deletingAssignedItemId = item.boxItemId;
|
||
_editingAssignedItemId = null;
|
||
_editingAssignedQuantity = '';
|
||
});
|
||
},
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
if (deleting)
|
||
Container(
|
||
height: 28,
|
||
padding: const EdgeInsets.only(left: 15, right: 12),
|
||
decoration: BoxDecoration(
|
||
color: Colors.red.shade50,
|
||
border: Border(bottom: BorderSide(color: Colors.red.shade200)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
'确认删除 ${item.boxNo} 号箱记录?',
|
||
style: const TextStyle(fontSize: 12, color: Colors.red),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
TextButton(
|
||
onPressed: _isSubmitting
|
||
? null
|
||
: () => _deleteAssignedItem(item),
|
||
style: TextButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 10,
|
||
vertical: 2,
|
||
),
|
||
minimumSize: Size.zero,
|
||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||
backgroundColor: Colors.red,
|
||
foregroundColor: Colors.white,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
),
|
||
child: const Text(
|
||
'删除',
|
||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700),
|
||
),
|
||
),
|
||
const SizedBox(width: 6),
|
||
TextButton(
|
||
onPressed: _isSubmitting
|
||
? null
|
||
: () => setState(() => _deletingAssignedItemId = null),
|
||
style: TextButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 10,
|
||
vertical: 2,
|
||
),
|
||
minimumSize: Size.zero,
|
||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||
),
|
||
child: const Text(
|
||
'取消',
|
||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildSingleBottomArea(bool disabled) {
|
||
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: (_) => _checkDuplicateBoxNo(),
|
||
onSubmitted: (_) => _submitFromKeyboard(),
|
||
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: 70,
|
||
height: 34,
|
||
child: TextField(
|
||
controller: _quantityController,
|
||
focusNode: _quantityFocusNode,
|
||
enabled: !disabled && !_isSubmitting,
|
||
keyboardType: TextInputType.none,
|
||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||
textAlign: TextAlign.center,
|
||
onChanged: (_) => setState(() {}),
|
||
onSubmitted: (_) => _submitFromKeyboard(),
|
||
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 ? _submit : 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,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
InputDecoration _compactInputDecoration({
|
||
required bool disabled,
|
||
required Color borderColor,
|
||
}) {
|
||
final disabledBorder = OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||
);
|
||
final activeBorder = OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
borderSide: BorderSide(color: borderColor, width: 1.5),
|
||
);
|
||
return InputDecoration(
|
||
contentPadding: EdgeInsets.zero,
|
||
border: activeBorder,
|
||
enabledBorder: activeBorder,
|
||
focusedBorder: activeBorder,
|
||
disabledBorder: disabledBorder,
|
||
filled: disabled,
|
||
fillColor: Colors.grey.shade100,
|
||
);
|
||
}
|
||
|
||
Widget _buildModePill() {
|
||
final isSingle = _mode == BoxingMode.singleCode;
|
||
final bgColor = isSingle ? Colors.blue.shade700 : Colors.orange.shade700;
|
||
|
||
return Material(
|
||
borderRadius: BorderRadius.circular(8),
|
||
color: bgColor,
|
||
child: InkWell(
|
||
borderRadius: BorderRadius.circular(8),
|
||
onTap: _isAutoProcessing ? null : _cycleMode,
|
||
child: Container(
|
||
height: 36,
|
||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
const Icon(Icons.swap_horiz, color: Colors.white, size: 18),
|
||
const SizedBox(width: 6),
|
||
Flexible(
|
||
child: FittedBox(
|
||
fit: BoxFit.scaleDown,
|
||
child: Text(
|
||
_modeLabel,
|
||
maxLines: 1,
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w800,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
'P2',
|
||
style: TextStyle(
|
||
color: Colors.white.withValues(alpha: 0.88),
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildManyToOneBody(ColorScheme colorScheme) {
|
||
final hasScan = _zongpaiNo != null && _phase == _Phase.scanned;
|
||
final visibleItems = _visibleManyToOnePackedItems;
|
||
|
||
return Column(
|
||
children: [
|
||
// Header bar: box number + paichan info merged
|
||
_buildManyToOneHeaderBar(),
|
||
// List header (sticky above scroll area)
|
||
_buildManyToOneListHeader(visibleItems.length),
|
||
// Scrollable list
|
||
Expanded(
|
||
child: visibleItems.isEmpty
|
||
? Center(
|
||
child: Text(
|
||
'尚未装入任何物料',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
color: Colors.grey.shade500,
|
||
fontStyle: FontStyle.italic,
|
||
),
|
||
),
|
||
)
|
||
: ListView.builder(
|
||
itemCount: visibleItems.length,
|
||
itemBuilder: (context, index) {
|
||
return _buildManyToOnePackedRow(
|
||
visibleItems[index],
|
||
index + 1,
|
||
);
|
||
},
|
||
),
|
||
),
|
||
// Fixed bottom area: scan bar + submit + finish
|
||
_buildManyToOneBottomArea(hasScan),
|
||
],
|
||
);
|
||
}
|
||
|
||
// ── Header bar: box number + paichan info ──
|
||
|
||
Widget _buildManyToOneHeaderBar() {
|
||
final hasDetail = _existingBoxes.isNotEmpty;
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: Colors.grey.shade50,
|
||
border: Border(bottom: BorderSide(color: Colors.grey.shade300)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
// Box number
|
||
const Text(
|
||
'箱号',
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.black54,
|
||
),
|
||
),
|
||
const SizedBox(width: 6),
|
||
SizedBox(
|
||
width: 52,
|
||
height: 32,
|
||
child: TextField(
|
||
controller: _boxNoController,
|
||
focusNode: _boxNoFocusNode,
|
||
enabled: !_isSubmitting,
|
||
keyboardType: TextInputType.none,
|
||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||
textAlign: TextAlign.center,
|
||
onEditingComplete: () {},
|
||
onSubmitted: (_) => _submitFromKeyboard(),
|
||
decoration: InputDecoration(
|
||
contentPadding: EdgeInsets.zero,
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
enabledBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
borderSide: BorderSide(color: Colors.grey.shade400),
|
||
),
|
||
focusedBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
borderSide: const BorderSide(color: Colors.blue, width: 1.5),
|
||
),
|
||
),
|
||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w800),
|
||
),
|
||
),
|
||
// Divider
|
||
Container(
|
||
width: 1,
|
||
height: 28,
|
||
margin: const EdgeInsets.symmetric(horizontal: 10),
|
||
color: Colors.grey.shade300,
|
||
),
|
||
// Paichan info
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(
|
||
_paichanNo ?? '--',
|
||
style: const TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
Text(
|
||
'已有 ${_existingBoxes.length} 箱 · 最大箱号 $_maxBoxNo',
|
||
style: TextStyle(fontSize: 11, color: Colors.grey.shade500),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(width: 10),
|
||
_buildDetailButton(hasDetail, Theme.of(context).colorScheme),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// ── List header ──
|
||
|
||
Widget _buildManyToOneListHeader(int count) {
|
||
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: Row(
|
||
children: [
|
||
const Expanded(flex: 22, child: Text('总排号', style: _headerStyle)),
|
||
const Expanded(flex: 24, child: Text('工令号', style: _headerStyle)),
|
||
const Expanded(
|
||
flex: 14,
|
||
child: Text('数量', textAlign: TextAlign.center, style: _headerStyle),
|
||
),
|
||
const SizedBox(
|
||
width: 64,
|
||
child: Text('操作', textAlign: TextAlign.center, style: _headerStyle),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// ── Packed item row ──
|
||
|
||
Widget _buildManyToOnePackedRow(_ManyToOnePackedItem item, int index) {
|
||
final editing = _editingPackedItemId == item.boxItemId;
|
||
final deleting = _deletingPackedItemId == item.boxItemId;
|
||
final totalText = item.totalQuantity?.toString() ?? '--';
|
||
|
||
final barColor = editing
|
||
? Colors.amber
|
||
: (deleting ? Colors.red : Colors.green);
|
||
|
||
final Color bgColor;
|
||
if (editing) {
|
||
bgColor = Colors.yellow.shade50;
|
||
} else if (deleting) {
|
||
bgColor = Colors.red.shade50;
|
||
} else {
|
||
bgColor = Colors.transparent;
|
||
}
|
||
|
||
final rowStyle = TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: Colors.black87,
|
||
);
|
||
|
||
return Column(
|
||
children: [
|
||
Container(
|
||
constraints: const BoxConstraints(minHeight: 36),
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||
decoration: BoxDecoration(
|
||
color: bgColor,
|
||
border: Border(
|
||
left: BorderSide(color: barColor, width: 3),
|
||
bottom: BorderSide(color: Colors.grey.shade200),
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
flex: 22,
|
||
child: Text(
|
||
item.zongpaiNo,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: rowStyle.copyWith(fontWeight: FontWeight.w700),
|
||
),
|
||
),
|
||
Expanded(
|
||
flex: 24,
|
||
child: Text(
|
||
item.workOrderNo ?? '--',
|
||
overflow: TextOverflow.ellipsis,
|
||
style: rowStyle,
|
||
),
|
||
),
|
||
Expanded(
|
||
flex: 14,
|
||
child: editing
|
||
? SizedBox(
|
||
width: 52,
|
||
height: 26,
|
||
child: TextField(
|
||
keyboardType: TextInputType.none,
|
||
inputFormatters: [
|
||
FilteringTextInputFormatter.digitsOnly,
|
||
],
|
||
textAlign: TextAlign.center,
|
||
controller: TextEditingController(
|
||
text: _editingPackedQuantity,
|
||
)..selection = TextSelection.collapsed(
|
||
offset: _editingPackedQuantity.length,
|
||
),
|
||
onChanged: (v) => _editingPackedQuantity = v,
|
||
decoration: InputDecoration(
|
||
contentPadding: const EdgeInsets.symmetric(
|
||
horizontal: 4,
|
||
),
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(4),
|
||
borderSide: const BorderSide(
|
||
color: Colors.amber,
|
||
width: 1.5,
|
||
),
|
||
),
|
||
),
|
||
style: const TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
)
|
||
: Text(
|
||
'${item.quantity} / $totalText',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w600,
|
||
color: deleting ? Colors.red : Colors.green.shade700,
|
||
),
|
||
),
|
||
),
|
||
SizedBox(
|
||
width: 64,
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
if (editing) ...[
|
||
_iconBtn(
|
||
Icons.check,
|
||
color: Colors.green,
|
||
onTap: _isSubmitting
|
||
? null
|
||
: () => _savePackedItem(item),
|
||
),
|
||
_iconBtn(
|
||
Icons.close,
|
||
color: Colors.grey,
|
||
onTap: _isSubmitting ? null : _cancelEditPackedItem,
|
||
),
|
||
] else ...[
|
||
_iconBtn(
|
||
Icons.edit,
|
||
color: Colors.grey.shade700,
|
||
onTap: _isSubmitting
|
||
? null
|
||
: () => _startEditPackedItem(item),
|
||
),
|
||
_iconBtn(
|
||
Icons.delete_outline,
|
||
color: Colors.red,
|
||
onTap: _isSubmitting
|
||
? null
|
||
: () {
|
||
setState(() {
|
||
_deletingPackedItemId = item.boxItemId;
|
||
_editingPackedItemId = null;
|
||
_editingPackedQuantity = '';
|
||
});
|
||
},
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
if (deleting)
|
||
Container(
|
||
height: 28,
|
||
padding: const EdgeInsets.only(left: 15, right: 12),
|
||
decoration: BoxDecoration(
|
||
color: Colors.red.shade50,
|
||
border: Border(bottom: BorderSide(color: Colors.red.shade200)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
'确认删除?',
|
||
style: const TextStyle(
|
||
fontSize: 12,
|
||
color: Colors.red,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
),
|
||
TextButton(
|
||
onPressed: _isSubmitting
|
||
? null
|
||
: () => _deletePackedItem(item),
|
||
style: TextButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 8,
|
||
vertical: 3,
|
||
),
|
||
minimumSize: Size.zero,
|
||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||
backgroundColor: Colors.red.shade100,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
),
|
||
child: const Text(
|
||
'删除',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w700,
|
||
color: Colors.red,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 4),
|
||
TextButton(
|
||
onPressed: _isSubmitting
|
||
? null
|
||
: () => setState(() => _deletingPackedItemId = null),
|
||
style: TextButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 8,
|
||
vertical: 3,
|
||
),
|
||
minimumSize: Size.zero,
|
||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||
),
|
||
child: const Text(
|
||
'取消',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w700,
|
||
color: Colors.grey,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
// ── Compact icon button helper ──
|
||
|
||
Widget _iconBtn(IconData icon, {Color? color, VoidCallback? onTap}) {
|
||
return SizedBox(
|
||
width: 28,
|
||
height: 28,
|
||
child: IconButton(
|
||
icon: Icon(icon, size: 16),
|
||
color: color,
|
||
onPressed: onTap,
|
||
padding: EdgeInsets.zero,
|
||
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ── Fixed bottom area ──
|
||
|
||
Widget _buildManyToOneBottomArea(bool hasScan) {
|
||
final hasItems = _visibleManyToOnePackedItems.isNotEmpty;
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
border: Border(top: BorderSide(color: Colors.grey.shade300)),
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
_buildManyToOneScanBar(hasScan),
|
||
_buildManyToOneSubmitRow(hasScan, hasItems),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildManyToOneScanBar(bool hasScan) {
|
||
if (!hasScan) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
||
color: Colors.grey.shade50,
|
||
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,
|
||
fontWeight: FontWeight.w400,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
||
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: 15),
|
||
const SizedBox(width: 6),
|
||
Expanded(
|
||
child: Text(
|
||
'${_zongpaiNo ?? ""} · ${_workOrderNo ?? "--"} · ${_erpQuantity ?? "--"}件',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.green.shade700,
|
||
),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildManyToOneSubmitRow(bool hasScan, bool hasItems) {
|
||
return Padding(
|
||
padding: const EdgeInsets.fromLTRB(12, 7, 12, 8),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.center,
|
||
children: [
|
||
// Left half: quantity label + input + confirm
|
||
Expanded(
|
||
flex: 1,
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.center,
|
||
children: [
|
||
Text(
|
||
'数量',
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w600,
|
||
color: hasScan ? Colors.black54 : Colors.grey.shade400,
|
||
),
|
||
),
|
||
const SizedBox(width: 6),
|
||
Expanded(
|
||
child: SizedBox(
|
||
height: 34,
|
||
child: TextField(
|
||
controller: _quantityController,
|
||
focusNode: _quantityFocusNode,
|
||
enabled: hasScan && !_isSubmitting,
|
||
keyboardType: TextInputType.none,
|
||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||
onChanged: (_) => setState(() {}),
|
||
onEditingComplete: () {},
|
||
onSubmitted: (_) => _submitFromKeyboard(),
|
||
decoration: InputDecoration(
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
enabledBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
borderSide: BorderSide(
|
||
color: _quantityTooHigh
|
||
? Colors.red
|
||
: Colors.grey.shade300,
|
||
),
|
||
),
|
||
disabledBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
borderSide: BorderSide(color: Colors.grey.shade200),
|
||
),
|
||
filled: !hasScan,
|
||
fillColor: Colors.grey.shade100,
|
||
),
|
||
style: const TextStyle(
|
||
fontSize: 15,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
SizedBox(
|
||
height: 34,
|
||
child: ElevatedButton(
|
||
onPressed: _canSubmit ? _submit : null,
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: _canSubmit
|
||
? Theme.of(context).colorScheme.primary
|
||
: Colors.grey.shade200,
|
||
foregroundColor: _canSubmit
|
||
? Colors.white
|
||
: Colors.grey.shade400,
|
||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||
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,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
// Right half: 完成本箱
|
||
Expanded(
|
||
flex: 1,
|
||
child: SizedBox(
|
||
height: 34,
|
||
child: OutlinedButton(
|
||
onPressed: hasItems ? _finishManyToOneBox : null,
|
||
style: OutlinedButton.styleFrom(
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
side: BorderSide(
|
||
color: hasItems
|
||
? Colors.grey.shade600
|
||
: Colors.grey.shade300,
|
||
),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Text(
|
||
'完成本箱',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w700,
|
||
color: hasItems ? Colors.black87 : Colors.grey.shade400,
|
||
),
|
||
),
|
||
const SizedBox(width: 6),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||
decoration: BoxDecoration(
|
||
color: hasItems
|
||
? Colors.grey.shade200
|
||
: Colors.grey.shade100,
|
||
borderRadius: BorderRadius.circular(3),
|
||
),
|
||
child: Text(
|
||
'P3',
|
||
style: TextStyle(
|
||
fontSize: 10,
|
||
fontWeight: FontWeight.w700,
|
||
color: hasItems ? Colors.black54 : Colors.grey.shade400,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
const _headerStyle = TextStyle(fontSize: 10, fontWeight: FontWeight.w700);
|