import 'dart:typed_data'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.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'; /// KYC 實名認證頁面 class KycPage extends StatefulWidget { final bool returnToWithdraw; const KycPage({super.key, this.returnToWithdraw = false}); @override State createState() => _KycPageState(); } class _KycPageState extends State { XFile? _frontFile; XFile? _backFile; Uint8List? _frontBytes; Uint8List? _backBytes; bool _isSubmitting = false; bool get _canSubmit => _frontFile != null && _backFile != null && !_isSubmitting; final _picker = ImagePicker(); 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: colorScheme.surface.withOpacity(0.0), elevation: 0, title: Text( '實名認證', style: AppTextStyles.headlineLarge(context), ), leading: IconButton( icon: Icon(LucideIcons.chevronLeft, color: colorScheme.onSurface), onPressed: () => Navigator.of(context).pop(), ), ), body: SingleChildScrollView( padding: AppSpacing.pagePadding, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 步驟指示器 _buildStepIndicator(colorScheme), SizedBox(height: AppSpacing.xl), // 主表單區 GlassPanel( padding: EdgeInsets.all(AppSpacing.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 標題區 Row( children: [ Container( padding: EdgeInsets.all(AppSpacing.sm), decoration: BoxDecoration( color: colorScheme.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(AppRadius.md), ), child: Icon( LucideIcons.shieldCheck, color: colorScheme.primary, size: 22, ), ), SizedBox(width: AppSpacing.md), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '身份驗證', style: AppTextStyles.headlineLarge(context), ), SizedBox(height: 2), Text( '上傳身份證正反面完成實名認證', style: AppTextStyles.bodyMedium(context), ), ], ), ], ), SizedBox(height: AppSpacing.xl), // 身份證正面上傳區 Text( '身份證正面(人像面)', style: AppTextStyles.headlineSmall(context), ), SizedBox(height: AppSpacing.sm), _buildUploadZone( imageFile: _frontFile, imageBytes: _frontBytes, label: '人像面', onTap: () => _pickImage(true), colorScheme: colorScheme, ), SizedBox(height: AppSpacing.lg), // 身份證反面上傳區 Text( '身份證反面(國徽面)', style: AppTextStyles.headlineSmall(context), ), SizedBox(height: AppSpacing.sm), _buildUploadZone( imageFile: _backFile, imageBytes: _backBytes, label: '國徽面', onTap: () => _pickImage(false), colorScheme: colorScheme, ), SizedBox(height: AppSpacing.xl), // 提交按鈕 SizedBox( width: double.infinity, child: NeonButton( text: _isSubmitting ? '提交中...' : '提交認證', type: NeonButtonType.primary, onPressed: _canSubmit ? _submitKyc : null, height: 48, showGlow: _canSubmit, ), ), ], ), ), SizedBox(height: AppSpacing.lg), // 底部安全提示 Container( padding: EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: AppColorScheme.up.withOpacity(0.06), borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all( color: AppColorScheme.up.withOpacity(0.12), ), ), child: Row( children: [ Icon( LucideIcons.lock, size: 16, color: AppColorScheme.up, ), SizedBox(width: AppSpacing.sm), Expanded( child: Text( '您的身份信息將被加密存儲,僅用於身份驗證', style: AppTextStyles.bodySmall(context).copyWith( color: AppColorScheme.up.withOpacity(0.8), ), ), ), ], ), ), ], ), ), ); } Widget _buildStepIndicator(ColorScheme colorScheme) { final isComplete = _frontFile != null && _backFile != null; return Row( children: [ _buildStepCircle( number: '1', label: '上傳證件', isActive: true, isComplete: isComplete, colorScheme: colorScheme, ), Expanded( child: Container( height: 2, color: isComplete ? AppColorScheme.up : colorScheme.outlineVariant.withOpacity(0.2), ), ), _buildStepCircle( number: '2', label: '認證完成', isActive: false, isComplete: false, colorScheme: colorScheme, isDone: isComplete, ), ], ); } Widget _buildStepCircle({ required String number, required String label, required bool isActive, required bool isComplete, required ColorScheme colorScheme, bool isDone = false, }) { final Color circleColor; final Color textColor; if (isDone || isComplete) { circleColor = AppColorScheme.up; textColor = colorScheme.onPrimary; } else if (isActive) { circleColor = colorScheme.primary; textColor = colorScheme.onPrimary; } 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 || isDone ? Icon(LucideIcons.check, size: 16, color: textColor) : Text( number, style: AppTextStyles.headlineMedium(context).copyWith( color: textColor, ), ), ), ), SizedBox(height: 4), Text( label, style: AppTextStyles.bodySmall(context), ), ], ); } 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.withOpacity(0.06) : colorScheme.surfaceContainerHigh.withOpacity(0.3), borderRadius: BorderRadius.circular(AppRadius.xl), border: Border.all( color: hasImage ? AppColorScheme.up.withOpacity(0.3) : colorScheme.surface.withOpacity(0.0), ), ), child: hasImage ? ClipRRect( borderRadius: BorderRadius.circular(AppRadius.xl), child: Stack( fit: StackFit.expand, children: [ // 圖片預覽 - 使用 memory 以兼容 Web Image.memory( imageBytes!, fit: BoxFit.cover, ), // 底部漸變遮罩 + 文字 Positioned( left: 0, right: 0, bottom: 0, child: Container( padding: EdgeInsets.symmetric( vertical: AppSpacing.sm, horizontal: AppSpacing.md), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, const Color(0x8A000000), ], ), borderRadius: BorderRadius.only( bottomLeft: Radius.circular(AppRadius.xl), bottomRight: Radius.circular(AppRadius.xl), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '$label已選擇', style: AppTextStyles.labelLarge(context).copyWith( color: colorScheme.onPrimary, ), ), GestureDetector( onTap: () { setState(() { if (label == '人像面') { _frontFile = null; _frontBytes = null; } else { _backFile = null; _backBytes = null; } }); }, child: Container( padding: EdgeInsets.all(4), decoration: BoxDecoration( color: colorScheme.onSurface.withOpacity(0.24), shape: BoxShape.circle, ), child: Icon( LucideIcons.x, size: 14, color: colorScheme.onSurface, ), ), ), ], ), ), ), ], ), ) : CustomPaint( painter: _DashedBorderPainter( color: colorScheme.onSurfaceVariant.withOpacity(0.2), borderRadius: AppRadius.xl, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( LucideIcons.camera, size: 28, color: colorScheme.onSurfaceVariant.withOpacity(0.5), ), SizedBox(height: AppSpacing.sm), Text( '點擊上傳$label', style: AppTextStyles.bodyLarge(context).copyWith( color: colorScheme.onSurfaceVariant.withOpacity(0.6), ), ), SizedBox(height: 4), Text( '支持 JPG、PNG 格式', style: AppTextStyles.bodySmall(context).copyWith( color: colorScheme.onSurfaceVariant.withOpacity(0.4), ), ), ], ), ), ), ); } Future _submitKyc() async { setState(() => _isSubmitting = true); try { final auth = context.read(); final response = await auth.submitKyc( _frontBytes!, _backBytes!, ); if (!mounted) return; if (response.success) { showDialog( context: context, builder: (ctx) => AlertDialog( title: Row( children: [ NeonIcon( icon: Icons.check_circle, color: AppColorScheme.up, size: 20, ), SizedBox(width: AppSpacing.sm), const Text('認證成功'), ], ), content: const Text('您的實名認證已通過,現在可以進行提現操作'), actions: [ TextButton( child: const Text('確定'), onPressed: () { Navigator.of(ctx).pop(); Navigator.of(context).pop(); }, ), ], ), ); } else { showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('認證失敗'), content: Text(response.message ?? '請稍後重試'), actions: [ TextButton( child: const Text('確定'), onPressed: () => Navigator.of(ctx).pop(), ), ], ), ); } } catch (e) { if (mounted) { showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('認證失敗'), content: Text(e.toString()), actions: [ TextButton( child: const Text('確定'), onPressed: () => Navigator.of(ctx).pop(), ), ], ), ); } } finally { if (mounted) setState(() => _isSubmitting = 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; final dashWidth = 6.0; final dashSpace = 4.0; final r = borderRadius; final path = Path() ..addRRect(RRect.fromRectAndRadius( Rect.fromLTWH(0, 0, size.width, size.height), Radius.circular(r), )); 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; } }