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:
MingNian
2026-07-09 15:04:02 +08:00
parent 1c020b8ae5
commit d82e006cf4
37 changed files with 1842 additions and 2096 deletions

View File

@@ -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),
),
],
),

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../core/app_colors.dart';
import '../../core/app_design_tokens.dart';
import '../../core/app_theme.dart';
import '../../core/navigation_provider.dart';
import '../../providers/auth_provider.dart';
@@ -38,37 +39,58 @@ class SettingsPage extends ConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_SettingsTile(
icon: LucideIcons.bluetooth,
title: '蓝牙设备',
onTap: () => pushRoute(ref, 'devices'),
),
const SizedBox(height: 8),
_SettingsTile(
icon: LucideIcons.bell,
title: '消息通知',
onTap: () => pushRoute(ref, 'notificationPrefs'),
),
const SizedBox(height: 8),
_SettingsTile(
icon: LucideIcons.info,
title: '关于小脉健康',
onTap: () =>
pushRoute(ref, 'staticText', params: {'type': 'about'}),
),
const SizedBox(height: 8),
_SettingsTile(
icon: LucideIcons.shield,
title: '隐私协议',
onTap: () =>
pushRoute(ref, 'staticText', params: {'type': 'privacy'}),
),
const SizedBox(height: 8),
_SettingsTile(
icon: Icons.description_outlined,
title: '服务协议',
onTap: () =>
pushRoute(ref, 'staticText', params: {'type': 'terms'}),
_SettingsGroup(
children: [
_SettingsTile(
icon: LucideIcons.bluetooth,
title: '蓝牙设备',
onTap: () => pushRoute(ref, 'devices'),
),
_SettingsTile(
icon: LucideIcons.bell,
title: '消息通知',
onTap: () => pushRoute(ref, 'notificationPrefs'),
),
_SettingsTile(
icon: LucideIcons.info,
title: '关于小脉健康',
onTap: () =>
pushRoute(ref, 'staticText', params: {'type': 'about'}),
),
_SettingsTile(
icon: LucideIcons.shield,
title: '隐私协议',
onTap: () => pushRoute(
ref,
'staticText',
params: {'type': 'privacy'},
),
),
_SettingsTile(
icon: Icons.fact_check_outlined,
title: '个人信息收集清单',
onTap: () => pushRoute(
ref,
'staticText',
params: {'type': 'personalInfoList'},
),
),
_SettingsTile(
icon: Icons.hub_outlined,
title: '第三方 SDK 共享清单',
onTap: () => pushRoute(
ref,
'staticText',
params: {'type': 'thirdPartySdkList'},
),
),
_SettingsTile(
icon: Icons.description_outlined,
title: '服务协议',
onTap: () =>
pushRoute(ref, 'staticText', params: {'type': 'terms'}),
),
],
),
const SizedBox(height: 24),
OutlinedButton.icon(
@@ -172,6 +194,39 @@ class SettingsPage extends ConsumerWidget {
}
}
class _SettingsGroup extends StatelessWidget {
final List<_SettingsTile> children;
const _SettingsGroup({required this.children});
@override
Widget build(BuildContext context) {
return Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: AppRadius.xlBorder,
),
child: Column(
children: [
for (var i = 0; i < children.length; i++) ...[
children[i],
if (i < children.length - 1)
const Padding(
padding: EdgeInsets.only(left: 52),
child: Divider(
height: 1,
thickness: 0.7,
color: Color(0xFFE8ECF2),
),
),
],
],
),
);
}
}
class _SettingsTile extends StatelessWidget {
final IconData icon;
final String title;
@@ -186,17 +241,10 @@ class _SettingsTile extends StatelessWidget {
Widget build(BuildContext context) {
return Material(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(14),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFE5E7EB)),
),
child: Row(
children: [
Icon(icon, color: Colors.black, size: 22),