feat: 饮食记录逻辑重构 + 通知管理分隔线修复 + 多页面 UI 调整
- 饮食: 新增 DietCommentaryPolicy + diet_record_logic, 饮食记录逻辑抽取复用 - 通知管理: 分隔线改为仿通知中心样式, 跳过图标区域只留文字下方 - 侧边栏: 对话记录操作栏浮层修复 + 常用功能间距收紧 - 智能体: 欢迎卡片去 400ms 延迟 + 胶囊去阴影 - 后端: AI 会话仓库新增方法 + 饮食评论策略 + 健康记录规则调整 - 其他: api_client IP 适配 + 多页面 UI 微调 + 新增测试
This commit is contained in:
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user