Initial commit: 健康管家 AI 健康陪伴助手
- Backend: .NET 10 Minimal API + EF Core + PostgreSQL - Frontend: Flutter + Riverpod + GoRouter + Dio - AI: DeepSeek LLM + Qwen VLM (OpenAI-compatible) - Auth: SMS + JWT (access/refresh tokens) - Features: AI chat, health tracking, medication management, diet analysis, exercise plans, doctor consultations, report analysis
This commit is contained in:
196
health_app/lib/pages/remaining_pages.dart
Normal file
196
health_app/lib/pages/remaining_pages.dart
Normal file
@@ -0,0 +1,196 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.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('运动计划')),
|
||||
body: plan.when(
|
||||
data: (data) {
|
||||
if (data == null) return _empty(context, '运动计划', '暂无运动计划');
|
||||
final items = (data['items'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
final weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||||
return ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (ctx, i) {
|
||||
final item = items[i];
|
||||
final day = item['dayOfWeek'] is int ? item['dayOfWeek'] as int : i;
|
||||
final isRest = item['isRestDay'] == true;
|
||||
final isDone = item['isCompleted'] == true;
|
||||
return ListTile(
|
||||
leading: Icon(isDone ? Icons.check_circle : Icons.circle_outlined, color: isDone ? const Color(0xFF43A047) : Colors.grey),
|
||||
title: Text('${weekDays[day]} ${isRest ? '休息日' : '${item['exerciseType']} ${item['durationMinutes']}分钟'}'),
|
||||
trailing: isDone ? const Text('✅ 已完成', style: TextStyle(fontSize: 14, color: Color(0xFF43A047))) : const Text('待完成', style: TextStyle(fontSize: 14, color: Color(0xFF999999))),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, _) => _empty(context, '运动计划', '暂无运动计划'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 复查列表
|
||||
class FollowUpListPage extends ConsumerWidget {
|
||||
const FollowUpListPage({super.key});
|
||||
@override Widget build(BuildContext context, WidgetRef ref) => _empty(context, '复查随访', '暂无复查安排');
|
||||
}
|
||||
|
||||
/// 健康档案
|
||||
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 ConsumerWidget {
|
||||
const HealthCalendarPage({super.key});
|
||||
@override Widget build(BuildContext context, WidgetRef ref) => _empty(context, '健康日历', '暂无数据');
|
||||
}
|
||||
|
||||
/// 静态文本页
|
||||
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': '关于'};
|
||||
return Scaffold(appBar: AppBar(title: Text(titles[type] ?? '')), body: const Center(child: Padding(padding: EdgeInsets.all(16), child: Text('内容后期填充', style: TextStyle(color: Color(0xFF999999))))));
|
||||
}
|
||||
}
|
||||
|
||||
/// 设备管理(占位)
|
||||
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),
|
||||
])),
|
||||
);
|
||||
Reference in New Issue
Block a user