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:
MingNian
2026-06-02 11:11:29 +08:00
commit 14d7c30d3d
144 changed files with 11436 additions and 0 deletions

View 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),
])),
);