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:
@@ -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('保存记录'),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user