fix: medication time slot picker, auto-expire, red dot logic, home greeting position

This commit is contained in:
MingNian
2026-05-21 16:43:43 +08:00
parent 4c85cd50be
commit a9d70aa130
3 changed files with 76 additions and 56 deletions

View File

@@ -40,10 +40,9 @@ export function HomePage() {
return ( return (
<div className="page"> <div className="page">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}> <div style={{ padding: '16px 0', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div> <div>
<div style={{ fontSize: 20, fontWeight: 700 }}>{user?.nickname || '用户'}</div> <div style={{ fontSize: 22, fontWeight: 700, color: '#1A1E2B' }}>{user?.nickname || '用户'}</div>
<div style={{ fontSize: 12, color: '#9CA3AF', marginTop: 2 }}></div>
</div> </div>
<button onClick={() => navigate('/notifications')} className={styles.notifyBtn}> <button onClick={() => navigate('/notifications')} className={styles.notifyBtn}>
🔔{unreadCount > 0 && <span style={{ position: 'absolute', top: -2, right: -2, width: 16, height: 16, borderRadius: 8, background: '#EF4444', color: '#fff', fontSize: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 600 }}>{unreadCount}</span>} 🔔{unreadCount > 0 && <span style={{ position: 'absolute', top: -2, right: -2, width: 16, height: 16, borderRadius: 8, background: '#EF4444', color: '#fff', fontSize: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 600 }}>{unreadCount}</span>}

View File

@@ -10,33 +10,33 @@ import styles from './MedicationEditPage.module.css';
export function MedicationEditPage() { export function MedicationEditPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const [drugName, setDrugName] = useState(''); const [drugName, setDrugName] = useState('');
const [dosage, setDosage] = useState(''); const [dosage, setDosage] = useState('');
const [frequency, setFrequency] = useState('每日1次');
const [timeSlots, setTimeSlots] = useState(['08:00']); const [timeSlots, setTimeSlots] = useState(['08:00']);
const [startDate, setStartDate] = useState(''); const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState(''); const [endDate, setEndDate] = useState('');
const [notes, setNotes] = useState(''); const [notes, setNotes] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const freqLabels: Record<string, string> = { const addTimeSlot = () => setTimeSlots([...timeSlots, '12:00']);
'每日1次': '每日一次', '每日2次': '每日两次', '每日3次': '每日三次', const removeTimeSlot = (i: number) => {
if (timeSlots.length <= 1) return;
setTimeSlots(timeSlots.filter((_, idx) => idx !== i));
}; };
const updateTimeSlot = (i: number, val: string) => {
const handleFreqChange = (f: string) => { setTimeSlots(timeSlots.map((s, idx) => idx === i ? val : s));
setFrequency(f);
if (f === '每日1次' || f === 'once_daily') setTimeSlots(['08:00']);
else if (f === '每日2次' || f === 'twice_daily') setTimeSlots(['08:00', '20:00']);
else setTimeSlots(['08:00', '14:00', '20:00']);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!drugName || !dosage) { toast('请填写药品名和剂量', 'error'); return; } if (!drugName || !dosage) { toast('请填写药品名和剂量', 'error'); return; }
if (timeSlots.length === 0) { toast('请设置至少一个服药时间', 'error'); return; }
const sorted = [...timeSlots].sort();
setLoading(true); setLoading(true);
try { try {
await medicationService.addMedication({ await medicationService.addMedication({
drugName, dosage, frequency, timeSlots, drugName, dosage,
frequency: `每日${sorted.length}`,
timeSlots: sorted,
startDate: startDate || new Date().toISOString().slice(0, 10), startDate: startDate || new Date().toISOString().slice(0, 10),
endDate: endDate || undefined, endDate: endDate || undefined,
notes, notes,
@@ -44,9 +44,7 @@ export function MedicationEditPage() {
}); });
toast('添加成功'); toast('添加成功');
navigate(-1); navigate(-1);
} finally { } finally { setLoading(false); }
setLoading(false);
}
}; };
return ( return (
@@ -57,45 +55,37 @@ export function MedicationEditPage() {
<label className={styles.sectionLabel}></label> <label className={styles.sectionLabel}></label>
<div className={styles.drugGrid}> <div className={styles.drugGrid}>
{COMMON_DRUGS.slice(0, 6).map((d) => ( {COMMON_DRUGS.slice(0, 6).map((d) => (
<button <button key={d} className={`${styles.drugChip} ${drugName === d ? styles.drugChipActive : ''}`} onClick={() => setDrugName(d)}>{d}</button>
key={d}
className={`${styles.drugChip} ${drugName === d ? styles.drugChipActive : ''}`}
onClick={() => setDrugName(d)}
>
{d}
</button>
))} ))}
</div> </div>
<Input placeholder="或手动输入药品名" value={drugName} onChange={(e) => setDrugName(e.target.value)} /> <Input placeholder="或手动输入" value={drugName} onChange={(e) => setDrugName(e.target.value)} />
</div> </div>
<Input label="剂量 (如 100mg)" value={dosage} onChange={(e) => setDosage(e.target.value)} placeholder="100mg" /> <Input label="剂量" value={dosage} onChange={(e) => setDosage(e.target.value)} placeholder="100mg" />
<div className={styles.section}> <div className={styles.section}>
<label className={styles.sectionLabel}></label> <label className={styles.sectionLabel}></label>
<div className={styles.freqRow}> {timeSlots.map((slot, i) => (
{(['每日1次', '每日2次', '每日3次'] as const).map((f) => ( <div key={i} style={{ display: 'flex', gap: 8, marginBottom: 8, alignItems: 'center' }}>
<button <input type="time" value={slot} onChange={(e) => updateTimeSlot(i, e.target.value)}
key={f} style={{ flex: 1, padding: '8px 12px', border: '1px solid #ddd', borderRadius: 8, fontSize: 14, fontFamily: 'inherit' }} />
className={`${styles.freqBtn} ${frequency === f ? styles.freqActive : ''}`} <button onClick={() => removeTimeSlot(i)} disabled={timeSlots.length <= 1}
onClick={() => handleFreqChange(f)} style={{ background: 'none', border: 'none', color: '#EF4444', fontSize: 18, cursor: 'pointer', padding: 4 }}></button>
> </div>
{freqLabels[f]} ))}
</button> <button onClick={addTimeSlot} style={{ padding: '6px 14px', border: '1px dashed #2563EB', borderRadius: 8, background: 'none', color: '#2563EB', fontSize: 13, cursor: 'pointer' }}>
))} +
</div> </button>
</div> </div>
<div className={styles.row}> <div className={styles.row}>
<Input label="开始日期" value={startDate} onChange={(e) => setStartDate(e.target.value)} type="date" /> <Input label="开始日期" value={startDate} onChange={(e) => setStartDate(e.target.value)} type="date" />
<Input label="结束日期" value={endDate} onChange={(e) => setEndDate(e.target.value)} type="date" /> <Input label="结束日期(可选)" value={endDate} onChange={(e) => setEndDate(e.target.value)} type="date" />
</div> </div>
<Input label="备注 (如饭后服用)" value={notes} onChange={(e) => setNotes(e.target.value)} /> <Input label="备注" value={notes} onChange={(e) => setNotes(e.target.value)} placeholder="如:饭后服用" />
<Button variant="primary" size="lg" fullWidth loading={loading} onClick={handleSubmit}> <Button variant="primary" size="lg" fullWidth loading={loading} onClick={handleSubmit}></Button>
</Button>
</div> </div>
<ToastContainer /> <ToastContainer />
</div> </div>

View File

@@ -3,18 +3,40 @@ import { useNavigate } from 'react-router-dom';
import { PageHeader } from '@/components/layout/PageHeader'; import { PageHeader } from '@/components/layout/PageHeader';
import { Card } from '@/components/common/Card'; import { Card } from '@/components/common/Card';
import { Empty } from '@/components/common/Empty'; import { Empty } from '@/components/common/Empty';
import { Badge } from '@/components/common/Badge';
import * as medicationService from '@/services/medication.service'; import * as medicationService from '@/services/medication.service';
import type { Medication } from '@/types'; import type { Medication, MedicationRecord } from '@/types';
import styles from './MedicationListPage.module.css'; import styles from './MedicationListPage.module.css';
export function MedicationListPage() { export function MedicationListPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const [medications, setMedications] = useState<Medication[]>([]); const [medications, setMedications] = useState<Medication[]>([]);
const [takenMap, setTakenMap] = useState<Record<string, boolean>>({});
const [tab, setTab] = useState<'active' | 'completed'>('active'); const [tab, setTab] = useState<'active' | 'completed'>('active');
useEffect(() => { useEffect(() => {
medicationService.getMedications().then(setMedications); medicationService.getMedications().then(async (meds) => {
const today = new Date().toISOString().split('T')[0];
// Auto-expire: check endDate
const updated = meds.map((m) => {
if (m.status === 'active' && m.endDate && m.endDate < today) {
return { ...m, status: 'completed' as const };
}
return m;
});
setMedications(updated);
// Check which meds have all slots taken today
const map: Record<string, boolean> = {};
for (const med of updated) {
if (med.status !== 'active') continue;
try {
const records = await medicationService.getMedicationRecords(med.id);
const todayRecords = records.filter((r) => r.takenAt?.startsWith(today) && r.isTaken);
map[med.id] = todayRecords.length >= med.timeSlots.length;
} catch { map[med.id] = true; }
}
setTakenMap(map);
});
}, []); }, []);
const filtered = medications.filter((m) => const filtered = medications.filter((m) =>
@@ -32,16 +54,25 @@ export function MedicationListPage() {
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<Empty icon="💊" message={tab === 'active' ? '暂无进行中的用药' : '暂无已结束的用药'} /> <Empty icon="💊" message={tab === 'active' ? '暂无进行中的用药' : '暂无已结束的用药'} />
) : ( ) : (
filtered.map((med) => ( filtered.map((med) => {
<Card key={med.id} className={styles.medCard} onClick={() => navigate(`/health/medications/${med.id}`)}> const allTaken = takenMap[med.id];
<div className={styles.medHeader}> return (
<span className={styles.medName}>{med.drugName}</span> <Card key={med.id} className={styles.medCard} onClick={() => navigate(`/health/medications/${med.id}`)}>
{med.status === 'active' && <Badge dot />} <div className={styles.medHeader}>
</div> <span className={styles.medName}>{med.drugName}</span>
<div className={styles.medDosage}>{med.dosage} · {med.timeSlots.join(', ')}</div> {med.status === 'active' && !allTaken && <span className={styles.unreadDot} />}
<div className={styles.medNote}>{med.notes}</div> {med.status === 'active' && allTaken && <span style={{ fontSize: 11, color: '#10B981' }}></span>}
</Card> </div>
)) <div className={styles.medDosage}>{med.dosage} · {med.frequency} · {med.timeSlots.join(', ')}</div>
{med.notes && <div className={styles.medNote}>{med.notes}</div>}
{med.status === 'active' && med.endDate && (
<div style={{ fontSize: 11, color: '#9CA3AF', marginTop: 4 }}>
{med.endDate}
</div>
)}
</Card>
);
})
)} )}
<button className={styles.fab} onClick={() => navigate('/health/medications/add')}> <button className={styles.fab} onClick={() => navigate('/health/medications/add')}>