feat: UI 清新风全面改造 — 朝露主题 + shadcn_ui + 全局字号放大
- 全新"朝露"主题:深青绿主色(#2D6A4F),暖白底,完整设计Token - 引入 shadcn_ui 组件库,MaterialApp 注入 ShadTheme - 新增 4 个公共组件:AppCard、AppMenuItem、AppEmptyState、AppTabChip - 全面消除硬编码颜色,统一使用 AppTheme Token - 全局字号 +3~4pt,图标等比放大 +3px - home/medication/profile/settings/login 等核心页面重写 - 路由和功能逻辑零改动
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -32,54 +33,216 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
}
|
||||
|
||||
void _startCountdown() async {
|
||||
for (var i = 60; i > 0; i--) { await Future.delayed(const Duration(seconds: 1)); if (!mounted) return; setState(() => _countdown = i - 1); }
|
||||
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 );
|
||||
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'));
|
||||
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: [Color(0xFFF0F2FF), Color(0xFFF0F2FF), Color(0xFFE8E4FF)])),
|
||||
child: SafeArea(child: Center(child: Padding(padding: const EdgeInsets.symmetric(horizontal: 32), child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
const SizedBox(height: 24),
|
||||
Container(width: 100, height: 100, decoration: BoxDecoration(color: const Color(0xFF6C5CE7).withAlpha(20), 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(Icons.favorite, size: 36, color: Color(0xFF6C5CE7))),
|
||||
])),
|
||||
const SizedBox(height: 20),
|
||||
const Text('健康管家', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFF1A1A1A))),
|
||||
const SizedBox(height: 8),
|
||||
Text('你的 AI 心脏健康管家', style: TextStyle(fontSize: 15, color: Colors.grey[500])),
|
||||
const SizedBox(height: 36),
|
||||
TextField(controller: _phoneCtrl, keyboardType: TextInputType.phone, maxLength: 11,
|
||||
decoration: InputDecoration(hintText: '请输入手机号', prefixIcon: const Padding(padding: EdgeInsets.only(left: 12), child: Text('+86', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500))), counterText: '', filled: true, fillColor: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF6C5CE7), width: 1.5)))),
|
||||
const SizedBox(height: 16),
|
||||
Row(children: [
|
||||
Expanded(child: TextField(controller: _codeCtrl, keyboardType: TextInputType.number, maxLength: 6,
|
||||
decoration: InputDecoration(hintText: '验证码', filled: true, fillColor: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF6C5CE7), width: 1.5)), counterText: ''))),
|
||||
const SizedBox(width: 12),
|
||||
GestureDetector(onTap: (_countdown > 0 || _sending) ? null : _sendSms, child: Container(width: 100, height: 48, alignment: Alignment.center, decoration: BoxDecoration(color: _countdown > 0 ? Colors.grey[300] : const Color(0xFF6C5CE7), borderRadius: BorderRadius.circular(12)), child: Text(_sending ? '发送中' : _countdown > 0 ? '${_countdown}s' : '获取验证码', style: TextStyle(fontSize: 14, color: _countdown > 0 ? Colors.grey[600] : Colors.white, fontWeight: FontWeight.w500)))),
|
||||
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),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
Align(alignment: Alignment.centerLeft, child: GestureDetector(onTap: () => setState(() => _agreed = !_agreed), child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Container(width: 20, height: 20, margin: const EdgeInsets.only(right: 6), decoration: BoxDecoration(shape: BoxShape.rectangle, color: _agreed ? const Color(0xFF6C5CE7) : Colors.transparent, border: Border.all(color: _agreed ? const Color(0xFF6C5CE7) : const Color(0xFFBDBDBD), width: 1.5), borderRadius: BorderRadius.circular(4)), child: _agreed ? const Icon(Icons.check, size: 14, color: Colors.white) : null),
|
||||
RichText(text: TextSpan(children: [TextSpan(text: '已阅读并同意', style: TextStyle(fontSize: 13, color: Colors.grey[600])), TextSpan(text: '《服务协议》', style: const TextStyle(fontSize: 13, color: Color(0xFF6C5CE7))), TextSpan(text: '和', style: TextStyle(fontSize: 13, color: Colors.grey[600])), TextSpan(text: '《隐私政策》', style: const TextStyle(fontSize: 13, color: Color(0xFF6C5CE7)))])),
|
||||
]))),
|
||||
if (_error != null) Padding(padding: const EdgeInsets.only(top: 12), child: Text(_error!, style: const TextStyle(color: Color(0xFFE53935), fontSize: 13))),
|
||||
const SizedBox(height: 24),
|
||||
GestureDetector(onTap: _loading ? null : _login, child: Container(width: double.infinity, height: 50, alignment: Alignment.center, decoration: BoxDecoration(gradient: const LinearGradient(colors: [Color(0xFFA8B5FA), Color(0xFF6C5CE7)]), borderRadius: BorderRadius.circular(25), boxShadow: [BoxShadow(color: const Color(0xFF6C5CE7).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: 17, color: Colors.white, fontWeight: FontWeight.w600, letterSpacing: 2)))),
|
||||
const SizedBox(height: 20),
|
||||
])))),
|
||||
))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user