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

@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import '../../core/navigation_provider.dart';
import '../../core/app_colors.dart';
import '../../core/app_theme.dart';
import '../../providers/auth_provider.dart';
import '../../utils/sse_handler.dart';
@@ -123,7 +124,9 @@ class DietNotifier extends Notifier<DietState> {
if (text.isNotEmpty) state = state.copyWith(commentary: text.trim());
}
}
} catch (_) {}
} catch (e) {
debugPrint('[Diet] 获取饮食点评失败: $e');
}
}
List<FoodItem> _parseFoodItems(String raw) {
@@ -243,7 +246,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
centerTitle: true,
),
body: state.imagePath == null
? const Center(child: Text('请从首页拍摄或选择食物照片', style: TextStyle(color: Color(0xFF999999))))
? const Center(child: Text('请从首页拍摄或选择食物照片', style: TextStyle(color: AppColors.textSecondary)))
: _buildResultView(context, ref),
);
}
@@ -263,11 +266,12 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
final totalCalories = state.foods.where((f) => f.selected).fold(0, (sum, f) => sum + f.calories);
return Container(
color: _kPageBg,
color: AppColors.background,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(children: [
_buildImagePreview(state.imagePath!),
const SizedBox(height: 20),
const SizedBox(height: 16),
_buildMealSelector(ref),
const SizedBox(height: 16),
if (state.isAnalyzing)
@@ -282,8 +286,8 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
_buildAiCommentary(state.commentary!),
],
],
const SizedBox(height: 24),
_buildSubmitButton(context, ref),
const SizedBox(height: 20),
_buildSubmitButton(),
const SizedBox(height: 16),
],
]),
@@ -293,29 +297,25 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
// ─────────── 图片预览 ───────────
Widget _buildImagePreview(String path) {
return Stack(
children: [
Container(
height: 220,
return Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Container(
height: 200,
width: MediaQuery.of(context).size.width * 0.75,
decoration: BoxDecoration(
color: const Color(0xFFE0E0E0),
image: DecorationImage(image: FileImage(File(path)), fit: BoxFit.cover),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 14, offset: const Offset(0, 4))],
),
),
Positioned(
left: 0, right: 0, bottom: 0,
child: Container(
height: 60,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.transparent, Color(0x40000000)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
foregroundDecoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.transparent, Colors.black38],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
],
),
);
}
@@ -351,9 +351,9 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
: null,
),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(m.icon, size: 23, color: isSelected ? Colors.white : _kSubText),
Icon(m.icon, size: 23, color: isSelected ? AppColors.textOnGradient : _kSubText),
const SizedBox(height: 4),
Text(m.label, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: isSelected ? Colors.white : _kSubText)),
Text(m.label, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: isSelected ? AppColors.textOnGradient : _kSubText)),
]),
),
),
@@ -416,7 +416,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(color: const Color(0xFFFFF8EE), borderRadius: BorderRadius.circular(8)),
decoration: BoxDecoration(color: AppColors.warningLight, borderRadius: BorderRadius.circular(8)),
child: Text('$totalCal kcal', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: _kWarning)),
),
IconButton(
@@ -441,9 +441,9 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: food.selected ? const Color(0xFFF6F4FF) : const Color(0xFFFAFAFA),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: food.selected ? _kPrimary.withAlpha(30) : const Color(0xFFEEEEEE)),
color: food.selected ? AppColors.iconBg : AppColors.background,
borderRadius: BorderRadius.circular(AppTheme.rMd),
border: Border.all(color: food.selected ? _kPrimary.withAlpha(30) : AppColors.border),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 10, 6, 10),
@@ -454,10 +454,10 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
width: 22, height: 22,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: food.selected ? _kPrimary : Colors.white,
border: Border.all(color: food.selected ? _kPrimary : const Color(0xFFCCCCCC), width: 2),
color: food.selected ? _kPrimary : AppTheme.surface,
border: Border.all(color: food.selected ? _kPrimary : AppColors.border, width: 2),
),
child: food.selected ? const Icon(Icons.check, size: 17, color: Colors.white) : null,
child: food.selected ? const Icon(Icons.check, size: 17, color: AppColors.textOnGradient) : null,
),
),
const SizedBox(width: 12),
@@ -514,7 +514,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
]),
),
IconButton(
icon: const Icon(Icons.close, size: 19, color: Color(0xFFBBBBBB)),
icon: const Icon(Icons.close, size: 19, color: AppColors.textHint),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () => ref.read(dietProvider.notifier).removeFood(food.id),
@@ -532,7 +532,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(colors: [_kPrimary, Color(0xFF8B7CF0)], begin: Alignment.topLeft, end: Alignment.bottomRight),
gradient: AppColors.primaryGradient,
borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(color: _kPrimary.withAlpha(40), blurRadius: 10, offset: const Offset(0, 4))],
),
@@ -548,12 +548,12 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
value: (totalCalories / 700).clamp(0.0, 1.0),
strokeWidth: 4,
backgroundColor: Colors.white24,
color: Colors.white,
color: AppColors.textOnGradient,
),
),
Column(mainAxisSize: MainAxisSize.min, children: [
Text('$totalCalories', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w800, color: Colors.white)),
Text('kcal', style: const TextStyle(fontSize: 13, color: Colors.white70)),
Text('$totalCalories', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w800, color: AppColors.textOnGradient)),
Text('kcal', style: const TextStyle(fontSize: 13, color: AppColors.textOnGradient)),
]),
],
),
@@ -561,14 +561,14 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
const SizedBox(width: 16),
Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text('本餐热量', style: TextStyle(fontSize: 17, color: Colors.white70)),
const Text('本餐热量', style: TextStyle(fontSize: 17, color: AppColors.textOnGradient)),
const SizedBox(height: 4),
Row(children: [
Expanded(child: _macroBar('碳水', 0.55, const Color(0xFFFFF9C4))),
Expanded(child: _macroBar('碳水', 0.55, AppColors.warningLight)),
const SizedBox(width: 8),
Expanded(child: _macroBar('蛋白', 0.25, const Color(0xFFD4C8FC))),
Expanded(child: _macroBar('蛋白', 0.25, AppColors.iconBg)),
const SizedBox(width: 8),
Expanded(child: _macroBar('脂肪', 0.20, const Color(0xFFFFCDD2))),
Expanded(child: _macroBar('脂肪', 0.20, AppColors.errorLight)),
]),
]),
),
@@ -582,7 +582,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
Row(children: [
Container(width: 6, height: 6, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
const SizedBox(width: 4),
Text(label, style: const TextStyle(fontSize: 13, color: Colors.white70)),
Text(label, style: const TextStyle(fontSize: 13, color: AppColors.textOnGradient)),
]),
const SizedBox(height: 3),
ClipRRect(
@@ -597,19 +597,22 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Container(
padding: const EdgeInsets.all(14),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _kSurface,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: _kBorder),
gradient: AppColors.gLightPurple,
borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(20), blurRadius: 12, offset: const Offset(0, 4))],
),
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
Container(
width: 32, height: 32,
decoration: BoxDecoration(color: _kPrimaryLight, borderRadius: BorderRadius.circular(8)),
child: const Icon(Icons.auto_awesome, size: 19, color: _kPrimary),
width: 36, height: 36,
decoration: BoxDecoration(
gradient: AppColors.primaryGradient,
borderRadius: const BorderRadius.all(Radius.circular(10)),
),
child: const Icon(Icons.auto_awesome, size: 20, color: AppColors.textOnGradient),
),
const SizedBox(width: 10),
const SizedBox(width: 12),
Expanded(child: Text(text, style: const TextStyle(fontSize: 16, color: _kText, height: 1.6))),
]),
),
@@ -617,47 +620,34 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
}
// ─────────── 保存按钮 ───────────
Widget _buildSubmitButton(BuildContext context, WidgetRef ref) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: () async {
try {
await ref.read(dietProvider.notifier).saveRecord();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('饮食记录已保存'),
backgroundColor: _kPrimary,
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 2),
));
popRoute(ref);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('保存失败: $e'),
backgroundColor: AppTheme.error,
behavior: SnackBarBehavior.floating,
));
}
}
},
style: ElevatedButton.styleFrom(
backgroundColor: _kPrimary,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rMd)),
textStyle: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600),
Widget _buildSubmitButton() {
return SizedBox(
width: double.infinity,
height: 52,
child: Container(
decoration: BoxDecoration(
gradient: AppColors.gPurplePink,
borderRadius: BorderRadius.circular(14),
boxShadow: [BoxShadow(color: AppColors.primary.withAlpha(20), blurRadius: 12, offset: const Offset(0, 4))],
),
padding: const EdgeInsets.all(1.5),
child: Material(
color: AppTheme.surface,
borderRadius: BorderRadius.circular(12.5),
child: InkWell(
onTap: () async {
try {
await ref.read(dietProvider.notifier).saveRecord();
if (mounted) popRoute(ref);
} catch (e) { debugPrint('[Diet] 保存记录失败: $e'); }
},
borderRadius: BorderRadius.circular(12),
child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.check_circle_outline, size: 22, color: AppColors.primary),
SizedBox(width: 8),
Text('保存记录', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.primary)),
]),
),
child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.check_circle_outline, size: 23),
SizedBox(width: 8),
Text('保存记录'),
]),
),
),
);