Files
video/app/lib/screens/auth/register_screen.dart
T
2026-10-16 13:51:00 +08:00

204 lines
6.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/network/api_exception.dart';
import '../../core/storage/local_store.dart';
import '../../core/theme/app_theme.dart';
import '../../providers/auth_provider.dart';
import '../../services/auth_service.dart';
/// 注册页:手机号 + 验证码 + 密码;邀请码选填(口令/模糊归因命中时自动预填)
class RegisterScreen extends ConsumerStatefulWidget {
const RegisterScreen({super.key});
@override
ConsumerState<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends ConsumerState<RegisterScreen> {
final _phone = TextEditingController();
final _code = TextEditingController();
final _password = TextEditingController();
final _nickname = TextEditingController();
late final TextEditingController _inviteCode;
bool _loading = false;
String? _error;
/// 验证码发送倒计时
int _countdown = 0;
Timer? _timer;
@override
void initState() {
super.initState();
// 首启归因命中的邀请码预填(用户可改可清空)
_inviteCode = TextEditingController(text: LocalStore.I.pendingInviteCode ?? '');
}
@override
void dispose() {
_timer?.cancel();
_phone.dispose();
_code.dispose();
_password.dispose();
_nickname.dispose();
_inviteCode.dispose();
super.dispose();
}
Future<void> _sendCode() async {
final phone = _phone.text.trim();
if (!RegExp(r'^1\d{10}$').hasMatch(phone)) {
setState(() => _error = '请输入正确的手机号');
return;
}
try {
final devCode = await AuthService.sendSmsCode(phone, 'register');
setState(() {
_error = null;
_countdown = 60;
});
if (devCode != null && mounted) {
// 短信通道未配置时的开发模式:直接展示验证码(上线配置短信后服务端不再返回)
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('开发模式验证码:$devCode(5 分钟内有效)'), duration: const Duration(seconds: 6)),
);
}
_timer?.cancel();
_timer = Timer.periodic(const Duration(seconds: 1), (t) {
if (_countdown <= 1) {
t.cancel();
setState(() => _countdown = 0);
} else {
setState(() => _countdown -= 1);
}
});
} on ApiException catch (e) {
setState(() => _error = e.message);
} catch (_) {
setState(() => _error = '网络异常,请稍后重试');
}
}
Future<void> _submit() async {
final phone = _phone.text.trim();
final code = _code.text.trim();
final password = _password.text;
if (phone.isEmpty || code.isEmpty || password.isEmpty) {
setState(() => _error = '请填写手机号、验证码和密码');
return;
}
setState(() {
_loading = true;
_error = null;
});
try {
final result = await AuthService.register(
type: 'phone',
account: phone,
code: code,
password: password,
nickname: _nickname.text.trim(),
inviteCode: _inviteCode.text.trim().toUpperCase(),
);
// 注册成功:pending 邀请码已被消费,清除本地标记
await LocalStore.I.clearPendingInviteCode();
await ref.read(authProvider.notifier).onLoginSuccess(result.user);
if (!mounted) return;
if (result.trialPlanName != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('注册成功,已赠送${result.trialPlanName}')),
);
}
context.go('/home');
} on ApiException catch (e) {
setState(() => _error = e.message);
} catch (_) {
setState(() => _error = '网络异常,请稍后重试');
} finally {
if (mounted) setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('注册')),
body: SafeArea(
child: ListView(
padding: const EdgeInsets.all(24),
children: [
const Text('创建账号', style: TextStyle(fontSize: 26, fontWeight: FontWeight.w700)),
const SizedBox(height: 32),
TextField(
controller: _phone,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(hintText: '手机号'),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
controller: _code,
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: '短信验证码'),
),
),
const SizedBox(width: 12),
SizedBox(
width: 120,
child: OutlinedButton(
onPressed: _countdown > 0 ? null : _sendCode,
child: Text(_countdown > 0 ? '${_countdown}s 后重发' : '获取验证码'),
),
),
],
),
const SizedBox(height: 16),
TextField(
controller: _password,
obscureText: true,
decoration: const InputDecoration(hintText: '密码(8-32 位,含字母和数字)'),
),
const SizedBox(height: 16),
TextField(
controller: _nickname,
decoration: const InputDecoration(hintText: '昵称(选填)'),
),
const SizedBox(height: 16),
TextField(
controller: _inviteCode,
textCapitalization: TextCapitalization.characters,
decoration: const InputDecoration(hintText: '邀请码(选填)'),
),
if (_error != null) ...[
const SizedBox(height: 12),
Text(_error!, style: const TextStyle(color: AppColors.primary, fontSize: 13)),
],
const SizedBox(height: 24),
ElevatedButton(
onPressed: _loading ? null : _submit,
child: _loading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('注册并登录'),
),
const SizedBox(height: 16),
TextButton(
onPressed: () => context.pop(),
child: const Text('已有账号?去登录', style: TextStyle(color: AppColors.textSecondary)),
),
],
),
),
);
}
}