- Home: add keyboard number shortcuts (1/2/3) for module navigation - Boxing: add P2 key mode switching with pill-style AppBar header - Registration: replace Switch with pill-style lock header matching boxing design - Add Android keyCode fallback for scanner device compatibility - Simplify home page cards to show only module name + shortcut hint - Add lock redesign HTML preview document Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2313 lines
71 KiB
Dart
2313 lines
71 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> {
|
||
static const _androidKeyCodeButtonR1 = 103;
|
||
|
||
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>[];
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
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() {
|
||
_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();
|
||
}
|
||
}
|
||
|
||
void _onKeyEvent(KeyEvent event) {
|
||
if (event is! KeyDownEvent || _isAutoProcessing) return;
|
||
if (_isModeSwitchKey(event)) {
|
||
_cycleMode();
|
||
}
|
||
}
|
||
|
||
bool _isModeSwitchKey(KeyEvent event) {
|
||
final key = event.logicalKey;
|
||
return key == LogicalKeyboardKey.select ||
|
||
key == LogicalKeyboardKey.gameButtonRight1 ||
|
||
_androidKeyCodeFromLogicalKey(key) == _androidKeyCodeButtonR1;
|
||
}
|
||
|
||
int? _androidKeyCodeFromLogicalKey(LogicalKeyboardKey key) {
|
||
final keyId = key.keyId;
|
||
const androidPlane = LogicalKeyboardKey.androidPlane;
|
||
if (keyId >= androidPlane && keyId < androidPlane + 0x100000000) {
|
||
return keyId - androidPlane;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
_feedbackService.trigger(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;
|
||
_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();
|
||
_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 {
|
||
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 (_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),
|
||
);
|
||
Future.delayed(const Duration(milliseconds: 1500), () {
|
||
if (mounted) setState(() => _resetState());
|
||
});
|
||
} 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 _onGoBack() {
|
||
setState(() => _resetState());
|
||
}
|
||
|
||
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 _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 (_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;
|
||
final isWaiting = _phase == _Phase.waiting && _zongpaiNo == null;
|
||
final showActionButtons =
|
||
_phase == _Phase.scanned && _mode == BoxingMode.singleCode;
|
||
|
||
_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: 12),
|
||
Expanded(child: _buildModeHeader()),
|
||
],
|
||
),
|
||
titleSpacing: 12,
|
||
),
|
||
body: Column(
|
||
children: [
|
||
if (_isAutoProcessing)
|
||
Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(
|
||
vertical: 8,
|
||
horizontal: 16,
|
||
),
|
||
color: Colors.orange.shade50,
|
||
child: Row(
|
||
children: [
|
||
SizedBox(
|
||
width: 16,
|
||
height: 16,
|
||
child: CircularProgressIndicator(
|
||
strokeWidth: 2,
|
||
color: Colors.orange.shade700,
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
'正在同步转运数据...',
|
||
style: TextStyle(
|
||
color: Colors.orange.shade900,
|
||
fontSize: 13,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
if (_autoWarnings.isNotEmpty)
|
||
Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(
|
||
vertical: 8,
|
||
horizontal: 16,
|
||
),
|
||
color: Colors.amber.shade100,
|
||
child: Text(
|
||
_autoWarnings.join(';'),
|
||
style: TextStyle(color: Colors.amber.shade900, fontSize: 13),
|
||
),
|
||
),
|
||
// 重复箱号警告条(保留,因为这是输入区关联的即时反馈)
|
||
if (_paichanSwitchNotice != null)
|
||
Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(
|
||
vertical: 8,
|
||
horizontal: 16,
|
||
),
|
||
color: Colors.blue.shade50,
|
||
child: Text(
|
||
_paichanSwitchNotice!,
|
||
style: TextStyle(color: Colors.blue.shade900, fontSize: 13),
|
||
),
|
||
),
|
||
if (_isDuplicateBoxNo && _phase == _Phase.scanned)
|
||
Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(
|
||
vertical: 8,
|
||
horizontal: 16,
|
||
),
|
||
color: Colors.amber.shade100,
|
||
child: Text(
|
||
'箱号 ${_boxNoController.text} 已存在,请重新输入',
|
||
style: TextStyle(color: Colors.amber.shade900, fontSize: 13),
|
||
),
|
||
),
|
||
|
||
Expanded(
|
||
child: _mode == BoxingMode.multiCode
|
||
? _buildManyToOneBody(colorScheme)
|
||
: SingleChildScrollView(
|
||
padding: const EdgeInsets.all(12),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
_buildScanArea(isWaiting),
|
||
const SizedBox(height: 12),
|
||
_buildInfoArea(isWaiting, colorScheme),
|
||
const Divider(height: 24),
|
||
if (_mode == BoxingMode.singleCode &&
|
||
!isWaiting &&
|
||
_currentZongpaiBoxes.isNotEmpty) ...[
|
||
_buildAssignedBoxes(),
|
||
const Divider(height: 24),
|
||
],
|
||
_buildInputArea(isWaiting),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
|
||
// 操作按钮(单码装箱 / 多码凑箱)
|
||
if (showActionButtons)
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 12,
|
||
vertical: 4,
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: OutlinedButton(
|
||
onPressed: _onGoBack,
|
||
style: OutlinedButton.styleFrom(
|
||
minimumSize: const Size.fromHeight(40),
|
||
),
|
||
child: const Text('返回'),
|
||
),
|
||
),
|
||
if (_mode == BoxingMode.singleCode &&
|
||
_editingBox == null) ...[
|
||
const SizedBox(width: 12),
|
||
const Expanded(child: SizedBox.shrink()),
|
||
],
|
||
if (_mode == BoxingMode.singleCode &&
|
||
_editingBox != null) ...[
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: ElevatedButton(
|
||
onPressed: _cancelEditAssigned,
|
||
style: ElevatedButton.styleFrom(
|
||
minimumSize: const Size.fromHeight(40),
|
||
),
|
||
child: const Text('取消'),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
|
||
if (_mode == BoxingMode.multiCode)
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 12,
|
||
vertical: 4,
|
||
),
|
||
child: OutlinedButton(
|
||
onPressed: _visibleManyToOnePackedItems.isEmpty
|
||
? null
|
||
: _finishManyToOneBox,
|
||
style: OutlinedButton.styleFrom(
|
||
minimumSize: const Size.fromHeight(40),
|
||
),
|
||
child: const Text('完成本箱'),
|
||
),
|
||
),
|
||
|
||
// 底部状态栏
|
||
StatusBar(dotColor: effectiveDot, text: effectiveText),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildModeHeader() {
|
||
final isSingle = _mode == BoxingMode.singleCode;
|
||
final backgroundColor = isSingle
|
||
? Colors.blue.shade700
|
||
: Colors.orange.shade700;
|
||
final foregroundColor = Colors.white;
|
||
|
||
return Material(
|
||
borderRadius: BorderRadius.circular(8),
|
||
color: backgroundColor,
|
||
child: InkWell(
|
||
borderRadius: BorderRadius.circular(8),
|
||
onTap: _isAutoProcessing ? null : _cycleMode,
|
||
child: Container(
|
||
width: double.infinity,
|
||
height: 36,
|
||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(Icons.swap_horiz, color: foregroundColor, size: 18),
|
||
const SizedBox(width: 6),
|
||
Flexible(
|
||
child: FittedBox(
|
||
fit: BoxFit.scaleDown,
|
||
child: Text(
|
||
_modeLabel,
|
||
maxLines: 1,
|
||
style: TextStyle(
|
||
color: foregroundColor,
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w800,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
'P2',
|
||
style: TextStyle(
|
||
color: foregroundColor.withValues(alpha: 0.88),
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildAssignedBoxes() {
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const Text(
|
||
'已分配',
|
||
style: TextStyle(fontSize: 13, color: Colors.black54),
|
||
),
|
||
const SizedBox(height: 6),
|
||
..._currentZongpaiBoxes.map(_buildAssignedBoxRow),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildAssignedBoxRow(CurrentZongpaiBoxData item) {
|
||
final editing = _editingAssignedItemId == item.boxItemId;
|
||
final deleting = _deletingAssignedItemId == item.boxItemId;
|
||
return Container(
|
||
margin: const EdgeInsets.only(bottom: 6),
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||
decoration: BoxDecoration(
|
||
color: editing ? Colors.orange.shade50 : Colors.grey.shade50,
|
||
border: Border.all(
|
||
color: editing ? Colors.orange : Colors.grey.shade300,
|
||
),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
'${item.boxNo}号箱',
|
||
style: const TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
if (editing)
|
||
SizedBox(
|
||
width: 56,
|
||
height: 32,
|
||
child: TextField(
|
||
keyboardType: TextInputType.none,
|
||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||
textAlign: TextAlign.center,
|
||
controller:
|
||
TextEditingController(text: _editingAssignedQuantity)
|
||
..selection = TextSelection.collapsed(
|
||
offset: _editingAssignedQuantity.length,
|
||
),
|
||
onChanged: (value) => _editingAssignedQuantity = value,
|
||
onEditingComplete: () {},
|
||
onSubmitted: (_) => _saveAssignedItem(item),
|
||
decoration: const InputDecoration(
|
||
contentPadding: EdgeInsets.symmetric(horizontal: 6),
|
||
border: OutlineInputBorder(),
|
||
),
|
||
),
|
||
)
|
||
else
|
||
Text(
|
||
'数量 ${item.quantity}',
|
||
style: const TextStyle(fontSize: 13),
|
||
),
|
||
const SizedBox(width: 4),
|
||
if (editing) ...[
|
||
IconButton(
|
||
tooltip: '保存',
|
||
visualDensity: VisualDensity.compact,
|
||
icon: const Icon(Icons.check, size: 18),
|
||
onPressed: _isSubmitting
|
||
? null
|
||
: () => _saveAssignedItem(item),
|
||
),
|
||
IconButton(
|
||
tooltip: '取消',
|
||
visualDensity: VisualDensity.compact,
|
||
icon: const Icon(Icons.close, size: 18),
|
||
onPressed: _isSubmitting ? null : _cancelEditAssigned,
|
||
),
|
||
] else ...[
|
||
IconButton(
|
||
tooltip: '修改',
|
||
visualDensity: VisualDensity.compact,
|
||
icon: const Icon(Icons.edit, size: 18),
|
||
onPressed: _isSubmitting
|
||
? null
|
||
: () => _startEditAssigned(item),
|
||
),
|
||
IconButton(
|
||
tooltip: '删除',
|
||
visualDensity: VisualDensity.compact,
|
||
icon: const Icon(Icons.delete_outline, size: 18),
|
||
onPressed: _isSubmitting
|
||
? null
|
||
: () {
|
||
setState(() {
|
||
_deletingAssignedItemId = item.boxItemId;
|
||
_editingAssignedItemId = null;
|
||
_editingAssignedQuantity = '';
|
||
});
|
||
},
|
||
),
|
||
],
|
||
],
|
||
),
|
||
if (deleting)
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 4),
|
||
child: Row(
|
||
children: [
|
||
const Expanded(
|
||
child: Text('确认删除此条记录?', style: TextStyle(fontSize: 13)),
|
||
),
|
||
TextButton(
|
||
onPressed: _isSubmitting
|
||
? null
|
||
: () => _deleteAssignedItem(item),
|
||
child: const Text('是'),
|
||
),
|
||
TextButton(
|
||
onPressed: _isSubmitting
|
||
? null
|
||
: () => setState(() => _deletingAssignedItemId = null),
|
||
child: const Text('否'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildManyToOneBody(ColorScheme colorScheme) {
|
||
final hasScan = _zongpaiNo != null && _phase == _Phase.scanned;
|
||
return SingleChildScrollView(
|
||
padding: const EdgeInsets.all(12),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
_buildManyToOneBoxNoPanel(),
|
||
if (_paichanNo != null) ...[
|
||
const SizedBox(height: 8),
|
||
_buildManyToOneInfoRow(colorScheme),
|
||
],
|
||
const SizedBox(height: 12),
|
||
_buildManyToOnePackedList(),
|
||
const Divider(height: 24),
|
||
_buildManyToOneScanArea(hasScan),
|
||
const SizedBox(height: 10),
|
||
_buildManyToOneSubmitArea(hasScan),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildManyToOneBoxNoPanel() {
|
||
return Column(
|
||
children: [
|
||
Row(
|
||
children: [
|
||
const Text(
|
||
'当前箱号',
|
||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
|
||
),
|
||
const SizedBox(width: 12),
|
||
SizedBox(
|
||
width: 96,
|
||
height: 44,
|
||
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.shade500),
|
||
),
|
||
),
|
||
style: const TextStyle(
|
||
fontSize: 24,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 12),
|
||
Divider(height: 1, color: Colors.grey.shade300),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildManyToOneInfoRow(ColorScheme colorScheme) {
|
||
return Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
'${_paichanNo ?? "--"} 已有${_existingBoxes.length}箱 最大箱号$_maxBoxNo',
|
||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
TextButton(
|
||
onPressed: _existingBoxes.isEmpty ? null : _openDetail,
|
||
style: TextButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||
minimumSize: Size.zero,
|
||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||
),
|
||
child: Text(
|
||
'详情 →',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
color: _existingBoxes.isEmpty
|
||
? Colors.grey.shade400
|
||
: colorScheme.primary,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildManyToOnePackedList() {
|
||
final visibleItems = _visibleManyToOnePackedItems;
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'已装入本箱(${visibleItems.length}项)',
|
||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w700),
|
||
),
|
||
const SizedBox(height: 6),
|
||
if (visibleItems.isEmpty)
|
||
Text(
|
||
'(空)',
|
||
style: TextStyle(fontSize: 13, color: Colors.grey.shade600),
|
||
)
|
||
else
|
||
...visibleItems.map(_buildManyToOnePackedRow),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildManyToOnePackedRow(_ManyToOnePackedItem item) {
|
||
final editing = _editingPackedItemId == item.boxItemId;
|
||
final deleting = _deletingPackedItemId == item.boxItemId;
|
||
final totalText = item.totalQuantity?.toString() ?? '--';
|
||
return Container(
|
||
margin: const EdgeInsets.only(bottom: 6),
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||
decoration: BoxDecoration(
|
||
color: editing ? Colors.orange.shade50 : Colors.grey.shade50,
|
||
border: Border.all(
|
||
color: editing ? Colors.orange : Colors.grey.shade300,
|
||
),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
'${item.zongpaiNo} ${item.workOrderNo ?? "--"}',
|
||
style: const TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
if (editing)
|
||
SizedBox(
|
||
width: 56,
|
||
height: 32,
|
||
child: TextField(
|
||
keyboardType: TextInputType.none,
|
||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||
textAlign: TextAlign.center,
|
||
controller:
|
||
TextEditingController(text: _editingPackedQuantity)
|
||
..selection = TextSelection.collapsed(
|
||
offset: _editingPackedQuantity.length,
|
||
),
|
||
onChanged: (value) => _editingPackedQuantity = value,
|
||
decoration: const InputDecoration(
|
||
contentPadding: EdgeInsets.symmetric(horizontal: 6),
|
||
border: OutlineInputBorder(),
|
||
),
|
||
),
|
||
)
|
||
else
|
||
Text(
|
||
'${item.quantity}/$totalText',
|
||
style: const TextStyle(fontSize: 13),
|
||
),
|
||
const SizedBox(width: 4),
|
||
if (editing) ...[
|
||
IconButton(
|
||
tooltip: '保存',
|
||
visualDensity: VisualDensity.compact,
|
||
icon: const Icon(Icons.check, size: 18),
|
||
onPressed: _isSubmitting ? null : () => _savePackedItem(item),
|
||
),
|
||
IconButton(
|
||
tooltip: '取消',
|
||
visualDensity: VisualDensity.compact,
|
||
icon: const Icon(Icons.close, size: 18),
|
||
onPressed: _isSubmitting ? null : _cancelEditPackedItem,
|
||
),
|
||
] else ...[
|
||
IconButton(
|
||
tooltip: '修改',
|
||
visualDensity: VisualDensity.compact,
|
||
icon: const Icon(Icons.edit, size: 18),
|
||
onPressed: _isSubmitting
|
||
? null
|
||
: () => _startEditPackedItem(item),
|
||
),
|
||
IconButton(
|
||
tooltip: '删除',
|
||
visualDensity: VisualDensity.compact,
|
||
icon: const Icon(Icons.delete_outline, size: 18),
|
||
onPressed: _isSubmitting
|
||
? null
|
||
: () {
|
||
setState(() {
|
||
_deletingPackedItemId = item.boxItemId;
|
||
_editingPackedItemId = null;
|
||
_editingPackedQuantity = '';
|
||
});
|
||
},
|
||
),
|
||
],
|
||
],
|
||
),
|
||
if (deleting)
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 4),
|
||
child: Row(
|
||
children: [
|
||
const Expanded(
|
||
child: Text('确认删除此条记录?', style: TextStyle(fontSize: 13)),
|
||
),
|
||
TextButton(
|
||
onPressed: _isSubmitting
|
||
? null
|
||
: () => _deletePackedItem(item),
|
||
child: const Text('是'),
|
||
),
|
||
TextButton(
|
||
onPressed: _isSubmitting
|
||
? null
|
||
: () => setState(() => _deletingPackedItemId = null),
|
||
child: const Text('否'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildManyToOneScanArea(bool hasScan) {
|
||
if (!hasScan) {
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.all(14),
|
||
decoration: BoxDecoration(
|
||
color: Colors.grey.shade100,
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: const Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(Icons.qr_code_scanner, color: Colors.grey, size: 20),
|
||
SizedBox(width: 8),
|
||
Text(
|
||
'请扫描执行卡二维码',
|
||
style: TextStyle(color: Colors.grey, fontSize: 14),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||
decoration: BoxDecoration(
|
||
border: Border.all(color: Colors.green, width: 1.5),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
const Icon(Icons.check_circle, color: Colors.green, size: 18),
|
||
const SizedBox(width: 6),
|
||
Expanded(
|
||
child: Text(
|
||
'${_zongpaiNo ?? ""} ${_workOrderNo ?? "--"} ${_erpQuantity ?? "--"}件',
|
||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildManyToOneSubmitArea(bool hasScan) {
|
||
return Row(
|
||
crossAxisAlignment: CrossAxisAlignment.end,
|
||
children: [
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'数量',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
color: hasScan ? Colors.black54 : Colors.grey,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
SizedBox(
|
||
height: 40,
|
||
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: const OutlineInputBorder(),
|
||
enabledBorder: OutlineInputBorder(
|
||
borderSide: BorderSide(
|
||
color: _quantityTooHigh
|
||
? Colors.red
|
||
: Colors.grey.shade400,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
SizedBox(
|
||
height: 40,
|
||
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,
|
||
),
|
||
child: _isSubmitting
|
||
? const SizedBox(
|
||
width: 18,
|
||
height: 18,
|
||
child: CircularProgressIndicator(
|
||
strokeWidth: 2,
|
||
color: Colors.white,
|
||
),
|
||
)
|
||
: const Text('确认', style: TextStyle(fontSize: 15)),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
// === 扫码区 Widget ===
|
||
|
||
Widget _buildScanArea(bool isWaiting) {
|
||
if (isWaiting) {
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: Colors.grey.shade100,
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: const Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(Icons.qr_code_scanner, color: Colors.grey, size: 20),
|
||
SizedBox(width: 8),
|
||
Text(
|
||
'请扫描执行卡二维码',
|
||
style: TextStyle(color: Colors.grey, fontSize: 14),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||
decoration: BoxDecoration(
|
||
border: Border.all(color: Colors.green, width: 2),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
const Icon(Icons.check_circle, color: Colors.green, size: 18),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
_zongpaiNo ?? '',
|
||
style: const TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.bold,
|
||
color: Colors.black87,
|
||
),
|
||
),
|
||
const Spacer(),
|
||
const Text(
|
||
'已识别',
|
||
style: TextStyle(fontSize: 12, color: Colors.green),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// === 信息区 Widget ===
|
||
|
||
Widget _buildInfoArea(bool isWaiting, ColorScheme colorScheme) {
|
||
final grey = isWaiting;
|
||
final paichanText = grey ? '--' : (_paichanNo ?? '--');
|
||
final workOrderText = grey ? '--' : (_workOrderNo ?? '--');
|
||
final quantityText = grey ? '--' : (_erpQuantity?.toString() ?? '--');
|
||
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
flex: 5,
|
||
child: Text(
|
||
paichanText,
|
||
style: TextStyle(
|
||
fontSize: 20,
|
||
fontWeight: FontWeight.w700,
|
||
color: grey ? Colors.grey : Colors.black87,
|
||
),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
flex: 4,
|
||
child: Text(
|
||
workOrderText,
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w700,
|
||
color: grey ? Colors.grey : Colors.black87,
|
||
),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
flex: 3,
|
||
child: Text(
|
||
quantityText,
|
||
textAlign: TextAlign.right,
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w700,
|
||
color: grey ? Colors.grey : Colors.black87,
|
||
),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 10),
|
||
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Row(
|
||
children: [
|
||
Text(
|
||
'已有箱数:',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
color: grey ? Colors.grey : Colors.black54,
|
||
),
|
||
),
|
||
Text(
|
||
grey ? '--' : '${_existingBoxes.length}箱',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
color: grey ? Colors.grey : Colors.black87,
|
||
),
|
||
),
|
||
const SizedBox(width: 16),
|
||
Text(
|
||
'最大箱号:',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
color: grey ? Colors.grey : Colors.black54,
|
||
),
|
||
),
|
||
Text(
|
||
grey ? '--' : '$_maxBoxNo',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
color: _maxBoxNo > 0
|
||
? Colors.amber.shade800
|
||
: Colors.grey,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
TextButton(
|
||
onPressed: grey || _existingBoxes.isEmpty ? null : _openDetail,
|
||
style: TextButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||
minimumSize: Size.zero,
|
||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||
),
|
||
child: Text(
|
||
'详情 →',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
color: grey || _existingBoxes.isEmpty
|
||
? Colors.grey.shade400
|
||
: colorScheme.primary,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
// === 输入区 Widget ===
|
||
|
||
Widget _buildInputArea(bool isWaiting) {
|
||
final enabled = !isWaiting && _phase == _Phase.scanned;
|
||
|
||
return Row(
|
||
crossAxisAlignment: CrossAxisAlignment.end,
|
||
children: [
|
||
// 箱号输入
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
_editingBox == null ? '箱号' : '修改箱号',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
color: enabled ? Colors.black54 : Colors.grey,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
SizedBox(
|
||
height: 40,
|
||
child: TextField(
|
||
controller: _boxNoController,
|
||
focusNode: _boxNoFocusNode,
|
||
enabled: enabled,
|
||
keyboardType: TextInputType.none,
|
||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||
onChanged: (_) => _checkDuplicateBoxNo(),
|
||
onEditingComplete: () {},
|
||
onSubmitted: (_) => _submitFromKeyboard(),
|
||
decoration: InputDecoration(
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
|
||
border: const OutlineInputBorder(),
|
||
enabledBorder: OutlineInputBorder(
|
||
borderSide: BorderSide(
|
||
color: _isDuplicateBoxNo
|
||
? Colors.amber
|
||
: Colors.grey.shade400,
|
||
),
|
||
),
|
||
),
|
||
style: const TextStyle(fontSize: 16),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
|
||
// 数量输入
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
_editingBox == null ? '数量' : '修改数量',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
color: enabled ? Colors.black54 : Colors.grey,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
SizedBox(
|
||
height: 40,
|
||
child: TextField(
|
||
controller: _quantityController,
|
||
focusNode: _quantityFocusNode,
|
||
enabled: enabled,
|
||
keyboardType: TextInputType.none,
|
||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||
onChanged: (_) => setState(() {}),
|
||
onEditingComplete: () {},
|
||
onSubmitted: (_) => _submitFromKeyboard(),
|
||
decoration: InputDecoration(
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
|
||
border: const OutlineInputBorder(),
|
||
enabledBorder: OutlineInputBorder(
|
||
borderSide: BorderSide(
|
||
color: _quantityTooHigh
|
||
? Colors.red
|
||
: Colors.grey.shade400,
|
||
),
|
||
),
|
||
),
|
||
style: const TextStyle(fontSize: 16),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
|
||
// 确认按钮
|
||
SizedBox(
|
||
height: 40,
|
||
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,
|
||
),
|
||
child: _isSubmitting
|
||
? const SizedBox(
|
||
width: 18,
|
||
height: 18,
|
||
child: CircularProgressIndicator(
|
||
strokeWidth: 2,
|
||
color: Colors.white,
|
||
),
|
||
)
|
||
: Text(
|
||
_editingBox == null ? '确认' : '保存',
|
||
style: const TextStyle(fontSize: 15),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|