import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:image_picker/image_picker.dart'; import '../../../core/theme/app_color_scheme.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_theme.dart'; import '../../../providers/auth_provider.dart'; import '../../components/glass_panel.dart'; import '../../components/neon_glow.dart'; import '../../components/material_input.dart'; import '../main/main_page.dart'; /// 註冊頁面(兩步註冊:賬號信息 + 身份證上傳) class RegisterPage extends StatefulWidget { const RegisterPage({super.key}); @override State createState() => _RegisterPageState(); } class _RegisterPageState extends State { int _currentStep = 0; // 0: 賬號信息, 1: 身份證上傳 // 第一步 final _usernameController = TextEditingController(); final _passwordController = TextEditingController(); final _confirmPasswordController = TextEditingController(); final _referralCodeController = TextEditingController(); final _formKey = GlobalKey(); bool _obscurePassword = true; bool _obscureConfirmPassword = true; // 第二步 XFile? _frontFile; XFile? _backFile; Uint8List? _frontBytes; Uint8List? _backBytes; final _picker = ImagePicker(); bool get _canSubmit => _frontFile != null && _backFile != null && !_isLoading; bool _isLoading = false; @override void dispose() { _usernameController.dispose(); _passwordController.dispose(); _confirmPasswordController.dispose(); _referralCodeController.dispose(); super.dispose(); } Future _pickImage(bool isFront) async { final picked = await _picker.pickImage( source: ImageSource.gallery, maxWidth: 1920, maxHeight: 1920, imageQuality: 85, ); if (picked != null) { final bytes = await picked.readAsBytes(); setState(() { if (isFront) { _frontFile = picked; _frontBytes = bytes; } else { _backFile = picked; _backBytes = bytes; } }); } } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Scaffold( backgroundColor: colorScheme.background, appBar: AppBar( backgroundColor: AppColorScheme.darkBackground.withValues(alpha: 0), elevation: 0, leading: IconButton( icon: Icon(Icons.chevron_left, color: colorScheme.onSurface), onPressed: _currentStep == 1 ? () => setState(() => _currentStep = 0) : () => Navigator.pop(context), ), ), body: SafeArea( child: SingleChildScrollView( padding: AppSpacing.pagePadding, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 步驟指示器 _buildStepIndicator(colorScheme), const SizedBox(height: AppSpacing.xl), // 內容區 _currentStep == 0 ? _buildStep1(colorScheme) : _buildStep2(colorScheme), ], ), ), ), ); } Widget _buildStepIndicator(ColorScheme colorScheme) { return Row( children: [ _buildStepCircle( number: '1', label: '賬號信息', isActive: true, isComplete: _currentStep > 0, colorScheme: colorScheme, ), Expanded( child: Container( height: 2, color: _currentStep > 0 ? AppColorScheme.up : colorScheme.outlineVariant.withValues(alpha: 0.2), ), ), _buildStepCircle( number: '2', label: '身份驗證', isActive: _currentStep >= 1, isComplete: false, colorScheme: colorScheme, ), ], ); } Widget _buildStepCircle({ required String number, required String label, required bool isActive, required bool isComplete, required ColorScheme colorScheme, }) { final Color circleColor; final Color textColor; if (isComplete) { circleColor = AppColorScheme.up; textColor = AppColorScheme.darkOnPrimary; } else if (isActive) { circleColor = colorScheme.primary; textColor = AppColorScheme.darkOnPrimary; } else { circleColor = colorScheme.surfaceContainerHigh; textColor = colorScheme.onSurfaceVariant; } return Column( children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: circleColor, shape: BoxShape.circle, ), child: Center( child: isComplete ? Icon(Icons.check, size: 16, color: textColor) : Text( number, style: AppTextStyles.headlineMedium(context).copyWith( fontWeight: FontWeight.bold, color: textColor, ), ), ), ), const SizedBox(height: AppSpacing.xs), Text( label, style: AppTextStyles.bodySmall(context).copyWith( color: colorScheme.onSurfaceVariant, ), ), ], ); } /// 第一步:賬號信息 Widget _buildStep1(ColorScheme colorScheme) { return Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 標題 Center( child: Text( '創建賬號', style: AppTextStyles.displaySmall(context).copyWith( fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), ), const SizedBox(height: AppSpacing.xxl), // 用戶名 MaterialInput( controller: _usernameController, labelText: '賬號', hintText: '請輸入賬號(4-20位字母數字)', prefixIcon: Icons.person_outline, validator: (value) { if (value == null || value.isEmpty) return '請輸入賬號'; if (value.length < 4) return '賬號過短'; if (value.length > 20) return '賬號過長'; return null; }, ), const SizedBox(height: AppSpacing.md), // 密碼 MaterialPasswordInput( controller: _passwordController, labelText: '密碼', hintText: '請輸入密碼(至少6位)', prefixIcon: Icons.lock_outline, validator: (value) { if (value == null || value.isEmpty) return '請輸入密碼'; if (value.length < 6) return '密碼過短'; return null; }, ), const SizedBox(height: AppSpacing.md), // 確認密碼 MaterialPasswordInput( controller: _confirmPasswordController, labelText: '確認密碼', hintText: '請再次輸入密碼', prefixIcon: Icons.lock_outline, validator: (value) { if (value == null || value.isEmpty) return '請再次輸入密碼'; if (value != _passwordController.text) return '兩次密碼不一致'; return null; }, ), const SizedBox(height: AppSpacing.md), // 推廣碼(可選) MaterialInput( controller: _referralCodeController, labelText: '推廣碼', hintText: '推廣碼(選填)', prefixIcon: Icons.card_giftcard, ), const SizedBox(height: AppSpacing.xl), // 下一步按鈕 SizedBox( width: double.infinity, child: NeonButton( text: '下一步', type: NeonButtonType.primary, onPressed: () { if (_formKey.currentState!.validate()) { setState(() => _currentStep = 1); } }, height: 48, showGlow: true, ), ), const SizedBox(height: AppSpacing.md), // 登錄鏈接 Center( child: TextButton( onPressed: () => Navigator.pop(context), child: Text( '已有賬號?立即登錄', style: AppTextStyles.headlineMedium(context), ), ), ), ], ), ); } /// 第二步:身份證上傳 Widget _buildStep2(ColorScheme colorScheme) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 標題區 GlassPanel( padding: const EdgeInsets.all(AppSpacing.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(AppSpacing.sm), decoration: BoxDecoration( color: colorScheme.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(AppRadius.md), ), child: Icon( Icons.shield, color: colorScheme.primary, size: 22, ), ), const SizedBox(width: AppSpacing.md), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '身份驗證', style: AppTextStyles.headlineLarge(context).copyWith( fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.xs), Text( '上傳身份證正反面完成註冊', style: AppTextStyles.bodyMedium(context).copyWith( color: colorScheme.onSurfaceVariant, ), ), ], ), ], ), const SizedBox(height: AppSpacing.xl), // 身份證正面 Text( '身份證正面(人像面)', style: AppTextStyles.bodyLarge(context).copyWith( fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.sm), _buildUploadZone( imageFile: _frontFile, imageBytes: _frontBytes, label: '人像面', onTap: () => _pickImage(true), colorScheme: colorScheme, ), const SizedBox(height: AppSpacing.lg), // 身份證反面 Text( '身份證反面(國徽面)', style: AppTextStyles.bodyLarge(context).copyWith( fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.sm), _buildUploadZone( imageFile: _backFile, imageBytes: _backBytes, label: '國徽面', onTap: () => _pickImage(false), colorScheme: colorScheme, ), const SizedBox(height: AppSpacing.xl), // 註冊按鈕 Consumer( builder: (context, auth, _) { return SizedBox( width: double.infinity, child: NeonButton( text: _isLoading ? '註冊中...' : '完成註冊', type: NeonButtonType.primary, onPressed: _canSubmit && !auth.isLoading ? _handleRegister : null, height: 48, showGlow: _canSubmit, ), ); }, ), ], ), ), const SizedBox(height: AppSpacing.lg), // 安全提示 Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: AppColorScheme.up.withValues(alpha: 0.06), borderRadius: AppRadius.radiusLg, border: Border.all( color: AppColorScheme.up.withValues(alpha: 0.12), ), ), child: Row( children: [ Icon(Icons.lock, size: 16, color: AppColorScheme.up), const SizedBox(width: AppSpacing.sm), Expanded( child: Text( '您的身份信息將被加密存儲,僅用於身份驗證', style: AppTextStyles.bodySmall(context).copyWith( color: AppColorScheme.up.withValues(alpha: 0.8), ), ), ), ], ), ), ], ); } Widget _buildUploadZone({ required XFile? imageFile, required Uint8List? imageBytes, required String label, required VoidCallback onTap, required ColorScheme colorScheme, }) { final hasImage = imageFile != null && imageBytes != null; return GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 300), width: double.infinity, height: 140, decoration: BoxDecoration( color: hasImage ? AppColorScheme.up.withValues(alpha: 0.06) : colorScheme.surfaceContainerHigh.withValues(alpha: 0.3), borderRadius: AppRadius.radiusXl, border: Border.all( color: hasImage ? AppColorScheme.up.withValues(alpha: 0.3) : Colors.transparent, ), ), child: hasImage ? ClipRRect( borderRadius: AppRadius.radiusXl, child: Stack( fit: StackFit.passthrough, children: [ Image.memory(imageBytes!, fit: BoxFit.cover), Positioned( left: 0, right: 0, bottom: 0, child: Container( padding: const EdgeInsets.symmetric( vertical: AppSpacing.sm, horizontal: AppSpacing.md, ), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ AppColorScheme.darkBackground.withValues(alpha: 0), AppColorScheme.darkSurfaceLowest .withValues(alpha: 0.6), ], ), borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(AppRadius.xl), bottomRight: Radius.circular(AppRadius.xl), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '$label已選擇', style: AppTextStyles.bodyMedium(context).copyWith( fontWeight: FontWeight.w600, color: AppColorScheme.darkOnPrimary, ), ), GestureDetector( onTap: () { setState(() { if (label == '人像面') { _frontFile = null; _frontBytes = null; } else { _backFile = null; _backBytes = null; } }); }, child: Container( padding: const EdgeInsets.all(AppSpacing.xs), decoration: BoxDecoration( color: AppColorScheme.darkOnPrimary .withValues(alpha: 0.15), shape: BoxShape.circle, ), child: Icon( Icons.close, size: 14, color: AppColorScheme.darkOnPrimary, ), ), ), ], ), ), ), ], ), ) : CustomPaint( painter: _DashedBorderPainter( color: colorScheme.onSurfaceVariant.withValues(alpha: 0.2), borderRadius: AppRadius.xl, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.camera_alt, size: 28, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5), ), const SizedBox(height: AppSpacing.sm), Text( '點擊上傳$label', style: AppTextStyles.bodyLarge(context).copyWith( color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6), ), ), const SizedBox(height: AppSpacing.xs), Text( '支持 JPG、PNG 格式', style: AppTextStyles.bodySmall(context).copyWith( color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4), ), ), ], ), ), ), ); } Future _handleRegister() async { if (!_canSubmit) return; setState(() => _isLoading = true); try { final auth = context.read(); final response = await auth.register( _usernameController.text.trim(), _passwordController.text, referralCode: _referralCodeController.text.trim().isEmpty ? null : _referralCodeController.text.trim(), frontBytes: _frontBytes!, backBytes: _backBytes!, ); if (!mounted) return; if (response.success) { Navigator.pushReplacement( context, MaterialPageRoute(builder: (_) => const MainPage()), ); } else { showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('註冊失敗'), content: Text(response.message ?? '請稍後重試'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('確定'), ), ], ), ); } } catch (e) { if (mounted) { showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('註冊失敗'), content: Text(e.toString()), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('確定'), ), ], ), ); } } finally { if (mounted) setState(() => _isLoading = false); } } } /// 虚线边框画筆 class _DashedBorderPainter extends CustomPainter { final Color color; final double borderRadius; _DashedBorderPainter({ required this.color, required this.borderRadius, }); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color ..strokeWidth = 1.5 ..style = PaintingStyle.stroke; const dashWidth = 6.0; const dashSpace = 4.0; final path = Path() ..addRRect(RRect.fromRectAndRadius( Rect.fromLTWH(0, 0, size.width, size.height), Radius.circular(borderRadius), )); final metrics = path.computeMetrics(); for (final metric in metrics) { double distance = 0; while (distance < metric.length) { final end = (distance + dashWidth).clamp(0.0, metric.length); canvas.drawPath(metric.extractPath(distance, end), paint); distance += dashWidth + dashSpace; } } } @override bool shouldRepaint(covariant _DashedBorderPainter oldDelegate) { return oldDelegate.color != color || oldDelegate.borderRadius != borderRadius; } }