Files
AI-Health/health_app/lib/pages/chart/trend_page.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

80 lines
3.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../providers/data_providers.dart';
/// 趋势图表页面
class TrendPage extends ConsumerStatefulWidget {
final String metricType;
const TrendPage({super.key, required this.metricType});
@override ConsumerState<TrendPage> createState() => _TrendPageState();
}
class _TrendPageState extends ConsumerState<TrendPage> {
int _period = 7;
@override Widget build(BuildContext context) {
final labels = {'blood_pressure': '血压趋势', 'heart_rate': '心率趋势', 'glucose': '血糖趋势', 'spo2': '血氧趋势', 'weight': '体重趋势'};
final service = ref.watch(healthServiceProvider);
return Scaffold(
appBar: AppBar(title: Text(labels[widget.metricType] ?? '趋势图表')),
body: Column(children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
_TimeChip(label: '7天', selected: _period == 7, onTap: () => setState(() => _period = 7)),
const SizedBox(width: 8), _TimeChip(label: '30天', selected: _period == 30, onTap: () => setState(() => _period = 30)),
const SizedBox(width: 8), _TimeChip(label: '90天', selected: _period == 90, onTap: () => setState(() => _period = 90)),
]),
),
Expanded(child: FutureBuilder<List<Map<String, dynamic>>>(
future: service.getTrend(widget.metricType, period: _period),
builder: (ctx, snap) {
if (snap.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
if (!snap.hasData || snap.data!.isEmpty) {
return Center(child: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.show_chart, size: 64, color: Colors.grey[300]),
const SizedBox(height: 12), Text('暂无足够数据', style: Theme.of(context).textTheme.bodyMedium),
]));
}
final records = snap.data!;
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: records.length,
itemBuilder: (ctx, i) {
final r = records[i];
String value;
if (widget.metricType == 'blood_pressure') {
value = '${r['systolic'] ?? '--'}/${r['diastolic'] ?? '--'} mmHg';
} else {
value = '${r['value'] ?? '--'}';
}
final isAbnormal = r['isAbnormal'] == true;
final date = r['recordedAt'] != null ? DateTime.parse(r['recordedAt']).toLocal().toString().substring(0, 16) : '--';
return ListTile(
title: Text(value, style: TextStyle(fontSize: 16, color: isAbnormal ? const Color(0xFFE53935) : null)),
subtitle: Text(date, style: const TextStyle(fontSize: 14, color: Color(0xFF999999))),
trailing: isAbnormal ? const Icon(Icons.warning_amber, color: Color(0xFFE53935), size: 20) : const Icon(Icons.check_circle, color: Color(0xFF43A047), size: 20),
);
},
);
},
)),
]),
);
}
}
class _TimeChip extends StatelessWidget {
final String label; final bool selected; final VoidCallback onTap;
const _TimeChip({required this.label, required this.selected, required this.onTap});
@override Widget build(BuildContext context) => GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
decoration: BoxDecoration(color: selected ? const Color(0xFF635BFF) : Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0xFF635BFF))),
child: Text(label, style: TextStyle(fontSize: 14, color: selected ? Colors.white : const Color(0xFF635BFF))),
),
);
}