refactor: 死代码清理 + 趋势图重构 + 侧边栏交互优化 + 智能体卡片去延迟
- 死代码清理: 删除 AppCard/AppMenuItem/AppTabChip/AppButtons 等 4 个未使用 Widget 文件; 清理 common_widgets/enterprise_widgets/app_status_badge 中未使用 class; 移除 HealthService 等未使用方法; 后端移除 ExerciseService.CreateFromItemsAsync/HealthArchiveService.GetOrCreateAsync/MedicationAgentHandler 死分支/ChatRequest DTO - 趋势图: 直线折线 + 渐变填充 + 阴影; 去网格; X 轴日+月份; Y 轴整数刻度最多 4 个; 点击数据点/录入记录显示上方浮层; 选中点变实心 - 侧边栏: 对话记录改单选删除(灰底高亮); 操作栏浮层修复触摸问题; 常用功能/健康仪表盘间距收紧; _Panel 去外框 - 智能体: 欢迎卡片去掉 400ms 延迟; 胶囊去阴影 - 其他: api_client IP 适配; 多页面 UI 微调; 新增 chat_provider/prelaunch_guardrails 测试
This commit is contained in:
@@ -144,191 +144,211 @@ class NotificationPrefsPage extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ── 推送总开关 ──
|
||||
_SectionTitle(title: '推送通知'),
|
||||
_SwitchTile(
|
||||
title: '允许推送通知',
|
||||
subtitle: '关闭后将不再收到任何系统推送',
|
||||
value: prefs['pushEnabled'] ?? true,
|
||||
onChanged: (v) => ref
|
||||
.read(notificationPrefsProvider.notifier)
|
||||
.toggle('pushEnabled'),
|
||||
_SettingsListSection(
|
||||
title: '推送通知',
|
||||
children: [
|
||||
_SwitchTile(
|
||||
title: '允许推送通知',
|
||||
subtitle: '关闭后将不再收到任何系统推送',
|
||||
value: prefs['pushEnabled'] ?? true,
|
||||
showDivider: false,
|
||||
onChanged: (v) => ref
|
||||
.read(notificationPrefsProvider.notifier)
|
||||
.toggle('pushEnabled'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── 各类通知开关 ──
|
||||
_SectionTitle(title: '通知类型'),
|
||||
_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,
|
||||
onChanged: (v) => ref
|
||||
.read(notificationPrefsProvider.notifier)
|
||||
.toggle('aiReply'),
|
||||
_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),
|
||||
|
||||
// ── 健康录入提醒 ──
|
||||
_SectionTitle(title: '健康录入提醒'),
|
||||
_SwitchTile(
|
||||
icon: Icons.alarm_on_rounded,
|
||||
iconBg: const Color(0xFFFEF3C7),
|
||||
iconColor: const Color(0xFFD97706),
|
||||
title: '每日录入提醒',
|
||||
subtitle: '每天上午提醒录入健康指标,已录入则不再打扰',
|
||||
value: prefs['healthRecord'] ?? true,
|
||||
onChanged: (v) => ref
|
||||
.read(notificationPrefsProvider.notifier)
|
||||
.toggle('healthRecord'),
|
||||
_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'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
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,
|
||||
onChanged: (v) => ref
|
||||
.read(notificationPrefsProvider.notifier)
|
||||
.toggle('healthRecord.weight'),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── 免打扰时段 ──
|
||||
_SectionTitle(title: '免打扰时段'),
|
||||
_SwitchTile(
|
||||
title: '开启免打扰模式',
|
||||
subtitle: dndOn ? '22:00 - 08:00 期间静音' : '关闭后全天接收通知',
|
||||
value: dndOn,
|
||||
onChanged: (v) => ref
|
||||
.read(notificationPrefsProvider.notifier)
|
||||
.toggle('dndEnabled'),
|
||||
),
|
||||
if (dndOn) ...[
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
_SettingsListSection(
|
||||
title: '免打扰时段',
|
||||
children: [
|
||||
_SwitchTile(
|
||||
title: '开启免打扰模式',
|
||||
subtitle: dndOn ? '22:00 - 08:00 期间静音' : '关闭后全天接收通知',
|
||||
value: dndOn,
|
||||
showDivider: dndOn,
|
||||
onChanged: (v) => ref
|
||||
.read(notificationPrefsProvider.notifier)
|
||||
.toggle('dndEnabled'),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (dndOn)
|
||||
Container(
|
||||
margin: EdgeInsets.zero,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Text(
|
||||
'~',
|
||||
style: TextStyle(
|
||||
fontSize: 19,
|
||||
color: AppTheme.textHint,
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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: 8),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
@@ -358,6 +378,30 @@ class _SectionTitle extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingsListSection extends StatelessWidget {
|
||||
final String title;
|
||||
final List<Widget> 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;
|
||||
@@ -365,6 +409,7 @@ class _SwitchTile extends StatelessWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final bool value;
|
||||
final bool showDivider;
|
||||
final ValueChanged<bool> onChanged;
|
||||
|
||||
const _SwitchTile({
|
||||
@@ -374,33 +419,38 @@ class _SwitchTile extends StatelessWidget {
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.value,
|
||||
this.showDivider = true,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 3),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
margin: EdgeInsets.zero,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
border: showDivider
|
||||
? Border(
|
||||
bottom: BorderSide(
|
||||
color: AppColors.borderLight.withValues(alpha: 0.75),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Container(
|
||||
width: 38,
|
||||
height: 38,
|
||||
width: 34,
|
||||
height: 34,
|
||||
decoration: BoxDecoration(
|
||||
color: iconBg ?? AppColors.iconBg,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(AppTheme.rSm),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 23,
|
||||
size: 20,
|
||||
color: iconColor ?? AppColors.primary,
|
||||
),
|
||||
),
|
||||
@@ -413,15 +463,17 @@ class _SwitchTile extends StatelessWidget {
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontSize: 16,
|
||||
color: AppColors.textPrimary,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
if (subtitle != null && subtitle!.isNotEmpty)
|
||||
const SizedBox(height: 2),
|
||||
if (subtitle != null && subtitle!.isNotEmpty)
|
||||
Text(
|
||||
subtitle!,
|
||||
style: TextStyle(fontSize: 15, color: AppTheme.textSub),
|
||||
style: TextStyle(fontSize: 13, color: AppTheme.textSub),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user