- C# 文件命名改为 snake_case(28 个文件重命名) - C# 类转换为主构造函数(8 个类) - 空 catch 添加异常类型(2 处) - 新建 GlobalUsings.cs(Health.Infrastructure、Health.WebApi) - Flutter 移除 go_router,改用 Riverpod 路由栈 - Flutter 移除 flutter_secure_storage,改用 sqflite 持久化 - 修复 Flutter 构建路径(Flutter SDK 迁至 D 盘) - 后端端口改为 0.0.0.0:5000,支持局域网访问
185 lines
6.0 KiB
Dart
185 lines
6.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../../core/navigation_provider.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;
|
|
}
|
|
goRoute(ref, 'home');
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final authState = ref.watch(authProvider);
|
|
|
|
// 已登录直接跳转
|
|
if (authState.isLoggedIn && !authState.isLoading) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => goRoute(ref, '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);
|
|
}
|