2089 lines
64 KiB
Dart
2089 lines
64 KiB
Dart
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, // 多码凑箱
|
|
}
|
|
|
|
// === 页面阶段 ===
|
|
|
|
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 {
|
|
const BoxingPage({super.key});
|
|
|
|
@override
|
|
State<BoxingPage> createState() => _BoxingPageState();
|
|
}
|
|
|
|
class _BoxingPageState extends State<BoxingPage> {
|
|
final _scannerService = ScannerService();
|
|
final _apiService = ApiService();
|
|
final _feedbackService = FeedbackService();
|
|
|
|
// 模式
|
|
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;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_boxNoController.addListener(_onBoxNoTextChanged);
|
|
_scannerService.scanResults.listen(_onScan);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_boxNoController.removeListener(_onBoxNoTextChanged);
|
|
_boxNoController.dispose();
|
|
_quantityController.dispose();
|
|
_boxNoFocusNode.dispose();
|
|
_quantityFocusNode.dispose();
|
|
_feedbackService.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
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) {
|
|
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<void> _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;
|
|
}
|
|
|
|
final result = await _apiService.fetchBoxInfo(
|
|
baseUrl: baseUrl,
|
|
zongpaiNo: zongpai,
|
|
);
|
|
|
|
if (!mounted) return;
|
|
|
|
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;
|
|
}
|
|
|
|
_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();
|
|
});
|
|
}
|
|
|
|
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<void> _submit() async {
|
|
if (!_canSubmit) return;
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
|
|
setState(() => _isSubmitting = false);
|
|
|
|
if (result.success) {
|
|
if (editing == null) {
|
|
_onSubmitSuccess(boxNo, quantity, result.boxItemId);
|
|
} else {
|
|
_onUpdateSuccess(editing, boxNo, quantity);
|
|
}
|
|
} else if (result.isDuplicate) {
|
|
_feedbackService.trigger(FeedbackEvent.duplicateBoxNo);
|
|
_showStatusOverride(
|
|
'该总排号在箱号 ${result.boxNo ?? ""} 已存在,请重新输入',
|
|
StatusDotColor.amber,
|
|
const Duration(seconds: 2),
|
|
);
|
|
} else {
|
|
final isNetwork = result.errorMessage == '网络异常,请检查网络连接';
|
|
_feedbackService.trigger(
|
|
isNetwork ? FeedbackEvent.networkError : FeedbackEvent.submitFailure,
|
|
);
|
|
_showStatusOverride(
|
|
result.errorMessage ?? '提交失败',
|
|
isNetwork ? StatusDotColor.yellow : StatusDotColor.red,
|
|
const Duration(seconds: 2),
|
|
);
|
|
}
|
|
}
|
|
|
|
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 (_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 Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('装箱编号'),
|
|
actions: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 4),
|
|
child: TextButton.icon(
|
|
onPressed: _cycleMode,
|
|
icon: const Icon(Icons.swap_horiz, size: 18),
|
|
label: Text(_modeLabel, style: const TextStyle(fontSize: 13)),
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: _mode == BoxingMode.multiCode
|
|
? Colors.orange
|
|
: colorScheme.primary,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
body: Column(
|
|
children: [
|
|
// 重复箱号警告条(保留,因为这是输入区关联的即时反馈)
|
|
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 _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),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|