feat: Cupertino滚轮选择器 + 品牌更名 + UI全面优化

- 日期/时间/次数选择器统一改为底部弹出Cupertino滚轮,无单位标签,双横线选区
- App更名为"小脉健康",副标题"血管病患者的AI健康管理助手"
- 健康仪表盘及侧边栏移除体重指标,侧边栏背景改为蓝白渐变
- 健康档案按钮移入功能入口网格,与其他入口样式统一
- 记数据智能体配色改为绿色渐变(#A1FFCE→#69DB8F)
- 隐私协议/关于页从Text改为MarkdownBody正确渲染
- 运动计划/饮食记录卡片添加边框和阴影
- 服药次数从chip改为滚轮选择器
- 日期显示格式统一为空格分隔(1999 01 01)
- 健康档案页背景改为纯白
This commit is contained in:
MingNian
2026-06-22 13:59:37 +08:00
parent a479e5e95d
commit 431b72d49a
15 changed files with 466 additions and 165 deletions

View File

@@ -264,14 +264,19 @@ class _LoginPageState extends ConsumerState<LoginPage> {
_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(

View File

@@ -26,7 +26,6 @@ class _TrendPageState extends ConsumerState<TrendPage> {
{'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<TrendPage> {
'heart_rate': 'bpm',
'glucose': 'mmol/L',
'spo2': '%',
'weight': 'kg',
};
static const _names = {
@@ -42,7 +40,6 @@ class _TrendPageState extends ConsumerState<TrendPage> {
'heart_rate': '心率',
'glucose': '血糖',
'spo2': '血氧',
'weight': '体重',
};
String get _unit => _units[_selected] ?? '';
@@ -62,7 +59,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
setState(() => _loading = true);
try {
final api = ref.read(apiClientProvider);
final types = ['BloodPressure', 'HeartRate', 'Glucose', 'SpO2', 'Weight'];
final types = ['BloodPressure', 'HeartRate', 'Glucose', 'SpO2'];
final all = <Map<String, dynamic>>[];
for (final t in types) {
try {
@@ -112,9 +109,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
? '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<TrendPage> {
? '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<TrendPage> {
return '🩸';
case 'spo2':
return '🫁';
case 'weight':
return '⚖️';
default:
return '📊';
}

View File

@@ -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(

View File

@@ -155,7 +155,7 @@ class _HomePageState extends ConsumerState<HomePage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'AI 健康管家',
'小脉健康',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
@@ -269,7 +269,7 @@ class _HomePageState extends ConsumerState<HomePage> {
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,

View File

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

View File

@@ -159,14 +159,22 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
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<MedicationEditPage> {
_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<MedicationEditPage> {
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<MedicationEditPage> {
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<MedicationEditPage> {
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<MedicationEditPage> {
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,

View File

@@ -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<ExercisePlanPage> {
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<HealthArchivePage> {
Future<void> _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<HealthArchivePage> {
@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<HealthArchivePage> {
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<HealthCalendarPage> {
Map<String, List<Map<String, dynamic>>> _events = {};
Map<String, dynamic>? _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),
),
),
),

View File

@@ -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) {

View File

@@ -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'}),