feat: UI全面改造 + 后端多项修复
【后端修复】 - 删除diet/consultation agent假工具声明 - 修复DayOfWeek实体注释 - 修复Vision API content序列化 - cleanup_service级联删除修复 - 用药提醒时区偏差修复 - 统一DateTime处理(UtcNow+8) - 新增UTC DateTime JSON转换器 【前端UI重构】 - 配色体系全面更新(#8B5CF6淡紫+#F0ECFF背景) - 登录页重设计 - 首页重设计(透明顶栏、渐变背景、胶囊输入区) - 聊天卡片加白蓝边框、渐变标题 - 侧边栏重构(渐变背景、合并顶部、删除底部设置) - 确认卡片可编辑字段恢复 - 所有子页面加返回按钮 - catch异常加日志 - 删除后refresh provider缓存
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
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});
|
||||
@@ -46,12 +48,15 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
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);
|
||||
final theme = ShadTheme.of(context);
|
||||
if (authState.isLoggedIn && !authState.isLoading) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => goRoute(ref, 'home'));
|
||||
}
|
||||
@@ -60,190 +65,150 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [AppTheme.bg, AppTheme.bg, AppTheme.primaryLight],
|
||||
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: 24),
|
||||
const SizedBox(height: 40),
|
||||
// Logo
|
||||
Container(
|
||||
width: 100, height: 100,
|
||||
width: 88, height: 88,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primary.withAlpha(25),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(44),
|
||||
boxShadow: [BoxShadow(color: AppColors.primary.withOpacity(0.15), blurRadius: 20, offset: const Offset(0, 8))],
|
||||
),
|
||||
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: 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(
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.backgroundSoft,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
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(
|
||||
hintText: '请输入手机号',
|
||||
hintStyle: TextStyle(fontSize: 15, color: AppColors.textHint),
|
||||
border: InputBorder.none,
|
||||
counterText: '',
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
),
|
||||
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),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 手机号输入
|
||||
_buildPhoneInput(theme),
|
||||
const SizedBox(height: AppTheme.sLg),
|
||||
// 验证码
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.backgroundSoft,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _codeCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 6,
|
||||
style: const TextStyle(fontSize: 16, color: AppColors.textPrimary),
|
||||
decoration: const InputDecoration(
|
||||
hintText: '验证码',
|
||||
hintStyle: TextStyle(fontSize: 15, color: AppColors.textHint),
|
||||
border: 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) ? AppColors.backgroundSoft : 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),
|
||||
|
||||
// 验证码输入
|
||||
_buildCodeRow(theme),
|
||||
const SizedBox(height: AppTheme.sSm),
|
||||
// 协议
|
||||
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.transparent,
|
||||
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)),
|
||||
])),
|
||||
]),
|
||||
),
|
||||
|
||||
// 协议勾选
|
||||
_buildAgreement(theme),
|
||||
|
||||
// 错误提示
|
||||
if (_error != null)
|
||||
Padding(padding: const EdgeInsets.only(top: AppTheme.sMd), child: Text(_error!, style: TextStyle(color: theme.colorScheme.destructive, fontSize: 16))),
|
||||
Padding(padding: const EdgeInsets.only(top: 12), child: Text(_error!, style: const TextStyle(color: AppColors.error, fontSize: 14))),
|
||||
|
||||
const SizedBox(height: AppTheme.rXl),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 登录按钮
|
||||
_buildLoginButton(theme),
|
||||
GestureDetector(
|
||||
onTap: _loading ? null : _login,
|
||||
child: Container(
|
||||
width: double.infinity, height: 52, alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: _loading
|
||||
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2.5, color: Colors.white))
|
||||
: const Text('登 录', style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.w600, letterSpacing: 2)),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: AppTheme.sXl),
|
||||
const SizedBox(height: 40),
|
||||
]),
|
||||
))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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