diff --git a/lib/pages/registration_page.dart b/lib/pages/registration_page.dart new file mode 100644 index 0000000..7a7df5a --- /dev/null +++ b/lib/pages/registration_page.dart @@ -0,0 +1,404 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.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/pages/settings_page.dart'; + +class RegistrationPage extends StatefulWidget { + const RegistrationPage({super.key}); + + @override + State createState() => _RegistrationPageState(); +} + +class _RegistrationPageState extends State { + final _scannerService = ScannerService(); + final _apiService = ApiService(); + + String? _zongpaiNo; + String? _locationCode; + CodeType? _locationType; + bool _isLocked = false; + bool _isSubmitting = false; + + String? _successMessage; + String? _snackbarMessage; + Color? _snackbarColor; + + @override + void initState() { + super.initState(); + _scannerService.scanResults.listen(_onScan); + } + + void _onScan(ScanResult result) { + final parsed = CodeParser.parse(result.barcode); + switch (parsed.type) { + case CodeType.zongpaiNo: + setState(() { + _zongpaiNo = parsed.value; + _snackbarMessage = null; + _successMessage = null; + }); + case CodeType.locationNormal: + case CodeType.locationTransit: + if (!_isLocked) { + setState(() { + _locationCode = parsed.value; + _locationType = parsed.type; + _snackbarMessage = null; + _successMessage = null; + }); + } + case CodeType.invalid: + _showFeedback('无效码,请重新扫描', isError: true); + } + } + + void _showFeedback(String message, {bool isError = false}) { + setState(() { + _snackbarMessage = message; + _snackbarColor = isError ? Colors.red.shade700 : Colors.green.shade700; + }); + Future.delayed(const Duration(seconds: 2), () { + if (mounted) setState(() => _snackbarMessage = null); + }); + } + + String get _statusText { + if (_isSubmitting) return '正在提交…'; + if (_successMessage != null) return _successMessage!; + if (_isLocked && _locationCode != null && _zongpaiNo == null) { + return '货位已锁定,请扫描下一张执行卡'; + } + final hasZ = _zongpaiNo != null; + final hasL = _locationCode != null; + if (hasZ && hasL) return '请确认信息并提交'; + if (hasZ && !hasL) return '请扫描目标货位号'; + if (!hasZ && hasL) return '请扫描执行卡'; + return '等待扫描总排号或货位号…'; + } + + bool get _canSubmit => + _zongpaiNo != null && _locationCode != null && !_isSubmitting; + + Future _submit() async { + if (!_canSubmit) return; + + final prefs = await SharedPreferences.getInstance(); + final baseUrl = prefs.getString('api_url') ?? ''; + if (baseUrl.isEmpty) { + _showFeedback('未配置 API 地址,请前往设置', isError: true); + return; + } + + setState(() => _isSubmitting = true); + + final result = await _apiService.registerLocation( + baseUrl: baseUrl, + zongpaiNo: _zongpaiNo!, + locationCode: _locationCode!, + ); + + if (!mounted) return; + + setState(() => _isSubmitting = false); + + if (result.success) { + setState(() { + _zongpaiNo = null; + if (!_isLocked) { + _locationCode = null; + _locationType = null; + } + _successMessage = + _isLocked ? '货位已锁定,请扫描下一张执行卡' : '上架成功'; + }); + _showFeedback('上架成功', isError: false); + Future.delayed(const Duration(milliseconds: 1500), () { + if (mounted) setState(() => _successMessage = null); + }); + } else if (result.isDuplicate) { + _showDuplicateDialog(result.duplicateInfo); + } else { + _showFeedback(result.errorMessage ?? '提交失败', isError: true); + } + } + + void _showDuplicateDialog(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: () => Navigator.pop(ctx), + child: const Text('关闭'), + ), + ], + ), + ); + } + + void _toggleLock(bool value) { + if (value && _locationCode == null) { + _showFeedback('请先扫描货位号', isError: true); + return; + } + setState(() => _isLocked = value); + } + + 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; + + return 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, + ), + IconButton( + icon: const Icon(Icons.settings), + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const SettingsPage()), + ), + ), + ], + ), + ), + ], + ), + body: Column( + children: [ + // Feedback banner + if (_snackbarMessage != null) + Container( + width: double.infinity, + padding: + const EdgeInsets.symmetric(vertical: 10, horizontal: 16), + color: _snackbarColor, + child: Text( + _snackbarMessage!, + style: const TextStyle(color: Colors.white, fontSize: 14), + textAlign: TextAlign.center, + ), + ), + + // Main form area + Expanded( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + // Zongpai number field + const Align( + alignment: Alignment.centerLeft, + child: Text('总排号', + style: TextStyle( + fontSize: 14, fontWeight: FontWeight.w500)), + ), + const SizedBox(height: 6), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 14), + decoration: BoxDecoration( + border: Border.all( + color: _zongpaiNo != null + ? Colors.green + : Colors.grey.shade400, + width: _zongpaiNo != null ? 2 : 1, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + _zongpaiNo ?? '', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: _zongpaiNo != null + ? Colors.black87 + : Colors.grey, + ), + ), + ), + + const SizedBox(height: 20), + + // Location field with type label + 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: 28), + + // 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, + ), + ) + : const Text('确 认 上 架', + style: TextStyle(fontSize: 18)), + ), + ), + ], + ), + ), + ), + + // Status bar at bottom + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + color: colorScheme.surfaceContainerHighest, + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: _isSubmitting + ? Colors.orange + : _successMessage != null + ? Colors.green + : Colors.blue, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + _statusText, + style: const TextStyle(fontSize: 14), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ], + ), + ); + } +}