- 全新"朝露"主题:深青绿主色(#2D6A4F),暖白底,完整设计Token - 引入 shadcn_ui 组件库,MaterialApp 注入 ShadTheme - 新增 4 个公共组件:AppCard、AppMenuItem、AppEmptyState、AppTabChip - 全面消除硬编码颜色,统一使用 AppTheme Token - 全局字号 +3~4pt,图标等比放大 +3px - home/medication/profile/settings/login 等核心页面重写 - 路由和功能逻辑零改动
250 lines
9.6 KiB
Dart
250 lines
9.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
|
import '../../core/app_theme.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);
|
|
final theme = ShadTheme.of(context);
|
|
if (authState.isLoggedIn && !authState.isLoading) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => goRoute(ref, 'home'));
|
|
}
|
|
|
|
return Scaffold(
|
|
body: Container(
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [AppTheme.bg, AppTheme.bg, AppTheme.primaryLight],
|
|
),
|
|
),
|
|
child: SafeArea(child: Center(child: SingleChildScrollView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
|
const SizedBox(height: 24),
|
|
// Logo
|
|
Container(
|
|
width: 100, height: 100,
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.primary.withAlpha(25),
|
|
borderRadius: BorderRadius.circular(50),
|
|
),
|
|
child: Stack(alignment: Alignment.center, children: [
|
|
Container(
|
|
width: 72, height: 72,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withAlpha(200),
|
|
borderRadius: BorderRadius.circular(36),
|
|
),
|
|
child: const Icon(LucideIcons.heart, size: 36, color: AppTheme.primary),
|
|
),
|
|
]),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Text('健康管家', style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: theme.colorScheme.foreground)),
|
|
const SizedBox(height: 8),
|
|
Text('你的 AI 心脏健康管家', style: TextStyle(fontSize: 18, color: theme.colorScheme.mutedForeground)),
|
|
const SizedBox(height: 36),
|
|
|
|
// 手机号输入
|
|
_buildPhoneInput(theme),
|
|
const SizedBox(height: AppTheme.sLg),
|
|
|
|
// 验证码输入
|
|
_buildCodeRow(theme),
|
|
const SizedBox(height: AppTheme.sSm),
|
|
|
|
// 协议勾选
|
|
_buildAgreement(theme),
|
|
|
|
// 错误提示
|
|
if (_error != null)
|
|
Padding(padding: const EdgeInsets.only(top: AppTheme.sMd), child: Text(_error!, style: TextStyle(color: theme.colorScheme.destructive, fontSize: 16))),
|
|
|
|
const SizedBox(height: AppTheme.rXl),
|
|
|
|
// 登录按钮
|
|
_buildLoginButton(theme),
|
|
|
|
const SizedBox(height: AppTheme.sXl),
|
|
]),
|
|
))),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPhoneInput(ShadThemeData theme) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.card,
|
|
borderRadius: BorderRadius.circular(AppTheme.rSm),
|
|
boxShadow: [AppTheme.shadowLight],
|
|
),
|
|
child: Row(children: [
|
|
const Padding(padding: EdgeInsets.only(left: AppTheme.sLg), child: Text('+86', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w500, color: AppTheme.text))),
|
|
Container(width: 1, height: 24, color: theme.colorScheme.border, margin: const EdgeInsets.symmetric(horizontal: AppTheme.sMd)),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _phoneCtrl,
|
|
keyboardType: TextInputType.phone,
|
|
maxLength: 11,
|
|
style: TextStyle(fontSize: 19, color: theme.colorScheme.foreground),
|
|
decoration: InputDecoration(
|
|
hintText: '请输入手机号',
|
|
hintStyle: TextStyle(color: theme.colorScheme.mutedForeground),
|
|
border: InputBorder.none,
|
|
counterText: '',
|
|
contentPadding: const EdgeInsets.symmetric(vertical: AppTheme.sLg),
|
|
),
|
|
),
|
|
),
|
|
]),
|
|
);
|
|
}
|
|
|
|
Widget _buildCodeRow(ShadThemeData theme) {
|
|
final codeDisabled = _countdown > 0 || _sending;
|
|
return Row(children: [
|
|
Expanded(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.card,
|
|
borderRadius: BorderRadius.circular(AppTheme.rSm),
|
|
boxShadow: [AppTheme.shadowLight],
|
|
),
|
|
child: TextField(
|
|
controller: _codeCtrl,
|
|
keyboardType: TextInputType.number,
|
|
maxLength: 6,
|
|
style: TextStyle(fontSize: 19, color: theme.colorScheme.foreground),
|
|
decoration: InputDecoration(
|
|
hintText: '验证码',
|
|
hintStyle: TextStyle(color: theme.colorScheme.mutedForeground),
|
|
border: InputBorder.none,
|
|
counterText: '',
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: AppTheme.sLg),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: AppTheme.sMd),
|
|
GestureDetector(
|
|
onTap: codeDisabled ? null : _sendSms,
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
width: 110, height: 52,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: codeDisabled ? theme.colorScheme.muted : AppTheme.primary,
|
|
borderRadius: BorderRadius.circular(AppTheme.rSm),
|
|
),
|
|
child: Text(
|
|
_sending ? '发送中' : _countdown > 0 ? '${_countdown}s' : '获取验证码',
|
|
style: TextStyle(
|
|
fontSize: 17,
|
|
color: codeDisabled ? theme.colorScheme.mutedForeground : Colors.white,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
]);
|
|
}
|
|
|
|
Widget _buildAgreement(ShadThemeData theme) {
|
|
return Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: 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 ? AppTheme.primary : Colors.transparent,
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(
|
|
color: _agreed ? AppTheme.primary : theme.colorScheme.border,
|
|
width: 1.5,
|
|
),
|
|
),
|
|
child: _agreed ? Icon(LucideIcons.check, size: 15, color: theme.colorScheme.primaryForeground) : null,
|
|
),
|
|
RichText(text: TextSpan(children: [
|
|
TextSpan(text: '已阅读并同意', style: TextStyle(fontSize: 16, color: theme.colorScheme.mutedForeground)),
|
|
TextSpan(text: '《服务协议》', style: const TextStyle(fontSize: 16, color: AppTheme.primary)),
|
|
TextSpan(text: '和', style: TextStyle(fontSize: 16, color: theme.colorScheme.mutedForeground)),
|
|
TextSpan(text: '《隐私政策》', style: const TextStyle(fontSize: 16, color: AppTheme.primary)),
|
|
])),
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLoginButton(ShadThemeData theme) {
|
|
return GestureDetector(
|
|
onTap: _loading ? null : _login,
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: 52,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(colors: [Color(0xFF9B8FEF), AppTheme.primary]),
|
|
borderRadius: BorderRadius.circular(AppTheme.rPill),
|
|
boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(80), blurRadius: 16, offset: const Offset(0, 8))],
|
|
),
|
|
child: _loading
|
|
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2.5, color: Colors.white))
|
|
: const Text('登 录', style: TextStyle(fontSize: 20, color: Colors.white, fontWeight: FontWeight.w600, letterSpacing: 2)),
|
|
),
|
|
);
|
|
}
|
|
}
|