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
This commit is contained in:
@@ -27,7 +27,6 @@ export function MedicationDetailPage() {
|
||||
|
||||
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) => {
|
||||
@@ -47,7 +46,6 @@ export function MedicationDetailPage() {
|
||||
|
||||
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();
|
||||
@@ -60,71 +58,116 @@ export function MedicationDetailPage() {
|
||||
return (
|
||||
<div className="page--no-tab">
|
||||
<PageHeader title={med.drugName} />
|
||||
|
||||
<Card className={styles.infoCard}>
|
||||
<div className={styles.infoTitle}>{med.drugName}</div>
|
||||
<div className={styles.infoRow}><span>剂量</span><span>{med.dosage}</span></div>
|
||||
<div className={styles.infoRow}><span>频次</span><span>{med.frequency} · {med.timeSlots.join(', ')}</span></div>
|
||||
<div className={styles.infoRow}><span>日期</span><span>{med.startDate} ~ {med.endDate || '长期'}</span></div>
|
||||
{med.notes && <div className={styles.infoRow}><span>备注</span><span>{med.notes}</span></div>}
|
||||
<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>
|
||||
|
||||
{/* Today's medication tracking */}
|
||||
{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 style={{ fontSize: 15, fontWeight: 600, marginBottom: 12 }}>
|
||||
今日服药 <span style={{ color: '#9CA3AF', fontWeight: 400, fontSize: 13 }}>{today}</span>
|
||||
</div>
|
||||
<div className={styles.todayTitle}>今日服药</div>
|
||||
<div className={styles.todayDate}>{today}</div>
|
||||
|
||||
{todaySlots.map((slot) => {
|
||||
const taken = slotTaken(slot);
|
||||
return (
|
||||
<div key={slot} style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: '10px 0', borderBottom: '1px solid #f0f0f0',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{
|
||||
width: 10, height: 10, borderRadius: 5,
|
||||
background: taken ? '#10B981' : '#E5E7EB',
|
||||
}} />
|
||||
<span style={{ fontSize: 14 }}>{slot}</span>
|
||||
<span style={{ fontSize: 12, color: taken ? '#10B981' : '#9CA3AF' }}>
|
||||
{taken ? '已服用' : '未服用'}
|
||||
</span>
|
||||
<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="outline" loading={loading} onClick={() => handleMarkTaken(slot)}>
|
||||
<Button size="sm" variant="primary" loading={loading} onClick={() => handleMarkTaken(slot)}>
|
||||
打卡
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div style={{ marginTop: 12, fontSize: 12, color: '#9CA3AF' }}>
|
||||
今日已服 {todaySlots.filter((s) => slotTaken(s)).length}/{todaySlots.length} 次
|
||||
|
||||
<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>
|
||||
|
||||
{/* 7-day adherence */}
|
||||
<Card className={styles.infoCard}>
|
||||
<div style={{ fontSize: 15, fontWeight: 600, marginBottom: 12 }}>近7天记录</div>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<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} style={{ flex: 1, textAlign: 'center' }}>
|
||||
<div style={{
|
||||
height: 40, borderRadius: 6, marginBottom: 4,
|
||||
background: pct === 100 ? '#10B981' : pct > 0 ? '#F59E0B' : '#E5E7EB',
|
||||
transition: 'background 0.3s',
|
||||
}} />
|
||||
<div style={{ fontSize: 10, color: '#9CA3AF' }}>{d.date.slice(5)}</div>
|
||||
<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 style={{ display: 'flex', gap: 12, marginTop: 8, fontSize: 11, color: '#9CA3AF' }}>
|
||||
<span style={{display:'inline-flex',alignItems:'center',gap:4}}><span style={{width:10,height:10,borderRadius:'50%',background:'#20C997',display:'inline-block'}}/> 全勤</span><span style={{display:'inline-flex',alignItems:'center',gap:4}}><span style={{width:10,height:10,borderRadius:'50%',background:'#F59E0B',display:'inline-block'}}/> 漏服</span><span style={{display:'inline-flex',alignItems:'center',gap:4}}><span style={{width:10,height:10,borderRadius:'50%',background:'#E4E8EE',display:'inline-block'}}/> 未开始</span>
|
||||
<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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user