import 'package:flutter/material.dart'; import '../../core/app_colors.dart'; import '../../core/app_theme.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/navigation_provider.dart'; import '../../providers/auth_provider.dart'; // ── 通知偏好状态 ── // 持久化到后端 /api/notification-prefs,启动时自动拉取一次。 final notificationPrefsProvider = NotifierProvider>( NotificationPrefsNotifier.new, ); class NotificationPrefsNotifier extends Notifier> { static const _defaults = { 'pushEnabled': true, 'medication': true, 'healthAlert': true, 'followUp': true, 'aiReply': false, 'dndEnabled': false, 'dndStart': false, 'dndEnd': false, // 健康录入提醒——总开关 + 5 子项 'healthRecord': true, 'healthRecord.bp': true, 'healthRecord.hr': true, 'healthRecord.glucose': true, 'healthRecord.spo2': true, 'healthRecord.weight': true, }; @override Map build() { Future.microtask(_loadFromBackend); return {..._defaults}; } Future _loadFromBackend() async { try { final api = ref.read(apiClientProvider); final res = await api.get('/api/notification-prefs'); final data = res.data['data'] as Map?; if (data == null) return; state = { ...state, 'medication': (data['medicationReminder'] as bool?) ?? true, 'healthAlert': (data['abnormalAlert'] as bool?) ?? true, 'followUp': (data['followUpReminder'] as bool?) ?? true, 'aiReply': (data['doctorReply'] as bool?) ?? false, 'healthRecord': (data['healthRecordReminder'] as bool?) ?? true, 'healthRecord.bp': (data['healthRecordReminderBloodPressure'] as bool?) ?? true, 'healthRecord.hr': (data['healthRecordReminderHeartRate'] as bool?) ?? true, 'healthRecord.glucose': (data['healthRecordReminderGlucose'] as bool?) ?? true, 'healthRecord.spo2': (data['healthRecordReminderSpO2'] as bool?) ?? true, 'healthRecord.weight': (data['healthRecordReminderWeight'] as bool?) ?? true, }; } catch (_) { // 网络异常时保留默认值 } } Future toggle(String key) async { final newValue = !(state[key] ?? false); state = {...state, key: newValue}; await _pushToBackend(key, newValue); } Future _pushToBackend(String key, bool value) async { // 把前端 key 映射到后端字段名 String? backendField = switch (key) { 'medication' => 'medicationReminder', 'healthAlert' => 'abnormalAlert', 'followUp' => 'followUpReminder', 'aiReply' => 'doctorReply', 'healthRecord' => 'healthRecordReminder', 'healthRecord.bp' => 'healthRecordReminderBloodPressure', 'healthRecord.hr' => 'healthRecordReminderHeartRate', 'healthRecord.glucose' => 'healthRecordReminderGlucose', 'healthRecord.spo2' => 'healthRecordReminderSpO2', 'healthRecord.weight' => 'healthRecordReminderWeight', _ => null, }; if (backendField == null) return; try { final api = ref.read(apiClientProvider); await api.put('/api/notification-prefs', data: {backendField: value}); } catch (_) { // 失败时回滚(避免界面与后端不同步) state = {...state, key: !value}; } } void setDndStart(TimeOfDay time) { state = {...state, 'dndStart': true}; } void setDndEnd(TimeOfDay time) { state = {...state, 'dndEnd': true}; } } // ── 页面 ── class NotificationPrefsPage extends ConsumerWidget { const NotificationPrefsPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final prefs = ref.watch(notificationPrefsProvider); final dndOn = prefs['dndEnabled'] ?? false; return GradientScaffold( appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, leading: IconButton( icon: const Icon( Icons.arrow_back_ios_new, color: AppColors.textPrimary, ), onPressed: () => popRoute(ref), ), title: Text( '消息通知', style: TextStyle( fontSize: 21, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), centerTitle: true, ), body: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── 推送总开关 ── _SettingsListSection( title: '推送通知', children: [ _SwitchTile( title: '允许推送通知', subtitle: '关闭后将不再收到任何系统推送', value: prefs['pushEnabled'] ?? true, showDivider: false, onChanged: (v) => ref .read(notificationPrefsProvider.notifier) .toggle('pushEnabled'), ), ], ), const SizedBox(height: 24), // ── 各类通知开关 ── _SettingsListSection( title: '通知类型', children: [ _SwitchTile( icon: Icons.medication_rounded, iconBg: AppColors.errorLight, iconColor: AppColors.error, title: '用药提醒', subtitle: '服药时间到达时提醒您', value: prefs['medication'] ?? true, onChanged: (v) => ref .read(notificationPrefsProvider.notifier) .toggle('medication'), ), _SwitchTile( icon: Icons.warning_amber_rounded, iconBg: AppColors.errorLight, iconColor: AppTheme.error, title: '健康异常提醒', subtitle: '检测到数据异常时及时通知', value: prefs['healthAlert'] ?? true, onChanged: (v) => ref .read(notificationPrefsProvider.notifier) .toggle('healthAlert'), ), _SwitchTile( icon: Icons.event_available_rounded, iconBg: AppColors.successLight, iconColor: AppColors.success, title: '复查日期提醒', subtitle: '复查日前一天提醒您预约', value: prefs['followUp'] ?? true, onChanged: (v) => ref .read(notificationPrefsProvider.notifier) .toggle('followUp'), ), _SwitchTile( icon: Icons.forum_outlined, iconBg: AppColors.iconBg, iconColor: AppTheme.primary, title: 'AI 回复通知', subtitle: 'AI 助手回复时发送通知', value: prefs['aiReply'] ?? false, showDivider: false, onChanged: (v) => ref .read(notificationPrefsProvider.notifier) .toggle('aiReply'), ), ], ), const SizedBox(height: 24), // ── 健康录入提醒 ── _SettingsListSection( title: '健康录入提醒', children: [ _SwitchTile( icon: Icons.alarm_on_rounded, iconBg: const Color(0xFFFEF3C7), iconColor: const Color(0xFFD97706), title: '每日录入提醒', subtitle: '每天上午提醒录入健康指标,已录入则不再打扰', value: prefs['healthRecord'] ?? true, showDivider: prefs['healthRecord'] ?? true, onChanged: (v) => ref .read(notificationPrefsProvider.notifier) .toggle('healthRecord'), ), if (prefs['healthRecord'] ?? true) ...[ _SwitchTile( title: ' · 血压', value: prefs['healthRecord.bp'] ?? true, onChanged: (v) => ref .read(notificationPrefsProvider.notifier) .toggle('healthRecord.bp'), ), _SwitchTile( title: ' · 心率', value: prefs['healthRecord.hr'] ?? true, onChanged: (v) => ref .read(notificationPrefsProvider.notifier) .toggle('healthRecord.hr'), ), _SwitchTile( title: ' · 血糖', value: prefs['healthRecord.glucose'] ?? true, onChanged: (v) => ref .read(notificationPrefsProvider.notifier) .toggle('healthRecord.glucose'), ), _SwitchTile( title: ' · 血氧', value: prefs['healthRecord.spo2'] ?? true, onChanged: (v) => ref .read(notificationPrefsProvider.notifier) .toggle('healthRecord.spo2'), ), _SwitchTile( title: ' · 体重', value: prefs['healthRecord.weight'] ?? true, showDivider: false, onChanged: (v) => ref .read(notificationPrefsProvider.notifier) .toggle('healthRecord.weight'), ), ], ], ), const SizedBox(height: 24), // ── 免打扰时段 ── _SettingsListSection( title: '免打扰时段', children: [ _SwitchTile( title: '开启免打扰模式', subtitle: dndOn ? '22:00 - 08:00 期间静音' : '关闭后全天接收通知', value: dndOn, showDivider: dndOn, onChanged: (v) => ref .read(notificationPrefsProvider.notifier) .toggle('dndEnabled'), ), if (dndOn) Container( margin: EdgeInsets.zero, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14, ), decoration: BoxDecoration(color: AppTheme.surface), child: Row( children: [ Expanded( child: _TimeButton( label: '开始', time: '22:00', onTap: () async { final picked = await showAppTimePicker( context, initialTime: const TimeOfDay( hour: 22, minute: 0, ), ); if (picked != null && context.mounted) { ref .read(notificationPrefsProvider.notifier) .setDndStart(picked); } }, ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Text( '~', style: TextStyle( fontSize: 19, color: AppTheme.textHint, ), ), ), Expanded( child: _TimeButton( label: '结束', time: '08:00', onTap: () async { final picked = await showAppTimePicker( context, initialTime: const TimeOfDay( hour: 8, minute: 0, ), ); if (picked != null && context.mounted) { ref .read(notificationPrefsProvider.notifier) .setDndEnd(picked); } }, ), ), ], ), ), ], ), const SizedBox(height: 40), ], ), ), ); } } // ── 子组件 ── class _SectionTitle extends StatelessWidget { final String title; const _SectionTitle({required this.title}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(left: 4, bottom: 10), child: Text( title, style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w600, color: AppColors.textSecondary, ), ), ); } } class _SettingsListSection extends StatelessWidget { final String title; final List children; const _SettingsListSection({required this.title, required this.children}); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _SectionTitle(title: title), ClipRRect( borderRadius: BorderRadius.circular(AppTheme.rSm), child: ColoredBox( color: AppTheme.surface, child: Column(children: children), ), ), ], ); } } class _SwitchTile extends StatelessWidget { final IconData? icon; final Color? iconBg; final Color? iconColor; final String title; final String? subtitle; final bool value; final bool showDivider; final ValueChanged onChanged; const _SwitchTile({ this.icon, this.iconBg, this.iconColor, required this.title, this.subtitle, required this.value, this.showDivider = true, required this.onChanged, }); @override Widget build(BuildContext context) { 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( 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, ), ), const SizedBox(width: 12), ], Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 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), ), ], ), ), Switch( value: value, onChanged: onChanged, activeThumbColor: AppTheme.primary, activeTrackColor: AppColors.primaryLight, ), ], ), ); } } class _TimeButton extends StatelessWidget { final String label; final String time; final VoidCallback onTap; const _TimeButton({ required this.label, required this.time, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( border: Border.all(color: AppColors.border), borderRadius: BorderRadius.circular(AppTheme.rSm), ), child: Column( children: [ Text( label, style: TextStyle(fontSize: 14, color: AppTheme.textHint), ), Text( time, style: const TextStyle( fontSize: 19, fontWeight: FontWeight.w600, color: AppTheme.primary, ), ), ], ), ), ); } }