- 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
80 lines
3.9 KiB
Dart
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))),
|
|
),
|
|
);
|
|
}
|