- sendImage: 本地预览→上传→远程URL替换 - doctorListProvider: 8s超时+mock医生fallback - currentExercisePlanProvider: 8s超时→显示空状态 - 用药编辑: try-catch防黑屏+刷新列表 - 服药打卡: 接入后端confirm()接口
689 lines
28 KiB
Dart
689 lines
28 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import '../core/navigation_provider.dart';
|
||
import '../providers/data_providers.dart';
|
||
|
||
/// 饮食记录列表
|
||
class DietRecordListPage extends ConsumerWidget {
|
||
const DietRecordListPage({super.key});
|
||
@override Widget build(BuildContext context, WidgetRef ref) {
|
||
final service = ref.watch(dietServiceProvider);
|
||
return Scaffold(
|
||
appBar: AppBar(title: const Text('饮食记录')),
|
||
body: FutureBuilder<List<Map<String, dynamic>>>(
|
||
future: service.getRecords(),
|
||
builder: (ctx, snap) {
|
||
if (snap.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
|
||
if (!snap.hasData || snap.data!.isEmpty) return _empty(context, '饮食记录', '暂无饮食记录,可通过「拍饮食」录入');
|
||
return ListView.builder(
|
||
itemCount: snap.data!.length,
|
||
itemBuilder: (ctx, i) {
|
||
final d = snap.data![i];
|
||
final items = (d['foodItems'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||
return Card(
|
||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||
child: ListTile(
|
||
title: Text('${d['mealType'] ?? ''} ${d['totalCalories'] ?? 0}千卡'),
|
||
subtitle: Text(items.map((f) => f['name']).join(' | ')),
|
||
trailing: _starWidget(d['healthScore']),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
Widget _starWidget(dynamic score) {
|
||
final s = score is int ? score : 3;
|
||
return Row(mainAxisSize: MainAxisSize.min, children: List.generate(5, (i) => Icon(Icons.star, size: 16, color: i < s ? const Color(0xFFF9A825) : Colors.grey[300])));
|
||
}
|
||
}
|
||
|
||
/// 运动计划页
|
||
class ExercisePlanPage extends ConsumerWidget {
|
||
const ExercisePlanPage({super.key});
|
||
@override Widget build(BuildContext context, WidgetRef ref) {
|
||
final plan = ref.watch(currentExercisePlanProvider);
|
||
return Scaffold(
|
||
appBar: AppBar(title: const Text('运动计划'), centerTitle: true),
|
||
floatingActionButton: FloatingActionButton.extended(
|
||
onPressed: () => _createDefaultPlan(ref, context),
|
||
icon: const Icon(Icons.add),
|
||
label: const Text('创建本周计划'),
|
||
backgroundColor: const Color(0xFF635BFF),
|
||
),
|
||
body: plan.when(
|
||
data: (data) {
|
||
if (data == null || data.isEmpty) return _empty(context, '运动计划', '暂无运动计划,点击右下角创建');
|
||
final items = (data['items'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||
final weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||
final completedCount = items.where((i) => i['isCompleted'] == true).length;
|
||
final totalCount = items.where((i) => i['isRestDay'] != true).length;
|
||
|
||
return ListView(children: [
|
||
_buildProgressCard(completedCount, totalCount),
|
||
const SizedBox(height: 16),
|
||
...items.asMap().entries.map((entry) {
|
||
final i = entry.key;
|
||
final item = entry.value;
|
||
final day = item['dayOfWeek'] is int ? item['dayOfWeek'] as int : i;
|
||
final isRest = item['isRestDay'] == true;
|
||
final isDone = item['isCompleted'] == true;
|
||
return _ExercisePlanItem(
|
||
day: weekDays[day],
|
||
dayIndex: day,
|
||
isRest: isRest,
|
||
isDone: isDone,
|
||
exerciseType: item['exerciseType']?.toString() ?? '',
|
||
duration: item['durationMinutes'] is int ? item['durationMinutes'] as int : 0,
|
||
onCheckIn: () => _checkIn(ref, item['id'], context),
|
||
);
|
||
}),
|
||
]);
|
||
},
|
||
loading: () => const Center(child: CircularProgressIndicator(color: Color(0xFF635BFF))),
|
||
error: (_, __) => _empty(context, '运动计划', '暂无运动计划,点击右下角创建'),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildProgressCard(int completed, int total) {
|
||
final progress = total > 0 ? (completed / total * 100).toInt() : 0;
|
||
return Container(
|
||
margin: const EdgeInsets.all(16),
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFF5F3FF),
|
||
borderRadius: BorderRadius.circular(20),
|
||
),
|
||
child: Column(children: [
|
||
const Text('🏃 本周运动进度', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||
const SizedBox(height: 12),
|
||
Row(children: [
|
||
Container(
|
||
width: 60,
|
||
height: 60,
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF635BFF),
|
||
borderRadius: BorderRadius.circular(30),
|
||
),
|
||
child: Center(
|
||
child: Text('$progress%', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white)),
|
||
),
|
||
),
|
||
const SizedBox(width: 16),
|
||
Expanded(
|
||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||
Text('已完成 $completed/$total 天', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||
const SizedBox(height: 8),
|
||
Container(
|
||
height: 8,
|
||
decoration: BoxDecoration(color: Colors.grey[200], borderRadius: BorderRadius.circular(4)),
|
||
child: FractionallySizedBox(
|
||
widthFactor: progress / 100,
|
||
child: Container(
|
||
decoration: BoxDecoration(color: const Color(0xFF635BFF), borderRadius: BorderRadius.circular(4)),
|
||
),
|
||
),
|
||
),
|
||
]),
|
||
),
|
||
]),
|
||
]),
|
||
);
|
||
}
|
||
|
||
void _createDefaultPlan(WidgetRef ref, BuildContext context) async {
|
||
final service = ref.read(exerciseServiceProvider);
|
||
final today = DateTime.now();
|
||
final monday = today.subtract(Duration(days: today.weekday - 1));
|
||
final items = List.generate(7, (i) => {
|
||
'dayOfWeek': i,
|
||
'exerciseType': i == 2 || i == 5 ? '休息' : '散步',
|
||
'durationMinutes': i == 2 || i == 5 ? 0 : 30,
|
||
'isRestDay': i == 2 || i == 5,
|
||
});
|
||
await service.createPlan({
|
||
'weekStartDate': '${monday.year}-${monday.month.toString().padLeft(2, '0')}-${monday.day.toString().padLeft(2, '0')}',
|
||
'items': items,
|
||
});
|
||
ref.invalidate(currentExercisePlanProvider);
|
||
if (!context.mounted) return;
|
||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||
content: Text('运动计划已创建 ✅'),
|
||
backgroundColor: Color(0xFF635BFF),
|
||
));
|
||
}
|
||
|
||
void _checkIn(WidgetRef ref, String itemId, BuildContext context) async {
|
||
final service = ref.read(exerciseServiceProvider);
|
||
await service.checkIn(itemId);
|
||
ref.invalidate(currentExercisePlanProvider);
|
||
if (!context.mounted) return;
|
||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||
content: Text('打卡成功 ✅'),
|
||
backgroundColor: Color(0xFF43A047),
|
||
));
|
||
}
|
||
}
|
||
|
||
class _ExercisePlanItem extends StatelessWidget {
|
||
final String day;
|
||
final int dayIndex;
|
||
final bool isRest;
|
||
final bool isDone;
|
||
final String exerciseType;
|
||
final int duration;
|
||
final VoidCallback onCheckIn;
|
||
|
||
const _ExercisePlanItem({
|
||
required this.day,
|
||
required this.dayIndex,
|
||
required this.isRest,
|
||
required this.isDone,
|
||
required this.exerciseType,
|
||
required this.duration,
|
||
required this.onCheckIn,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final today = DateTime.now().weekday - 1;
|
||
final isToday = dayIndex == today;
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: isToday ? const Color(0xFFFEFCE8) : Colors.white,
|
||
borderRadius: BorderRadius.circular(16),
|
||
border: isToday ? Border.all(color: const Color(0xFFFCD34D), width: 2) : null,
|
||
boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(8), blurRadius: 4, offset: const Offset(0, 2))],
|
||
),
|
||
child: Row(children: [
|
||
Container(
|
||
width: 40,
|
||
height: 40,
|
||
decoration: BoxDecoration(
|
||
color: isDone ? const Color(0xFFDCFCE7) : isRest ? const Color(0xFFF3F4F6) : const Color(0xFFF5F3FF),
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: isDone
|
||
? const Icon(Icons.check, size: 20, color: Color(0xFF43A047))
|
||
: isRest
|
||
? const Icon(Icons.coffee, size: 20, color: Color(0xFF999999))
|
||
: const Icon(Icons.directions_run, size: 20, color: Color(0xFF635BFF)),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||
Row(children: [
|
||
Text(day, style: TextStyle(fontSize: 16, fontWeight: isToday ? FontWeight.w600 : FontWeight.w500)),
|
||
if (isToday) const SizedBox(width: 4),
|
||
if (isToday) const Text('(今天)', style: TextStyle(fontSize: 12, color: Color(0xFFF59E0B))),
|
||
]),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
isRest ? '休息日,好好休息' : '$exerciseType $duration分钟',
|
||
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
||
),
|
||
]),
|
||
),
|
||
if (!isRest && !isDone)
|
||
ElevatedButton(
|
||
onPressed: onCheckIn,
|
||
child: const Text('打卡'),
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFF635BFF),
|
||
foregroundColor: Colors.white,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||
),
|
||
),
|
||
if (isDone)
|
||
const Text('已完成', style: TextStyle(fontSize: 14, color: Color(0xFF43A047))),
|
||
]),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 复查列表
|
||
class FollowUpListPage extends ConsumerWidget {
|
||
const FollowUpListPage({super.key});
|
||
@override Widget build(BuildContext context, WidgetRef ref) {
|
||
return Scaffold(
|
||
appBar: AppBar(title: const Text('复查随访'), centerTitle: true),
|
||
floatingActionButton: FloatingActionButton(
|
||
onPressed: () => _showAddDialog(context),
|
||
child: const Icon(Icons.add),
|
||
backgroundColor: const Color(0xFF635BFF),
|
||
),
|
||
body: ListView(children: _mockFollowUps.map((item) => _FollowUpItem(item: item)).toList()),
|
||
);
|
||
}
|
||
|
||
void _showAddDialog(BuildContext context) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (ctx) => AlertDialog(
|
||
title: const Text('添加复查提醒'),
|
||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||
TextField(decoration: const InputDecoration(labelText: '医院名称')),
|
||
const SizedBox(height: 12),
|
||
TextField(decoration: const InputDecoration(labelText: '科室')),
|
||
const SizedBox(height: 12),
|
||
TextField(decoration: const InputDecoration(labelText: '日期', hintText: 'YYYY-MM-DD')),
|
||
const SizedBox(height: 12),
|
||
TextField(decoration: const InputDecoration(labelText: '备注')),
|
||
]),
|
||
actions: [
|
||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消')),
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.pop(ctx);
|
||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||
content: Text('复查提醒已添加 ✅'),
|
||
backgroundColor: Color(0xFF635BFF),
|
||
));
|
||
},
|
||
child: const Text('保存'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
final _mockFollowUps = [
|
||
{'id': '1', 'hospital': '协和医院', 'department': '心内科', 'date': '2025-01-20', 'type': '复诊', 'status': 'upcoming', 'notes': '常规复查,带齐病历'},
|
||
{'id': '2', 'hospital': '人民医院', 'department': '骨科', 'date': '2025-01-25', 'type': '复查', 'status': 'upcoming', 'notes': '术后3个月复查'},
|
||
{'id': '3', 'hospital': '协和医院', 'department': '心内科', 'date': '2024-12-15', 'type': '复诊', 'status': 'completed', 'notes': '已完成'},
|
||
];
|
||
|
||
class _FollowUpItem extends StatelessWidget {
|
||
final Map<String, dynamic> item;
|
||
const _FollowUpItem({required this.item});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final isCompleted = item['status'] == 'completed';
|
||
return Container(
|
||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(16),
|
||
boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(10), blurRadius: 4, offset: const Offset(0, 2))],
|
||
),
|
||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||
Row(children: [
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
color: isCompleted ? const Color(0xFFDCFCE7) : const Color(0xFFFEFCE8),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Text(
|
||
isCompleted ? '已完成' : '待就诊',
|
||
style: TextStyle(fontSize: 12, color: isCompleted ? const Color(0xFF43A047) : const Color(0xFFF59E0B)),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Text(item['type']?.toString() ?? '', style: TextStyle(fontSize: 14, color: Colors.grey[500])),
|
||
]),
|
||
const SizedBox(height: 12),
|
||
Text(item['hospital']?.toString() ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||
const SizedBox(height: 4),
|
||
Text('${item['department']} · ${item['date']}', style: TextStyle(fontSize: 14, color: Colors.grey[500])),
|
||
if ((item['notes']?.toString() ?? '').isNotEmpty) ...[
|
||
const SizedBox(height: 8),
|
||
Text(item['notes']?.toString() ?? '', style: TextStyle(fontSize: 14, color: Colors.grey[600])),
|
||
],
|
||
]),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 健康档案
|
||
class HealthArchivePage extends ConsumerWidget {
|
||
const HealthArchivePage({super.key});
|
||
@override Widget build(BuildContext context, WidgetRef ref) {
|
||
final service = ref.watch(userServiceProvider);
|
||
return Scaffold(
|
||
appBar: AppBar(title: const Text('健康档案')),
|
||
body: FutureBuilder<Map<String, dynamic>?>(
|
||
future: service.getHealthArchive(),
|
||
builder: (ctx, snap) {
|
||
if (snap.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
|
||
final data = snap.data;
|
||
if (data == null || data.isEmpty) return _empty(context, '暂无健康档案', '可通过 AI 对话或手动填写');
|
||
return ListView(
|
||
padding: const EdgeInsets.all(16),
|
||
children: [
|
||
_Section(title: '基本信息', children: [
|
||
_Field('诊断', data['diagnosis']), _Field('手术类型', data['surgeryType']),
|
||
_Field('手术日期', data['surgeryDate']),
|
||
]),
|
||
_Section(title: '病史与限制', children: [
|
||
_Field('过敏史', _listStr(data['allergies'])),
|
||
_Field('饮食限制', _listStr(data['dietRestrictions'])),
|
||
_Field('慢性病史', _listStr(data['chronicDiseases'])),
|
||
_Field('家族病史', data['familyHistory']),
|
||
]),
|
||
],
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
String _listStr(dynamic list) => list is List ? list.join('、') : '--';
|
||
}
|
||
|
||
class _Section extends StatelessWidget {
|
||
final String title; final List<Widget> children;
|
||
const _Section({required this.title, required this.children});
|
||
@override Widget build(BuildContext context) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||
Padding(padding: const EdgeInsets.only(bottom: 8, top: 16), child: Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)))),
|
||
...children,
|
||
]);
|
||
}
|
||
|
||
class _Field extends StatelessWidget {
|
||
final String label; final String? value;
|
||
const _Field(this.label, this.value);
|
||
@override Widget build(BuildContext context) => Padding(
|
||
padding: const EdgeInsets.only(bottom: 6),
|
||
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||
SizedBox(width: 80, child: Text('$label:', style: const TextStyle(fontSize: 14, color: Color(0xFF666666)))),
|
||
Expanded(child: Text(value ?? '--', style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)))),
|
||
]),
|
||
);
|
||
}
|
||
|
||
/// 编辑资料
|
||
class EditProfilePage extends ConsumerStatefulWidget {
|
||
const EditProfilePage({super.key});
|
||
@override ConsumerState<EditProfilePage> createState() => _EditProfilePageState();
|
||
}
|
||
class _EditProfilePageState extends ConsumerState<EditProfilePage> {
|
||
final _nameCtrl = TextEditingController(); final _genderCtrl = TextEditingController(); final _birthCtrl = TextEditingController();
|
||
@override void dispose() { _nameCtrl.dispose(); _genderCtrl.dispose(); _birthCtrl.dispose(); super.dispose(); }
|
||
@override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) => _load()); }
|
||
void _load() async {
|
||
final p = await ref.read(userServiceProvider).getProfile();
|
||
if (p != null && mounted) {
|
||
setState(() { _nameCtrl.text = p['name'] ?? ''; _genderCtrl.text = p['gender'] ?? ''; _birthCtrl.text = p['birthDate'] ?? ''; });
|
||
}
|
||
}
|
||
Future<void> _save() async {
|
||
await ref.read(userServiceProvider).updateProfile(name: _nameCtrl.text, gender: _genderCtrl.text, birthDate: _birthCtrl.text);
|
||
if (mounted) Navigator.pop(context);
|
||
}
|
||
@override Widget build(BuildContext context) => Scaffold(
|
||
appBar: AppBar(title: const Text('编辑资料')),
|
||
body: ListView(padding: const EdgeInsets.all(16), children: [
|
||
TextField(controller: _nameCtrl, decoration: const InputDecoration(labelText: '姓名')),
|
||
const SizedBox(height: 16), TextField(controller: _genderCtrl, decoration: const InputDecoration(labelText: '性别')),
|
||
const SizedBox(height: 16), TextField(controller: _birthCtrl, decoration: const InputDecoration(labelText: '出生日期', hintText: 'YYYY-MM-DD')),
|
||
const SizedBox(height: 32), SizedBox(width: double.infinity, child: ElevatedButton(onPressed: _save, child: const Text('保存'))),
|
||
]),
|
||
);
|
||
}
|
||
|
||
/// 健康日历
|
||
class HealthCalendarPage extends ConsumerStatefulWidget {
|
||
const HealthCalendarPage({super.key});
|
||
@override ConsumerState<HealthCalendarPage> createState() => _HealthCalendarPageState();
|
||
}
|
||
|
||
class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
|
||
DateTime _currentMonth = DateTime.now();
|
||
|
||
@override Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(title: const Text('健康日历'), centerTitle: true),
|
||
body: Column(children: [
|
||
_buildMonthHeader(),
|
||
_buildWeekdayHeader(),
|
||
_buildCalendarGrid(),
|
||
const SizedBox(height: 16),
|
||
_buildLegend(),
|
||
]),
|
||
);
|
||
}
|
||
|
||
Widget _buildMonthHeader() {
|
||
return Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
IconButton(
|
||
icon: const Icon(Icons.chevron_left, size: 32),
|
||
onPressed: () => setState(() => _currentMonth = DateTime(_currentMonth.year, _currentMonth.month - 1)),
|
||
),
|
||
Text(
|
||
'${_currentMonth.year}年${_currentMonth.month}月',
|
||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.chevron_right, size: 32),
|
||
onPressed: () => setState(() => _currentMonth = DateTime(_currentMonth.year, _currentMonth.month + 1)),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildWeekdayHeader() {
|
||
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
|
||
return Row(children: weekdays.map((day) => Expanded(
|
||
child: Center(child: Text(day, style: TextStyle(fontSize: 14, color: Colors.grey[500]))),
|
||
)).toList());
|
||
}
|
||
|
||
Widget _buildCalendarGrid() {
|
||
final firstDay = DateTime(_currentMonth.year, _currentMonth.month, 1);
|
||
final lastDay = DateTime(_currentMonth.year, _currentMonth.month + 1, 0);
|
||
final daysInMonth = lastDay.day;
|
||
final startWeekday = firstDay.weekday % 7;
|
||
|
||
final days = List.generate(42, (i) {
|
||
final dayIndex = i - startWeekday;
|
||
if (dayIndex < 0 || dayIndex >= daysInMonth) return null;
|
||
return dayIndex + 1;
|
||
});
|
||
|
||
return Expanded(
|
||
child: GridView.builder(
|
||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 7),
|
||
itemCount: 42,
|
||
itemBuilder: (ctx, i) {
|
||
final day = days[i];
|
||
if (day == null) return const SizedBox();
|
||
return _buildDayCell(day);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildDayCell(int day) {
|
||
final date = DateTime(_currentMonth.year, _currentMonth.month, day);
|
||
final today = DateTime.now();
|
||
final isToday = date.year == today.year && date.month == today.month && date.day == today.day;
|
||
final events = _getEvents(date);
|
||
|
||
return Container(
|
||
decoration: isToday ? BoxDecoration(
|
||
color: const Color(0xFF635BFF),
|
||
borderRadius: BorderRadius.circular(20),
|
||
) : null,
|
||
child: Stack(
|
||
alignment: Alignment.center,
|
||
children: [
|
||
Text(
|
||
'$day',
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
color: isToday ? Colors.white : Colors.black,
|
||
fontWeight: isToday ? FontWeight.w600 : FontWeight.normal,
|
||
),
|
||
),
|
||
if (events.isNotEmpty)
|
||
Positioned(
|
||
bottom: 4,
|
||
child: Row(children: events.map((type) => Container(
|
||
width: 6,
|
||
height: 6,
|
||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||
decoration: BoxDecoration(
|
||
color: _getEventColor(type),
|
||
borderRadius: BorderRadius.circular(3),
|
||
),
|
||
)).toList()),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
List<String> _getEvents(DateTime date) {
|
||
final events = <String>[];
|
||
if (date.day == 5 || date.day == 12 || date.day == 19 || date.day == 26) events.add('medication');
|
||
if (date.day == 8 || date.day == 15 || date.day == 22 || date.day == 29) events.add('exercise');
|
||
if (date.day == 20) events.add('followup');
|
||
return events;
|
||
}
|
||
|
||
Color _getEventColor(String type) {
|
||
switch (type) {
|
||
case 'medication': return const Color(0xFF635BFF);
|
||
case 'exercise': return const Color(0xFF43A047);
|
||
case 'followup': return const Color(0xFFF59E0B);
|
||
default: return Colors.grey;
|
||
}
|
||
}
|
||
|
||
Widget _buildLegend() {
|
||
final items = [
|
||
{'color': const Color(0xFF635BFF), 'label': '用药提醒'},
|
||
{'color': const Color(0xFF43A047), 'label': '运动计划'},
|
||
{'color': const Color(0xFFF59E0B), 'label': '复查随访'},
|
||
];
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: items.map((item) => Row(children: [
|
||
Container(width: 10, height: 10, decoration: BoxDecoration(color: item['color'] as Color, borderRadius: BorderRadius.circular(5))),
|
||
const SizedBox(width: 4),
|
||
Text(item['label'] as String, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
|
||
const SizedBox(width: 20),
|
||
])).toList()),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 静态文本页
|
||
class StaticTextPage extends ConsumerWidget {
|
||
final String type;
|
||
const StaticTextPage({super.key, required this.type});
|
||
@override Widget build(BuildContext context, WidgetRef ref) {
|
||
final titles = {'privacy': '隐私协议', 'terms': '服务协议', 'about': '关于健康管家'};
|
||
final contents = {
|
||
'privacy': '''## 隐私政策
|
||
|
||
更新日期:2026年1月1日
|
||
|
||
### 一、信息收集
|
||
我们收集以下类型的信息:
|
||
- 账户信息:手机号、昵称、头像(您主动提供)
|
||
- 健康数据:血压、心率、血糖、血氧、体重等健康指标记录
|
||
- 用药信息:药品名称、剂量、服药时间等用药计划数据
|
||
- 饮食记录:通过拍照或手动录入的饮食数据
|
||
- 设备信息:设备型号、操作系统版本(用于适配优化)
|
||
- 日志信息:App 使用情况、崩溃报告
|
||
|
||
### 二、信息使用
|
||
我们使用您的信息用于以下目的:
|
||
- 提供和改进健康管理服务
|
||
- AI 健康分析和个性化建议
|
||
- 用药提醒和复查通知推送
|
||
- App 功能优化和问题修复
|
||
|
||
### 三、信息保护
|
||
- 所有健康数据均采用 HTTPS 加密传输
|
||
- 数据存储于安全服务器,采用行业标准的加密措施
|
||
- 我们不会向任何第三方出售、出租或共享您的个人健康数据
|
||
- 医生仅可查看其签约患者的数据,且需经过您的授权
|
||
|
||
### 四、信息保留
|
||
- 对话记录保留 30 天后自动删除
|
||
- 您可以随时删除自己的健康数据和对话记录
|
||
- 账号注销后,所有数据将在 7 天内永久删除
|
||
|
||
### 五、您的权利
|
||
- 查看和导出您的个人数据
|
||
- 修改不准确的个人信息
|
||
- 删除不需要的数据
|
||
- 注销账号并清除所有数据
|
||
- 关闭推送通知
|
||
|
||
### 六、联系我们
|
||
如有任何关于隐私的问题,请联系:
|
||
邮箱:privacy@healthbutler.com
|
||
电话:400-xxx-xxxx''',
|
||
'about': '''## 关于健康管家
|
||
|
||
版本:v1.0.0 (Build 20260101)
|
||
|
||
### 产品介绍
|
||
健康管家是一款面向心脏术后康复患者的私人 AI 健康管理应用。以对话为核心交互方式,患者可以通过自然语言记录健康数据、获取饮食运动建议、管理用药、解读检查报告。
|
||
|
||
### 核心功能
|
||
- AI 智能问诊:基于大语言模型的健康咨询服务
|
||
- 健康数据管理:血压、心率、血糖、血氧、体重的记录与趋势分析
|
||
- 智能用药管理:AI 解析处方,自动生成用药计划和提醒
|
||
- 饮食识别分析:拍照即可识别食物种类、估算热量营养素
|
||
- 报告智能解读:上传检查报告,AI 自动提取指标并预解读
|
||
- 运动计划管理:制定和追踪每日运动目标
|
||
- 在线医生问诊:与签约医生进行远程咨询
|
||
|
||
### 开发团队
|
||
由专业医疗团队与 AI 技术团队联合打造。
|
||
|
||
### 技术支持
|
||
如遇到问题或有建议,请通过以下方式联系我们:
|
||
- 在线客服:App 内「设置」→「意见反馈」
|
||
- 客服热线:400-xxx-xxxx(工作日 9:00-18:00)
|
||
|
||
### 版权声明
|
||
© 2025-2026 健康管家团队。保留所有权利。
|
||
本软件受中华人民共和国著作权法保护。''',
|
||
};
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
backgroundColor: Colors.white,
|
||
elevation: 0,
|
||
leading: IconButton(icon: const Icon(Icons.chevron_left), onPressed: () => popRoute(ref)),
|
||
title: Text(titles[type] ?? '', style: const TextStyle(color: Color(0xFF1A1A1A), fontWeight: FontWeight.w600)),
|
||
centerTitle: true,
|
||
),
|
||
body: SingleChildScrollView(
|
||
padding: const EdgeInsets.all(20),
|
||
child: Text(contents[type] ?? '内容加载中...', style: const TextStyle(fontSize: 14, height: 1.8, color: Color(0xFF333333))),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 设备管理(占位)
|
||
class DeviceManagementPage extends ConsumerWidget {
|
||
const DeviceManagementPage({super.key});
|
||
@override Widget build(BuildContext context, WidgetRef ref) => _empty(context, '设备管理', '暂无绑定设备');
|
||
}
|
||
|
||
Widget _empty(BuildContext context, String title, String subtitle) => Scaffold(
|
||
appBar: AppBar(title: Text(title)),
|
||
body: Center(child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||
Icon(Icons.inbox_outlined, size: 64, color: Colors.grey[300]),
|
||
const SizedBox(height: 12), Text(subtitle, style: Theme.of(context).textTheme.bodyMedium),
|
||
])),
|
||
);
|