feat: 用药提醒功能 + 移除医生相关页面

- 后端新增 GET /api/medications/reminders 接口
- 前端任务卡片区显示真实用药提醒
- 移除 DoctorListPage/DoctorChatPage 路由
- 移除"找医生"面板按钮
- 医生端另做 Web 页面
This commit is contained in:
MingNian
2026-06-03 15:11:12 +08:00
parent 0e49b9a952
commit 07ddf2577a
12 changed files with 399 additions and 45 deletions

View File

@@ -15,6 +15,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
int _period = 7;
bool _showAllRecords = false;
late List<Map<String, dynamic>> _data;
final _chartKey = GlobalKey();
static const _labels = {
'blood_pressure': '血压趋势',
@@ -420,20 +421,24 @@ class _TrendPageState extends ConsumerState<TrendPage> {
],
),
const SizedBox(height: 12),
SizedBox(
height: 200,
child: CustomPaint(
painter: _TrendChartPainter(
data: _data,
metricType: widget.metricType,
isDualLine: _isDualLine,
yMin: yRange.min,
yMax: yRange.max,
yStep: yRange.step,
formatDateLabel: _formatDateLabel,
formatValue: _formatValue,
GestureDetector(
key: _chartKey,
onTapDown: (details) => _onChartTap(details, yRange),
child: SizedBox(
height: 200,
child: CustomPaint(
painter: _TrendChartPainter(
data: _data,
metricType: widget.metricType,
isDualLine: _isDualLine,
yMin: yRange.min,
yMax: yRange.max,
yStep: yRange.step,
formatDateLabel: _formatDateLabel,
formatValue: _formatValue,
),
size: Size.infinite,
),
size: Size.infinite,
),
),
],
@@ -570,6 +575,101 @@ class _TrendPageState extends ConsumerState<TrendPage> {
),
);
}
// ==================== 图表点击检测 ====================
void _onChartTap(TapDownDetails details, ({double min, double max, double step}) yRange) {
final renderBox = _chartKey.currentContext?.findRenderObject() as RenderBox?;
if (renderBox == null || _data.length < 2) return;
final localPosition = renderBox.globalToLocal(details.globalPosition);
const leftPadding = 44.0;
const rightPadding = 8.0;
final chartW = renderBox.size.width - leftPadding - rightPadding;
// 找到 x 方向最近的数据点
int nearestIndex = 0;
double minDist = double.infinity;
for (int i = 0; i < _data.length; i++) {
final pointX = leftPadding + (chartW * i / (_data.length - 1));
final dist = (localPosition.dx - pointX).abs();
if (dist < minDist) {
minDist = dist;
nearestIndex = i;
}
}
// 点击偏离数据点太远则不响应
if (minDist > 40) return;
final item = _data[nearestIndex];
final date = item['date'] as DateTime;
final val = item['value'] as num;
final val2 = item['value2'] as num?;
final status = _getStatus(val, value2: val2);
String displayValue;
if (_isDualLine) {
displayValue = '${_formatValue(val)} / ${_formatValue(val2 ?? 0)} ${_getUnit()}';
} else {
displayValue = '${_formatValue(val)} ${_getUnit()}';
}
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(
_formatDateTime(date),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
displayValue,
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFF333333)),
),
const SizedBox(height: 16),
Row(
children: [
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: _getStatusColor(status),
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: _getStatusColor(status).withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(12),
),
child: Text(
status,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: _getStatusColor(status),
),
),
),
],
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('关闭', style: TextStyle(color: Color(0xFF635BFF))),
),
],
),
);
}
}
// ============================================================