feat: 二级页面色彩刷新 + 用药/通知/设备重构 + 后端健康档案/通知管线增强 + 大量测试
## 后端 - 健康档案: 新增手术状态字段 + EF 迁移; HealthArchiveService 新增查询方法 - 健康记录: HealthRecordService 新增批量/统计方法; 契约扩展 - 用药: 新增 MedicationScheduleStatus 枚举; MedicationService 排班逻辑调整 - 通知: EfUserNotificationPipeline 重构; 新增 EfReminderCatchUpService; 通知管线支持更多场景 - 用户: UserService 账号删除逻辑; 新增 local_account_file_cleanup; EfUserRepository 扩展 - AI: medication_agent_handler 微调; prompt_manager 优化; AiConversationService 上下文处理 - Endpoint: doctor/medication/exercise/health/notification/user 等多接口调整 - BackgroundService: health_record_reminder_service 重构, 提醒补漏逻辑 - 测试: 新增 account_deletion/doctor_endpoint/medication_schedule/medication_update/prompt_manager 测试 ## 前端 - UI 系统: app_theme 大幅重构; app_colors/app_design_tokens/app_module_visuals 调整; 二级页面色彩刷新 - 主页: home_page 背景渐变 + 消息列表提取 _HomeMessages + 通知检查逻辑; chat_messages_view 全面重构 - 用药: medication_list/edit/checkin 三页重构, 新增 medication_ui_logic 抽取 - 通知: notification_prefs_page 重构, 新增 notification_prefs_logic; notification_center 优化 - 设备: device_management 重构, 新增 device_sync_ui_logic; device_scan 优化 - 趋势图: trend_page 大幅重构 - 登录: login_page 重构 - 个人资料: 新增 profile_edit_page; profile_page 优化 - 运动: 新增 exercise/ 目录 + care_plan_ui_logic - 其他: remaining_pages/report_pages/health_drawer/admin/doctor 等多页面调整 - 组件: common_widgets/app_empty_state/app_error_state/app_future_view/app_toast/ai_content 优化 - Provider: chat_provider/consultation_provider/data_providers/auth_provider 调整 - AndroidManifest: 移除多余权限 - 测试: 新增 ai_content/care_plan/home_message/login_flow/medication_checkin/medication_ui/notification_prefs/profile_device/secondary_page/swipe_delete 等大量测试 ## 文档 - 新增 ui-design-system.md 设计系统文档 - 新增 secondary-page-color-refresh 计划 + specs 目录
This commit is contained in:
@@ -45,8 +45,7 @@ Future<DateTime?> showAppDatePicker(
|
||||
final monthCtrl = FixedExtentScrollController(initialItem: m - 1);
|
||||
final dayCtrl = FixedExtentScrollController(initialItem: d - 1);
|
||||
|
||||
int daysInMonth(int year, int month) =>
|
||||
DateTime(year, month + 1, 0).day;
|
||||
int daysInMonth(int year, int month) => DateTime(year, month + 1, 0).day;
|
||||
|
||||
void clampDay() {
|
||||
final maxDay = daysInMonth(y, m);
|
||||
@@ -70,12 +69,22 @@ Future<DateTime?> showAppDatePicker(
|
||||
children: [
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: const Text('取消', style: TextStyle(fontSize: 16, color: Color(0xFF94A3B8))),
|
||||
child: const Text(
|
||||
'取消',
|
||||
style: TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
),
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: const Text('确定', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.primary)),
|
||||
child: const Text(
|
||||
'确定',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
clampDay();
|
||||
Navigator.pop(ctx, DateTime(y, m, d));
|
||||
@@ -87,37 +96,68 @@ Future<DateTime?> showAppDatePicker(
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: _wrapPicker(CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: yearCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) {
|
||||
y = minDate.year + i;
|
||||
clampDay();
|
||||
final maxDay = daysInMonth(y, m);
|
||||
dayCtrl.jumpTo((d - 1).clamp(0, maxDay - 1).toDouble());
|
||||
},
|
||||
children: [for (var i = minDate.year; i <= maxDate.year; i++) Center(child: Text('$i', style: _pickerItemStyle))],
|
||||
))),
|
||||
Expanded(child: _wrapPicker(CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: monthCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) {
|
||||
m = i + 1;
|
||||
clampDay();
|
||||
final maxDay = daysInMonth(y, m);
|
||||
dayCtrl.jumpTo((d - 1).clamp(0, maxDay - 1).toDouble());
|
||||
},
|
||||
children: [for (var i = 1; i <= 12; i++) Center(child: Text(i.toString().padLeft(2, '0'), style: _pickerItemStyle))],
|
||||
))),
|
||||
Expanded(child: _wrapPicker(CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: dayCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) => d = i + 1,
|
||||
children: [for (var i = 1; i <= daysInMonth(y, m); i++) Center(child: Text(i.toString().padLeft(2, '0'), style: _pickerItemStyle))],
|
||||
))),
|
||||
Expanded(
|
||||
child: _wrapPicker(
|
||||
CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: yearCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) {
|
||||
y = minDate.year + i;
|
||||
clampDay();
|
||||
final maxDay = daysInMonth(y, m);
|
||||
dayCtrl.jumpTo((d - 1).clamp(0, maxDay - 1).toDouble());
|
||||
},
|
||||
children: [
|
||||
for (var i = minDate.year; i <= maxDate.year; i++)
|
||||
Center(child: Text('$i', style: _pickerItemStyle)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _wrapPicker(
|
||||
CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: monthCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) {
|
||||
m = i + 1;
|
||||
clampDay();
|
||||
final maxDay = daysInMonth(y, m);
|
||||
dayCtrl.jumpTo((d - 1).clamp(0, maxDay - 1).toDouble());
|
||||
},
|
||||
children: [
|
||||
for (var i = 1; i <= 12; i++)
|
||||
Center(
|
||||
child: Text(
|
||||
i.toString().padLeft(2, '0'),
|
||||
style: _pickerItemStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _wrapPicker(
|
||||
CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: dayCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) => d = i + 1,
|
||||
children: [
|
||||
for (var i = 1; i <= daysInMonth(y, m); i++)
|
||||
Center(
|
||||
child: Text(
|
||||
i.toString().padLeft(2, '0'),
|
||||
style: _pickerItemStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -153,13 +193,24 @@ Future<TimeOfDay?> showAppTimePicker(
|
||||
children: [
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: const Text('取消', style: TextStyle(fontSize: 16, color: Color(0xFF94A3B8))),
|
||||
child: const Text(
|
||||
'取消',
|
||||
style: TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
),
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: const Text('确定', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.primary)),
|
||||
onPressed: () => Navigator.pop(ctx, TimeOfDay(hour: hour, minute: minute)),
|
||||
child: const Text(
|
||||
'确定',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
onPressed: () =>
|
||||
Navigator.pop(ctx, TimeOfDay(hour: hour, minute: minute)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -168,20 +219,46 @@ Future<TimeOfDay?> showAppTimePicker(
|
||||
child: Row(
|
||||
children: [
|
||||
const Spacer(flex: 1),
|
||||
Expanded(flex: 2, child: _wrapPicker(CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: hourCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) => hour = i,
|
||||
children: [for (var i = 0; i <= 23; i++) Center(child: Text(i.toString().padLeft(2, '0'), style: _pickerItemStyle))],
|
||||
))),
|
||||
Expanded(flex: 2, child: _wrapPicker(CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: minuteCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) => minute = i,
|
||||
children: [for (var i = 0; i <= 59; i++) Center(child: Text(i.toString().padLeft(2, '0'), style: _pickerItemStyle))],
|
||||
))),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _wrapPicker(
|
||||
CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: hourCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) => hour = i,
|
||||
children: [
|
||||
for (var i = 0; i <= 23; i++)
|
||||
Center(
|
||||
child: Text(
|
||||
i.toString().padLeft(2, '0'),
|
||||
style: _pickerItemStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _wrapPicker(
|
||||
CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: minuteCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) => minute = i,
|
||||
children: [
|
||||
for (var i = 0; i <= 59; i++)
|
||||
Center(
|
||||
child: Text(
|
||||
i.toString().padLeft(2, '0'),
|
||||
style: _pickerItemStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(flex: 1),
|
||||
],
|
||||
),
|
||||
@@ -211,8 +288,7 @@ Future<DateTime?> showAppDateTimePicker(
|
||||
final hourCtrl = FixedExtentScrollController(initialItem: h);
|
||||
final minuteCtrl = FixedExtentScrollController(initialItem: min);
|
||||
|
||||
int daysInMonth(int year, int month) =>
|
||||
DateTime(year, month + 1, 0).day;
|
||||
int daysInMonth(int year, int month) => DateTime(year, month + 1, 0).day;
|
||||
|
||||
void clampDay() {
|
||||
final maxDay = daysInMonth(y, m);
|
||||
@@ -236,12 +312,22 @@ Future<DateTime?> showAppDateTimePicker(
|
||||
children: [
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: const Text('取消', style: TextStyle(fontSize: 16, color: Color(0xFF94A3B8))),
|
||||
child: const Text(
|
||||
'取消',
|
||||
style: TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
),
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: const Text('确定', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.primary)),
|
||||
child: const Text(
|
||||
'确定',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
clampDay();
|
||||
Navigator.pop(ctx, DateTime(y, m, d, h, min));
|
||||
@@ -253,41 +339,102 @@ Future<DateTime?> showAppDateTimePicker(
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: _wrapPicker(CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: yearCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) { y = minDate.year + i; clampDay(); },
|
||||
children: [for (var i = minDate.year; i <= maxDate.year; i++) Center(child: Text('$i', style: _pickerItemStyle))],
|
||||
))),
|
||||
Expanded(child: _wrapPicker(CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: monthCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) { m = i + 1; clampDay(); },
|
||||
children: [for (var i = 1; i <= 12; i++) Center(child: Text(i.toString().padLeft(2, '0'), style: _pickerItemStyle))],
|
||||
))),
|
||||
Expanded(child: _wrapPicker(CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: dayCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) => d = i + 1,
|
||||
children: [for (var i = 1; i <= daysInMonth(y, m); i++) Center(child: Text(i.toString().padLeft(2, '0'), style: _pickerItemStyle))],
|
||||
))),
|
||||
Expanded(child: _wrapPicker(CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: hourCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) => h = i,
|
||||
children: [for (var i = 0; i <= 23; i++) Center(child: Text(i.toString().padLeft(2, '0'), style: _pickerItemStyle))],
|
||||
))),
|
||||
Expanded(child: _wrapPicker(CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: minuteCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) => min = i,
|
||||
children: [for (var i = 0; i <= 59; i++) Center(child: Text(i.toString().padLeft(2, '0'), style: _pickerItemStyle))],
|
||||
))),
|
||||
Expanded(
|
||||
child: _wrapPicker(
|
||||
CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: yearCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) {
|
||||
y = minDate.year + i;
|
||||
clampDay();
|
||||
},
|
||||
children: [
|
||||
for (var i = minDate.year; i <= maxDate.year; i++)
|
||||
Center(child: Text('$i', style: _pickerItemStyle)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _wrapPicker(
|
||||
CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: monthCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) {
|
||||
m = i + 1;
|
||||
clampDay();
|
||||
},
|
||||
children: [
|
||||
for (var i = 1; i <= 12; i++)
|
||||
Center(
|
||||
child: Text(
|
||||
i.toString().padLeft(2, '0'),
|
||||
style: _pickerItemStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _wrapPicker(
|
||||
CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: dayCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) => d = i + 1,
|
||||
children: [
|
||||
for (var i = 1; i <= daysInMonth(y, m); i++)
|
||||
Center(
|
||||
child: Text(
|
||||
i.toString().padLeft(2, '0'),
|
||||
style: _pickerItemStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _wrapPicker(
|
||||
CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: hourCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) => h = i,
|
||||
children: [
|
||||
for (var i = 0; i <= 23; i++)
|
||||
Center(
|
||||
child: Text(
|
||||
i.toString().padLeft(2, '0'),
|
||||
style: _pickerItemStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _wrapPicker(
|
||||
CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: minuteCtrl,
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) => min = i,
|
||||
children: [
|
||||
for (var i = 0; i <= 59; i++)
|
||||
Center(
|
||||
child: Text(
|
||||
i.toString().padLeft(2, '0'),
|
||||
style: _pickerItemStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -322,28 +469,47 @@ Future<int?> showAppCountPicker(
|
||||
children: [
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: const Text('取消', style: TextStyle(fontSize: 16, color: Color(0xFF94A3B8))),
|
||||
child: const Text(
|
||||
'取消',
|
||||
style: TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
),
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: const Text('确定', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.primary)),
|
||||
child: const Text(
|
||||
'确定',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
onPressed: () => Navigator.pop(ctx, selected),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _wrapPicker(CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: FixedExtentScrollController(initialItem: selected - min),
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) => selected = i + min,
|
||||
children: [
|
||||
for (var i = min; i <= max; i++)
|
||||
Center(child: Text(label.isEmpty ? '$i' : '$i$label', style: _pickerItemStyle)),
|
||||
],
|
||||
)),
|
||||
child: _wrapPicker(
|
||||
CupertinoPicker(
|
||||
selectionOverlay: null,
|
||||
scrollController: FixedExtentScrollController(
|
||||
initialItem: selected - min,
|
||||
),
|
||||
itemExtent: 40,
|
||||
onSelectedItemChanged: (i) => selected = i + min,
|
||||
children: [
|
||||
for (var i = min; i <= max; i++)
|
||||
Center(
|
||||
child: Text(
|
||||
label.isEmpty ? '$i' : '$i$label',
|
||||
style: _pickerItemStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -362,7 +528,16 @@ class AppBackground extends StatelessWidget {
|
||||
final content = safeArea ? SafeArea(child: child) : child;
|
||||
|
||||
// 全局浅灰白背景,已弃用紫色渐变。
|
||||
return ColoredBox(color: const Color(0xFFF6F8FC), child: content);
|
||||
return DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFFF0EDFF), Color(0xFFE6ECFF)],
|
||||
),
|
||||
),
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,10 +561,13 @@ class GradientScaffold extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
// 内部页面统一使用浅灰白底,对话流首页因直接使用 AppBackground 仍保留紫色渐变。
|
||||
backgroundColor: const Color(0xFFF6F8FC),
|
||||
// 二级页面用浅中性画布承托白色内容组;首页拥有自己的独立背景实现。
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: appBar,
|
||||
body: body,
|
||||
body: ColoredBox(
|
||||
color: AppColors.background,
|
||||
child: SizedBox.expand(child: body ?? const SizedBox.shrink()),
|
||||
),
|
||||
floatingActionButton: floatingActionButton,
|
||||
drawer: drawer,
|
||||
bottomNavigationBar: bottomNavigationBar,
|
||||
@@ -485,22 +663,22 @@ class AppTheme {
|
||||
surface: surface,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
scaffoldBackgroundColor: Colors.transparent,
|
||||
scaffoldBackgroundColor: AppColors.background,
|
||||
splashColor: primary.withValues(alpha: 0.06),
|
||||
highlightColor: Colors.transparent,
|
||||
hoverColor: primaryLight.withValues(alpha: 0.55),
|
||||
fontFamily: null,
|
||||
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: Colors.white.withValues(alpha: 0.9),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: text,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
scrolledUnderElevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
titleTextStyle: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w700,
|
||||
titleTextStyle: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: text,
|
||||
),
|
||||
toolbarHeight: 52,
|
||||
|
||||
Reference in New Issue
Block a user