Files
monisuo/flutter_monisuo/lib/ui/pages/mine/kyc_page.dart
sion123 f5ac578892 docs(theme): update documentation and clean up deprecated color scheme definitions
Removed outdated compatibility aliases and deprecated methods from AppColorScheme,
and updated CLAUDE.md to reflect new theme system requirements with centralized
color management and no hard-coded values in UI components.
2026-04-05 23:37:27 +08:00

543 lines
17 KiB
Dart

import 'dart:typed_data';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.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<KycPage> createState() => _KycPageState();
}
class _KycPageState extends State<KycPage> {
XFile? _frontFile;
XFile? _backFile;
Uint8List? _frontBytes;
Uint8List? _backBytes;
bool _isSubmitting = false;
bool get _canSubmit =>
_frontFile != null && _backFile != null && !_isSubmitting;
final _picker = ImagePicker();
Future<void> _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: [
const Color(0x00000000),
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<void> _submitKyc() async {
setState(() => _isSubmitting = true);
try {
final auth = context.read<AuthProvider>();
final response = await auth.submitKyc(
_frontBytes!,
_backBytes!,
);
if (!mounted) return;
if (response.success) {
showShadDialog(
context: context,
builder: (ctx) => ShadDialog.alert(
title: Row(
children: [
NeonIcon(
icon: Icons.check_circle,
color: AppColorScheme.up,
size: 20,
),
SizedBox(width: AppSpacing.sm),
const Text('认证成功'),
],
),
description: const Text('您的实名认证已通过,现在可以进行提现操作'),
actions: [
ShadButton(
child: const Text('确定'),
onPressed: () {
Navigator.of(ctx).pop();
Navigator.of(context).pop();
},
),
],
),
);
} else {
showShadDialog(
context: context,
builder: (ctx) => ShadDialog.alert(
title: const Text('认证失败'),
description: Text(response.message ?? '请稍后重试'),
actions: [
ShadButton(
child: const Text('确定'),
onPressed: () => Navigator.of(ctx).pop(),
),
],
),
);
}
} catch (e) {
if (mounted) {
showShadDialog(
context: context,
builder: (ctx) => ShadDialog.alert(
title: const Text('认证失败'),
description: Text(e.toString()),
actions: [
ShadButton(
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;
}
}