Files
video/app/lib/screens/auth/register_screen.dart
T
2026-10-17 09:41:11 +08:00

286 lines
9.2 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 _account = TextEditingController();
final _code = TextEditingController();
final _password = TextEditingController();
final _nickname = TextEditingController();
late final TextEditingController _inviteCode;
/// 注册方式:phone / email
String _mode = 'phone';
bool get _isEmail => _mode == 'email';
bool _loading = false;
String? _error;
/// 验证码发送倒计时
int _countdown = 0;
Timer? _timer;
/// 邮箱格式校验
static final _emailReg = RegExp(r'^[\w.+-]+@[\w-]+\.[\w.-]+$');
@override
void initState() {
super.initState();
// 首启归因命中的邀请码预填(用户可改可清空)
_inviteCode = TextEditingController(text: LocalStore.I.pendingInviteCode ?? '');
}
@override
void dispose() {
_timer?.cancel();
_account.dispose();
_code.dispose();
_password.dispose();
_nickname.dispose();
_inviteCode.dispose();
super.dispose();
}
void _switchMode(String mode) {
if (mode == _mode) return;
setState(() {
_mode = mode;
_error = null;
// 切换方式后账号和验证码需要重新填写
_account.clear();
_code.clear();
});
}
Future<void> _sendCode() async {
final account = _account.text.trim();
if (_isEmail) {
if (!_emailReg.hasMatch(account)) {
setState(() => _error = '请输入正确的邮箱地址');
return;
}
} else {
if (!RegExp(r'^1\d{10}$').hasMatch(account)) {
setState(() => _error = '请输入正确的手机号');
return;
}
}
try {
// 未配置短信/邮件通道时服务端走开发模式,直接返回验证码
final devCode = _isEmail
? await AuthService.sendEmailCode(account, 'register')
: await AuthService.sendSmsCode(account, '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 account = _account.text.trim();
final code = _code.text.trim();
final password = _password.text;
if (account.isEmpty || code.isEmpty || password.isEmpty) {
setState(() => _error = _isEmail ? '请填写邮箱、验证码和密码' : '请填写手机号、验证码和密码');
return;
}
setState(() {
_loading = true;
_error = null;
});
try {
final result = await AuthService.register(
type: _mode,
account: account,
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: 24),
// 手机号 / 邮箱 切换
_ModeSwitch(current: _mode, onChanged: _switchMode),
const SizedBox(height: 24),
TextField(
controller: _account,
keyboardType: _isEmail ? TextInputType.emailAddress : TextInputType.phone,
decoration: InputDecoration(hintText: _isEmail ? '邮箱' : '手机号'),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
controller: _code,
keyboardType: TextInputType.number,
decoration: InputDecoration(hintText: _isEmail ? '邮箱验证码' : '短信验证码'),
),
),
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)),
),
],
),
),
);
}
}
/// 注册方式切换条(手机号 / 邮箱)
class _ModeSwitch extends StatelessWidget {
const _ModeSwitch({required this.current, required this.onChanged});
final String current;
final ValueChanged<String> onChanged;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.all(3),
child: Row(
children: [
_tab(context, '手机号', 'phone'),
_tab(context, '邮箱', 'email'),
],
),
);
}
Widget _tab(BuildContext context, String label, String value) {
final selected = current == value;
return Expanded(
child: GestureDetector(
onTap: () => onChanged(value),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 9),
alignment: Alignment.center,
decoration: BoxDecoration(
color: selected ? Theme.of(context).scaffoldBackgroundColor : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
color: selected ? AppColors.textPrimary : AppColors.textSecondary,
),
),
),
),
);
}
}