feat: 通知推送/免打扰偏好 + 饮食记录侧滑删除修复 + 聊天交互优化

- 通知: 新增推送开关/免打扰时段偏好持久化 + EF 迁移; 通知管线支持免打扰过滤
- 饮食: 侧滑删除 confirmDismiss 按方向放行(修复删不掉); 新增 diet_nutrition_widgets; 饮食录入页重构
- 聊天: 智能体胶囊点击锁防误触; 流式状态 select 优化; 卡片入场动画
- 主页: 消息列表提取 _HomeMessages + 侧滑手势打开抽屉
- 其他: api_client IP 适配; 通知/用药/饮食端点微调; 测试更新
This commit is contained in:
MingNian
2026-07-13 10:39:34 +08:00
parent 335a3e6440
commit e654c1e0cc
19 changed files with 2140 additions and 343 deletions

View File

@@ -10,11 +10,11 @@ import '../../providers/auth_provider.dart';
// 持久化到后端 /api/notification-prefs启动时自动拉取一次。
final notificationPrefsProvider =
NotifierProvider<NotificationPrefsNotifier, Map<String, bool>>(
NotifierProvider<NotificationPrefsNotifier, Map<String, dynamic>>(
NotificationPrefsNotifier.new,
);
class NotificationPrefsNotifier extends Notifier<Map<String, bool>> {
class NotificationPrefsNotifier extends Notifier<Map<String, dynamic>> {
static const _defaults = {
'pushEnabled': true,
'medication': true,
@@ -22,8 +22,8 @@ class NotificationPrefsNotifier extends Notifier<Map<String, bool>> {
'followUp': true,
'aiReply': false,
'dndEnabled': false,
'dndStart': false,
'dndEnd': false,
'dndStartMinutes': 22 * 60,
'dndEndMinutes': 8 * 60,
// 健康录入提醒——总开关 + 5 子项
'healthRecord': true,
'healthRecord.bp': true,
@@ -34,7 +34,7 @@ class NotificationPrefsNotifier extends Notifier<Map<String, bool>> {
};
@override
Map<String, bool> build() {
Map<String, dynamic> build() {
Future.microtask(_loadFromBackend);
return {..._defaults};
}
@@ -47,6 +47,11 @@ class NotificationPrefsNotifier extends Notifier<Map<String, bool>> {
if (data == null) return;
state = {
...state,
'pushEnabled': (data['pushEnabled'] as bool?) ?? true,
'dndEnabled': (data['dndEnabled'] as bool?) ?? false,
'dndStartMinutes':
(data['dndStartMinutes'] as num?)?.toInt() ?? 22 * 60,
'dndEndMinutes': (data['dndEndMinutes'] as num?)?.toInt() ?? 8 * 60,
'medication': (data['medicationReminder'] as bool?) ?? true,
'healthAlert': (data['abnormalAlert'] as bool?) ?? true,
'followUp': (data['followUpReminder'] as bool?) ?? true,
@@ -87,6 +92,8 @@ class NotificationPrefsNotifier extends Notifier<Map<String, bool>> {
'healthRecord.glucose' => 'healthRecordReminderGlucose',
'healthRecord.spo2' => 'healthRecordReminderSpO2',
'healthRecord.weight' => 'healthRecordReminderWeight',
'pushEnabled' => 'pushEnabled',
'dndEnabled' => 'dndEnabled',
_ => null,
};
if (backendField == null) return;
@@ -99,12 +106,24 @@ class NotificationPrefsNotifier extends Notifier<Map<String, bool>> {
}
}
void setDndStart(TimeOfDay time) {
state = {...state, 'dndStart': true};
Future<void> setDndStart(TimeOfDay time) async {
await _setTime('dndStartMinutes', time.hour * 60 + time.minute);
}
void setDndEnd(TimeOfDay time) {
state = {...state, 'dndEnd': true};
Future<void> setDndEnd(TimeOfDay time) async {
await _setTime('dndEndMinutes', time.hour * 60 + time.minute);
}
Future<void> _setTime(String key, int minutes) async {
final previous = state[key];
state = {...state, key: minutes};
try {
await ref
.read(apiClientProvider)
.put('/api/notification-prefs', data: {key: minutes});
} catch (_) {
state = {...state, key: previous};
}
}
}
@@ -116,7 +135,11 @@ class NotificationPrefsPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final prefs = ref.watch(notificationPrefsProvider);
final dndOn = prefs['dndEnabled'] ?? false;
final dndOn = prefs['dndEnabled'] as bool? ?? false;
final dndStart = prefs['dndStartMinutes'] as int? ?? 22 * 60;
final dndEnd = prefs['dndEndMinutes'] as int? ?? 8 * 60;
String formatMinutes(int value) =>
'${(value ~/ 60).toString().padLeft(2, '0')}:${(value % 60).toString().padLeft(2, '0')}';
return GradientScaffold(
appBar: AppBar(
@@ -278,7 +301,9 @@ class NotificationPrefsPage extends ConsumerWidget {
children: [
_SwitchTile(
title: '开启免打扰模式',
subtitle: dndOn ? '22:00 - 08:00 期间静音' : '关闭后全天接收通知',
subtitle: dndOn
? '${formatMinutes(dndStart)} - ${formatMinutes(dndEnd)} 期间静音'
: '关闭后全天接收通知',
value: dndOn,
showDivider: dndOn,
onChanged: (v) => ref
@@ -298,13 +323,13 @@ class NotificationPrefsPage extends ConsumerWidget {
Expanded(
child: _TimeButton(
label: '开始',
time: '22:00',
time: formatMinutes(dndStart),
onTap: () async {
final picked = await showAppTimePicker(
context,
initialTime: const TimeOfDay(
hour: 22,
minute: 0,
initialTime: TimeOfDay(
hour: dndStart ~/ 60,
minute: dndStart % 60,
),
);
if (picked != null && context.mounted) {
@@ -328,13 +353,13 @@ class NotificationPrefsPage extends ConsumerWidget {
Expanded(
child: _TimeButton(
label: '结束',
time: '08:00',
time: formatMinutes(dndEnd),
onTap: () async {
final picked = await showAppTimePicker(
context,
initialTime: const TimeOfDay(
hour: 8,
minute: 0,
initialTime: TimeOfDay(
hour: dndEnd ~/ 60,
minute: dndEnd % 60,
),
);
if (picked != null && context.mounted) {