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 =