feat: 饮食记录逻辑重构 + 通知管理分隔线修复 + 多页面 UI 调整

- 饮食: 新增 DietCommentaryPolicy + diet_record_logic, 饮食记录逻辑抽取复用
- 通知管理: 分隔线改为仿通知中心样式, 跳过图标区域只留文字下方
- 侧边栏: 对话记录操作栏浮层修复 + 常用功能间距收紧
- 智能体: 欢迎卡片去 400ms 延迟 + 胶囊去阴影
- 后端: AI 会话仓库新增方法 + 饮食评论策略 + 健康记录规则调整
- 其他: api_client IP 适配 + 多页面 UI 微调 + 新增测试
This commit is contained in:
MingNian
2026-07-12 22:49:38 +08:00
parent d82e006cf4
commit 335a3e6440
33 changed files with 2056 additions and 1283 deletions

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../core/app_colors.dart';
import '../../core/app_design_tokens.dart';
import '../../core/app_theme.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/navigation_provider.dart';
@@ -231,35 +232,35 @@ class NotificationPrefsPage extends ConsumerWidget {
),
if (prefs['healthRecord'] ?? true) ...[
_SwitchTile(
title: ' · 血压',
title: '血压',
value: prefs['healthRecord.bp'] ?? true,
onChanged: (v) => ref
.read(notificationPrefsProvider.notifier)
.toggle('healthRecord.bp'),
),
_SwitchTile(
title: ' · 心率',
title: '心率',
value: prefs['healthRecord.hr'] ?? true,
onChanged: (v) => ref
.read(notificationPrefsProvider.notifier)
.toggle('healthRecord.hr'),
),
_SwitchTile(
title: ' · 血糖',
title: '血糖',
value: prefs['healthRecord.glucose'] ?? true,
onChanged: (v) => ref
.read(notificationPrefsProvider.notifier)
.toggle('healthRecord.glucose'),
),
_SwitchTile(
title: ' · 血氧',
title: '血氧',
value: prefs['healthRecord.spo2'] ?? true,
onChanged: (v) => ref
.read(notificationPrefsProvider.notifier)
.toggle('healthRecord.spo2'),
),
_SwitchTile(
title: ' · 体重',
title: '体重',
value: prefs['healthRecord.weight'] ?? true,
showDivider: false,
onChanged: (v) => ref
@@ -391,7 +392,7 @@ class _SettingsListSection extends StatelessWidget {
children: [
_SectionTitle(title: title),
ClipRRect(
borderRadius: BorderRadius.circular(AppTheme.rSm),
borderRadius: AppRadius.mdBorder,
child: ColoredBox(
color: AppTheme.surface,
child: Column(children: children),
@@ -425,71 +426,127 @@ class _SwitchTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final dense = icon == null && (subtitle == null || subtitle!.isEmpty);
return Container(
margin: EdgeInsets.zero,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
decoration: BoxDecoration(
color: AppTheme.surface,
border: showDivider
? Border(
bottom: BorderSide(
color: AppColors.borderLight.withValues(alpha: 0.75),
),
)
: null,
),
child: Row(
decoration: BoxDecoration(color: AppTheme.surface),
child: Column(
children: [
if (icon != null) ...[
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: iconBg ?? AppColors.iconBg,
borderRadius: BorderRadius.circular(AppTheme.rSm),
),
child: Icon(
icon,
size: 20,
color: iconColor ?? AppColors.primary,
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 14,
vertical: dense ? 8 : 14,
),
const SizedBox(width: 12),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Row(
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
if (icon != null) ...[
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: iconBg ?? AppColors.iconBg,
borderRadius: AppRadius.smBorder,
),
child: Icon(
icon,
size: 20,
color: iconColor ?? AppColors.primary,
),
),
const SizedBox(width: 12),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: dense ? 15 : 16,
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
),
if (subtitle != null && subtitle!.isNotEmpty)
const SizedBox(height: 2),
if (subtitle != null && subtitle!.isNotEmpty)
Text(
subtitle!,
style: TextStyle(
fontSize: 13,
color: AppTheme.textSub,
),
),
],
),
),
if (subtitle != null && subtitle!.isNotEmpty)
const SizedBox(height: 2),
if (subtitle != null && subtitle!.isNotEmpty)
Text(
subtitle!,
style: TextStyle(fontSize: 13, color: AppTheme.textSub),
),
_AppSwitch(value: value, onChanged: onChanged),
],
),
),
Switch(
value: value,
onChanged: onChanged,
activeThumbColor: AppTheme.primary,
activeTrackColor: AppColors.primaryLight,
),
if (showDivider)
Padding(
padding: EdgeInsets.only(left: icon != null ? 60 : 14),
child: const Divider(
height: 1,
thickness: 0.7,
color: Color(0xFFE8ECF2),
),
),
],
),
);
}
}
class _AppSwitch extends StatelessWidget {
final bool value;
final ValueChanged<bool> onChanged;
const _AppSwitch({required this.value, required this.onChanged});
@override
Widget build(BuildContext context) {
final trackColor = value ? AppColors.primary : Colors.white;
final borderColor = value
? AppColors.primary.withValues(alpha: 0.38)
: AppColors.border;
return Semantics(
button: true,
toggled: value,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => onChanged(!value),
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
curve: Curves.easeOutCubic,
width: 46,
height: 28,
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: trackColor,
borderRadius: AppRadius.pillBorder,
border: Border.all(color: borderColor, width: 1),
),
child: AnimatedAlign(
duration: const Duration(milliseconds: 160),
curve: Curves.easeOutCubic,
alignment: value ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: value ? Colors.white : AppColors.textHint,
shape: BoxShape.circle,
),
),
),
),
),
);
}
}
class _TimeButton extends StatelessWidget {
final String label;
final String time;
@@ -508,7 +565,7 @@ class _TimeButton extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
border: Border.all(color: AppColors.border),
borderRadius: BorderRadius.circular(AppTheme.rSm),
borderRadius: AppRadius.mdBorder,
),
child: Column(
children: [