From 4c85cd50bef7e3f104318f73b38058bb8118efd6 Mon Sep 17 00:00:00 2001 From: MingNian <1281442923@qq.com> Date: Thu, 21 May 2026 16:32:20 +0800 Subject: [PATCH] fix: patient report shows interpretation, medication daily tracking, followup bugs, home overview restored, doctor renamed --- .../src/components/layout/DoctorLayout.tsx | 2 +- frontend-patient/src/pages/home/HomePage.tsx | 124 ++++++++-------- .../pages/medication/MedicationDetailPage.tsx | 134 ++++++++++++++---- .../src/pages/services/FollowUpEditPage.tsx | 29 ++-- .../src/pages/services/FollowUpListPage.tsx | 2 +- .../src/pages/services/ReportDetailPage.tsx | 108 +++++++++----- .../src/services/followup.service.ts | 4 +- 7 files changed, 257 insertions(+), 146 deletions(-) diff --git a/frontend-doctor/src/components/layout/DoctorLayout.tsx b/frontend-doctor/src/components/layout/DoctorLayout.tsx index 99c1515..15d92ce 100644 --- a/frontend-doctor/src/components/layout/DoctorLayout.tsx +++ b/frontend-doctor/src/components/layout/DoctorLayout.tsx @@ -6,7 +6,7 @@ const navItems = [ { to: '/patients', label: '患者管理', icon: '👥' }, { to: '/consultations', label: '在线问诊', icon: '💬' }, { to: '/reports', label: '报告审核', icon: '📋' }, - { to: '/follow-ups', label: '随访管理', icon: '📅' }, + { to: '/follow-ups', label: '复查管理', icon: '📅' }, ]; const sidebarBg = '#0F1D3D'; diff --git a/frontend-patient/src/pages/home/HomePage.tsx b/frontend-patient/src/pages/home/HomePage.tsx index d2ecb76..8c96b13 100644 --- a/frontend-patient/src/pages/home/HomePage.tsx +++ b/frontend-patient/src/pages/home/HomePage.tsx @@ -1,24 +1,20 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Card } from '@/components/common/Card'; -import { Empty } from '@/components/common/Empty'; -import { Badge } from '@/components/common/Badge'; -import { PageHeader } from '@/components/layout/PageHeader'; import { useAuth } from '@/hooks/useAuth'; import { useNotificationStore } from '@/stores/notification.store'; import * as healthService from '@/services/health.service'; -import { MEASUREMENT_TYPES, HEALTH_TIPS } from '@/utils/constants'; import { getBPRiskLevel } from '@/utils/format'; import type { HealthStats } from '@/types'; import styles from './HomePage.module.css'; const QUICK_ACTIONS = [ - { label: '测血压', icon: '💓', path: '/health/records?type=blood_pressure' }, - { label: '记用药', icon: '💊', path: '/health/medications' }, - { label: '在线问诊', icon: '💬', path: '/services/consultation' }, - { label: '报告解读', icon: '📋', path: '/services/reports' }, - { label: '健康日历', icon: '📅', path: '/health/calendar' }, - { label: '运动饮食', icon: '🏃', path: '/health/exercise-diet' }, + { key: 'bp', label: '血压', icon: '💓', path: '/health/records?type=blood_pressure', badge: false }, + { key: 'med', label: '用药', icon: '💊', path: '/health/medications', badge: false }, + { key: 'chat', label: '问诊', icon: '💬', path: '/services/consultation', badge: true }, + { key: 'report', label: '报告', icon: '📋', path: '/services/reports', badge: true }, + { key: 'calendar', label: '日历', icon: '📅', path: '/health/calendar', badge: false }, + { key: 'followup', label: '复查', icon: '🏥', path: '/services/follow-ups', badge: false }, ]; export function HomePage() { @@ -26,7 +22,6 @@ export function HomePage() { const { user } = useAuth(); const { unreadCount, fetchNotifications } = useNotificationStore(); const [stats, setStats] = useState([]); - const [tipIndex, setTipIndex] = useState(0); useEffect(() => { healthService.getLatestStats().then(setStats); @@ -35,6 +30,8 @@ export function HomePage() { const bpStats = stats.find((s) => s.type === 'blood_pressure'); const hrStats = stats.find((s) => s.type === 'heart_rate'); + const sugarStats = stats.find((s) => s.type === 'blood_sugar'); + const spo2Stats = stats.find((s) => s.type === 'spo2'); const bpValue = bpStats?.latest?.value; const systolic = typeof bpValue === 'object' ? bpValue.systolic : null; @@ -43,76 +40,77 @@ export function HomePage() { return (
- navigate('/notifications')}> - 🔔 - {unreadCount > 0 && } - - } - /> +
+
+
你好,{user?.nickname || '用户'}
+
今天感觉如何?
+
+ +
{/* Health Overview */} - {bpStats?.latest && hrStats?.latest ? ( - -
- 健康概览 - 最新记录 -
-
-
- 血压 + +
+ 健康概览 + 最新记录 +
+
+
+ 血压 + {systolic ? (
- + {systolic} / - + {diastolic}
- mmHg -
-
-
- 心率 - {Number(hrStats.latest.value)} - bpm -
+ ) : --/--} + mmHg
- - ) : ( - - )} +
+
+ 心率 + {hrStats?.latest ? Number(hrStats.latest.value) : '--'} + bpm +
+
+
+ 血糖 + {sugarStats?.latest ? Number(sugarStats.latest.value) : '--'} + mmol/L +
+
+
+ 血氧 + {spo2Stats?.latest ? Number(spo2Stats.latest.value) : '--'} + % +
+
+ {/* Quick Actions */}
{QUICK_ACTIONS.map((action) => ( - ))}
- - {/* Health Tip */} - setTipIndex((prev) => (prev + 1) % HEALTH_TIPS.length)} - > -
- 💡 - 健康小贴士 - 点击换一条 -
-

{HEALTH_TIPS[tipIndex]}

-
); } diff --git a/frontend-patient/src/pages/medication/MedicationDetailPage.tsx b/frontend-patient/src/pages/medication/MedicationDetailPage.tsx index 7281c0f..d12f572 100644 --- a/frontend-patient/src/pages/medication/MedicationDetailPage.tsx +++ b/frontend-patient/src/pages/medication/MedicationDetailPage.tsx @@ -1,33 +1,60 @@ import { useEffect, useState } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { PageHeader } from '@/components/layout/PageHeader'; import { Card } from '@/components/common/Card'; import { Button } from '@/components/common/Button'; -import { PieChart } from '@/components/charts/PieChart'; +import { ToastContainer, toast } from '@/components/common/Toast'; import * as medicationService from '@/services/medication.service'; -import type { Medication, MedicationAdherence } from '@/types'; +import type { Medication, MedicationRecord } from '@/types'; import styles from './MedicationDetailPage.module.css'; export function MedicationDetailPage() { const { id } = useParams<{ id: string }>(); - const navigate = useNavigate(); - const [medications, setMedications] = useState([]); - const [adherence, setAdherence] = useState(null); + const [med, setMed] = useState(null); + const [records, setRecords] = useState([]); + const [loading, setLoading] = useState(false); - useEffect(() => { - medicationService.getMedications().then(setMedications); - if (id) medicationService.getAdherence(id).then(setAdherence).catch(() => {}); - }, [id]); + const load = () => { + if (!id) return; + medicationService.getMedications().then((meds) => { + const m = meds.find((x) => x.id === id); + if (m) setMed(m); + }); + medicationService.getMedicationRecords(id).then(setRecords); + }; - const med = medications.find((m) => m.id === id); + useEffect(() => { load(); }, [id]); + + const today = new Date().toISOString().split('T')[0]; + const todayRecords = records.filter((r) => r.takenAt?.startsWith(today) || !r.takenAt); + const todayTaken = todayRecords.filter((r) => r.isTaken); + const todaySlots = med?.timeSlots || []; + + const handleMarkTaken = async (slot: string) => { + if (!id) return; + setLoading(true); + try { + await medicationService.markTaken(id, slot); + toast('已记录'); + load(); + } catch { toast('失败', 'error'); } + finally { setLoading(false); } + }; if (!med) { - return ( -
- -
药品不存在
-
- ); + return
药品不存在
; + } + + const slotTaken = (slot: string) => todayRecords.some((r) => r.timeSlot === slot && r.isTaken); + + // Recent 7 days adherence + const last7Days: { date: string; taken: number; total: number }[] = []; + for (let i = 6; i >= 0; i--) { + const d = new Date(); + d.setDate(d.getDate() - i); + const ds = d.toISOString().split('T')[0]; + const dayRecords = records.filter((r) => r.takenAt?.startsWith(ds) && r.isTaken); + last7Days.push({ date: ds, taken: dayRecords.length, total: todaySlots.length }); } return ( @@ -36,24 +63,69 @@ export function MedicationDetailPage() {
{med.drugName}
剂量{med.dosage}
-
服用时间{med.timeSlots.join(', ')}
+
频次{med.frequency} · {med.timeSlots.join(', ')}
日期{med.startDate} ~ {med.endDate || '长期'}
-
状态{med.status === 'active' ? '进行中' : '已结束'}
{med.notes &&
备注{med.notes}
}
- {adherence && ( - -
近30天依从性
-
{adherence.rate}%
- -
- )} + {/* Today's medication tracking */} + +
+ 今日服药 {today} +
+ {todaySlots.map((slot) => { + const taken = slotTaken(slot); + return ( +
+
+
+ {slot} + + {taken ? '已服用' : '未服用'} + +
+ {!taken && med.status === 'active' && ( + + )} +
+ ); + })} +
+ 今日已服 {todaySlots.filter((s) => slotTaken(s)).length}/{todaySlots.length} 次 +
+ + + {/* 7-day adherence */} + +
近7天记录
+
+ {last7Days.map((d) => { + const pct = d.total > 0 ? (d.taken / d.total) * 100 : 0; + return ( +
+
0 ? '#F59E0B' : '#E5E7EB', + transition: 'background 0.3s', + }} /> +
{d.date.slice(5)}
+
+ ); + })} +
+
+ 🟢 全勤🟡 漏服⬜ 未开始 +
+ +
); } diff --git a/frontend-patient/src/pages/services/FollowUpEditPage.tsx b/frontend-patient/src/pages/services/FollowUpEditPage.tsx index cac2b0b..c77c44c 100644 --- a/frontend-patient/src/pages/services/FollowUpEditPage.tsx +++ b/frontend-patient/src/pages/services/FollowUpEditPage.tsx @@ -11,21 +11,27 @@ export function FollowUpEditPage() { const navigate = useNavigate(); const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); - const [doctorName, setDoctorName] = useState(''); + const [notes, setNotes] = useState(''); const [scheduledAt, setScheduledAt] = useState(''); const [loading, setLoading] = useState(false); const handleSubmit = async () => { if (!title) { toast('请填写标题', 'error'); return; } setLoading(true); - await followupService.addFollowUp({ - title, description, - scheduledAt: scheduledAt || new Date().toISOString(), - status: 'upcoming', - reminderEnabled: true, - }); - toast('添加成功'); - navigate(-1); + try { + await followupService.addFollowUp({ + title, description, notes, + scheduledAt: scheduledAt || new Date().toISOString(), + status: 'upcoming', + reminderEnabled: true, + }); + toast('添加成功'); + navigate(-1); + } catch { + toast('添加失败', 'error'); + } finally { + setLoading(false); + } }; return ( @@ -33,12 +39,11 @@ export function FollowUpEditPage() {
setTitle(e.target.value)} placeholder="如:PCI术后3个月复查" /> - setDoctorName(e.target.value)} placeholder="医生姓名" /> - setDescription(e.target.value)} placeholder="复查描述" /> + setDescription(e.target.value)} placeholder="简单描述复查目的" /> setScheduledAt(e.target.value)} type="datetime-local" />
-