fix: medication time slot picker, auto-expire, red dot logic, home greeting position
This commit is contained in:
@@ -40,10 +40,9 @@ export function HomePage() {
|
||||
|
||||
return (
|
||||
<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 style={{ fontSize: 20, fontWeight: 700 }}>你好,{user?.nickname || '用户'}</div>
|
||||
<div style={{ fontSize: 12, color: '#9CA3AF', marginTop: 2 }}>今天感觉如何?</div>
|
||||
<div style={{ fontSize: 22, fontWeight: 700, color: '#1A1E2B' }}>你好,{user?.nickname || '用户'}</div>
|
||||
</div>
|
||||
<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>}
|
||||
|
||||
@@ -10,33 +10,33 @@ import styles from './MedicationEditPage.module.css';
|
||||
|
||||
export function MedicationEditPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [drugName, setDrugName] = useState('');
|
||||
const [dosage, setDosage] = useState('');
|
||||
const [frequency, setFrequency] = useState('每日1次');
|
||||
const [timeSlots, setTimeSlots] = useState(['08:00']);
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [notes, setNotes] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const freqLabels: Record<string, string> = {
|
||||
'每日1次': '每日一次', '每日2次': '每日两次', '每日3次': '每日三次',
|
||||
const addTimeSlot = () => setTimeSlots([...timeSlots, '12:00']);
|
||||
const removeTimeSlot = (i: number) => {
|
||||
if (timeSlots.length <= 1) return;
|
||||
setTimeSlots(timeSlots.filter((_, idx) => idx !== i));
|
||||
};
|
||||
|
||||
const handleFreqChange = (f: string) => {
|
||||
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 updateTimeSlot = (i: number, val: string) => {
|
||||
setTimeSlots(timeSlots.map((s, idx) => idx === i ? val : s));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!drugName || !dosage) { toast('请填写药品名和剂量', 'error'); return; }
|
||||
if (timeSlots.length === 0) { toast('请设置至少一个服药时间', 'error'); return; }
|
||||
const sorted = [...timeSlots].sort();
|
||||
setLoading(true);
|
||||
try {
|
||||
await medicationService.addMedication({
|
||||
drugName, dosage, frequency, timeSlots,
|
||||
drugName, dosage,
|
||||
frequency: `每日${sorted.length}次`,
|
||||
timeSlots: sorted,
|
||||
startDate: startDate || new Date().toISOString().slice(0, 10),
|
||||
endDate: endDate || undefined,
|
||||
notes,
|
||||
@@ -44,9 +44,7 @@ export function MedicationEditPage() {
|
||||
});
|
||||
toast('添加成功');
|
||||
navigate(-1);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} finally { setLoading(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -57,45 +55,37 @@ export function MedicationEditPage() {
|
||||
<label className={styles.sectionLabel}>药品名称</label>
|
||||
<div className={styles.drugGrid}>
|
||||
{COMMON_DRUGS.slice(0, 6).map((d) => (
|
||||
<button
|
||||
key={d}
|
||||
className={`${styles.drugChip} ${drugName === d ? styles.drugChipActive : ''}`}
|
||||
onClick={() => setDrugName(d)}
|
||||
>
|
||||
{d}
|
||||
</button>
|
||||
<button key={d} className={`${styles.drugChip} ${drugName === d ? styles.drugChipActive : ''}`} onClick={() => setDrugName(d)}>{d}</button>
|
||||
))}
|
||||
</div>
|
||||
<Input placeholder="或手动输入药品名" value={drugName} onChange={(e) => setDrugName(e.target.value)} />
|
||||
<Input placeholder="或手动输入" value={drugName} onChange={(e) => setDrugName(e.target.value)} />
|
||||
</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}>
|
||||
<label className={styles.sectionLabel}>服用频次</label>
|
||||
<div className={styles.freqRow}>
|
||||
{(['每日1次', '每日2次', '每日3次'] as const).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
className={`${styles.freqBtn} ${frequency === f ? styles.freqActive : ''}`}
|
||||
onClick={() => handleFreqChange(f)}
|
||||
>
|
||||
{freqLabels[f]}
|
||||
</button>
|
||||
))}
|
||||
<label className={styles.sectionLabel}>服药时间</label>
|
||||
{timeSlots.map((slot, i) => (
|
||||
<div key={i} style={{ display: 'flex', gap: 8, marginBottom: 8, alignItems: 'center' }}>
|
||||
<input type="time" value={slot} onChange={(e) => updateTimeSlot(i, e.target.value)}
|
||||
style={{ flex: 1, padding: '8px 12px', border: '1px solid #ddd', borderRadius: 8, fontSize: 14, fontFamily: 'inherit' }} />
|
||||
<button onClick={() => removeTimeSlot(i)} disabled={timeSlots.length <= 1}
|
||||
style={{ background: 'none', border: 'none', color: '#EF4444', fontSize: 18, cursor: 'pointer', padding: 4 }}>✕</button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={addTimeSlot} style={{ padding: '6px 14px', border: '1px dashed #2563EB', borderRadius: 8, background: 'none', color: '#2563EB', fontSize: 13, cursor: 'pointer' }}>
|
||||
+ 添加时间点
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.row}>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<Button variant="primary" size="lg" fullWidth loading={loading} onClick={handleSubmit}>保存</Button>
|
||||
</div>
|
||||
<ToastContainer />
|
||||
</div>
|
||||
|
||||
@@ -3,18 +3,40 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { PageHeader } from '@/components/layout/PageHeader';
|
||||
import { Card } from '@/components/common/Card';
|
||||
import { Empty } from '@/components/common/Empty';
|
||||
import { Badge } from '@/components/common/Badge';
|
||||
import * as medicationService from '@/services/medication.service';
|
||||
import type { Medication } from '@/types';
|
||||
import type { Medication, MedicationRecord } from '@/types';
|
||||
import styles from './MedicationListPage.module.css';
|
||||
|
||||
export function MedicationListPage() {
|
||||
const navigate = useNavigate();
|
||||
const [medications, setMedications] = useState<Medication[]>([]);
|
||||
const [takenMap, setTakenMap] = useState<Record<string, boolean>>({});
|
||||
const [tab, setTab] = useState<'active' | 'completed'>('active');
|
||||
|
||||
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) =>
|
||||
@@ -32,16 +54,25 @@ export function MedicationListPage() {
|
||||
{filtered.length === 0 ? (
|
||||
<Empty icon="💊" message={tab === 'active' ? '暂无进行中的用药' : '暂无已结束的用药'} />
|
||||
) : (
|
||||
filtered.map((med) => (
|
||||
filtered.map((med) => {
|
||||
const allTaken = takenMap[med.id];
|
||||
return (
|
||||
<Card key={med.id} className={styles.medCard} onClick={() => navigate(`/health/medications/${med.id}`)}>
|
||||
<div className={styles.medHeader}>
|
||||
<span className={styles.medName}>{med.drugName}</span>
|
||||
{med.status === 'active' && <Badge dot />}
|
||||
{med.status === 'active' && !allTaken && <span className={styles.unreadDot} />}
|
||||
{med.status === 'active' && allTaken && <span style={{ fontSize: 11, color: '#10B981' }}>✓</span>}
|
||||
</div>
|
||||
<div className={styles.medDosage}>{med.dosage} · {med.timeSlots.join(', ')}</div>
|
||||
<div className={styles.medNote}>{med.notes}</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')}>
|
||||
|
||||
Reference in New Issue
Block a user