feat: A1 网络层与鉴权 + A2 账号登录对接
- 引入 dio,封装统一 ApiClient(BaseUrl/Token 注入/401 自动刷新/{code,message,data} 解包)
- TokenStorage 令牌持久化,AuthController 登录状态管理
- AuthRepository 封装 otp/login/logout 接口
- 登录页对接手机号/邮箱验证码登录(Google 登录待 OAuth client 配置)
This commit is contained in:
1 parent
015677d1a8
commit
a7ac3b7fc7
9 files changed
+602
-78
No files matched your search
@@ -1,11 +1,12 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../core/data/services/auth_controller.dart';
|
||||
import '../home/home_view.dart';
|
||||
|
||||
/// 登录页:Google / 手机号 / 邮箱 三种登录入口 + 游客浏览。
|
||||
/// 当前为 UI 占位实现,真实登录逻辑在接入后端 /app/auth/** 后补充。
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@@ -23,6 +24,132 @@ class _LoginPageState extends State<LoginPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showLoginSheet(String targetType) async {
|
||||
final auth = context.read<AuthController>();
|
||||
final targetCtrl = TextEditingController();
|
||||
final codeCtrl = TextEditingController();
|
||||
final isPhone = targetType == 'PHONE';
|
||||
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: const Color(0xFF161822),
|
||||
builder: (sheetContext) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(sheetContext).viewInsets.bottom,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
isPhone ? 'Phone Login' : 'Email Login',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: targetCtrl,
|
||||
keyboardType: isPhone
|
||||
? TextInputType.phone
|
||||
: TextInputType.emailAddress,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
decoration: InputDecoration(
|
||||
hintText: isPhone ? 'Phone number' : 'Email',
|
||||
hintStyle: const TextStyle(color: Colors.white38),
|
||||
filled: true,
|
||||
fillColor: Colors.white.withValues(alpha: 0.05),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: codeCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Code',
|
||||
hintStyle: const TextStyle(color: Colors.white38),
|
||||
filled: true,
|
||||
fillColor: Colors.white.withValues(alpha: 0.05),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
try {
|
||||
await auth.sendOtp(
|
||||
scene: 'LOGIN',
|
||||
targetType: targetType,
|
||||
target: targetCtrl.text.trim(),
|
||||
);
|
||||
if (sheetContext.mounted) {
|
||||
ScaffoldMessenger.of(sheetContext).showSnackBar(
|
||||
const SnackBar(content: Text('Code sent')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (sheetContext.mounted) {
|
||||
ScaffoldMessenger.of(sheetContext).showSnackBar(
|
||||
SnackBar(content: Text('$e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('Send Code'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
try {
|
||||
await auth.loginWithCode(
|
||||
targetType: targetType,
|
||||
target: targetCtrl.text.trim(),
|
||||
code: codeCtrl.text.trim(),
|
||||
);
|
||||
if (sheetContext.mounted) {
|
||||
Navigator.of(sheetContext).pop();
|
||||
}
|
||||
_enterHome();
|
||||
} catch (e) {
|
||||
if (sheetContext.mounted) {
|
||||
ScaffoldMessenger.of(sheetContext).showSnackBar(
|
||||
SnackBar(content: Text('$e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: _orange,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
child: const Text('Sign In'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -86,14 +213,14 @@ class _LoginPageState extends State<LoginPage> {
|
||||
icon: Icons.phone_iphone,
|
||||
iconColor: _orange,
|
||||
label: 'Continue with Phone Number',
|
||||
onTap: _enterHome,
|
||||
onTap: () => _showLoginSheet('PHONE'),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_DarkButton(
|
||||
icon: Icons.email_outlined,
|
||||
iconColor: _purple,
|
||||
label: 'Continue with Email',
|
||||
onTap: _enterHome,
|
||||
onTap: () => _showLoginSheet('EMAIL'),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const _BonusPill(),
|
||||
|
||||
Reference in new issue
Block a user