Initial commit: 健康管家 AI 健康陪伴助手
- Backend: .NET 10 Minimal API + EF Core + PostgreSQL - Frontend: Flutter + Riverpod + GoRouter + Dio - AI: DeepSeek LLM + Qwen VLM (OpenAI-compatible) - Auth: SMS + JWT (access/refresh tokens) - Features: AI chat, health tracking, medication management, diet analysis, exercise plans, doctor consultations, report analysis
This commit is contained in:
184
health_app/lib/pages/auth/login_page.dart
Normal file
184
health_app/lib/pages/auth/login_page.dart
Normal file
@@ -0,0 +1,184 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
|
||||
/// 登录页——手机号 + 验证码
|
||||
class LoginPage extends ConsumerStatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
final _phoneCtrl = TextEditingController();
|
||||
final _codeCtrl = TextEditingController();
|
||||
bool _agreed = false;
|
||||
bool _sending = false;
|
||||
int _countdown = 0;
|
||||
bool _loading = false;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_phoneCtrl.dispose();
|
||||
_codeCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _sendSms() async {
|
||||
final phone = _phoneCtrl.text.trim();
|
||||
if (phone.length != 11 || !phone.startsWith('1')) {
|
||||
setState(() => _error = '请输入正确的手机号');
|
||||
return;
|
||||
}
|
||||
setState(() { _sending = true; _error = null; });
|
||||
final result = await ref.read(authProvider.notifier).sendSms(phone);
|
||||
setState(() { _sending = false; });
|
||||
if (result.error != null) {
|
||||
setState(() => _error = result.error);
|
||||
return;
|
||||
}
|
||||
// 开发阶段自动填充验证码
|
||||
if (result.devCode != null) {
|
||||
_codeCtrl.text = result.devCode!;
|
||||
}
|
||||
setState(() => _countdown = 60);
|
||||
_startCountdown();
|
||||
}
|
||||
|
||||
void _startCountdown() async {
|
||||
for (var i = 60; i > 0; i--) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (!mounted) return;
|
||||
setState(() => _countdown = i - 1);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _login() async {
|
||||
if (!_agreed) {
|
||||
setState(() => _error = '请阅读并同意服务协议和隐私政策');
|
||||
return;
|
||||
}
|
||||
setState(() { _loading = true; _error = null; });
|
||||
final err = await ref.read(authProvider.notifier).login(
|
||||
_phoneCtrl.text.trim(),
|
||||
_codeCtrl.text.trim(),
|
||||
);
|
||||
setState(() => _loading = false);
|
||||
if (err != null) {
|
||||
setState(() => _error = err);
|
||||
return;
|
||||
}
|
||||
if (mounted) context.go('/home');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authState = ref.watch(authProvider);
|
||||
|
||||
// 已登录直接跳转
|
||||
if (authState.isLoggedIn && !authState.isLoading) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => context.go('/home'));
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 80),
|
||||
// Logo
|
||||
Icon(Icons.local_hospital, size: 64, color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(height: 16),
|
||||
Text('健康管家', style: Theme.of(context).textTheme.headlineLarge),
|
||||
const SizedBox(height: 8),
|
||||
Text('您的 AI 健康陪伴助手', style: Theme.of(context).textTheme.bodyMedium),
|
||||
const SizedBox(height: 48),
|
||||
|
||||
// 手机号
|
||||
TextField(
|
||||
controller: _phoneCtrl,
|
||||
keyboardType: TextInputType.phone,
|
||||
maxLength: 11,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '手机号',
|
||||
prefixText: '+86 ',
|
||||
counterText: '',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 验证码
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _codeCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 6,
|
||||
decoration: const InputDecoration(hintText: '验证码', counterText: ''),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(
|
||||
width: 120,
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
onPressed: (_countdown > 0 || _sending) ? null : _sendSms,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _countdown > 0 ? Colors.grey[300] : null,
|
||||
),
|
||||
child: Text(
|
||||
_sending ? '发送中' : _countdown > 0 ? '${_countdown}s' : '获取验证码',
|
||||
style: TextStyle(fontSize: 14, color: _countdown > 0 ? Colors.grey[600] : null),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 协议勾选
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(value: _agreed, onChanged: (v) => setState(() => _agreed = v ?? false)),
|
||||
GestureDetector(
|
||||
onTap: () => setState(() => _agreed = !_agreed),
|
||||
child: Text('已阅读并同意《服务协议》《隐私政策》', style: Theme.of(context).textTheme.labelMedium),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 登录按钮
|
||||
if (_error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Text(_error!, style: const TextStyle(color: AppColors.errorRed, fontSize: 14)),
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
onPressed: _loading ? null : _login,
|
||||
child: _loading
|
||||
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||
: const Text('登 录'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 引用 AppTheme 颜色
|
||||
class AppColors {
|
||||
static const Color errorRed = Color(0xFFE53935);
|
||||
}
|
||||
Reference in New Issue
Block a user