import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sunny_mochi/core/utils/validator.dart'; import 'package:sunny_mochi/core/widgets/app_button.dart'; import 'package:sunny_mochi/core/widgets/app_text_field.dart'; import 'package:sunny_mochi/core/widgets/app_toast.dart'; import 'package:sunny_mochi/core/widgets/count_down_button.dart'; import 'package:sunny_mochi/features/auth/presentation/notifiers/auth_notifier.dart'; /// 通用登录页骨架 — 手机号 + 双模式(短信验证码 / 密码)。 /// /// TODO: 替换为项目品牌 UI(logo / 背景 / 色彩)。 class LoginPage extends ConsumerStatefulWidget { const LoginPage({super.key}); @override ConsumerState createState() => _LoginPageState(); } class _LoginPageState extends ConsumerState with SingleTickerProviderStateMixin { late final TabController _tabCtrl; final _phoneCtrl = TextEditingController(); final _codeCtrl = TextEditingController(); final _passwordCtrl = TextEditingController(); bool _obscurePassword = true; @override void initState() { super.initState(); _tabCtrl = TabController(length: 2, vsync: this); } @override void dispose() { _tabCtrl.dispose(); _phoneCtrl.dispose(); _codeCtrl.dispose(); _passwordCtrl.dispose(); super.dispose(); } bool get _isSms => _tabCtrl.index == 0; Future _onSendCode() async { final phone = _phoneCtrl.text.trim(); if (!Validator.isValidPhone(phone)) { if (mounted) { AppToast.show(context, '请输入有效的手机号码', type: ToastType.warning); } return; } try { final echoCode = await ref .read(authProvider.notifier) .sendSmsCode(phone); if (echoCode != null && mounted) { _codeCtrl.text = echoCode; // dev/test 环境自动填入 } } on Object catch (e) { if (mounted) { AppToast.show(context, e.toString(), type: ToastType.error); } } } Future _onLogin() async { final phone = _phoneCtrl.text.trim(); if (!Validator.isValidPhone(phone)) { AppToast.show(context, '请输入有效的手机号码', type: ToastType.warning); return; } if (_isSms) { final code = _codeCtrl.text.trim(); if (code.isEmpty) { AppToast.show(context, '请输入验证码', type: ToastType.warning); return; } await ref.read(authProvider.notifier).loginWithPhone(phone, code); } else { final pwd = _passwordCtrl.text; if (pwd.isEmpty) { AppToast.show(context, '请输入密码', type: ToastType.warning); return; } await ref .read(authProvider.notifier) .loginWithPassword(phone, pwd); } } @override Widget build(BuildContext context) { final authState = ref.watch(authProvider); final isLoading = authState is AuthLoading; ref.listen(authProvider, (_, next) { if (next is AuthError && mounted) { AppToast.show(context, next.message, type: ToastType.error); } }); return Scaffold( body: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 48), // TODO: 替换为项目 Logo const FlutterLogo(size: 64), const SizedBox(height: 32), Text( '欢迎登录', style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 32), // 手机号 AppTextField( controller: _phoneCtrl, label: '手机号', hint: '请输入手机号', keyboardType: TextInputType.phone, maxLength: 11, prefixIcon: const Icon(Icons.phone_outlined), ), const SizedBox(height: 16), // 模式切换 Tab TabBar( controller: _tabCtrl, onTap: (_) => setState(() {}), tabs: const [ Tab(text: '短信验证码'), Tab(text: '密码登录'), ], ), const SizedBox(height: 16), // 验证码 / 密码输入 if (_isSms) Row( children: [ Expanded( child: AppTextField( controller: _codeCtrl, label: '验证码', hint: '请输入验证码', keyboardType: TextInputType.number, maxLength: 6, ), ), const SizedBox(width: 12), CountDownButton(onSend: _onSendCode), ], ) else AppTextField( controller: _passwordCtrl, label: '密码', hint: '请输入密码', obscureText: _obscurePassword, suffix: IconButton( icon: Icon( _obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, ), onPressed: () => setState(() => _obscurePassword = !_obscurePassword), ), ), const SizedBox(height: 32), AppButton( label: '登录', onPressed: _onLogin, loading: isLoading, ), ], ), ), ), ); } }