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:
MingNian
2026-06-02 11:11:29 +08:00
commit 14d7c30d3d
144 changed files with 11436 additions and 0 deletions

View 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);
}