- 登录页:输入框白底+渐变紫蓝外框,去掉focus内框;登录按钮白底紫字;协议勾选白底 - 首页胶囊:适老化加大(内边距14x8/字号15/图标17/圆角18) - 欢迎卡片底部提示:各智能体专属引导文案,字号加大颜色加深 - 今日健康卡片:健康指标改一行总结(异常提醒/正常显示);用药显示具体药品名+状态 - 发送按钮:蓝→紫渐变 - 设置页菜单:白色卡片+圆角16+轻阴影 - IP改localhost:adb reverse解决IP变动问题 - 服药打卡快捷按钮:改为跳转打卡页,不再一键全打
236 lines
10 KiB
Dart
236 lines
10 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
|
import '../../core/app_colors.dart';
|
|
import '../../core/app_theme.dart';
|
|
import '../../core/navigation_provider.dart';
|
|
import '../../providers/auth_provider.dart';
|
|
import '../../providers/data_providers.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; }
|
|
ref.invalidate(latestHealthProvider);
|
|
ref.invalidate(medicationListProvider);
|
|
ref.invalidate(medicationReminderProvider);
|
|
ref.invalidate(currentExercisePlanProvider);
|
|
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: Container(
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [Color(0xFFEEF2FF), Color(0xFFF5F3FF)],
|
|
),
|
|
),
|
|
child: SafeArea(child: Center(child: SingleChildScrollView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
|
const SizedBox(height: 40),
|
|
// Logo
|
|
Container(
|
|
width: 88, height: 88,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(44),
|
|
boxShadow: [BoxShadow(color: AppColors.primary.withOpacity(0.15), blurRadius: 20, offset: const Offset(0, 8))],
|
|
),
|
|
child: const Icon(LucideIcons.heart, size: 40, color: AppColors.primary),
|
|
),
|
|
const SizedBox(height: 20),
|
|
const Text('健康管家', style: TextStyle(fontSize: 28, fontWeight: FontWeight.w700, color: AppColors.textPrimary)),
|
|
const SizedBox(height: 6),
|
|
const Text('你的 AI 心脏健康管家', style: TextStyle(fontSize: 15, color: AppColors.textSecondary)),
|
|
const SizedBox(height: 40),
|
|
|
|
// 手机号
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
gradient: AppColors.primaryGradient,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
padding: const EdgeInsets.all(1.5),
|
|
child: Container(
|
|
height: 52,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(9),
|
|
),
|
|
child: Row(children: [
|
|
const Padding(padding: EdgeInsets.only(left: 16), child: Text('+86', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: AppColors.textPrimary))),
|
|
Container(width: 1, height: 24, color: AppColors.border, margin: const EdgeInsets.symmetric(horizontal: 12)),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _phoneCtrl,
|
|
keyboardType: TextInputType.phone,
|
|
maxLength: 11,
|
|
style: const TextStyle(fontSize: 16, color: AppColors.textPrimary),
|
|
decoration: const InputDecoration(
|
|
filled: false,
|
|
hintText: '请输入手机号',
|
|
hintStyle: TextStyle(fontSize: 15, color: AppColors.textHint),
|
|
border: InputBorder.none,
|
|
enabledBorder: InputBorder.none,
|
|
focusedBorder: InputBorder.none,
|
|
counterText: '',
|
|
contentPadding: EdgeInsets.symmetric(vertical: 14),
|
|
),
|
|
),
|
|
),
|
|
]),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// 验证码
|
|
Row(children: [
|
|
Expanded(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
gradient: AppColors.primaryGradient,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
padding: const EdgeInsets.all(1.5),
|
|
child: Container(
|
|
height: 52,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(9),
|
|
),
|
|
child: TextField(
|
|
controller: _codeCtrl,
|
|
keyboardType: TextInputType.number,
|
|
maxLength: 6,
|
|
style: const TextStyle(fontSize: 16, color: AppColors.textPrimary),
|
|
decoration: const InputDecoration(
|
|
filled: false,
|
|
hintText: '验证码',
|
|
hintStyle: TextStyle(fontSize: 15, color: AppColors.textHint),
|
|
border: InputBorder.none,
|
|
enabledBorder: InputBorder.none,
|
|
focusedBorder: InputBorder.none,
|
|
counterText: '',
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
GestureDetector(
|
|
onTap: (_countdown > 0 || _sending) ? null : _sendSms,
|
|
child: Container(
|
|
width: 120, height: 52,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: (_countdown > 0 || _sending) ? Colors.white : AppColors.primary,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Text(
|
|
_sending ? '发送中' : _countdown > 0 ? '${_countdown}s' : '获取验证码',
|
|
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500,
|
|
color: (_countdown > 0 || _sending) ? AppColors.textHint : Colors.white),
|
|
),
|
|
),
|
|
),
|
|
]),
|
|
const SizedBox(height: 12),
|
|
|
|
// 协议
|
|
GestureDetector(
|
|
onTap: () => setState(() => _agreed = !_agreed),
|
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
|
Container(
|
|
width: 18, height: 18, margin: const EdgeInsets.only(right: 6),
|
|
decoration: BoxDecoration(
|
|
color: _agreed ? AppColors.primary : Colors.white,
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(color: _agreed ? AppColors.primary : AppColors.border, width: 1.5),
|
|
),
|
|
child: _agreed ? const Icon(Icons.check, size: 14, color: Colors.white) : null,
|
|
),
|
|
RichText(text: TextSpan(children: [
|
|
TextSpan(text: '已阅读并同意', style: TextStyle(fontSize: 13, color: AppColors.textHint)),
|
|
TextSpan(text: '《服务协议》', style: TextStyle(fontSize: 13, color: AppColors.primary)),
|
|
TextSpan(text: '和', style: TextStyle(fontSize: 13, color: AppColors.textHint)),
|
|
TextSpan(text: '《隐私政策》', style: TextStyle(fontSize: 13, color: AppColors.primary)),
|
|
])),
|
|
]),
|
|
),
|
|
|
|
if (_error != null)
|
|
Padding(padding: const EdgeInsets.only(top: 12), child: Text(_error!, style: const TextStyle(color: AppColors.error, fontSize: 14))),
|
|
|
|
const SizedBox(height: 32),
|
|
|
|
// 登录按钮
|
|
GestureDetector(
|
|
onTap: _loading ? null : _login,
|
|
child: Container(
|
|
width: double.infinity, height: 52, alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: AppColors.primary, width: 1.5),
|
|
),
|
|
child: _loading
|
|
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2.5, color: AppColors.primary))
|
|
: const Text('登 录', style: TextStyle(fontSize: 18, color: AppColors.primary, fontWeight: FontWeight.w600, letterSpacing: 2)),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 40),
|
|
]),
|
|
))),
|
|
),
|
|
);
|
|
}
|
|
}
|