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 = >[]; for (final t in types) { try { @@ -112,9 +109,7 @@ class _TrendPageState extends ConsumerState { ? 'HeartRate' : _selected == 'glucose' ? 'Glucose' - : _selected == 'spo2' - ? 'SpO2' - : 'Weight'; + : 'SpO2'; setState(() { _filtered = _allRecords.where((r) => r['type'] == typeName).toList(); _filtered.sort( @@ -204,9 +199,7 @@ class _TrendPageState extends ConsumerState { ? 'HeartRate' : _selected == 'glucose' ? 'Glucose' - : _selected == 'spo2' - ? 'SpO2' - : 'Weight', + : 'SpO2', 'source': 'Manual', 'recordedAt': DateTime.now().toUtc().toIso8601String(), 'unit': _unit, @@ -679,8 +672,6 @@ class _TrendPageState extends ConsumerState { return '🩸'; case 'spo2': return '🫁'; - case 'weight': - return '⚖️'; default: return '📊'; } diff --git a/health_app/lib/pages/doctor/doctor_followup_edit_page.dart b/health_app/lib/pages/doctor/doctor_followup_edit_page.dart index eab0980..acf4508 100644 --- a/health_app/lib/pages/doctor/doctor_followup_edit_page.dart +++ b/health_app/lib/pages/doctor/doctor_followup_edit_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/app_colors.dart'; +import '../../core/app_theme.dart'; import '../../core/navigation_provider.dart'; import '../../providers/auth_provider.dart'; @@ -177,8 +178,8 @@ class _DoctorFollowUpEditPageState Expanded( child: GestureDetector( onTap: () async { - final d = await showDatePicker( - context: context, + final d = await showAppDatePicker( + context, initialDate: _date, firstDate: DateTime.now(), lastDate: DateTime.now().add(const Duration(days: 365)), @@ -186,7 +187,7 @@ class _DoctorFollowUpEditPageState if (d != null) setState(() => _date = d); }, child: _pickerBox( - '${_date.year}-${_date.month.toString().padLeft(2, '0')}-${_date.day.toString().padLeft(2, '0')}', + '${_date.year} ${_date.month.toString().padLeft(2, '0')} ${_date.day.toString().padLeft(2, '0')}', ), ), ), @@ -194,10 +195,7 @@ class _DoctorFollowUpEditPageState Expanded( child: GestureDetector( onTap: () async { - final t = await showTimePicker( - context: context, - initialTime: _time, - ); + final t = await showAppTimePicker(context, initialTime: _time); if (t != null) setState(() => _time = t); }, child: _pickerBox( diff --git a/health_app/lib/pages/home/home_page.dart b/health_app/lib/pages/home/home_page.dart index 4846bc6..7508c35 100644 --- a/health_app/lib/pages/home/home_page.dart +++ b/health_app/lib/pages/home/home_page.dart @@ -155,7 +155,7 @@ class _HomePageState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - 'AI 健康管家', + '小脉健康', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, @@ -269,7 +269,7 @@ class _HomePageState extends ConsumerState { ActiveAgent.health => const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Color(0xFFFFAFBD), Color(0xFFC9FFBF)], + colors: [Color(0xFFA1FFCE), Color(0xFF69DB8F)], ), ActiveAgent.diet => const LinearGradient( begin: Alignment.topCenter, diff --git a/health_app/lib/pages/home/widgets/chat_messages_view.dart b/health_app/lib/pages/home/widgets/chat_messages_view.dart index eeb9d4d..e7f8985 100644 --- a/health_app/lib/pages/home/widgets/chat_messages_view.dart +++ b/health_app/lib/pages/home/widgets/chat_messages_view.dart @@ -89,7 +89,7 @@ class ChatMessagesView extends ConsumerWidget { ), const SizedBox(height: 16), Text( - '开始和 AI 健康管家对话吧', + '开始和小脉健康对话吧', style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 8), @@ -1181,7 +1181,7 @@ class ChatMessagesView extends ConsumerWidget { ), const SizedBox(width: 6), const Text( - '健康管家', + '小脉健康', style: TextStyle(fontSize: 15, color: AppColors.textHint), ), const SizedBox(width: 4), @@ -1343,11 +1343,11 @@ class ChatMessagesView extends ConsumerWidget { verticalGradient: true, ), ActiveAgent.health => _AgentColors( - gradient: [Color(0xFFFFAFBD), Color(0xFFC9FFBF)], - bg: const Color(0xFFF7FDEB), - border: const Color(0xFFEAF7BF), - iconBg: const Color(0xFFF9FCEB), - accent: const Color(0xFF8BD982), + gradient: [Color(0xFFA1FFCE), Color(0xFF69DB8F)], + bg: const Color(0xFFF4FEF2), + border: const Color(0xFFD4F0C8), + iconBg: const Color(0xFFF0FCEF), + accent: const Color(0xFF52B87A), verticalGradient: true, ), ActiveAgent.diet => _AgentColors( @@ -1404,7 +1404,7 @@ class ChatMessagesView extends ConsumerWidget { ), ActiveAgent.report => (LucideIcons.fileText, '报告分析', '上传体检报告,AI 辅助解读'), ActiveAgent.exercise => (LucideIcons.activity, '运动', '制定运动计划,打卡记录进度'), - _ => (Icons.forum_outlined, 'AI 助手', '您的智能健康管家'), + _ => (Icons.forum_outlined, 'AI 助手', '血管病患者的 AI 健康管理助手'), }; } @@ -1692,7 +1692,6 @@ class ChatMessagesView extends ConsumerWidget { decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(28), - border: Border.all(color: Color(0xFFE2E8F0), width: 2), boxShadow: AppColors.cardShadow, ), clipBehavior: Clip.antiAlias, diff --git a/health_app/lib/pages/medication/medication_edit_page.dart b/health_app/lib/pages/medication/medication_edit_page.dart index 93fab17..fc92e09 100644 --- a/health_app/lib/pages/medication/medication_edit_page.dart +++ b/health_app/lib/pages/medication/medication_edit_page.dart @@ -159,14 +159,22 @@ class _MedicationEditPageState extends ConsumerState { const SizedBox(height: 16), // 频率选择 _label('每日服药次数'), const SizedBox(height: 8), - Wrap( - spacing: 8, - children: List.generate( - 4, - (i) => _chip( - '${i + 1}次', - _timesPerDay == i + 1, - () => _updateTimes(i + 1), + GestureDetector( + onTap: () async { + final n = await showAppCountPicker(context, initialValue: _timesPerDay, min: 1, max: 4, label: ' 次'); + if (n != null) _updateTimes(n); + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppTheme.surface, + borderRadius: BorderRadius.circular(AppTheme.rMd), + border: Border.all(color: AppColors.border), + ), + child: Text( + '$_timesPerDay 次/天', + style: const TextStyle(fontSize: 18), ), ), ), @@ -180,8 +188,8 @@ class _MedicationEditPageState extends ConsumerState { _timesPerDay, (i) => GestureDetector( onTap: () async { - final t = await showTimePicker( - context: context, + final t = await showAppTimePicker( + context, initialTime: _times[i], ); if (t != null) setState(() => _times[i] = t); @@ -324,8 +332,8 @@ class _MedicationEditPageState extends ConsumerState { const SizedBox(height: 6), GestureDetector( onTap: () async { - final d = await showDatePicker( - context: context, + final d = await showAppDatePicker( + context, initialDate: val, firstDate: DateTime(2024), lastDate: DateTime(2030), @@ -341,7 +349,7 @@ class _MedicationEditPageState extends ConsumerState { border: Border.all(color: AppColors.border), ), child: Text( - '${val.month}/${val.day}', + '${val.year} ${val.month.toString().padLeft(2, '0')} ${val.day.toString().padLeft(2, '0')}', style: const TextStyle(fontSize: 18), ), ), @@ -359,8 +367,8 @@ class _MedicationEditPageState extends ConsumerState { const SizedBox(height: 6), GestureDetector( onTap: () async { - final d = await showDatePicker( - context: context, + final d = await showAppDatePicker( + context, initialDate: val ?? DateTime.now(), firstDate: DateTime(2024), lastDate: DateTime(2030), @@ -378,7 +386,7 @@ class _MedicationEditPageState extends ConsumerState { child: Row( children: [ Text( - val != null ? '${val.month}/${val.day}' : '不设置', + val != null ? '${val.year} ${val.month.toString().padLeft(2, '0')} ${val.day.toString().padLeft(2, '0')}' : '不设置', style: TextStyle( fontSize: 18, color: val != null ? null : AppTheme.textHint, diff --git a/health_app/lib/pages/remaining_pages.dart b/health_app/lib/pages/remaining_pages.dart index 25e0e07..019ad8f 100644 --- a/health_app/lib/pages/remaining_pages.dart +++ b/health_app/lib/pages/remaining_pages.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/app_colors.dart'; import '../core/app_theme.dart'; @@ -582,6 +583,8 @@ class DietRecordDetailPage extends ConsumerWidget { decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(AppTheme.rMd), + border: Border.all(color: const Color(0xFFE8ECF0)), + boxShadow: [AppTheme.shadowLight], ), child: Row( children: [ @@ -629,6 +632,8 @@ class DietRecordDetailPage extends ConsumerWidget { decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(AppTheme.rMd), + border: Border.all(color: const Color(0xFFE8ECF0)), + boxShadow: [AppTheme.shadowLight], ), child: Row( children: [ @@ -765,6 +770,8 @@ class _ExercisePlanPageState extends ConsumerState { decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(AppTheme.rMd), + border: Border.all(color: const Color(0xFFE8ECF0)), + boxShadow: [AppTheme.shadowLight], ), child: Row( children: [ @@ -998,8 +1005,8 @@ class _ExercisePlanCreatePageState const SizedBox(height: 6), GestureDetector( onTap: () async { - final d = await showDatePicker( - context: context, + final d = await showAppDatePicker( + context, initialDate: val, firstDate: DateTime(2024), lastDate: DateTime(2030), @@ -1014,7 +1021,7 @@ class _ExercisePlanCreatePageState borderRadius: BorderRadius.circular(12), ), child: Text( - '${val.year}-${val.month.toString().padLeft(2, '0')}-${val.day.toString().padLeft(2, '0')}', + '${val.year} ${val.month.toString().padLeft(2, '0')} ${val.day.toString().padLeft(2, '0')}', style: const TextStyle(fontSize: 19), ), ), @@ -1032,7 +1039,7 @@ class _ExercisePlanCreatePageState const SizedBox(height: 6), GestureDetector( onTap: () async { - final picked = await showTimePicker(context: context, initialTime: _reminderTime); + final picked = await showAppTimePicker(context, initialTime: _reminderTime); if (picked != null && mounted) setState(() => _reminderTime = picked); }, child: Container( @@ -1320,12 +1327,11 @@ class _HealthArchivePageState extends ConsumerState { Future _pickDate(void Function(String) onPicked) async { final now = DateTime.now(); - final d = await showDatePicker( - context: context, + final d = await showAppDatePicker( + context, initialDate: DateTime(now.year - 30), firstDate: DateTime(1900), lastDate: now, - locale: const Locale('zh'), ); if (d != null) { onPicked( @@ -1400,7 +1406,8 @@ class _HealthArchivePageState extends ConsumerState { @override Widget build(BuildContext context) { if (_loading) { - return GradientScaffold( + return Scaffold( + backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.white, elevation: 0, @@ -1417,7 +1424,8 @@ class _HealthArchivePageState extends ConsumerState { body: const Center(child: CircularProgressIndicator()), ); } - return GradientScaffold( + return Scaffold( + backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.white, elevation: 0, @@ -1718,7 +1726,7 @@ class _DateF extends StatelessWidget { child: Row( children: [ Text( - value.isEmpty ? '点击选择日期' : value, + value.isEmpty ? '点击选择日期' : value.replaceAll('-', ' '), style: TextStyle( fontSize: 16, color: value.isEmpty @@ -1769,7 +1777,7 @@ class _DateF2 extends StatelessWidget { child: Row( children: [ Text( - value.isEmpty ? (hint ?? '点击选择') : value, + value.isEmpty ? (hint ?? '点击选择') : value.replaceAll('-', ' '), style: TextStyle( fontSize: 13, color: value.isEmpty @@ -1833,9 +1841,9 @@ class _HealthCalendarPageState extends ConsumerState { Map>> _events = {}; Map? _selectedDayData; - static const _medBlue = Color(0xFF0000FF); - static const _exGreen = Color(0xFF00FF00); - static const _fupOrange = Color(0xFFFFA500); + static const _medBlue = Color(0xFF3B82F6); + static const _exGreen = Color(0xFF22C55E); + static const _fupOrange = Color(0xFFF59E0B); @override void initState() { @@ -2308,7 +2316,7 @@ class StaticTextPage extends ConsumerWidget { const StaticTextPage({super.key, required this.type}); @override Widget build(BuildContext context, WidgetRef ref) { - final titles = {'privacy': '隐私协议', 'terms': '服务协议', 'about': '关于健康管家'}; + final titles = {'privacy': '隐私协议', 'terms': '服务协议', 'about': '关于小脉健康'}; final contents = { 'privacy': '''## 隐私政策 @@ -2352,12 +2360,12 @@ class StaticTextPage extends ConsumerWidget { 如有任何关于隐私的问题,请联系: 邮箱:privacy@healthbutler.com 电话:400-xxx-xxxx''', - 'about': '''## 关于健康管家 + 'about': '''## 关于小脉健康 版本:v1.0.0 (Build 20260101) ### 产品介绍 -健康管家是一款面向心脏术后康复患者的私人 AI 健康管理应用。以对话为核心交互方式,患者可以通过自然语言记录健康数据、获取饮食运动建议、管理用药、解读检查报告。 +小脉健康是一款面向血管病患者的 AI 健康管理应用。以对话为核心交互方式,患者可以通过自然语言记录健康数据、获取饮食运动建议、管理用药、解读检查报告。 ### 核心功能 - AI 智能问诊:基于大语言模型的健康咨询服务 @@ -2377,7 +2385,7 @@ class StaticTextPage extends ConsumerWidget { - 客服热线:400-xxx-xxxx(工作日 9:00-18:00) ### 版权声明 -© 2025-2026 健康管家团队。保留所有权利。 +© 2025-2026 小脉健康团队。保留所有权利。 本软件受中华人民共和国著作权法保护。''', }; return GradientScaffold( @@ -2399,12 +2407,15 @@ class StaticTextPage extends ConsumerWidget { ), body: SingleChildScrollView( padding: const EdgeInsets.all(20), - child: Text( - contents[type] ?? '内容加载中...', - style: TextStyle( - fontSize: 17, - height: 1.8, - color: AppColors.textPrimary, + child: MarkdownBody( + data: contents[type] ?? '内容加载中...', + selectable: true, + styleSheet: MarkdownStyleSheet( + p: const TextStyle(fontSize: 17, height: 1.8, color: AppColors.textPrimary), + h1: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: AppColors.textPrimary, height: 1.5), + h2: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: AppColors.textPrimary, height: 1.5), + h3: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: AppColors.textPrimary, height: 1.5), + listBullet: const TextStyle(fontSize: 17, height: 1.8, color: AppColors.textPrimary), ), ), ), diff --git a/health_app/lib/pages/settings/notification_prefs_page.dart b/health_app/lib/pages/settings/notification_prefs_page.dart index c497c2f..6e3ba19 100644 --- a/health_app/lib/pages/settings/notification_prefs_page.dart +++ b/health_app/lib/pages/settings/notification_prefs_page.dart @@ -167,8 +167,8 @@ class NotificationPrefsPage extends ConsumerWidget { label: '开始', time: '22:00', onTap: () async { - final picked = await showTimePicker( - context: context, + final picked = await showAppTimePicker( + context, initialTime: const TimeOfDay(hour: 22, minute: 0), ); if (picked != null && context.mounted) { @@ -194,8 +194,8 @@ class NotificationPrefsPage extends ConsumerWidget { label: '结束', time: '08:00', onTap: () async { - final picked = await showTimePicker( - context: context, + final picked = await showAppTimePicker( + context, initialTime: const TimeOfDay(hour: 8, minute: 0), ); if (picked != null && context.mounted) { diff --git a/health_app/lib/pages/settings/settings_pages.dart b/health_app/lib/pages/settings/settings_pages.dart index 7a3e08e..dfac263 100644 --- a/health_app/lib/pages/settings/settings_pages.dart +++ b/health_app/lib/pages/settings/settings_pages.dart @@ -81,7 +81,7 @@ class SettingsPage extends ConsumerWidget { children: [ _SettingsTile( icon: LucideIcons.info, - title: '关于健康管家', + title: '关于小脉健康', colors: const [Color(0xFF60A5FA), Color(0xFF0891B2)], onTap: () => pushRoute(ref, 'staticText', params: {'type': 'about'}), diff --git a/health_app/lib/widgets/health_drawer.dart b/health_app/lib/widgets/health_drawer.dart index 868b426..b258d38 100644 --- a/health_app/lib/widgets/health_drawer.dart +++ b/health_app/lib/widgets/health_drawer.dart @@ -20,10 +20,10 @@ class HealthDrawer extends ConsumerWidget { child: DecoratedBox( decoration: const BoxDecoration( gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Color(0xFFE0C3FC), Color(0xFFFFFFFF), Color(0xFFDCEEFF)], - stops: [0.0, 0.48, 1.0], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFF87CEEB), Color(0xFFFFFFFF)], + stops: [0.0, 0.35], ), ), child: SafeArea( @@ -36,8 +36,6 @@ class HealthDrawer extends ConsumerWidget { children: [ _AccountHeader(user: auth.user, ref: ref), const SizedBox(height: 18), - _ArchiveAction(ref: ref), - const SizedBox(height: 18), _HealthDashboard(latestHealth: latestHealth, ref: ref), const SizedBox(height: 18), _NavigationSection(ref: ref), @@ -141,64 +139,6 @@ class _AccountHeader extends StatelessWidget { } } -class _ArchiveAction extends StatelessWidget { - final WidgetRef ref; - const _ArchiveAction({required this.ref}); - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: () => pushRoute(ref, 'healthArchive'), - borderRadius: BorderRadius.circular(24), - child: Container( - padding: const EdgeInsets.fromLTRB(16, 14, 14, 14), - decoration: BoxDecoration( - gradient: const LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [Color(0xFFE0C3FC), Color(0xFF8EC5FC)], - ), - borderRadius: BorderRadius.circular(24), - border: Border.all(color: Colors.white, width: 1.4), - ), - child: Row( - children: [ - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.82), - borderRadius: BorderRadius.circular(16), - ), - child: const Icon( - Icons.folder_shared_rounded, - color: Color(0xFF14B8A6), - size: 23, - ), - ), - const SizedBox(width: 12), - const Expanded( - child: Text( - '健康档案', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w900, - color: AppColors.textPrimary, - ), - ), - ), - const Icon( - Icons.arrow_forward_ios_rounded, - size: 16, - color: AppColors.textHint, - ), - ], - ), - ), - ); - } -} - class _HealthDashboard extends StatelessWidget { final AsyncValue> latestHealth; final WidgetRef ref; @@ -217,7 +157,7 @@ class _HealthDashboard extends StatelessWidget { backgroundGradient: const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Color(0xFF00F2FE), Color(0xFF4FACFE)], + colors: [Color(0xFF89F7FE), Color(0xFF66A6FF)], ), child: latestHealth.when( data: (data) => _DashboardMetrics(data: data, ref: ref), @@ -243,6 +183,7 @@ class _DashboardMetrics extends StatelessWidget { _MetricInfo( label: '血压', value: _bpText(data['BloodPressure']), + unit: 'mmHg', routeType: 'blood_pressure', ), _MetricInfo( @@ -263,12 +204,6 @@ class _DashboardMetrics extends StatelessWidget { unit: '%', routeType: 'spo2', ), - _MetricInfo( - label: '体重', - value: _metricVal(data['Weight']), - unit: 'kg', - routeType: 'weight', - ), ]; return Row( @@ -321,18 +256,18 @@ class _MetricTile extends StatelessWidget { onTap: onTap, borderRadius: BorderRadius.circular(18), child: Container( - height: 92, + height: 100, padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 10), decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.30), + color: Colors.white.withValues(alpha: 0.22), borderRadius: BorderRadius.circular(18), - border: Border.all(color: Colors.white.withValues(alpha: 0.50)), + border: Border.all(color: Colors.white.withValues(alpha: 0.40)), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( - height: 28, + height: 32, child: FittedBox( fit: BoxFit.scaleDown, child: Text( @@ -340,7 +275,7 @@ class _MetricTile extends StatelessWidget { maxLines: 1, textAlign: TextAlign.center, style: const TextStyle( - fontSize: 22, + fontSize: 26, fontWeight: FontWeight.w900, color: Colors.white, ), @@ -350,13 +285,13 @@ class _MetricTile extends StatelessWidget { if (info.unit != null) Text( info.unit!, - style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: Color(0xE0FFFFFF)), + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xE0FFFFFF)), ), const SizedBox(height: 2), Text( info.label, style: const TextStyle( - fontSize: 12, + fontSize: 14, fontWeight: FontWeight.w800, color: Colors.white, ), @@ -375,6 +310,12 @@ class _NavigationSection extends StatelessWidget { @override Widget build(BuildContext context) { final items = [ + _NavItem( + icon: Icons.folder_shared_rounded, + title: '档案', + route: 'healthArchive', + colors: const [Color(0xFF6EE7B7), Color(0xFF14B8A6)], + ), _NavItem( icon: Icons.description_rounded, title: '报告', @@ -445,6 +386,7 @@ class _NavTile extends StatelessWidget { const _NavTile({required this.item, required this.onTap}); String get _title => switch (item.route) { + 'healthArchive' => '健康档案', 'reports' => '报告管理', 'medications' => '用药管理', 'dietRecords' => '饮食记录', @@ -455,6 +397,7 @@ class _NavTile extends StatelessWidget { }; List get _colors => switch (item.route) { + 'healthArchive' => const [Color(0xFF34D399), Color(0xFF059669)], 'reports' => const [Color(0xFF60A5FA), Color(0xFF2563EB)], 'medications' => const [Color(0xFFA78BFA), Color(0xFF7C3AED)], 'dietRecords' => const [Color(0xFFF6D365), Color(0xFFF97316)], diff --git a/health_app/test/widget_test.dart b/health_app/test/widget_test.dart index a0e7886..f4d2d61 100644 --- a/health_app/test/widget_test.dart +++ b/health_app/test/widget_test.dart @@ -17,7 +17,7 @@ void main() { await tester.pumpWidget(const ProviderScope(child: MaterialApp(home: LoginPage()))); await tester.pumpAndSettle(); - expect(find.text('健康管家'), findsOneWidget); + expect(find.text('小脉健康'), findsOneWidget); expect(find.text('登 录'), findsOneWidget); }); diff --git a/start-dev.bat b/start-dev.bat index ff109c0..ff23274 100644 --- a/start-dev.bat +++ b/start-dev.bat @@ -14,16 +14,13 @@ echo [1/2] Starting PostgreSQL... if errorlevel 1 (echo PG already running) else (echo PG started) echo. -echo [2/3] Starting .NET Backend... +echo [2/2] Starting .NET Backend... start "Backend" cmd /k "cd /d D:\health_project\backend\src\Health.WebApi && dotnet run" echo Waiting for backend... timeout /t 8 /nobreak >nul echo. -echo [3/3] Setup phone connection... -adb reverse tcp:5000 tcp:5000 2>nul -if errorlevel 1 (echo USB not connected, skip) else (echo Phone -> PC forwarding OK) -echo. - -echo All done. Open the app on your phone. +echo All done. Phone connects via WiFi to this PC. +echo If your PC IP changed, rebuild app with: +echo flutter run -d ^ --dart-define=API_BASE_URL=http://^:5000 pause