feat: Cupertino滚轮选择器 + 品牌更名 + UI全面优化
- 日期/时间/次数选择器统一改为底部弹出Cupertino滚轮,无单位标签,双横线选区 - App更名为"小脉健康",副标题"血管病患者的AI健康管理助手" - 健康仪表盘及侧边栏移除体重指标,侧边栏背景改为蓝白渐变 - 健康档案按钮移入功能入口网格,与其他入口样式统一 - 记数据智能体配色改为绿色渐变(#A1FFCE→#69DB8F) - 隐私协议/关于页从Text改为MarkdownBody正确渲染 - 运动计划/饮食记录卡片添加边框和阴影 - 服药次数从chip改为滚轮选择器 - 日期显示格式统一为空格分隔(1999 01 01) - 健康档案页背景改为纯白
This commit is contained in:
@@ -12,7 +12,7 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
|
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
|
||||||
<application
|
<application
|
||||||
android:label="health_app"
|
android:label="小脉健康"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class HealthApp extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: '健康管家',
|
title: '小脉健康',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: AppTheme.lightTheme,
|
theme: AppTheme.lightTheme,
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [
|
||||||
|
|||||||
@@ -1,7 +1,356 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
import 'app_colors.dart';
|
import 'app_colors.dart';
|
||||||
|
|
||||||
|
const _pickerItemStyle = TextStyle(fontSize: 22, fontWeight: FontWeight.w500);
|
||||||
|
const _lineColor = Color(0xFFCBD5E1);
|
||||||
|
|
||||||
|
Widget _wrapPicker(Widget picker) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
picker,
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(height: 1, color: _lineColor),
|
||||||
|
const SizedBox(height: 38),
|
||||||
|
Container(height: 1, color: _lineColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DateTime?> 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<DateTime>(
|
||||||
|
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<TimeOfDay?> 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<TimeOfDay>(
|
||||||
|
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<DateTime?> 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<DateTime>(
|
||||||
|
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<int?> showAppCountPicker(
|
||||||
|
BuildContext context, {
|
||||||
|
required int initialValue,
|
||||||
|
int min = 1,
|
||||||
|
int max = 10,
|
||||||
|
String label = '',
|
||||||
|
}) {
|
||||||
|
var selected = initialValue;
|
||||||
|
return showCupertinoModalPopup<int>(
|
||||||
|
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 {
|
class AppBackground extends StatelessWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final bool safeArea;
|
final bool safeArea;
|
||||||
|
|||||||
@@ -264,14 +264,19 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
_BrandMark(),
|
_BrandMark(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
const Text(
|
||||||
'健康管家',
|
'小脉健康',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 26,
|
fontSize: 26,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
color: AppColors.textPrimary,
|
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(
|
Text(
|
||||||
_isLogin ? '登录你的健康账户' : '创建新的健康账户',
|
_isLogin ? '登录你的健康账户' : '创建新的健康账户',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
|||||||
{'key': 'heart_rate', 'label': '心率', 'color': Color(0xFFF59E0B)},
|
{'key': 'heart_rate', 'label': '心率', 'color': Color(0xFFF59E0B)},
|
||||||
{'key': 'glucose', 'label': '血糖', 'color': Color(0xFF4F6EF7)},
|
{'key': 'glucose', 'label': '血糖', 'color': Color(0xFF4F6EF7)},
|
||||||
{'key': 'spo2', 'label': '血氧', 'color': Color(0xFF20C997)},
|
{'key': 'spo2', 'label': '血氧', 'color': Color(0xFF20C997)},
|
||||||
{'key': 'weight', 'label': '体重', 'color': Color(0xFF845EF7)},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
static const _units = {
|
static const _units = {
|
||||||
@@ -34,7 +33,6 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
|||||||
'heart_rate': 'bpm',
|
'heart_rate': 'bpm',
|
||||||
'glucose': 'mmol/L',
|
'glucose': 'mmol/L',
|
||||||
'spo2': '%',
|
'spo2': '%',
|
||||||
'weight': 'kg',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static const _names = {
|
static const _names = {
|
||||||
@@ -42,7 +40,6 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
|||||||
'heart_rate': '心率',
|
'heart_rate': '心率',
|
||||||
'glucose': '血糖',
|
'glucose': '血糖',
|
||||||
'spo2': '血氧',
|
'spo2': '血氧',
|
||||||
'weight': '体重',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
String get _unit => _units[_selected] ?? '';
|
String get _unit => _units[_selected] ?? '';
|
||||||
@@ -62,7 +59,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
|||||||
setState(() => _loading = true);
|
setState(() => _loading = true);
|
||||||
try {
|
try {
|
||||||
final api = ref.read(apiClientProvider);
|
final api = ref.read(apiClientProvider);
|
||||||
final types = ['BloodPressure', 'HeartRate', 'Glucose', 'SpO2', 'Weight'];
|
final types = ['BloodPressure', 'HeartRate', 'Glucose', 'SpO2'];
|
||||||
final all = <Map<String, dynamic>>[];
|
final all = <Map<String, dynamic>>[];
|
||||||
for (final t in types) {
|
for (final t in types) {
|
||||||
try {
|
try {
|
||||||
@@ -112,9 +109,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
|||||||
? 'HeartRate'
|
? 'HeartRate'
|
||||||
: _selected == 'glucose'
|
: _selected == 'glucose'
|
||||||
? 'Glucose'
|
? 'Glucose'
|
||||||
: _selected == 'spo2'
|
: 'SpO2';
|
||||||
? 'SpO2'
|
|
||||||
: 'Weight';
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_filtered = _allRecords.where((r) => r['type'] == typeName).toList();
|
_filtered = _allRecords.where((r) => r['type'] == typeName).toList();
|
||||||
_filtered.sort(
|
_filtered.sort(
|
||||||
@@ -204,9 +199,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
|||||||
? 'HeartRate'
|
? 'HeartRate'
|
||||||
: _selected == 'glucose'
|
: _selected == 'glucose'
|
||||||
? 'Glucose'
|
? 'Glucose'
|
||||||
: _selected == 'spo2'
|
: 'SpO2',
|
||||||
? 'SpO2'
|
|
||||||
: 'Weight',
|
|
||||||
'source': 'Manual',
|
'source': 'Manual',
|
||||||
'recordedAt': DateTime.now().toUtc().toIso8601String(),
|
'recordedAt': DateTime.now().toUtc().toIso8601String(),
|
||||||
'unit': _unit,
|
'unit': _unit,
|
||||||
@@ -679,8 +672,6 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
|||||||
return '🩸';
|
return '🩸';
|
||||||
case 'spo2':
|
case 'spo2':
|
||||||
return '🫁';
|
return '🫁';
|
||||||
case 'weight':
|
|
||||||
return '⚖️';
|
|
||||||
default:
|
default:
|
||||||
return '📊';
|
return '📊';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../core/app_colors.dart';
|
import '../../core/app_colors.dart';
|
||||||
|
import '../../core/app_theme.dart';
|
||||||
import '../../core/navigation_provider.dart';
|
import '../../core/navigation_provider.dart';
|
||||||
import '../../providers/auth_provider.dart';
|
import '../../providers/auth_provider.dart';
|
||||||
|
|
||||||
@@ -177,8 +178,8 @@ class _DoctorFollowUpEditPageState
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final d = await showDatePicker(
|
final d = await showAppDatePicker(
|
||||||
context: context,
|
context,
|
||||||
initialDate: _date,
|
initialDate: _date,
|
||||||
firstDate: DateTime.now(),
|
firstDate: DateTime.now(),
|
||||||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||||
@@ -186,7 +187,7 @@ class _DoctorFollowUpEditPageState
|
|||||||
if (d != null) setState(() => _date = d);
|
if (d != null) setState(() => _date = d);
|
||||||
},
|
},
|
||||||
child: _pickerBox(
|
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(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final t = await showTimePicker(
|
final t = await showAppTimePicker(context, initialTime: _time);
|
||||||
context: context,
|
|
||||||
initialTime: _time,
|
|
||||||
);
|
|
||||||
if (t != null) setState(() => _time = t);
|
if (t != null) setState(() => _time = t);
|
||||||
},
|
},
|
||||||
child: _pickerBox(
|
child: _pickerBox(
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ class _HomePageState extends ConsumerState<HomePage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'AI 健康管家',
|
'小脉健康',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -269,7 +269,7 @@ class _HomePageState extends ConsumerState<HomePage> {
|
|||||||
ActiveAgent.health => const LinearGradient(
|
ActiveAgent.health => const LinearGradient(
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [Color(0xFFFFAFBD), Color(0xFFC9FFBF)],
|
colors: [Color(0xFFA1FFCE), Color(0xFF69DB8F)],
|
||||||
),
|
),
|
||||||
ActiveAgent.diet => const LinearGradient(
|
ActiveAgent.diet => const LinearGradient(
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class ChatMessagesView extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'开始和 AI 健康管家对话吧',
|
'开始和小脉健康对话吧',
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -1181,7 +1181,7 @@ class ChatMessagesView extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
const Text(
|
const Text(
|
||||||
'健康管家',
|
'小脉健康',
|
||||||
style: TextStyle(fontSize: 15, color: AppColors.textHint),
|
style: TextStyle(fontSize: 15, color: AppColors.textHint),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
@@ -1343,11 +1343,11 @@ class ChatMessagesView extends ConsumerWidget {
|
|||||||
verticalGradient: true,
|
verticalGradient: true,
|
||||||
),
|
),
|
||||||
ActiveAgent.health => _AgentColors(
|
ActiveAgent.health => _AgentColors(
|
||||||
gradient: [Color(0xFFFFAFBD), Color(0xFFC9FFBF)],
|
gradient: [Color(0xFFA1FFCE), Color(0xFF69DB8F)],
|
||||||
bg: const Color(0xFFF7FDEB),
|
bg: const Color(0xFFF4FEF2),
|
||||||
border: const Color(0xFFEAF7BF),
|
border: const Color(0xFFD4F0C8),
|
||||||
iconBg: const Color(0xFFF9FCEB),
|
iconBg: const Color(0xFFF0FCEF),
|
||||||
accent: const Color(0xFF8BD982),
|
accent: const Color(0xFF52B87A),
|
||||||
verticalGradient: true,
|
verticalGradient: true,
|
||||||
),
|
),
|
||||||
ActiveAgent.diet => _AgentColors(
|
ActiveAgent.diet => _AgentColors(
|
||||||
@@ -1404,7 +1404,7 @@ class ChatMessagesView extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
ActiveAgent.report => (LucideIcons.fileText, '报告分析', '上传体检报告,AI 辅助解读'),
|
ActiveAgent.report => (LucideIcons.fileText, '报告分析', '上传体检报告,AI 辅助解读'),
|
||||||
ActiveAgent.exercise => (LucideIcons.activity, '运动', '制定运动计划,打卡记录进度'),
|
ActiveAgent.exercise => (LucideIcons.activity, '运动', '制定运动计划,打卡记录进度'),
|
||||||
_ => (Icons.forum_outlined, 'AI 助手', '您的智能健康管家'),
|
_ => (Icons.forum_outlined, 'AI 助手', '血管病患者的 AI 健康管理助手'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1692,7 +1692,6 @@ class ChatMessagesView extends ConsumerWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(28),
|
borderRadius: BorderRadius.circular(28),
|
||||||
border: Border.all(color: Color(0xFFE2E8F0), width: 2),
|
|
||||||
boxShadow: AppColors.cardShadow,
|
boxShadow: AppColors.cardShadow,
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
|
|||||||
@@ -159,14 +159,22 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// 频率选择
|
// 频率选择
|
||||||
_label('每日服药次数'), const SizedBox(height: 8),
|
_label('每日服药次数'), const SizedBox(height: 8),
|
||||||
Wrap(
|
GestureDetector(
|
||||||
spacing: 8,
|
onTap: () async {
|
||||||
children: List.generate(
|
final n = await showAppCountPicker(context, initialValue: _timesPerDay, min: 1, max: 4, label: ' 次');
|
||||||
4,
|
if (n != null) _updateTimes(n);
|
||||||
(i) => _chip(
|
},
|
||||||
'${i + 1}次',
|
child: Container(
|
||||||
_timesPerDay == i + 1,
|
width: double.infinity,
|
||||||
() => _updateTimes(i + 1),
|
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,
|
_timesPerDay,
|
||||||
(i) => GestureDetector(
|
(i) => GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final t = await showTimePicker(
|
final t = await showAppTimePicker(
|
||||||
context: context,
|
context,
|
||||||
initialTime: _times[i],
|
initialTime: _times[i],
|
||||||
);
|
);
|
||||||
if (t != null) setState(() => _times[i] = t);
|
if (t != null) setState(() => _times[i] = t);
|
||||||
@@ -324,8 +332,8 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
|||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final d = await showDatePicker(
|
final d = await showAppDatePicker(
|
||||||
context: context,
|
context,
|
||||||
initialDate: val,
|
initialDate: val,
|
||||||
firstDate: DateTime(2024),
|
firstDate: DateTime(2024),
|
||||||
lastDate: DateTime(2030),
|
lastDate: DateTime(2030),
|
||||||
@@ -341,7 +349,7 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
|||||||
border: Border.all(color: AppColors.border),
|
border: Border.all(color: AppColors.border),
|
||||||
),
|
),
|
||||||
child: Text(
|
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),
|
style: const TextStyle(fontSize: 18),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -359,8 +367,8 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
|||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final d = await showDatePicker(
|
final d = await showAppDatePicker(
|
||||||
context: context,
|
context,
|
||||||
initialDate: val ?? DateTime.now(),
|
initialDate: val ?? DateTime.now(),
|
||||||
firstDate: DateTime(2024),
|
firstDate: DateTime(2024),
|
||||||
lastDate: DateTime(2030),
|
lastDate: DateTime(2030),
|
||||||
@@ -378,7 +386,7 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
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(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
color: val != null ? null : AppTheme.textHint,
|
color: val != null ? null : AppTheme.textHint,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../core/app_colors.dart';
|
import '../core/app_colors.dart';
|
||||||
import '../core/app_theme.dart';
|
import '../core/app_theme.dart';
|
||||||
@@ -582,6 +583,8 @@ class DietRecordDetailPage extends ConsumerWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||||
|
border: Border.all(color: const Color(0xFFE8ECF0)),
|
||||||
|
boxShadow: [AppTheme.shadowLight],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -629,6 +632,8 @@ class DietRecordDetailPage extends ConsumerWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||||
|
border: Border.all(color: const Color(0xFFE8ECF0)),
|
||||||
|
boxShadow: [AppTheme.shadowLight],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -765,6 +770,8 @@ class _ExercisePlanPageState extends ConsumerState<ExercisePlanPage> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||||
|
border: Border.all(color: const Color(0xFFE8ECF0)),
|
||||||
|
boxShadow: [AppTheme.shadowLight],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -998,8 +1005,8 @@ class _ExercisePlanCreatePageState
|
|||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final d = await showDatePicker(
|
final d = await showAppDatePicker(
|
||||||
context: context,
|
context,
|
||||||
initialDate: val,
|
initialDate: val,
|
||||||
firstDate: DateTime(2024),
|
firstDate: DateTime(2024),
|
||||||
lastDate: DateTime(2030),
|
lastDate: DateTime(2030),
|
||||||
@@ -1014,7 +1021,7 @@ class _ExercisePlanCreatePageState
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Text(
|
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),
|
style: const TextStyle(fontSize: 19),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1032,7 +1039,7 @@ class _ExercisePlanCreatePageState
|
|||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final picked = await showTimePicker(context: context, initialTime: _reminderTime);
|
final picked = await showAppTimePicker(context, initialTime: _reminderTime);
|
||||||
if (picked != null && mounted) setState(() => _reminderTime = picked);
|
if (picked != null && mounted) setState(() => _reminderTime = picked);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -1320,12 +1327,11 @@ class _HealthArchivePageState extends ConsumerState<HealthArchivePage> {
|
|||||||
|
|
||||||
Future<void> _pickDate(void Function(String) onPicked) async {
|
Future<void> _pickDate(void Function(String) onPicked) async {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final d = await showDatePicker(
|
final d = await showAppDatePicker(
|
||||||
context: context,
|
context,
|
||||||
initialDate: DateTime(now.year - 30),
|
initialDate: DateTime(now.year - 30),
|
||||||
firstDate: DateTime(1900),
|
firstDate: DateTime(1900),
|
||||||
lastDate: now,
|
lastDate: now,
|
||||||
locale: const Locale('zh'),
|
|
||||||
);
|
);
|
||||||
if (d != null) {
|
if (d != null) {
|
||||||
onPicked(
|
onPicked(
|
||||||
@@ -1400,7 +1406,8 @@ class _HealthArchivePageState extends ConsumerState<HealthArchivePage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_loading) {
|
if (_loading) {
|
||||||
return GradientScaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
@@ -1417,7 +1424,8 @@ class _HealthArchivePageState extends ConsumerState<HealthArchivePage> {
|
|||||||
body: const Center(child: CircularProgressIndicator()),
|
body: const Center(child: CircularProgressIndicator()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return GradientScaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
@@ -1718,7 +1726,7 @@ class _DateF extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
value.isEmpty ? '点击选择日期' : value,
|
value.isEmpty ? '点击选择日期' : value.replaceAll('-', ' '),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: value.isEmpty
|
color: value.isEmpty
|
||||||
@@ -1769,7 +1777,7 @@ class _DateF2 extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
value.isEmpty ? (hint ?? '点击选择') : value,
|
value.isEmpty ? (hint ?? '点击选择') : value.replaceAll('-', ' '),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: value.isEmpty
|
color: value.isEmpty
|
||||||
@@ -1833,9 +1841,9 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
|
|||||||
Map<String, List<Map<String, dynamic>>> _events = {};
|
Map<String, List<Map<String, dynamic>>> _events = {};
|
||||||
Map<String, dynamic>? _selectedDayData;
|
Map<String, dynamic>? _selectedDayData;
|
||||||
|
|
||||||
static const _medBlue = Color(0xFF0000FF);
|
static const _medBlue = Color(0xFF3B82F6);
|
||||||
static const _exGreen = Color(0xFF00FF00);
|
static const _exGreen = Color(0xFF22C55E);
|
||||||
static const _fupOrange = Color(0xFFFFA500);
|
static const _fupOrange = Color(0xFFF59E0B);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -2308,7 +2316,7 @@ class StaticTextPage extends ConsumerWidget {
|
|||||||
const StaticTextPage({super.key, required this.type});
|
const StaticTextPage({super.key, required this.type});
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final titles = {'privacy': '隐私协议', 'terms': '服务协议', 'about': '关于健康管家'};
|
final titles = {'privacy': '隐私协议', 'terms': '服务协议', 'about': '关于小脉健康'};
|
||||||
final contents = {
|
final contents = {
|
||||||
'privacy': '''## 隐私政策
|
'privacy': '''## 隐私政策
|
||||||
|
|
||||||
@@ -2352,12 +2360,12 @@ class StaticTextPage extends ConsumerWidget {
|
|||||||
如有任何关于隐私的问题,请联系:
|
如有任何关于隐私的问题,请联系:
|
||||||
邮箱:privacy@healthbutler.com
|
邮箱:privacy@healthbutler.com
|
||||||
电话:400-xxx-xxxx''',
|
电话:400-xxx-xxxx''',
|
||||||
'about': '''## 关于健康管家
|
'about': '''## 关于小脉健康
|
||||||
|
|
||||||
版本:v1.0.0 (Build 20260101)
|
版本:v1.0.0 (Build 20260101)
|
||||||
|
|
||||||
### 产品介绍
|
### 产品介绍
|
||||||
健康管家是一款面向心脏术后康复患者的私人 AI 健康管理应用。以对话为核心交互方式,患者可以通过自然语言记录健康数据、获取饮食运动建议、管理用药、解读检查报告。
|
小脉健康是一款面向血管病患者的 AI 健康管理应用。以对话为核心交互方式,患者可以通过自然语言记录健康数据、获取饮食运动建议、管理用药、解读检查报告。
|
||||||
|
|
||||||
### 核心功能
|
### 核心功能
|
||||||
- AI 智能问诊:基于大语言模型的健康咨询服务
|
- AI 智能问诊:基于大语言模型的健康咨询服务
|
||||||
@@ -2377,7 +2385,7 @@ class StaticTextPage extends ConsumerWidget {
|
|||||||
- 客服热线:400-xxx-xxxx(工作日 9:00-18:00)
|
- 客服热线:400-xxx-xxxx(工作日 9:00-18:00)
|
||||||
|
|
||||||
### 版权声明
|
### 版权声明
|
||||||
© 2025-2026 健康管家团队。保留所有权利。
|
© 2025-2026 小脉健康团队。保留所有权利。
|
||||||
本软件受中华人民共和国著作权法保护。''',
|
本软件受中华人民共和国著作权法保护。''',
|
||||||
};
|
};
|
||||||
return GradientScaffold(
|
return GradientScaffold(
|
||||||
@@ -2399,12 +2407,15 @@ class StaticTextPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Text(
|
child: MarkdownBody(
|
||||||
contents[type] ?? '内容加载中...',
|
data: contents[type] ?? '内容加载中...',
|
||||||
style: TextStyle(
|
selectable: true,
|
||||||
fontSize: 17,
|
styleSheet: MarkdownStyleSheet(
|
||||||
height: 1.8,
|
p: const TextStyle(fontSize: 17, height: 1.8, color: AppColors.textPrimary),
|
||||||
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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -167,8 +167,8 @@ class NotificationPrefsPage extends ConsumerWidget {
|
|||||||
label: '开始',
|
label: '开始',
|
||||||
time: '22:00',
|
time: '22:00',
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final picked = await showTimePicker(
|
final picked = await showAppTimePicker(
|
||||||
context: context,
|
context,
|
||||||
initialTime: const TimeOfDay(hour: 22, minute: 0),
|
initialTime: const TimeOfDay(hour: 22, minute: 0),
|
||||||
);
|
);
|
||||||
if (picked != null && context.mounted) {
|
if (picked != null && context.mounted) {
|
||||||
@@ -194,8 +194,8 @@ class NotificationPrefsPage extends ConsumerWidget {
|
|||||||
label: '结束',
|
label: '结束',
|
||||||
time: '08:00',
|
time: '08:00',
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final picked = await showTimePicker(
|
final picked = await showAppTimePicker(
|
||||||
context: context,
|
context,
|
||||||
initialTime: const TimeOfDay(hour: 8, minute: 0),
|
initialTime: const TimeOfDay(hour: 8, minute: 0),
|
||||||
);
|
);
|
||||||
if (picked != null && context.mounted) {
|
if (picked != null && context.mounted) {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
_SettingsTile(
|
_SettingsTile(
|
||||||
icon: LucideIcons.info,
|
icon: LucideIcons.info,
|
||||||
title: '关于健康管家',
|
title: '关于小脉健康',
|
||||||
colors: const [Color(0xFF60A5FA), Color(0xFF0891B2)],
|
colors: const [Color(0xFF60A5FA), Color(0xFF0891B2)],
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
pushRoute(ref, 'staticText', params: {'type': 'about'}),
|
pushRoute(ref, 'staticText', params: {'type': 'about'}),
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ class HealthDrawer extends ConsumerWidget {
|
|||||||
child: DecoratedBox(
|
child: DecoratedBox(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomCenter,
|
||||||
colors: [Color(0xFFE0C3FC), Color(0xFFFFFFFF), Color(0xFFDCEEFF)],
|
colors: [Color(0xFF87CEEB), Color(0xFFFFFFFF)],
|
||||||
stops: [0.0, 0.48, 1.0],
|
stops: [0.0, 0.35],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
@@ -36,8 +36,6 @@ class HealthDrawer extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
_AccountHeader(user: auth.user, ref: ref),
|
_AccountHeader(user: auth.user, ref: ref),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
_ArchiveAction(ref: ref),
|
|
||||||
const SizedBox(height: 18),
|
|
||||||
_HealthDashboard(latestHealth: latestHealth, ref: ref),
|
_HealthDashboard(latestHealth: latestHealth, ref: ref),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
_NavigationSection(ref: ref),
|
_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 {
|
class _HealthDashboard extends StatelessWidget {
|
||||||
final AsyncValue<Map<String, dynamic>> latestHealth;
|
final AsyncValue<Map<String, dynamic>> latestHealth;
|
||||||
final WidgetRef ref;
|
final WidgetRef ref;
|
||||||
@@ -217,7 +157,7 @@ class _HealthDashboard extends StatelessWidget {
|
|||||||
backgroundGradient: const LinearGradient(
|
backgroundGradient: const LinearGradient(
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [Color(0xFF00F2FE), Color(0xFF4FACFE)],
|
colors: [Color(0xFF89F7FE), Color(0xFF66A6FF)],
|
||||||
),
|
),
|
||||||
child: latestHealth.when(
|
child: latestHealth.when(
|
||||||
data: (data) => _DashboardMetrics(data: data, ref: ref),
|
data: (data) => _DashboardMetrics(data: data, ref: ref),
|
||||||
@@ -243,6 +183,7 @@ class _DashboardMetrics extends StatelessWidget {
|
|||||||
_MetricInfo(
|
_MetricInfo(
|
||||||
label: '血压',
|
label: '血压',
|
||||||
value: _bpText(data['BloodPressure']),
|
value: _bpText(data['BloodPressure']),
|
||||||
|
unit: 'mmHg',
|
||||||
routeType: 'blood_pressure',
|
routeType: 'blood_pressure',
|
||||||
),
|
),
|
||||||
_MetricInfo(
|
_MetricInfo(
|
||||||
@@ -263,12 +204,6 @@ class _DashboardMetrics extends StatelessWidget {
|
|||||||
unit: '%',
|
unit: '%',
|
||||||
routeType: 'spo2',
|
routeType: 'spo2',
|
||||||
),
|
),
|
||||||
_MetricInfo(
|
|
||||||
label: '体重',
|
|
||||||
value: _metricVal(data['Weight']),
|
|
||||||
unit: 'kg',
|
|
||||||
routeType: 'weight',
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
@@ -321,18 +256,18 @@ class _MetricTile extends StatelessWidget {
|
|||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
borderRadius: BorderRadius.circular(18),
|
borderRadius: BorderRadius.circular(18),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 92,
|
height: 100,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withValues(alpha: 0.30),
|
color: Colors.white.withValues(alpha: 0.22),
|
||||||
borderRadius: BorderRadius.circular(18),
|
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(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 28,
|
height: 32,
|
||||||
child: FittedBox(
|
child: FittedBox(
|
||||||
fit: BoxFit.scaleDown,
|
fit: BoxFit.scaleDown,
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -340,7 +275,7 @@ class _MetricTile extends StatelessWidget {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 22,
|
fontSize: 26,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
@@ -350,13 +285,13 @@ class _MetricTile extends StatelessWidget {
|
|||||||
if (info.unit != null)
|
if (info.unit != null)
|
||||||
Text(
|
Text(
|
||||||
info.unit!,
|
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),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
info.label,
|
info.label,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
@@ -375,6 +310,12 @@ class _NavigationSection extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final items = [
|
final items = [
|
||||||
|
_NavItem(
|
||||||
|
icon: Icons.folder_shared_rounded,
|
||||||
|
title: '档案',
|
||||||
|
route: 'healthArchive',
|
||||||
|
colors: const [Color(0xFF6EE7B7), Color(0xFF14B8A6)],
|
||||||
|
),
|
||||||
_NavItem(
|
_NavItem(
|
||||||
icon: Icons.description_rounded,
|
icon: Icons.description_rounded,
|
||||||
title: '报告',
|
title: '报告',
|
||||||
@@ -445,6 +386,7 @@ class _NavTile extends StatelessWidget {
|
|||||||
const _NavTile({required this.item, required this.onTap});
|
const _NavTile({required this.item, required this.onTap});
|
||||||
|
|
||||||
String get _title => switch (item.route) {
|
String get _title => switch (item.route) {
|
||||||
|
'healthArchive' => '健康档案',
|
||||||
'reports' => '报告管理',
|
'reports' => '报告管理',
|
||||||
'medications' => '用药管理',
|
'medications' => '用药管理',
|
||||||
'dietRecords' => '饮食记录',
|
'dietRecords' => '饮食记录',
|
||||||
@@ -455,6 +397,7 @@ class _NavTile extends StatelessWidget {
|
|||||||
};
|
};
|
||||||
|
|
||||||
List<Color> get _colors => switch (item.route) {
|
List<Color> get _colors => switch (item.route) {
|
||||||
|
'healthArchive' => const [Color(0xFF34D399), Color(0xFF059669)],
|
||||||
'reports' => const [Color(0xFF60A5FA), Color(0xFF2563EB)],
|
'reports' => const [Color(0xFF60A5FA), Color(0xFF2563EB)],
|
||||||
'medications' => const [Color(0xFFA78BFA), Color(0xFF7C3AED)],
|
'medications' => const [Color(0xFFA78BFA), Color(0xFF7C3AED)],
|
||||||
'dietRecords' => const [Color(0xFFF6D365), Color(0xFFF97316)],
|
'dietRecords' => const [Color(0xFFF6D365), Color(0xFFF97316)],
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ void main() {
|
|||||||
await tester.pumpWidget(const ProviderScope(child: MaterialApp(home: LoginPage())));
|
await tester.pumpWidget(const ProviderScope(child: MaterialApp(home: LoginPage())));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('健康管家'), findsOneWidget);
|
expect(find.text('小脉健康'), findsOneWidget);
|
||||||
expect(find.text('登 录'), findsOneWidget);
|
expect(find.text('登 录'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,16 +14,13 @@ echo [1/2] Starting PostgreSQL...
|
|||||||
if errorlevel 1 (echo PG already running) else (echo PG started)
|
if errorlevel 1 (echo PG already running) else (echo PG started)
|
||||||
echo.
|
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"
|
start "Backend" cmd /k "cd /d D:\health_project\backend\src\Health.WebApi && dotnet run"
|
||||||
echo Waiting for backend...
|
echo Waiting for backend...
|
||||||
timeout /t 8 /nobreak >nul
|
timeout /t 8 /nobreak >nul
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
echo [3/3] Setup phone connection...
|
echo All done. Phone connects via WiFi to this PC.
|
||||||
adb reverse tcp:5000 tcp:5000 2>nul
|
echo If your PC IP changed, rebuild app with:
|
||||||
if errorlevel 1 (echo USB not connected, skip) else (echo Phone -> PC forwarding OK)
|
echo flutter run -d ^<device^> --dart-define=API_BASE_URL=http://^<pc-ip^>:5000
|
||||||
echo.
|
|
||||||
|
|
||||||
echo All done. Open the app on your phone.
|
|
||||||
pause
|
pause
|
||||||
|
|||||||
Reference in New Issue
Block a user