already have location bindings (on_shelf or transferred) - Replace single on_shelf pre-check with unified conflict detection for both normal shelf and transit targets - Show conflict dialog listing duplicate and/or transferred items with details before any API calls are made - Prevents partial batch failures where some items succeed before a conflicting item is encountered Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1465 lines
44 KiB
Dart
1465 lines
44 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/feedback_service.dart';
|
||
import 'package:pad_scanner/services/registration_linking.dart';
|
||
import 'package:vibration/vibration.dart';
|
||
import 'package:pad_scanner/pages/boxing_page.dart';
|
||
import 'package:pad_scanner/widgets/status_bar.dart';
|
||
|
||
class RegistrationPage extends StatefulWidget {
|
||
const RegistrationPage({super.key});
|
||
|
||
@override
|
||
State<RegistrationPage> createState() => _RegistrationPageState();
|
||
}
|
||
|
||
class _RegistrationPageState extends State<RegistrationPage> {
|
||
final _scannerService = ScannerService();
|
||
final _apiService = ApiService();
|
||
final _feedbackService = FeedbackService();
|
||
final _focusNode = FocusNode();
|
||
|
||
StreamSubscription<ScanResult>? _scanSubscription;
|
||
final _zongpaiNos = <String>[];
|
||
String? _locationCode;
|
||
CodeType? _locationType;
|
||
bool _isLocked = false;
|
||
bool _isSubmitting = false;
|
||
|
||
StatusDotColor _statusDot = StatusDotColor.blue;
|
||
String _statusText = '等待扫描总排号或货位号…';
|
||
String? _statusOverrideText;
|
||
StatusDotColor? _statusOverrideDot;
|
||
|
||
bool _overviewLoading = false;
|
||
bool _overviewNotFound = false;
|
||
String? _overviewError;
|
||
String? _overviewZongpaiNo;
|
||
PaichaOverviewResult? _overview;
|
||
int _overviewRequestId = 0;
|
||
|
||
/// Whether a cross-paicha warning dialog is currently showing.
|
||
bool _crossPaichaPending = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_startScanListening();
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
_focusNode.requestFocus();
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_scanSubscription?.cancel();
|
||
_focusNode.dispose();
|
||
_feedbackService.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
void _startScanListening() {
|
||
_scanSubscription ??= _scannerService.scanResults.listen(_onScan);
|
||
}
|
||
|
||
Future<void> _stopScanListening() async {
|
||
final subscription = _scanSubscription;
|
||
_scanSubscription = null;
|
||
await subscription?.cancel();
|
||
}
|
||
|
||
void _restartScanListening() {
|
||
_scanSubscription?.cancel();
|
||
_scanSubscription = null;
|
||
_startScanListening();
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (mounted) {
|
||
_focusNode.requestFocus();
|
||
}
|
||
});
|
||
}
|
||
|
||
void _onKeyEvent(KeyEvent event) {
|
||
if (event is! KeyDownEvent) return;
|
||
if (_isLockToggleKey(event)) {
|
||
_toggleLock(!_isLocked);
|
||
return;
|
||
}
|
||
if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||
_submit();
|
||
}
|
||
}
|
||
|
||
bool _isLockToggleKey(KeyEvent event) {
|
||
final key = event.logicalKey;
|
||
return key == LogicalKeyboardKey.select ||
|
||
key == LogicalKeyboardKey.gameButtonRight1;
|
||
}
|
||
|
||
void _onScan(ScanResult result) {
|
||
final parsed = CodeParser.parse(result.barcode);
|
||
switch (parsed.type) {
|
||
case CodeType.zongpaiNo:
|
||
_handleZongpaiScan(parsed.value);
|
||
case CodeType.locationNormal:
|
||
case CodeType.locationTransit:
|
||
if (!_isLocked) {
|
||
_feedbackService.trigger(FeedbackEvent.scanValid);
|
||
setState(() {
|
||
_locationCode = parsed.value;
|
||
_locationType = parsed.type;
|
||
_clearStatusOverride();
|
||
});
|
||
} else {}
|
||
case CodeType.invalid:
|
||
_feedbackService.trigger(FeedbackEvent.scanInvalid);
|
||
_showStatusOverride(
|
||
'无效码:${result.barcode}',
|
||
StatusDotColor.red,
|
||
const Duration(seconds: 2),
|
||
);
|
||
}
|
||
}
|
||
|
||
void _handleZongpaiScan(String zongpaiNo) {
|
||
// If cross-paicha dialog is showing, just vibrate and discard
|
||
if (_crossPaichaPending) {
|
||
_triggerDoubleVibration();
|
||
return;
|
||
}
|
||
|
||
// In locked mode with existing data, check for cross-paicha scan
|
||
if (_isLocked &&
|
||
_zongpaiNos.isNotEmpty &&
|
||
_overview?.success == true &&
|
||
!_overview!.items.any((item) => item.zongpaiNo == zongpaiNo)) {
|
||
_crossPaichaPending = true;
|
||
_triggerDoubleVibration();
|
||
_showCrossPaichaDialog(zongpaiNo);
|
||
return;
|
||
}
|
||
|
||
final shouldRefresh = _overviewZongpaiNo != zongpaiNo;
|
||
_feedbackService.trigger(FeedbackEvent.scanValid);
|
||
setState(() {
|
||
if (_isLocked) {
|
||
if (!_zongpaiNos.contains(zongpaiNo)) {
|
||
_zongpaiNos.add(zongpaiNo);
|
||
}
|
||
} else {
|
||
if (_zongpaiNos.isEmpty) {
|
||
_zongpaiNos.add(zongpaiNo);
|
||
} else {
|
||
_zongpaiNos[0] = zongpaiNo;
|
||
}
|
||
}
|
||
_clearStatusOverride();
|
||
});
|
||
if (shouldRefresh) {
|
||
_loadOverview(zongpaiNo);
|
||
}
|
||
}
|
||
|
||
void _triggerDoubleVibration() {
|
||
Vibration.vibrate(pattern: [0, 200, 100, 200]);
|
||
}
|
||
|
||
void _removeZongpai(String zongpaiNo) {
|
||
setState(() {
|
||
_zongpaiNos.remove(zongpaiNo);
|
||
});
|
||
}
|
||
|
||
void _clearStatusOverride() {
|
||
_statusOverrideText = null;
|
||
_statusOverrideDot = null;
|
||
}
|
||
|
||
void _showStatusOverride(String text, StatusDotColor dot, Duration duration) {
|
||
setState(() {
|
||
_statusOverrideText = text;
|
||
_statusOverrideDot = dot;
|
||
});
|
||
Future.delayed(duration, () {
|
||
if (mounted) setState(() => _clearStatusOverride());
|
||
});
|
||
}
|
||
|
||
void _updateBaseStatus() {
|
||
if (_isSubmitting) {
|
||
_statusDot = StatusDotColor.orange;
|
||
_statusText = '正在提交…';
|
||
return;
|
||
}
|
||
if (_isLocked && _locationCode != null && _zongpaiNos.isEmpty) {
|
||
_statusDot = StatusDotColor.blue;
|
||
_statusText = '货位已锁定,请扫描下一张执行卡';
|
||
return;
|
||
}
|
||
final hasZongpai = _zongpaiNos.isNotEmpty;
|
||
final hasLocation = _locationCode != null;
|
||
if (hasZongpai && hasLocation) {
|
||
_statusDot = StatusDotColor.blue;
|
||
_statusText = '请确认信息并提交';
|
||
} else if (hasZongpai) {
|
||
_statusDot = StatusDotColor.blue;
|
||
_statusText = '请扫描目标货位号';
|
||
} else if (hasLocation) {
|
||
_statusDot = StatusDotColor.blue;
|
||
_statusText = '请扫描执行卡';
|
||
} else {
|
||
_statusDot = StatusDotColor.blue;
|
||
_statusText = '等待扫描总排号或货位号…';
|
||
}
|
||
}
|
||
|
||
bool get _canSubmit =>
|
||
_zongpaiNos.isNotEmpty && _locationCode != null && !_isSubmitting;
|
||
|
||
bool get _isTransitTarget => isTransitTarget(_locationType, _locationCode);
|
||
|
||
/// Find all scanned items that are already on shelf.
|
||
List<PaichaOverviewItem> _findOnShelfItemsInScanned() {
|
||
if (_overview?.success != true) return [];
|
||
final scannedSet = _zongpaiNos.toSet();
|
||
return _overview!.items
|
||
.where((item) =>
|
||
scannedSet.contains(item.zongpaiNo) && item.status == 'on_shelf')
|
||
.toList();
|
||
}
|
||
|
||
/// Find all scanned items that are already transferred.
|
||
List<PaichaOverviewItem> _findTransferredItemsInScanned() {
|
||
if (_overview?.success != true) return [];
|
||
final scannedSet = _zongpaiNos.toSet();
|
||
return _overview!.items
|
||
.where((item) =>
|
||
scannedSet.contains(item.zongpaiNo) && item.status == 'transferred')
|
||
.toList();
|
||
}
|
||
|
||
/// Whether any scanned item already has a location binding (on_shelf or transferred).
|
||
bool _hasAnyLocatedInScanned() {
|
||
if (_overview?.success != true) return false;
|
||
final scannedSet = _zongpaiNos.toSet();
|
||
return _overview!.items.any((item) =>
|
||
scannedSet.contains(item.zongpaiNo) &&
|
||
(item.status == 'on_shelf' || item.status == 'transferred'));
|
||
}
|
||
|
||
Future<String?> _baseUrl() async {
|
||
final configService = AppConfigService();
|
||
final baseUrl = await configService.getString('api_url') ?? '';
|
||
if (baseUrl.isEmpty) {
|
||
return null;
|
||
}
|
||
return baseUrl;
|
||
}
|
||
|
||
Future<void> _loadOverview(String zongpaiNo, {bool force = false}) async {
|
||
if (!force && _overviewZongpaiNo == zongpaiNo && _overview != null) {
|
||
return;
|
||
}
|
||
final requestId = ++_overviewRequestId;
|
||
setState(() {
|
||
_overviewZongpaiNo = zongpaiNo;
|
||
_overviewLoading = true;
|
||
_overviewNotFound = false;
|
||
_overviewError = null;
|
||
});
|
||
|
||
final baseUrl = await _baseUrl();
|
||
if (!mounted || requestId != _overviewRequestId) return;
|
||
if (baseUrl == null) {
|
||
setState(() {
|
||
_overviewLoading = false;
|
||
_overviewError = '未配置 API 地址,请前往设置';
|
||
});
|
||
return;
|
||
}
|
||
|
||
final result = await _apiService.fetchPaichaOverview(
|
||
baseUrl: baseUrl,
|
||
zongpaiNo: zongpaiNo,
|
||
);
|
||
if (!mounted || requestId != _overviewRequestId) return;
|
||
|
||
setState(() {
|
||
_overviewLoading = false;
|
||
if (result.success) {
|
||
_overview = result;
|
||
_overviewNotFound = false;
|
||
_overviewError = null;
|
||
} else if (result.notFound) {
|
||
_overview = null;
|
||
_overviewNotFound = true;
|
||
_overviewError = null;
|
||
} else {
|
||
_overviewError = result.errorMessage ?? '加载失败,点击重试';
|
||
}
|
||
});
|
||
}
|
||
|
||
Future<void> _refreshCurrentOverview() async {
|
||
final zongpaiNo = _overviewZongpaiNo;
|
||
if (zongpaiNo == null) return;
|
||
await _loadOverview(zongpaiNo, force: true);
|
||
}
|
||
|
||
Future<void> _submit() async {
|
||
if (!_canSubmit) return;
|
||
|
||
final baseUrl = await _baseUrl();
|
||
if (baseUrl == null) {
|
||
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||
_showStatusOverride(
|
||
'未配置 API 地址,请前往设置',
|
||
StatusDotColor.red,
|
||
const Duration(seconds: 2),
|
||
);
|
||
return;
|
||
}
|
||
|
||
setState(() => _isSubmitting = true);
|
||
|
||
// For transit target: block if any scanned item already has a location
|
||
if (_isTransitTarget) {
|
||
if (_hasAnyLocatedInScanned()) {
|
||
final onShelfItems = _findOnShelfItemsInScanned();
|
||
final transferredItems = _findTransferredItemsInScanned();
|
||
setState(() => _isSubmitting = false);
|
||
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||
_showLocationConflictDialog(
|
||
onShelfItems: onShelfItems,
|
||
transferredItems: transferredItems,
|
||
);
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (_isLocked && _zongpaiNos.length > 1) {
|
||
if (_isTransitTarget && !await _ensureBatchSamePaicha(baseUrl)) {
|
||
if (!mounted) return;
|
||
setState(() => _isSubmitting = false);
|
||
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||
_showStatusOverride(
|
||
batchPaichaMismatchMessage,
|
||
StatusDotColor.red,
|
||
const Duration(seconds: 2),
|
||
);
|
||
return;
|
||
}
|
||
// Pre-check: block entire batch if any item already has a location binding
|
||
if (!_isTransitTarget) {
|
||
final onShelfItems = _findOnShelfItemsInScanned();
|
||
final transferredItems = _findTransferredItemsInScanned();
|
||
if (onShelfItems.isNotEmpty || transferredItems.isNotEmpty) {
|
||
setState(() => _isSubmitting = false);
|
||
_feedbackService.trigger(FeedbackEvent.duplicateError);
|
||
_showLocationConflictDialog(
|
||
onShelfItems: onShelfItems,
|
||
transferredItems: transferredItems,
|
||
);
|
||
return;
|
||
}
|
||
}
|
||
await _submitBatch(baseUrl);
|
||
} else {
|
||
await _submitOne(baseUrl, _zongpaiNos.first);
|
||
}
|
||
}
|
||
|
||
Future<bool> _ensureBatchSamePaicha(String baseUrl) async {
|
||
final paichaByCode = <String, String>{};
|
||
final cachedOverview = _overview;
|
||
if (cachedOverview?.success == true && cachedOverview!.paichaNo != null) {
|
||
for (final item in cachedOverview.items) {
|
||
paichaByCode[item.zongpaiNo] = cachedOverview.paichaNo!;
|
||
}
|
||
}
|
||
|
||
for (final zongpaiNo in _zongpaiNos) {
|
||
if (paichaByCode.containsKey(zongpaiNo)) continue;
|
||
final result = await _apiService.fetchPaichaOverview(
|
||
baseUrl: baseUrl,
|
||
zongpaiNo: zongpaiNo,
|
||
);
|
||
if (!result.success || result.paichaNo == null) {
|
||
return false;
|
||
}
|
||
for (final item in result.items) {
|
||
paichaByCode[item.zongpaiNo] = result.paichaNo!;
|
||
}
|
||
}
|
||
|
||
return allSamePaicha(_zongpaiNos.map((code) => paichaByCode[code]));
|
||
}
|
||
|
||
Future<void> _submitOne(String baseUrl, String zongpaiNo) async {
|
||
final result = await _apiService.registerLocation(
|
||
baseUrl: baseUrl,
|
||
zongpaiNo: zongpaiNo,
|
||
locationCode: _locationCode!,
|
||
);
|
||
|
||
if (!mounted) return;
|
||
|
||
setState(() => _isSubmitting = false);
|
||
|
||
if (result.success) {
|
||
_feedbackService.trigger(FeedbackEvent.submitSuccess);
|
||
if (_isTransitTarget) {
|
||
setState(() => _isSubmitting = false);
|
||
await _navigateToBoxing(
|
||
mode: BoxingMode.singleCode,
|
||
codes: [zongpaiNo],
|
||
codesToClear: [zongpaiNo],
|
||
clearLocation: !_isLocked,
|
||
);
|
||
return;
|
||
}
|
||
setState(() {
|
||
_zongpaiNos.remove(zongpaiNo);
|
||
if (!_isLocked) {
|
||
_locationCode = null;
|
||
_locationType = null;
|
||
}
|
||
});
|
||
final msg = result.isOffShelfSuccess
|
||
? '下架成功'
|
||
: (_isLocked ? '货位已锁定,请扫描下一张执行卡' : '上架成功');
|
||
_showStatusOverride(
|
||
msg,
|
||
StatusDotColor.green,
|
||
const Duration(milliseconds: 1500),
|
||
);
|
||
await _refreshCurrentOverview();
|
||
} else if (result.isDuplicate) {
|
||
_feedbackService.trigger(FeedbackEvent.duplicateError);
|
||
_showDuplicateDialog(zongpaiNo, result.duplicateInfo);
|
||
} else if (result.isAlreadyOffShelf) {
|
||
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||
_showStatusOverride(
|
||
result.errorMessage ?? '该总排号已下架至转运区域,不可重新上架',
|
||
StatusDotColor.red,
|
||
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),
|
||
);
|
||
}
|
||
}
|
||
|
||
Future<void> _submitBatch(String baseUrl) async {
|
||
int successCount = 0;
|
||
int offShelfCount = 0;
|
||
final failed = <String>[];
|
||
final toRemove = <String>[];
|
||
final transitCodes = <String>[];
|
||
|
||
for (final zongpaiNo in List.of(_zongpaiNos)) {
|
||
final result = await _apiService.registerLocation(
|
||
baseUrl: baseUrl,
|
||
zongpaiNo: zongpaiNo,
|
||
locationCode: _locationCode!,
|
||
);
|
||
if (result.success) {
|
||
successCount++;
|
||
if (result.isOffShelfSuccess) {
|
||
offShelfCount++;
|
||
}
|
||
toRemove.add(zongpaiNo);
|
||
if (_isTransitTarget) {
|
||
transitCodes.add(zongpaiNo);
|
||
}
|
||
} else {
|
||
failed.add(zongpaiNo);
|
||
if (result.isDuplicate) {
|
||
if (_isTransitTarget && transitCodes.isNotEmpty) {
|
||
setState(() {
|
||
_zongpaiNos.removeWhere((item) => toRemove.contains(item));
|
||
_isSubmitting = false;
|
||
});
|
||
await _navigateToBoxing(
|
||
mode: BoxingMode.multiCode,
|
||
codes: transitCodes,
|
||
codesToClear: toRemove,
|
||
clearLocation: false,
|
||
returnMessage: '成功 $successCount 条,失败 ${failed.length} 条',
|
||
);
|
||
return;
|
||
}
|
||
setState(() {
|
||
_zongpaiNos.removeWhere((item) => toRemove.contains(item));
|
||
_isSubmitting = false;
|
||
});
|
||
_feedbackService.trigger(FeedbackEvent.duplicateError);
|
||
_showDuplicateDialog(zongpaiNo, result.duplicateInfo);
|
||
return;
|
||
}
|
||
if (result.isAlreadyOffShelf) {
|
||
if (_isTransitTarget && transitCodes.isNotEmpty) {
|
||
setState(() {
|
||
_zongpaiNos.removeWhere((item) => toRemove.contains(item));
|
||
_isSubmitting = false;
|
||
});
|
||
await _navigateToBoxing(
|
||
mode: BoxingMode.multiCode,
|
||
codes: transitCodes,
|
||
codesToClear: toRemove,
|
||
clearLocation: false,
|
||
returnMessage: '成功 $successCount 条,失败 ${failed.length} 条',
|
||
);
|
||
return;
|
||
}
|
||
setState(() {
|
||
_zongpaiNos.removeWhere((item) => toRemove.contains(item));
|
||
_isSubmitting = false;
|
||
});
|
||
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||
_showStatusOverride(
|
||
result.errorMessage ?? '该总排号已下架至转运区域,不可重新上架',
|
||
StatusDotColor.red,
|
||
const Duration(seconds: 2),
|
||
);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!mounted) return;
|
||
|
||
setState(() {
|
||
_zongpaiNos.removeWhere((item) => toRemove.contains(item));
|
||
_isSubmitting = false;
|
||
});
|
||
|
||
if (_isTransitTarget && transitCodes.isNotEmpty) {
|
||
await _navigateToBoxing(
|
||
mode: BoxingMode.multiCode,
|
||
codes: transitCodes,
|
||
codesToClear: toRemove,
|
||
clearLocation: failed.isEmpty,
|
||
returnMessage: failed.isEmpty
|
||
? null
|
||
: '成功 $successCount 条,失败 ${failed.length} 条',
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (failed.isEmpty) {
|
||
_feedbackService.trigger(FeedbackEvent.submitSuccess);
|
||
final msg = offShelfCount == successCount
|
||
? '批量下架成功($successCount 条)'
|
||
: '批量上架成功($successCount 条)';
|
||
_showStatusOverride(
|
||
msg,
|
||
StatusDotColor.green,
|
||
const Duration(milliseconds: 1500),
|
||
);
|
||
await _refreshCurrentOverview();
|
||
} else {
|
||
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||
_showStatusOverride(
|
||
'成功 $successCount 条,失败 ${failed.length} 条',
|
||
StatusDotColor.red,
|
||
const Duration(seconds: 2),
|
||
);
|
||
}
|
||
}
|
||
|
||
Future<void> _navigateToBoxing({
|
||
required BoxingMode mode,
|
||
required List<String> codes,
|
||
required List<String> codesToClear,
|
||
required bool clearLocation,
|
||
String? returnMessage,
|
||
}) async {
|
||
await _refreshCurrentOverview();
|
||
if (!mounted) return;
|
||
final navigator = Navigator.of(context);
|
||
await _stopScanListening();
|
||
await navigator.push(
|
||
MaterialPageRoute(
|
||
builder: (_) => BoxingPage(
|
||
arguments: BoxingPageArguments(
|
||
initialMode: mode,
|
||
autoScanCodes: codes,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
if (!mounted) return;
|
||
await Future<void>.delayed(const Duration(milliseconds: 120));
|
||
if (!mounted) return;
|
||
_restartScanListening();
|
||
setState(() {
|
||
_zongpaiNos.removeWhere((item) => codesToClear.contains(item));
|
||
if (clearLocation) {
|
||
_locationCode = null;
|
||
_locationType = null;
|
||
}
|
||
});
|
||
await _refreshCurrentOverview();
|
||
if (!mounted) return;
|
||
if (returnMessage != null) {
|
||
_showStatusOverride(
|
||
returnMessage,
|
||
StatusDotColor.red,
|
||
const Duration(seconds: 2),
|
||
);
|
||
}
|
||
}
|
||
|
||
void _showDuplicateDialog(String zongpaiNo, Map<String, dynamic>? info) {
|
||
showDialog(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (ctx) => AlertDialog(
|
||
backgroundColor: Colors.red.shade50,
|
||
title: const Row(
|
||
children: [
|
||
Icon(Icons.warning, color: Colors.red),
|
||
SizedBox(width: 8),
|
||
Text('重复上架', style: TextStyle(color: Colors.red)),
|
||
],
|
||
),
|
||
content: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text('总排号:$zongpaiNo'),
|
||
const SizedBox(height: 4),
|
||
Text('已登记货位:${info?["location_code"] ?? "未知"}'),
|
||
const SizedBox(height: 4),
|
||
Text('登记时间:${info?["registered_at"] ?? "未知"}'),
|
||
const SizedBox(height: 12),
|
||
const Text(
|
||
'请核查实物,确认是否操作错误。',
|
||
style: TextStyle(fontWeight: FontWeight.bold),
|
||
),
|
||
],
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () {
|
||
_feedbackService.stopAlert();
|
||
Navigator.pop(ctx);
|
||
},
|
||
child: const Text('关闭'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
void _showLocationConflictDialog({
|
||
required List<PaichaOverviewItem> onShelfItems,
|
||
required List<PaichaOverviewItem> transferredItems,
|
||
}) {
|
||
final contentParts = <Widget>[];
|
||
|
||
if (onShelfItems.isNotEmpty) {
|
||
contentParts.add(
|
||
Text(
|
||
'重复上架',
|
||
style: TextStyle(
|
||
fontWeight: FontWeight.bold,
|
||
color: Colors.red.shade700,
|
||
),
|
||
),
|
||
);
|
||
for (final item in onShelfItems) {
|
||
contentParts.addAll([
|
||
Text('总排号:${item.zongpaiNo}'),
|
||
Text('已登记货位:${item.locationCode ?? "未知"}'),
|
||
const SizedBox(height: 6),
|
||
]);
|
||
}
|
||
}
|
||
|
||
if (transferredItems.isNotEmpty) {
|
||
if (onShelfItems.isNotEmpty) {
|
||
contentParts.add(const Divider());
|
||
contentParts.add(const SizedBox(height: 4));
|
||
}
|
||
contentParts.add(
|
||
Text(
|
||
'货物已转运',
|
||
style: TextStyle(
|
||
fontWeight: FontWeight.bold,
|
||
color: Colors.orange.shade700,
|
||
),
|
||
),
|
||
);
|
||
for (final item in transferredItems) {
|
||
contentParts.addAll([
|
||
Text('总排号:${item.zongpaiNo}'),
|
||
Text('转运货位:${item.locationCode ?? "未知"}'),
|
||
const SizedBox(height: 6),
|
||
]);
|
||
}
|
||
}
|
||
|
||
contentParts.add(const SizedBox(height: 4));
|
||
contentParts.add(
|
||
const Text(
|
||
'请核查实物,确认是否操作错误。',
|
||
style: TextStyle(fontWeight: FontWeight.bold),
|
||
),
|
||
);
|
||
|
||
showDialog(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (ctx) => AlertDialog(
|
||
backgroundColor: Colors.red.shade50,
|
||
title: const Row(
|
||
children: [
|
||
Icon(Icons.warning, color: Colors.red),
|
||
SizedBox(width: 8),
|
||
Text('操作冲突', style: TextStyle(color: Colors.red)),
|
||
],
|
||
),
|
||
content: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: contentParts,
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () {
|
||
_feedbackService.stopAlert();
|
||
Navigator.pop(ctx);
|
||
},
|
||
child: const Text('关闭'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
void _showCrossPaichaDialog(String newZongpaiNo) {
|
||
final currentPaicha = _overview?.paichaNo ?? '--';
|
||
final dialogFocus = FocusNode();
|
||
var dismissed = false;
|
||
var selectedIndex = 0; // 0 = "否" (default), 1 = "是"
|
||
|
||
void dismissAsNo() {
|
||
if (dismissed) return;
|
||
dismissed = true;
|
||
Navigator.of(context).pop();
|
||
setState(() {
|
||
_crossPaichaPending = false;
|
||
});
|
||
}
|
||
|
||
void confirmSwitch() {
|
||
if (dismissed) return;
|
||
dismissed = true;
|
||
Navigator.of(context).pop();
|
||
_switchToNewPaicha(newZongpaiNo);
|
||
}
|
||
|
||
showDialog(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (ctx) => StatefulBuilder(
|
||
builder: (ctx, setDialogState) {
|
||
return KeyboardListener(
|
||
focusNode: dialogFocus,
|
||
onKeyEvent: (event) {
|
||
if (event is! KeyDownEvent) return;
|
||
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||
setDialogState(() => selectedIndex = 0);
|
||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||
setDialogState(() => selectedIndex = 1);
|
||
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||
selectedIndex == 0 ? dismissAsNo() : confirmSwitch();
|
||
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||
dismissAsNo();
|
||
}
|
||
},
|
||
child: PopScope(
|
||
canPop: false,
|
||
onPopInvokedWithResult: (didPop, _) {
|
||
if (!didPop) dismissAsNo();
|
||
},
|
||
child: AlertDialog(
|
||
backgroundColor: Colors.orange.shade50,
|
||
title: Row(
|
||
children: [
|
||
Icon(Icons.warning_amber, color: Colors.orange.shade800),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
'跨排产号扫描',
|
||
style: TextStyle(color: Colors.orange.shade900),
|
||
),
|
||
],
|
||
),
|
||
content: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
Text(
|
||
'总排号 $newZongpaiNo 不属于当前排产号($currentPaicha),'
|
||
'是否切换到新的排产号?\n\n'
|
||
'选择「是」将放弃当前已扫描的所有数据。',
|
||
style: const TextStyle(fontSize: 14),
|
||
),
|
||
const SizedBox(height: 16),
|
||
_dialogOptionBtn(
|
||
label: '否',
|
||
selected: selectedIndex == 0,
|
||
onTap: dismissAsNo,
|
||
),
|
||
const SizedBox(height: 8),
|
||
_dialogOptionBtn(
|
||
label: '是,切换排产号',
|
||
selected: selectedIndex == 1,
|
||
onTap: confirmSwitch,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
);
|
||
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
dialogFocus.requestFocus();
|
||
});
|
||
}
|
||
|
||
Widget _dialogOptionBtn({
|
||
required String label,
|
||
required bool selected,
|
||
required VoidCallback onTap,
|
||
}) {
|
||
return SizedBox(
|
||
width: double.infinity,
|
||
height: 40,
|
||
child: ElevatedButton(
|
||
onPressed: onTap,
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor:
|
||
selected ? Colors.blue.shade700 : Colors.grey.shade200,
|
||
foregroundColor: selected ? Colors.white : Colors.black87,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||
),
|
||
child: Text(
|
||
label,
|
||
style: TextStyle(
|
||
fontWeight: selected ? FontWeight.w800 : FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
void _switchToNewPaicha(String newZongpaiNo) {
|
||
_feedbackService.trigger(FeedbackEvent.paichanSwitch);
|
||
setState(() {
|
||
_crossPaichaPending = false;
|
||
_zongpaiNos.clear();
|
||
_zongpaiNos.add(newZongpaiNo);
|
||
_overview = null;
|
||
_overviewNotFound = false;
|
||
_overviewError = null;
|
||
_clearStatusOverride();
|
||
});
|
||
_loadOverview(newZongpaiNo);
|
||
}
|
||
|
||
void _toggleLock(bool value) {
|
||
if (value && _locationCode == null) {
|
||
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||
_showStatusOverride(
|
||
'请先扫描货位号',
|
||
StatusDotColor.red,
|
||
const Duration(seconds: 2),
|
||
);
|
||
return;
|
||
}
|
||
setState(() {
|
||
_isLocked = value;
|
||
if (!value && _zongpaiNos.length > 1) {
|
||
_zongpaiNos.removeRange(0, _zongpaiNos.length - 1);
|
||
}
|
||
});
|
||
}
|
||
|
||
String _locationLabel(CodeType? type) {
|
||
if (type == CodeType.locationTransit) return '转运区域';
|
||
return '普通货架';
|
||
}
|
||
|
||
Color _locationLabelColor(CodeType? type) {
|
||
if (type == CodeType.locationTransit) return Colors.orange;
|
||
return Colors.blue;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final colorScheme = Theme.of(context).colorScheme;
|
||
_updateBaseStatus();
|
||
final effectiveDot = _statusOverrideDot ?? _statusDot;
|
||
final effectiveText = _statusOverrideText ?? _statusText;
|
||
|
||
return KeyboardListener(
|
||
focusNode: _focusNode,
|
||
onKeyEvent: _onKeyEvent,
|
||
child: Scaffold(
|
||
appBar: AppBar(
|
||
title: Row(
|
||
children: [
|
||
const Text('上架登记'),
|
||
const SizedBox(width: 12),
|
||
Expanded(child: _buildLockHeader()),
|
||
],
|
||
),
|
||
titleSpacing: 12,
|
||
),
|
||
body: Column(
|
||
children: [
|
||
_buildPaichaHeader(),
|
||
if (_overview?.success == true) _buildTableHead(),
|
||
Expanded(child: _buildListBody()),
|
||
_buildBottomBar(colorScheme),
|
||
StatusBar(dotColor: effectiveDot, text: effectiveText),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildLockHeader() {
|
||
final backgroundColor =
|
||
_isLocked ? Colors.orange.shade700 : Colors.blue.shade700;
|
||
final label = _isLocked ? '货位锁定' : '未锁定';
|
||
|
||
return Material(
|
||
borderRadius: BorderRadius.circular(8),
|
||
color: backgroundColor,
|
||
child: InkWell(
|
||
borderRadius: BorderRadius.circular(8),
|
||
onTap: () => _toggleLock(!_isLocked),
|
||
child: Container(
|
||
width: double.infinity,
|
||
height: 36,
|
||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
const Icon(Icons.swap_horiz, color: Colors.white, size: 18),
|
||
const SizedBox(width: 6),
|
||
Flexible(
|
||
child: FittedBox(
|
||
fit: BoxFit.scaleDown,
|
||
child: Text(
|
||
label,
|
||
maxLines: 1,
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w800,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
'P2',
|
||
style: TextStyle(
|
||
color: Colors.white.withValues(alpha: 0.88),
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildLocationChip() {
|
||
final color = _locationLabelColor(_locationType);
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: color.withValues(alpha: 0.15),
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
child: Text(
|
||
_locationLabel(_locationType),
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
color: color,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// === Paicha Header ===
|
||
|
||
Widget _buildPaichaHeader() {
|
||
final overview = _overview;
|
||
final hasData = overview?.success == true;
|
||
final hasLoc = _locationCode != null;
|
||
|
||
int totalCount = 0, shelvedCount = 0, transferredCount = 0;
|
||
if (hasData && overview != null) {
|
||
totalCount = overview.totalCount;
|
||
for (final item in overview.items) {
|
||
switch (item.status) {
|
||
case 'on_shelf':
|
||
shelvedCount++;
|
||
case 'transferred':
|
||
transferredCount++;
|
||
}
|
||
}
|
||
}
|
||
|
||
return Container(
|
||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 6),
|
||
decoration: BoxDecoration(
|
||
color: Colors.grey.shade50,
|
||
border: Border(bottom: BorderSide(color: Colors.grey.shade300)),
|
||
),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.center,
|
||
children: [
|
||
Text(
|
||
hasData ? (overview!.paichaNo ?? '--') : '--',
|
||
style: TextStyle(
|
||
fontSize: 24,
|
||
fontWeight: FontWeight.w800,
|
||
color: hasData ? Colors.black87 : Colors.grey.shade400,
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.end,
|
||
children: [
|
||
if (hasLoc)
|
||
Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(
|
||
_locationCode!,
|
||
style: const TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.black54,
|
||
),
|
||
),
|
||
const SizedBox(width: 5),
|
||
_buildLocationChip(),
|
||
],
|
||
),
|
||
Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
_buildStatTag(
|
||
'共 $totalCount',
|
||
const Color(0xFFE3F2FD),
|
||
const Color(0xFF1565C0),
|
||
hasData,
|
||
),
|
||
_buildStatTag(
|
||
'上架 $shelvedCount',
|
||
const Color(0xFFE8F5E9),
|
||
const Color(0xFF2E7D32),
|
||
hasData,
|
||
),
|
||
_buildStatTag(
|
||
'转运 $transferredCount',
|
||
const Color(0xFFFFF3E0),
|
||
const Color(0xFFE65100),
|
||
hasData,
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildStatTag(String text, Color bg, Color fg, bool hasData) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 1),
|
||
margin: const EdgeInsets.only(left: 5),
|
||
decoration: BoxDecoration(
|
||
color: hasData ? bg : const Color(0xFFF5F5F5),
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
child: Text(
|
||
text,
|
||
style: TextStyle(
|
||
fontSize: 10,
|
||
fontWeight: FontWeight.w700,
|
||
color: hasData ? fg : Colors.grey.shade400,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// === Table Head ===
|
||
|
||
Widget _buildTableHead() {
|
||
return Container(
|
||
height: 28,
|
||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||
decoration: BoxDecoration(
|
||
color: Colors.grey.shade200,
|
||
border: Border(bottom: BorderSide(color: Colors.grey.shade400)),
|
||
),
|
||
child: const Row(
|
||
children: [
|
||
Expanded(flex: 22, child: Text('总排号', textAlign: TextAlign.center, style: _headerStyle)),
|
||
Expanded(flex: 18, child: Text('工令号', textAlign: TextAlign.center, style: _headerStyle)),
|
||
Expanded(
|
||
flex: 12,
|
||
child: Text(
|
||
'数量',
|
||
textAlign: TextAlign.center,
|
||
style: _headerStyle,
|
||
),
|
||
),
|
||
Expanded(
|
||
flex: 28,
|
||
child: Text(
|
||
'货位号',
|
||
textAlign: TextAlign.center,
|
||
style: _headerStyle,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// === List Body ===
|
||
|
||
Widget _buildListBody() {
|
||
if (_overviewZongpaiNo == null) {
|
||
return _buildListMessage('扫描总排号后自动显示排产号上架情况');
|
||
}
|
||
if (_overviewLoading && _overview == null) {
|
||
return _buildListMessage('正在加载排产号上架情况…');
|
||
}
|
||
if (_overviewNotFound) {
|
||
return _buildListMessage('暂无排产信息');
|
||
}
|
||
if (_overviewError != null && _overview == null) {
|
||
return _buildListError(_overviewError!);
|
||
}
|
||
final overview = _overview;
|
||
if (overview == null || !overview.success) {
|
||
return _buildListMessage('扫描总排号后自动显示排产号上架情况');
|
||
}
|
||
|
||
final scannedSet = _zongpaiNos.toSet();
|
||
final scanned = <PaichaOverviewItem>[];
|
||
final unscanned = <PaichaOverviewItem>[];
|
||
for (final item in overview.items) {
|
||
if (scannedSet.contains(item.zongpaiNo)) {
|
||
scanned.add(item);
|
||
} else {
|
||
unscanned.add(item);
|
||
}
|
||
}
|
||
|
||
final hasDivider = scanned.isNotEmpty && unscanned.isNotEmpty;
|
||
|
||
return Column(
|
||
children: [
|
||
if (_overviewError != null)
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||
color: Colors.red.shade50,
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
_overviewError!,
|
||
style: const TextStyle(fontSize: 12, color: Colors.red),
|
||
),
|
||
),
|
||
TextButton(
|
||
onPressed: _overviewZongpaiNo == null
|
||
? null
|
||
: () => _loadOverview(_overviewZongpaiNo!, force: true),
|
||
child: const Text('重试'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Expanded(
|
||
child: ListView.builder(
|
||
itemCount: scanned.length +
|
||
(hasDivider ? 1 : 0) +
|
||
unscanned.length,
|
||
itemBuilder: (context, index) {
|
||
if (index < scanned.length) {
|
||
return _buildListRow(scanned[index], isScanned: true);
|
||
}
|
||
if (index == scanned.length && hasDivider) {
|
||
return _buildSectionDivider(unscanned.length);
|
||
}
|
||
final unscannedIdx =
|
||
index - scanned.length - (hasDivider ? 1 : 0);
|
||
return _buildListRow(unscanned[unscannedIdx], isScanned: false);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildSectionDivider(int count) {
|
||
return Container(
|
||
height: 24,
|
||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||
decoration: BoxDecoration(
|
||
border: Border(bottom: BorderSide(color: Colors.grey.shade200)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
const Expanded(child: Divider(height: 0)),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||
child: Text(
|
||
'以下 $count 项未操作',
|
||
style: TextStyle(
|
||
fontSize: 10,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.grey.shade500,
|
||
),
|
||
),
|
||
),
|
||
const Expanded(child: Divider(height: 0)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildListRow(PaichaOverviewItem item, {required bool isScanned}) {
|
||
final rowStyle = TextStyle(
|
||
fontSize: 15,
|
||
fontWeight: isScanned ? FontWeight.w700 : FontWeight.w500,
|
||
color: Colors.black87,
|
||
);
|
||
|
||
final Color barColor;
|
||
final Color bgColor;
|
||
if (isScanned) {
|
||
final alreadyOnShelf = item.status == 'on_shelf' && !_isTransitTarget;
|
||
if (alreadyOnShelf) {
|
||
barColor = Colors.red;
|
||
bgColor = Colors.red.shade50;
|
||
} else {
|
||
barColor = const Color(0xFF43A047);
|
||
bgColor = const Color(0xFFF1F8E9);
|
||
}
|
||
} else {
|
||
barColor = _barColor(item.status);
|
||
bgColor = _rowBgColor(item.status);
|
||
}
|
||
|
||
return Container(
|
||
constraints: const BoxConstraints(minHeight: 36),
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||
decoration: BoxDecoration(
|
||
color: bgColor,
|
||
border: Border(
|
||
left: BorderSide(color: barColor, width: 3),
|
||
bottom: BorderSide(color: Colors.grey.shade200),
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
flex: 22,
|
||
child: Text(
|
||
item.zongpaiNo,
|
||
textAlign: TextAlign.center,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: rowStyle,
|
||
),
|
||
),
|
||
Expanded(
|
||
flex: 18,
|
||
child: Text(
|
||
item.workOrderNo ?? '--',
|
||
textAlign: TextAlign.center,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: rowStyle,
|
||
),
|
||
),
|
||
Expanded(
|
||
flex: 12,
|
||
child: Text(
|
||
item.quantity.toString(),
|
||
textAlign: TextAlign.center,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: rowStyle,
|
||
),
|
||
),
|
||
Expanded(
|
||
flex: 28,
|
||
child: Text(
|
||
item.locationCode ?? '\u2014',
|
||
textAlign: TextAlign.center,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: rowStyle,
|
||
),
|
||
),
|
||
if (isScanned && _isLocked) ...[
|
||
const SizedBox(width: 6),
|
||
SizedBox(
|
||
width: 28,
|
||
height: 28,
|
||
child: IconButton(
|
||
icon: Icon(Icons.delete_outline, size: 16),
|
||
color: Colors.red,
|
||
onPressed: () => _removeZongpai(item.zongpaiNo),
|
||
padding: EdgeInsets.zero,
|
||
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Color _barColor(String status) {
|
||
switch (status) {
|
||
case 'on_shelf':
|
||
return const Color(0xFF2196F3);
|
||
case 'transferred':
|
||
return const Color(0xFFFF9800);
|
||
default:
|
||
return Colors.grey.shade400;
|
||
}
|
||
}
|
||
|
||
Color _rowBgColor(String status) {
|
||
switch (status) {
|
||
case 'on_shelf':
|
||
return const Color(0xFFE3F2FD);
|
||
case 'transferred':
|
||
return const Color(0xFFFFF3E0);
|
||
default:
|
||
return const Color(0xFFF5F5F5);
|
||
}
|
||
}
|
||
|
||
Widget _buildListMessage(String text) {
|
||
return Center(
|
||
child: Text(
|
||
text,
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(fontSize: 13, color: Colors.grey.shade600),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildListError(String text) {
|
||
return Center(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(
|
||
text,
|
||
textAlign: TextAlign.center,
|
||
style: const TextStyle(fontSize: 13, color: Colors.red),
|
||
),
|
||
const SizedBox(height: 6),
|
||
TextButton(
|
||
onPressed: _overviewZongpaiNo == null
|
||
? null
|
||
: () => _loadOverview(_overviewZongpaiNo!, force: true),
|
||
child: const Text('重试'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// === Bottom Bar ===
|
||
|
||
Widget _buildBottomBar(ColorScheme colorScheme) {
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
border: Border(top: BorderSide(color: Colors.grey.shade300)),
|
||
),
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
child: SizedBox(
|
||
width: double.infinity,
|
||
height: 34,
|
||
child: ElevatedButton(
|
||
onPressed: _canSubmit ? _submit : null,
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: registrationSubmitColor(
|
||
canSubmit: _canSubmit,
|
||
isTransitTarget: _isTransitTarget,
|
||
primaryColor: colorScheme.primary,
|
||
),
|
||
foregroundColor:
|
||
_canSubmit ? Colors.white : Colors.grey.shade600,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
),
|
||
child: _isSubmitting
|
||
? const SizedBox(
|
||
width: 18,
|
||
height: 18,
|
||
child: CircularProgressIndicator(
|
||
strokeWidth: 2,
|
||
color: Colors.white,
|
||
),
|
||
)
|
||
: Text(
|
||
registrationSubmitLabel(
|
||
isTransitTarget: _isTransitTarget,
|
||
isLocked: _isLocked,
|
||
zongpaiCount: _zongpaiNos.length,
|
||
),
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
const _headerStyle = TextStyle(fontSize: 10, fontWeight: FontWeight.w700);
|