Files
monisuo/flutter_monisuo/lib/ui/pages/auth/register_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

675 lines
22 KiB
Dart

import 'dart:typed_data';
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';
import '../main/main_page.dart';
/// 注册页面(两步注册:账号信息 + 身份证上传)
class RegisterPage extends StatefulWidget {
const RegisterPage({super.key});
@override
State<RegisterPage> createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
int _currentStep = 0; // 0: 账号信息, 1: 身份证上传
// 第一步
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
final _referralCodeController = TextEditingController();
final _formKey = GlobalKey<FormState>();
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<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: AppColorScheme.darkBackground.withValues(alpha: 0),
elevation: 0,
leading: IconButton(
icon: Icon(LucideIcons.chevronLeft, 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),
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(LucideIcons.check, size: 16, color: textColor)
: Text(
number,
style: AppTextStyles.headlineMedium(context).copyWith(
fontWeight: FontWeight.bold,
color: textColor,
),
),
),
),
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,
),
),
),
SizedBox(height: AppSpacing.xxl),
// 用户名
TextFormField(
controller: _usernameController,
style: TextStyle(color: colorScheme.onSurface),
decoration: InputDecoration(
hintText: '请输入账号(4-20位字母数字)',
prefixIcon: Icon(Icons.person_outline, color: colorScheme.onSurfaceVariant),
),
validator: (value) {
if (value == null || value.isEmpty) return '请输入账号';
if (value.length < 4) return '账号至少4位';
if (value.length > 20) return '账号最多20位';
return null;
},
),
SizedBox(height: AppSpacing.md),
// 密码
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
style: TextStyle(color: colorScheme.onSurface),
decoration: InputDecoration(
hintText: '请输入密码(至少6位)',
prefixIcon: Icon(Icons.lock_outline, color: colorScheme.onSurfaceVariant),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility_off : Icons.visibility,
color: colorScheme.onSurfaceVariant,
),
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
),
),
validator: (value) {
if (value == null || value.isEmpty) return '请输入密码';
if (value.length < 6) return '密码至少6位';
return null;
},
),
SizedBox(height: AppSpacing.md),
// 确认密码
TextFormField(
controller: _confirmPasswordController,
obscureText: _obscureConfirmPassword,
style: TextStyle(color: colorScheme.onSurface),
decoration: InputDecoration(
hintText: '请再次输入密码',
prefixIcon: Icon(Icons.lock_outline, color: colorScheme.onSurfaceVariant),
suffixIcon: IconButton(
icon: Icon(
_obscureConfirmPassword ? Icons.visibility_off : Icons.visibility,
color: colorScheme.onSurfaceVariant,
),
onPressed: () => setState(() => _obscureConfirmPassword = !_obscureConfirmPassword),
),
),
validator: (value) {
if (value == null || value.isEmpty) return '请再次输入密码';
if (value != _passwordController.text) return '两次密码不一致';
return null;
},
),
SizedBox(height: AppSpacing.md),
// 推广码(可选)
TextFormField(
controller: _referralCodeController,
style: TextStyle(color: colorScheme.onSurface),
decoration: InputDecoration(
hintText: '推广码(选填)',
prefixIcon: Icon(Icons.card_giftcard, color: colorScheme.onSurfaceVariant),
),
),
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,
),
),
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: EdgeInsets.all(AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: colorScheme.primary.withValues(alpha: 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).copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
SizedBox(height: AppSpacing.xs),
Text(
'上传身份证正反面完成注册',
style: AppTextStyles.bodyMedium(context).copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
],
),
SizedBox(height: AppSpacing.xl),
// 身份证正面
Text(
'身份证正面(人像面)',
style: AppTextStyles.bodyLarge(context).copyWith(
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
SizedBox(height: AppSpacing.sm),
_buildUploadZone(
imageFile: _frontFile,
imageBytes: _frontBytes,
label: '人像面',
onTap: () => _pickImage(true),
colorScheme: colorScheme,
),
SizedBox(height: AppSpacing.lg),
// 身份证反面
Text(
'身份证反面(国徽面)',
style: AppTextStyles.bodyLarge(context).copyWith(
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
SizedBox(height: AppSpacing.sm),
_buildUploadZone(
imageFile: _backFile,
imageBytes: _backBytes,
label: '国徽面',
onTap: () => _pickImage(false),
colorScheme: colorScheme,
),
SizedBox(height: AppSpacing.xl),
// 注册按钮
Consumer<AuthProvider>(
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,
),
);
},
),
],
),
),
SizedBox(height: AppSpacing.lg),
// 安全提示
Container(
padding: 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(LucideIcons.lock, size: 16, color: AppColorScheme.up),
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) : AppColorScheme.darkBackground.withValues(alpha: 0),
),
),
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: 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: 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: EdgeInsets.all(AppSpacing.xs),
decoration: BoxDecoration(
color: AppColorScheme.darkOnPrimary.withValues(alpha: 0.15),
shape: BoxShape.circle,
),
child: Icon(LucideIcons.x, 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(
LucideIcons.camera,
size: 28,
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
),
SizedBox(height: AppSpacing.sm),
Text(
'点击上传$label',
style: AppTextStyles.bodyLarge(context).copyWith(
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
),
),
SizedBox(height: AppSpacing.xs),
Text(
'支持 JPG、PNG 格式',
style: AppTextStyles.bodySmall(context).copyWith(
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
),
),
],
),
),
),
);
}
Future<void> _handleRegister() async {
if (!_canSubmit) return;
setState(() => _isLoading = true);
try {
final auth = context.read<AuthProvider>();
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 {
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(() => _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;
final dashWidth = 6.0;
final 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;
}
}