import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../providers/auth_provider.dart'; import '../../providers/data_providers.dart'; class TrendPage extends ConsumerStatefulWidget { final String metricType; const TrendPage({super.key, required this.metricType}); @override ConsumerState createState() => _TrendPageState(); } class _TrendPageState extends ConsumerState { int _period = 7; List> _records = []; bool _loading = true; final _value1Ctrl = TextEditingController(); final _value2Ctrl = TextEditingController(); static const _titles = { 'blood_pressure': '血压', 'heart_rate': '心率', 'glucose': '血糖', 'spo2': '血氧', 'weight': '体重', }; static const _units = { 'blood_pressure': 'mmHg', 'heart_rate': 'bpm', 'glucose': 'mmol/L', 'spo2': '%', 'weight': 'kg', }; String get _title => _titles[widget.metricType] ?? ''; String get _unit => _units[widget.metricType] ?? ''; bool get _isBP => widget.metricType == 'blood_pressure'; @override void initState() { super.initState(); _load(); } @override void dispose() { _value1Ctrl.dispose(); _value2Ctrl.dispose(); super.dispose(); } Future _load() async { setState(() => _loading = true); try { final api = ref.read(apiClientProvider); final res = await api.get('/api/health-records/trend', queryParameters: {'type': widget.metricType, 'period': _period}); final list = (res.data['data'] as List?)?.cast>() ?? []; final data = list.map((r) { final v = r['value']; return { 'date': DateTime.tryParse(r['recordedAt']?.toString() ?? '') ?? DateTime.now(), 'value': _isBP ? r['systolic'] : (v is num ? v : null), if (_isBP) 'value2': r['diastolic'], 'isAbnormal': r['isAbnormal'] == true, }; }).toList(); if (mounted) setState(() { _records = data; _loading = false; }); } catch (_) { if (mounted) setState(() => _loading = false); } } Future _addRecord() async { final v1 = double.tryParse(_value1Ctrl.text.trim()); if (v1 == null) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请输入数值'))); return; } try { final api = ref.read(apiClientProvider); final body = { 'type': widget.metricType == 'blood_pressure' ? 'BloodPressure' : widget.metricType == 'heart_rate' ? 'HeartRate' : widget.metricType == 'glucose' ? 'Glucose' : widget.metricType == 'spo2' ? 'SpO2' : 'Weight', 'source': 'Manual', 'recordedAt': DateTime.now().toIso8601String(), 'unit': _unit, }; if (_isBP) { body['systolic'] = v1.toInt(); final v2 = double.tryParse(_value2Ctrl.text.trim()); body['diastolic'] = v2?.toInt() ?? 0; } else { body['value'] = v1; } await api.post('/api/health-records', data: body); _value1Ctrl.clear(); _value2Ctrl.clear(); ref.invalidate(latestHealthProvider); _load(); if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已记录'), backgroundColor: Color(0xFF43A047))); } catch (_) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('记录失败'), backgroundColor: Color(0xFFE53935))); } } String _statusText(Map r) { if (r['isAbnormal'] != true) return '正常'; if (_isBP) { final s = r['value'] as int? ?? 0; final d = r['value2'] as int? ?? 0; if (s >= 140 || d >= 90) return '偏高'; if (s <= 89 || d <= 59) return '偏低'; } else { final v = r['value'] as num? ?? 0; final ranges = {'heart_rate': [60, 100], 'glucose': [3.9, 6.1], 'spo2': [95, 100], 'weight': [18.5, 24.9]}; final range = ranges[widget.metricType]; if (range != null) { if (v > range[1]) return '偏高'; if (v < range[0]) return '偏低'; } } return '异常'; } Color _statusColor(String s) { switch (s) { case '偏高': return const Color(0xFFE53935); case '偏低': return const Color(0xFFFF9800); default: return const Color(0xFF43A047); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF8F9FC), appBar: AppBar(title: Text('$_title趋势'), centerTitle: true), body: _loading ? const Center(child: CircularProgressIndicator(color: Color(0xFF6C5CE7))) : SingleChildScrollView(padding: const EdgeInsets.all(16), child: Column(children: [ _buildChartSection(), const SizedBox(height: 20), _buildManualEntry(), const SizedBox(height: 20), _buildRecordList(), const SizedBox(height: 40), ])), ); } Widget _buildChartSection() { final valid = _records.where((r) => r['value'] != null).toList(); if (valid.isEmpty) { return Container( height: 200, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)), child: const Center(child: Text('暂无数据,请在下方手动录入', style: TextStyle(color: Color(0xFFBBBBBB)))), ); } final values = valid.map((r) => (r['value'] as num).toDouble()).toList(); final maxVal = values.reduce((a, b) => a > b ? a : b); final minVal = values.reduce((a, b) => a < b ? a : b); final range = maxVal - minVal; final normalized = values.map((v) => range > 0 ? (v - minVal) / range : 0.5).toList(); // Time chips return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)), child: Column(children: [ Row(mainAxisAlignment: MainAxisAlignment.center, children: [ _chip('7天', 7), _chip('30天', 30), _chip('90天', 90), ]), const SizedBox(height: 16), SizedBox(height: 160, child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: List.generate(normalized.length, (i) { final h = normalized[i] * 140 + 10; return Expanded(child: Padding( padding: EdgeInsets.symmetric(horizontal: i < normalized.length - 1 ? 2 : 0), child: Column(mainAxisAlignment: MainAxisAlignment.end, children: [ Container(height: h, decoration: BoxDecoration( color: valid[i]['isAbnormal'] == true ? const Color(0xFFF56C6C) : const Color(0xFF6C5CE7), borderRadius: BorderRadius.circular(4), )), ]), )); }), )), const SizedBox(height: 8), Text('最近${valid.length}条记录 · $_unit', style: const TextStyle(fontSize: 12, color: Color(0xFFBBBBBB))), ]), ); } Widget _chip(String label, int p) { final sel = _period == p; return GestureDetector( onTap: () { _period = p; _load(); }, child: Container( margin: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), decoration: BoxDecoration( color: sel ? const Color(0xFF6C5CE7) : const Color(0xFFEDEAFF), borderRadius: BorderRadius.circular(16), ), child: Text(label, style: TextStyle(fontSize: 13, color: sel ? Colors.white : const Color(0xFF6C5CE7), fontWeight: FontWeight.w500)), ), ); } Widget _buildManualEntry() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('手动录入', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 12), if (_isBP) Row(children: [ Expanded(child: TextField(controller: _value1Ctrl, keyboardType: TextInputType.number, decoration: const InputDecoration(hintText: '收缩压 (mmHg)', filled: true, fillColor: Color(0xFFF4F5FA), border: InputBorder.none))), const SizedBox(width: 12), Expanded(child: TextField(controller: _value2Ctrl, keyboardType: TextInputType.number, decoration: const InputDecoration(hintText: '舒张压 (mmHg)', filled: true, fillColor: Color(0xFFF4F5FA), border: InputBorder.none))), ]) else TextField(controller: _value1Ctrl, keyboardType: TextInputType.number, decoration: InputDecoration(hintText: '$_title ($_unit)', filled: true, fillColor: const Color(0xFFF4F5FA), border: InputBorder.none)), const SizedBox(height: 12), SizedBox(width: double.infinity, child: ElevatedButton(onPressed: _addRecord, style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF6C5CE7), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), child: const Text('记录')), ), ]), ); } Widget _buildRecordList() { if (_records.isEmpty) return const SizedBox.shrink(); return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('历史记录', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 8), ..._records.where((r) => r['value'] != null).toList().reversed.take(20).map((r) { final date = r['date'] as DateTime; final v1 = r['value']; final st = _statusText(r); final sc = _statusColor(st); return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), child: Row(children: [ Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('${date.month}/${date.day} ${date.hour}:${date.minute.toString().padLeft(2, '0')}', style: const TextStyle(fontSize: 13, color: Color(0xFF888888))), const SizedBox(height: 4), Text(_isBP ? '${v1 ?? '--'}/${r['value2'] ?? '--'} $_unit' : '${v1 ?? '--'} $_unit', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700)), ])), Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration(color: sc.withAlpha(20), borderRadius: BorderRadius.circular(8)), child: Text(st, style: TextStyle(fontSize: 12, color: sc, fontWeight: FontWeight.w500))), ]), ); }), ]); } }