Files
AI-Health/health_app/lib/pages/remaining_pages.dart
MingNian 14d7c30d3d 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
2026-06-02 11:11:29 +08:00

197 lines
9.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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