Files
flutter-template/lib/features/auth/presentation/pages/login_page.dart
T
SkyJourney 61017f1c39 chore: 初始化脚手架 — sunny_mochi 企业级 Flutter 模板
Core 基础设施:Flavor 三包体系、Drift+SQLCipher 加密数据库
7 拦截器 Dio 网络栈、CrashReporter 崩溃日志、Sealed Failure 错误体系
5 色板主题系统、Auth 认证骨架(Clean Architecture)、Dev Panel、Mock Adapter

通过验证:flutter analyze(0 errors)、flutter test(全绿)、staging APK 74.8MB
2026-05-14 12:51:05 +08:00

189 lines
5.9 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: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: 替换为项目品牌 UIlogo / 背景 / 色彩)。
class LoginPage extends ConsumerStatefulWidget {
const LoginPage({super.key});
@override
ConsumerState<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends ConsumerState<LoginPage>
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<void> _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<void> _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,
),
],
),
),
),
);
}
}