- Android 添加相机/存储权限,拍照和相册功能可用 - AI 回复支持 Markdown 渲染(加粗/表格不再显示**乱码) - 附件按钮接线,支持拍照/相册/文件选择 - 智能体面板按钮全部接线(拍照/上传/手动录入/导航) - 侧边栏 AI 录入后自动刷新健康数据 - 运动计划页增加创建按钮 + 打卡功能 - 后端运动计划支持 AI 创建和打卡(Tool Calling) - 修复 CreateExercisePlanRequest JSON 反序列化
225 lines
10 KiB
Dart
225 lines
10 KiB
Dart
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('运动计划')),
|
||
floatingActionButton: FloatingActionButton.extended(
|
||
onPressed: () => _createDefaultPlan(ref),
|
||
icon: const Icon(Icons.add),
|
||
label: const Text('创建本周计划'),
|
||
),
|
||
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 = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||
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 ? null : IconButton(icon: const Icon(Icons.check, color: Color(0xFF43A047)), onPressed: () { _checkIn(ref, item['id']); }),
|
||
);
|
||
},
|
||
);
|
||
},
|
||
loading: () => const Center(child: CircularProgressIndicator()),
|
||
error: (_, __) => _empty(context, '运动计划', '暂无运动计划,点击右下角创建'),
|
||
),
|
||
);
|
||
}
|
||
|
||
void _createDefaultPlan(WidgetRef ref) 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);
|
||
}
|
||
|
||
void _checkIn(WidgetRef ref, String itemId) async {
|
||
final service = ref.read(exerciseServiceProvider);
|
||
await service.checkIn(itemId);
|
||
ref.invalidate(currentExercisePlanProvider);
|
||
}
|
||
}
|
||
|
||
/// 复查列表
|
||
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),
|
||
])),
|
||
);
|