Files
soft/frontend-patient/src/pages/medication/MedicationDetailPage.tsx
MingNian 39ab6062b5 feat: medication reminders, follow-up/visit separation, health record page
Backend:
- MedicationService: today-summary with missed detection (local time)
- FollowUpService: doctor-initiated follow-ups filter, AddAsync supports Notes
- FollowUpController: type query param (followup/recheck)
- MedicationController: today-summary endpoint
- Auth: UpdateProfileRequest→class, StentDate/StentType, soft-delete fix

Patient frontend:
- HomePage: date display, medication reminder cards with missed status
- MedicationListPage: beautified with delete button, slot preview
- MedicationDetailPage: redesigned with progress bars, new CSS
- ProfilePage: beautified menu icons, health record link
- HealthRecordPage: new page with indicators, history, meds, reports
- ServicesHub: added doctor-visit card
- VisitListPage: doctor-initiated follow-ups view
- EditProfilePage: removed height/weight, added stent fields
- Fixed getProfile field mappings (nickname, height, weight, stent)

Doctor frontend:
- Layout: added 随访管理 sidebar item with SVG icon
- FollowUpListPage: recheck-only filter, complete/delete buttons, collapsed completed
- VisitListPage/EditPage: doctor follow-up management
- PatientListPage: added stentType column
- Dashboard: fixed pending reports endpoint
- ReportListPage/DetailPage: fixed uploadedAt field
- ChatPage: SignalR real-time, dynamic hostname
2026-05-25 14:48:05 +08:00

175 lines
6.9 KiB
TypeScript

import { useEffect, useState } from 'react';
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 { ToastContainer, toast } from '@/components/common/Toast';
import * as medicationService from '@/services/medication.service';
import type { Medication, MedicationRecord } from '@/types';
import styles from './MedicationDetailPage.module.css';
export function MedicationDetailPage() {
const { id } = useParams<{ id: string }>();
const [med, setMed] = useState<Medication | null>(null);
const [records, setRecords] = useState<MedicationRecord[]>([]);
const [loading, setLoading] = useState(false);
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);
};
useEffect(() => { load(); }, [id]);
const today = new Date().toISOString().split('T')[0];
const todayRecords = records.filter((r) => r.takenAt?.startsWith(today) || !r.takenAt);
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 <div className="page--no-tab"><PageHeader title="药品详情" /><div style={{ padding: 40, textAlign: 'center', color: '#9CA3AF' }}></div></div>;
}
const slotTaken = (slot: string) => todayRecords.some((r) => r.timeSlot === slot && r.isTaken);
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 (
<div className="page--no-tab">
<PageHeader title={med.drugName} />
<Card className={styles.infoCard}>
<div className={styles.heroHeader}>
<div className={styles.heroIcon}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="4" y="5" width="16" height="14" rx="4" />
<path d="M10 9v6M14 9v6" />
</svg>
</div>
<div className={styles.heroInfo}>
<div className={styles.heroName}>{med.drugName}</div>
<div className={styles.heroMeta}>{med.dosage} · {med.frequency}</div>
</div>
</div>
<div className={styles.infoGrid}>
<div className={styles.infoItem}>
<div className={styles.infoLabel}></div>
<div className={styles.infoValue}>{med.dosage}</div>
</div>
<div className={styles.infoItem}>
<div className={styles.infoLabel}></div>
<div className={styles.infoValue}>{med.frequency}</div>
</div>
<div className={styles.infoItem}>
<div className={styles.infoLabel}></div>
<div className={styles.infoValue}>{med.timeSlots.join(', ')}</div>
</div>
<div className={styles.infoItem}>
<div className={styles.infoLabel}></div>
<div className={styles.infoValue}>{med.startDate} ~ {med.endDate || '长期'}</div>
</div>
</div>
</Card>
{med.notes && (
<Card className={styles.infoCard}>
<div className={styles.infoLabel} style={{ marginBottom: 4 }}></div>
<div style={{ fontSize: 13, color: 'var(--color-text-secondary)', lineHeight: 1.6 }}>{med.notes}</div>
</Card>
)}
<Card className={styles.infoCard}>
<div className={styles.todayTitle}></div>
<div className={styles.todayDate}>{today}</div>
{todaySlots.map((slot) => {
const taken = slotTaken(slot);
return (
<div key={slot} className={`${styles.slotRow} ${taken ? styles.slotRowTaken : styles.slotRowPending}`}>
<div className={styles.slotLeft}>
<div className={`${styles.slotCircle} ${taken ? styles.slotCircleTaken : styles.slotCirclePending}`}>
{taken ? '✓' : slot}
</div>
<div>
<div className={styles.slotTime}>{slot}</div>
<div className={styles.slotLabel}>{taken ? '已服用' : '待服用'}</div>
</div>
</div>
{!taken && med.status === 'active' && (
<Button size="sm" variant="primary" loading={loading} onClick={() => handleMarkTaken(slot)}>
</Button>
)}
</div>
);
})}
<div className={styles.todaySummary}>
<span className={styles.todayProgress}>
{todaySlots.filter((s) => slotTaken(s)).length}/{todaySlots.length}
</span>
<div className={styles.todayProgressBar}>
<div className={styles.todayProgressFill} style={{
width: `${todaySlots.length > 0 ? (todaySlots.filter((s) => slotTaken(s)).length / todaySlots.length) * 100 : 0}%`,
}} />
</div>
</div>
</Card>
<Card className={styles.infoCard}>
<div className={styles.chartTitle}>7</div>
<div className={styles.chartBars}>
{last7Days.map((d) => {
const pct = d.total > 0 ? (d.taken / d.total) * 100 : 0;
const height = d.total > 0 ? Math.max(8, (d.taken / d.total) * 60) : 8;
return (
<div key={d.date} className={styles.chartBarWrap}>
<div
className={`${styles.chartBar} ${pct === 100 ? styles.chartBarFull : pct > 0 ? styles.chartBarPartial : styles.chartBarEmpty}`}
style={{ height }}
/>
<div className={styles.chartDate}>{d.date.slice(5)}</div>
</div>
);
})}
</div>
<div className={styles.chartLegend}>
<div className={styles.chartLegendItem}>
<span className={`${styles.chartLegendDot} ${styles.chartLegendDotFull}`} />
</div>
<div className={styles.chartLegendItem}>
<span className={`${styles.chartLegendDot} ${styles.chartLegendDotPartial}`} />
</div>
<div className={styles.chartLegendItem}>
<span className={`${styles.chartLegendDot} ${styles.chartLegendDotEmpty}`} />
</div>
</div>
</Card>
<ToastContainer />
</div>
);
}