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/widgets/status_bar.dart'; class RegistrationPage extends StatefulWidget { const RegistrationPage({super.key}); @override State createState() => _RegistrationPageState(); } class _RegistrationPageState extends State { final _scannerService = ScannerService(); final _apiService = ApiService(); final _feedbackService = FeedbackService(); final _focusNode = FocusNode(); final _zongpaiNos = []; String? _locationCode; CodeType? _locationType; bool _isLocked = false; bool _isSubmitting = false; // Status bar state StatusDotColor _statusDot = StatusDotColor.blue; String _statusText = '等待扫描总排号或货位号…'; String? _statusOverrideText; StatusDotColor? _statusOverrideDot; @override void initState() { super.initState(); _scannerService.scanResults.listen(_onScan); WidgetsBinding.instance.addPostFrameCallback((_) { _focusNode.requestFocus(); }); } @override void dispose() { _focusNode.dispose(); _feedbackService.dispose(); super.dispose(); } void _onKeyEvent(KeyEvent event) { if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.enter) { _submit(); } } void _onScan(ScanResult result) { final parsed = CodeParser.parse(result.barcode); switch (parsed.type) { case CodeType.zongpaiNo: _feedbackService.trigger(FeedbackEvent.scanValid); setState(() { if (_isLocked) { final idx = _zongpaiNos.indexOf(parsed.value); if (idx >= 0) { _zongpaiNos[idx] = parsed.value; } else { _zongpaiNos.add(parsed.value); } } else { if (_zongpaiNos.isNotEmpty) { _zongpaiNos[0] = parsed.value; } else { _zongpaiNos.add(parsed.value); } } _clearStatusOverride(); }); case CodeType.locationNormal: case CodeType.locationTransit: if (!_isLocked) { _feedbackService.trigger(FeedbackEvent.scanValid); setState(() { _locationCode = parsed.value; _locationType = parsed.type; _clearStatusOverride(); }); } case CodeType.invalid: _feedbackService.trigger(FeedbackEvent.scanInvalid); _showStatusOverride( '无效码:${result.barcode}', StatusDotColor.red, const Duration(seconds: 2), ); } } void _removeZongpai(int index) { setState(() { _zongpaiNos.removeAt(index); }); } // --- Status bar management --- 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 hasZ = _zongpaiNos.isNotEmpty; final hasL = _locationCode != null; if (hasZ && hasL) { _statusDot = StatusDotColor.blue; _statusText = '请确认信息并提交'; } else if (hasZ && !hasL) { _statusDot = StatusDotColor.blue; _statusText = '请扫描目标货位号'; } else if (!hasZ && hasL) { _statusDot = StatusDotColor.blue; _statusText = '请扫描执行卡'; } else { _statusDot = StatusDotColor.blue; _statusText = '等待扫描总排号或货位号…'; } } bool get _canSubmit => _zongpaiNos.isNotEmpty && _locationCode != null && !_isSubmitting; Future _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; } setState(() => _isSubmitting = true); if (_isLocked && _zongpaiNos.length > 1) { await _submitBatch(baseUrl); } else { await _submitOne(baseUrl, _zongpaiNos.first); } } Future _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); setState(() { _zongpaiNos.remove(zongpaiNo); if (!_isLocked) { _locationCode = null; _locationType = null; } }); final msg = _isLocked ? '货位已锁定,请扫描下一张执行卡' : '上架成功'; _showStatusOverride(msg, StatusDotColor.green, const Duration(milliseconds: 1500)); } else if (result.isDuplicate) { _feedbackService.trigger(FeedbackEvent.duplicateError); _showDuplicateDialog(zongpaiNo, result.duplicateInfo); } 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 _submitBatch(String baseUrl) async { int successCount = 0; final failed = []; final toRemove = []; for (final zp in List.of(_zongpaiNos)) { final result = await _apiService.registerLocation( baseUrl: baseUrl, zongpaiNo: zp, locationCode: _locationCode!, ); if (result.success) { successCount++; toRemove.add(zp); } else { failed.add(zp); if (result.isDuplicate) { setState(() { _zongpaiNos.removeWhere((e) => toRemove.contains(e)); _isSubmitting = false; }); _feedbackService.trigger(FeedbackEvent.duplicateError); _showDuplicateDialog(zp, result.duplicateInfo); return; } } } if (!mounted) return; setState(() { _zongpaiNos.removeWhere((e) => toRemove.contains(e)); _isSubmitting = false; }); if (failed.isEmpty) { _feedbackService.trigger(FeedbackEvent.submitSuccess); _showStatusOverride( '批量上架成功($successCount 条)', StatusDotColor.green, const Duration(milliseconds: 1500), ); } else { _feedbackService.trigger(FeedbackEvent.submitFailure); _showStatusOverride( '成功 $successCount 条,失败 ${failed.length} 条', StatusDotColor.red, const Duration(seconds: 2), ); } } void _showDuplicateDialog(String zongpaiNo, Map? 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 _toggleLock(bool value) { if (value && _locationCode == null) { _feedbackService.trigger(FeedbackEvent.submitFailure); _showStatusOverride( '请先扫描货位号', StatusDotColor.red, const Duration(seconds: 2), ); return; } setState(() { _isLocked = value; if (!value) { if (_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; // Compute effective status _updateBaseStatus(); final effectiveDot = _statusOverrideDot ?? _statusDot; final effectiveText = _statusOverrideText ?? _statusText; return KeyboardListener( focusNode: _focusNode, onKeyEvent: _onKeyEvent, child: Scaffold( appBar: AppBar( title: const Text('上架登记'), actions: [ Padding( padding: const EdgeInsets.only(right: 4), child: Row( children: [ const Text('锁定货位', style: TextStyle(fontSize: 13)), Switch(value: _isLocked, onChanged: _toggleLock), ], ), ), ], ), body: Column( children: [ // Main form area (no top banner) Expanded( child: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ // ---- 目标货位 (上方) ---- Row( children: [ const Text( '目标货位', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), ), if (_locationCode != null) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: _locationLabelColor( _locationType, ).withValues(alpha: 0.15), borderRadius: BorderRadius.circular(4), ), child: Text( _locationLabel(_locationType), style: TextStyle( fontSize: 11, color: _locationLabelColor(_locationType), fontWeight: FontWeight.bold, ), ), ), ], ], ), const SizedBox(height: 6), Container( width: double.infinity, padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 14, ), decoration: BoxDecoration( border: Border.all( color: _locationCode != null ? Colors.green : Colors.grey.shade400, width: _locationCode != null ? 2 : 1, ), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Expanded( child: Text( _locationCode ?? '', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: _locationCode != null ? Colors.black87 : Colors.grey, ), ), ), if (_isLocked) const Icon( Icons.lock, color: Colors.orange, size: 20, ), ], ), ), const SizedBox(height: 16), // ---- 总排号 (下方) ---- Row( children: [ const Text( '总排号', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), ), if (_isLocked && _zongpaiNos.isNotEmpty) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: Colors.blue.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(4), ), child: Text( '${_zongpaiNos.length} 条', style: const TextStyle( fontSize: 11, color: Colors.blue, fontWeight: FontWeight.bold, ), ), ), ], ], ), const SizedBox(height: 6), if (_isLocked) ...[ // 锁定模式:列表显示多条总排号 Expanded( child: _zongpaiNos.isEmpty ? Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade400), borderRadius: BorderRadius.circular(8), ), child: const Text( '', style: TextStyle( fontSize: 20, color: Colors.grey, ), ), ) : ListView.separated( itemCount: _zongpaiNos.length, separatorBuilder: (_, __) => const SizedBox(height: 6), itemBuilder: (context, index) { return Dismissible( key: ValueKey('${_zongpaiNos[index]}-$index'), direction: DismissDirection.endToStart, onDismissed: (_) => _removeZongpai(index), background: Container( alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 16), decoration: BoxDecoration( color: Colors.red.shade100, borderRadius: BorderRadius.circular(8), ), child: const Icon( Icons.delete, color: Colors.red, ), ), child: Container( 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: [ Expanded( child: Text( _zongpaiNos[index], style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black87, ), ), ), IconButton( icon: const Icon( Icons.close, size: 20, ), onPressed: () => _removeZongpai(index), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), ), ); }, ), ), ] else ...[ // 单次模式:单个显示 Container( width: double.infinity, padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 14, ), decoration: BoxDecoration( border: Border.all( color: _zongpaiNos.isNotEmpty ? Colors.green : Colors.grey.shade400, width: _zongpaiNos.isNotEmpty ? 2 : 1, ), borderRadius: BorderRadius.circular(8), ), child: Text( _zongpaiNos.isNotEmpty ? _zongpaiNos.first : '', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: _zongpaiNos.isNotEmpty ? Colors.black87 : Colors.grey, ), ), ), ], const SizedBox(height: 16), // Submit button SizedBox( width: double.infinity, height: 48, child: ElevatedButton( onPressed: _canSubmit ? _submit : null, style: ElevatedButton.styleFrom( backgroundColor: _canSubmit ? colorScheme.primary : Colors.grey.shade300, foregroundColor: _canSubmit ? Colors.white : Colors.grey.shade600, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: _isSubmitting ? const SizedBox( width: 22, height: 22, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text( _isLocked && _zongpaiNos.length > 1 ? '批量上架(${_zongpaiNos.length} 条)' : '确认上架(P1)', style: const TextStyle(fontSize: 18), ), ), ), ], ), ), ), // Bottom status bar StatusBar(dotColor: effectiveDot, text: effectiveText), ], ), ), ); } }