feat: 通知推送/免打扰偏好 + 饮食记录侧滑删除修复 + 聊天交互优化
- 通知: 新增推送开关/免打扰时段偏好持久化 + EF 迁移; 通知管线支持免打扰过滤 - 饮食: 侧滑删除 confirmDismiss 按方向放行(修复删不掉); 新增 diet_nutrition_widgets; 饮食录入页重构 - 聊天: 智能体胶囊点击锁防误触; 流式状态 select 优化; 卡片入场动画 - 主页: 消息列表提取 _HomeMessages + 侧滑手势打开抽屉 - 其他: api_client IP 适配; 通知/用药/饮食端点微调; 测试更新
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user