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 createState() => _RegisterScreenState(); } class _RegisterScreenState extends ConsumerState { 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 _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 _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 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, ), ), ), ), ); } }