From 431b72d49ac5102cc868cc7a283600b19f69df64 Mon Sep 17 00:00:00 2001
From: MingNian <1281442923@qq.com>
Date: Mon, 22 Jun 2026 13:59:37 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20Cupertino=E6=BB=9A=E8=BD=AE=E9=80=89?=
=?UTF-8?q?=E6=8B=A9=E5=99=A8=20+=20=E5=93=81=E7=89=8C=E6=9B=B4=E5=90=8D?=
=?UTF-8?q?=20+=20UI=E5=85=A8=E9=9D=A2=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 日期/时间/次数选择器统一改为底部弹出Cupertino滚轮,无单位标签,双横线选区
- App更名为"小脉健康",副标题"血管病患者的AI健康管理助手"
- 健康仪表盘及侧边栏移除体重指标,侧边栏背景改为蓝白渐变
- 健康档案按钮移入功能入口网格,与其他入口样式统一
- 记数据智能体配色改为绿色渐变(#A1FFCE→#69DB8F)
- 隐私协议/关于页从Text改为MarkdownBody正确渲染
- 运动计划/饮食记录卡片添加边框和阴影
- 服药次数从chip改为滚轮选择器
- 日期显示格式统一为空格分隔(1999 01 01)
- 健康档案页背景改为纯白
---
.../android/app/src/main/AndroidManifest.xml | 2 +-
health_app/lib/app.dart | 2 +-
health_app/lib/core/app_theme.dart | 349 ++++++++++++++++++
health_app/lib/pages/auth/login_page.dart | 9 +-
health_app/lib/pages/chart/trend_page.dart | 15 +-
.../doctor/doctor_followup_edit_page.dart | 12 +-
health_app/lib/pages/home/home_page.dart | 4 +-
.../home/widgets/chat_messages_view.dart | 17 +-
.../medication/medication_edit_page.dart | 40 +-
health_app/lib/pages/remaining_pages.dart | 59 +--
.../settings/notification_prefs_page.dart | 8 +-
.../lib/pages/settings/settings_pages.dart | 2 +-
health_app/lib/widgets/health_drawer.dart | 99 ++---
health_app/test/widget_test.dart | 2 +-
start-dev.bat | 11 +-
15 files changed, 466 insertions(+), 165 deletions(-)
diff --git a/health_app/android/app/src/main/AndroidManifest.xml b/health_app/android/app/src/main/AndroidManifest.xml
index c0f4685..f2db025 100644
--- a/health_app/android/app/src/main/AndroidManifest.xml
+++ b/health_app/android/app/src/main/AndroidManifest.xml
@@ -12,7 +12,7 @@
showAppDatePicker(
+ BuildContext context, {
+ required DateTime initialDate,
+ DateTime? firstDate,
+ DateTime? lastDate,
+}) {
+ final minDate = firstDate ?? DateTime(1900);
+ final maxDate = lastDate ?? DateTime(2100);
+ var y = initialDate.year.clamp(minDate.year, maxDate.year);
+ var m = initialDate.month;
+ var d = initialDate.day;
+ final yearCtrl = FixedExtentScrollController(initialItem: y - minDate.year);
+ final monthCtrl = FixedExtentScrollController(initialItem: m - 1);
+ final dayCtrl = FixedExtentScrollController(initialItem: d - 1);
+
+ int daysInMonth(int year, int month) =>
+ DateTime(year, month + 1, 0).day;
+
+ void clampDay() {
+ final maxDay = daysInMonth(y, m);
+ if (d > maxDay) d = maxDay;
+ }
+
+ return showCupertinoModalPopup(
+ context: context,
+ builder: (ctx) => Container(
+ height: 320,
+ decoration: const BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ child: Column(
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ CupertinoButton(
+ padding: EdgeInsets.zero,
+ 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: () {
+ clampDay();
+ Navigator.pop(ctx, DateTime(y, m, d));
+ },
+ ),
+ ],
+ ),
+ ),
+ 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))],
+ ))),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+}
+
+Future showAppTimePicker(
+ BuildContext context, {
+ required TimeOfDay initialTime,
+}) {
+ var hour = initialTime.hour;
+ var minute = initialTime.minute;
+ final hourCtrl = FixedExtentScrollController(initialItem: hour);
+ final minuteCtrl = FixedExtentScrollController(initialItem: minute);
+
+ return showCupertinoModalPopup(
+ context: context,
+ builder: (ctx) => Container(
+ height: 320,
+ decoration: const BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ child: Column(
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ CupertinoButton(
+ padding: EdgeInsets.zero,
+ 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)),
+ ),
+ ],
+ ),
+ ),
+ Expanded(
+ 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))],
+ ))),
+ const Spacer(flex: 1),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+}
+
+Future showAppDateTimePicker(
+ BuildContext context, {
+ required DateTime initialDate,
+ DateTime? firstDate,
+ DateTime? lastDate,
+}) {
+ final minDate = firstDate ?? DateTime(1900);
+ final maxDate = lastDate ?? DateTime(2100);
+ var y = initialDate.year.clamp(minDate.year, maxDate.year);
+ var m = initialDate.month;
+ var d = initialDate.day;
+ var h = initialDate.hour;
+ var min = initialDate.minute;
+ final yearCtrl = FixedExtentScrollController(initialItem: y - minDate.year);
+ final monthCtrl = FixedExtentScrollController(initialItem: m - 1);
+ final dayCtrl = FixedExtentScrollController(initialItem: d - 1);
+ final hourCtrl = FixedExtentScrollController(initialItem: h);
+ final minuteCtrl = FixedExtentScrollController(initialItem: min);
+
+ int daysInMonth(int year, int month) =>
+ DateTime(year, month + 1, 0).day;
+
+ void clampDay() {
+ final maxDay = daysInMonth(y, m);
+ if (d > maxDay) d = maxDay;
+ }
+
+ return showCupertinoModalPopup(
+ context: context,
+ builder: (ctx) => Container(
+ height: 320,
+ decoration: const BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ child: Column(
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ CupertinoButton(
+ padding: EdgeInsets.zero,
+ 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: () {
+ clampDay();
+ Navigator.pop(ctx, DateTime(y, m, d, h, min));
+ },
+ ),
+ ],
+ ),
+ ),
+ 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))],
+ ))),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+}
+
+Future showAppCountPicker(
+ BuildContext context, {
+ required int initialValue,
+ int min = 1,
+ int max = 10,
+ String label = '',
+}) {
+ var selected = initialValue;
+ return showCupertinoModalPopup(
+ context: context,
+ builder: (ctx) => Container(
+ height: 320,
+ decoration: const BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ child: Column(
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ CupertinoButton(
+ padding: EdgeInsets.zero,
+ 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, 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)),
+ ],
+ )),
+ ),
+ ],
+ ),
+ ),
+ );
+}
+
class AppBackground extends StatelessWidget {
final Widget child;
final bool safeArea;
diff --git a/health_app/lib/pages/auth/login_page.dart b/health_app/lib/pages/auth/login_page.dart
index 302c776..b65c184 100644
--- a/health_app/lib/pages/auth/login_page.dart
+++ b/health_app/lib/pages/auth/login_page.dart
@@ -264,14 +264,19 @@ class _LoginPageState extends ConsumerState {
_BrandMark(),
const SizedBox(height: 16),
const Text(
- '健康管家',
+ '小脉健康',
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.w900,
color: AppColors.textPrimary,
),
),
- const SizedBox(height: 6),
+ const SizedBox(height: 4),
+ const Text(
+ '血管病患者的 AI 健康管理助手',
+ style: TextStyle(fontSize: 13, color: AppColors.textSecondary),
+ ),
+ const SizedBox(height: 8),
Text(
_isLogin ? '登录你的健康账户' : '创建新的健康账户',
style: const TextStyle(
diff --git a/health_app/lib/pages/chart/trend_page.dart b/health_app/lib/pages/chart/trend_page.dart
index ada2b85..e4971ae 100644
--- a/health_app/lib/pages/chart/trend_page.dart
+++ b/health_app/lib/pages/chart/trend_page.dart
@@ -26,7 +26,6 @@ class _TrendPageState extends ConsumerState {
{'key': 'heart_rate', 'label': '心率', 'color': Color(0xFFF59E0B)},
{'key': 'glucose', 'label': '血糖', 'color': Color(0xFF4F6EF7)},
{'key': 'spo2', 'label': '血氧', 'color': Color(0xFF20C997)},
- {'key': 'weight', 'label': '体重', 'color': Color(0xFF845EF7)},
];
static const _units = {
@@ -34,7 +33,6 @@ class _TrendPageState extends ConsumerState {
'heart_rate': 'bpm',
'glucose': 'mmol/L',
'spo2': '%',
- 'weight': 'kg',
};
static const _names = {
@@ -42,7 +40,6 @@ class _TrendPageState extends ConsumerState {
'heart_rate': '心率',
'glucose': '血糖',
'spo2': '血氧',
- 'weight': '体重',
};
String get _unit => _units[_selected] ?? '';
@@ -62,7 +59,7 @@ class _TrendPageState extends ConsumerState {
setState(() => _loading = true);
try {
final api = ref.read(apiClientProvider);
- final types = ['BloodPressure', 'HeartRate', 'Glucose', 'SpO2', 'Weight'];
+ final types = ['BloodPressure', 'HeartRate', 'Glucose', 'SpO2'];
final all =