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:
MingNian
2026-06-10 18:27:38 +08:00
parent 3964cf2bcb
commit 63d2092c24
46 changed files with 2003 additions and 1431 deletions

View File

@@ -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)),
),
);
}
}