家庭动态:老人/家属发言、三种可见范围,家庭内部内容不会播给老人。 共同照护:家庭备忘录、任务创建、认领、完成与交接记录。 用药闭环:计划、未来 7 天提醒实例、确认服用、延后、异常反馈。 风险闭环:红橙黄蓝分级、30 分钟去重、推送、认领、处理与误报记录。 报平安:家属发起、老人语音回应、状态同步。 Web 导航升级为守护、家庭、照护、陪伴、我的。 设备接口已升级为设备令牌认证,UUID 不再单独承担认证。
74 lines
10 KiB
TypeScript
74 lines
10 KiB
TypeScript
"use client";
|
||
|
||
import { useRouter } from "next/navigation";
|
||
import { FormEvent, useState } from "react";
|
||
|
||
async function api(path: string, method: string, body: Record<string, unknown>) {
|
||
const response = await fetch(path, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
||
const payload = await response.json().catch(() => ({}));
|
||
if (!response.ok) throw new Error(payload.error || "操作失败,请稍后再试。");
|
||
return payload;
|
||
}
|
||
|
||
function Notice({ text }: { text: string }) { return text ? <p style={{ margin: "8px 0 0", color: "var(--success)", fontSize: 12 }}>{text}</p> : null; }
|
||
|
||
export function FamilyPostForm({ deviceUuid }: { deviceUuid: string }) {
|
||
const router = useRouter(); const [notice, setNotice] = useState(""); const [busy, setBusy] = useState(false);
|
||
async function submit(event: FormEvent<HTMLFormElement>) {
|
||
event.preventDefault(); setBusy(true); const form = new FormData(event.currentTarget);
|
||
try { await api("/api/family-feed", "POST", { deviceUuid, content: form.get("content"), visibility: form.get("visibility") }); event.currentTarget.reset(); setNotice("已发布到家庭动态。"); router.refresh(); }
|
||
catch (e) { setNotice(e instanceof Error ? e.message : "发布失败。"); } finally { setBusy(false); }
|
||
}
|
||
return <form className="card card-pad" onSubmit={submit}><textarea className="field" name="content" required maxLength={1000} placeholder="分享一句问候,或和家人同步今天的情况……" /><div className="button-row" style={{ marginTop: 8 }}><select className="field" name="visibility" defaultValue="FAMILY_AND_ELDER"><option value="FAMILY_AND_ELDER">老人和全家可见</option><option value="ELDER_ONLY">只发给老人</option><option value="FAMILY_ONLY">仅家庭内部</option></select><button className="btn primary" disabled={busy}>{busy ? "发布中" : "发布"}</button></div><Notice text={notice} /></form>;
|
||
}
|
||
|
||
export function MemoForm({ deviceUuid }: { deviceUuid: string }) {
|
||
const router = useRouter(); const [notice, setNotice] = useState("");
|
||
async function submit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); const form = new FormData(event.currentTarget); try { await api("/api/memos", "POST", { deviceUuid, title: form.get("title"), content: form.get("content"), priority: form.get("priority"), visibility: form.get("visibility") }); event.currentTarget.reset(); setNotice("家庭备忘已保存。"); router.refresh(); } catch (e) { setNotice(e instanceof Error ? e.message : "保存失败。"); } }
|
||
return <form className="card card-pad" onSubmit={submit}><input className="field" name="title" required maxLength={80} placeholder="备忘标题,如:医保卡位置" /><textarea className="field" style={{ marginTop: 8 }} name="content" required maxLength={1000} placeholder="写下家人需要共同记住的信息" /><div className="button-row" style={{ marginTop: 8 }}><select className="field" name="priority"><option value="NORMAL">普通</option><option value="IMPORTANT">重要</option><option value="PINNED">长期置顶</option></select><select className="field" name="visibility"><option value="FAMILY_AND_ELDER">可告诉老人</option><option value="FAMILY_ONLY">仅家人可见</option></select><button className="btn primary">保存</button></div><Notice text={notice} /></form>;
|
||
}
|
||
|
||
export function TaskForm({ deviceUuid }: { deviceUuid: string }) {
|
||
const router = useRouter(); const [notice, setNotice] = useState("");
|
||
async function submit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); const form = new FormData(event.currentTarget); try { await api("/api/care-tasks", "POST", { deviceUuid, title: form.get("title"), description: form.get("description"), dueAt: form.get("dueAt") }); event.currentTarget.reset(); setNotice("照护任务已创建。"); router.refresh(); } catch (e) { setNotice(e instanceof Error ? e.message : "创建失败。"); } }
|
||
return <form className="card card-pad" onSubmit={submit}><input className="field" name="title" required maxLength={100} placeholder="任务,如:陪爷爷周三复诊" /><input className="field" style={{ marginTop: 8 }} name="description" maxLength={500} placeholder="补充说明(可选)" /><div className="button-row" style={{ marginTop: 8 }}><input className="field" name="dueAt" type="datetime-local" /><button className="btn primary">创建任务</button></div><Notice text={notice} /></form>;
|
||
}
|
||
|
||
export function TaskActions({ taskId, status }: { taskId: string; status: string }) {
|
||
const router = useRouter(); const [busy, setBusy] = useState(false);
|
||
async function act(action: string) { setBusy(true); try { let note: string | undefined; if (action === "complete") note = window.prompt("补充完成说明(可留空)") || undefined; await api("/api/care-tasks", "PATCH", { taskId, action, note }); router.refresh(); } finally { setBusy(false); } }
|
||
if (status === "COMPLETED") return <span className="tag">已完成</span>;
|
||
return <div className="button-row"><button className="btn small" disabled={busy} onClick={() => void act("claim")}>{status === "CLAIMED" ? "接手人" : "我来处理"}</button><button className="btn primary small" disabled={busy} onClick={() => void act("complete")}>完成</button></div>;
|
||
}
|
||
|
||
export function MedicationForm({ deviceUuid }: { deviceUuid: string }) {
|
||
const router = useRouter(); const [notice, setNotice] = useState("");
|
||
async function submit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); const form = new FormData(event.currentTarget); try { await api("/api/medications", "POST", { deviceUuid, name: form.get("name"), dosage: form.get("dosage"), times: form.get("times"), instructions: form.get("instructions"), source: form.get("source"), stockCount: Number(form.get("stockCount") || 0) || undefined }); event.currentTarget.reset(); setNotice("未来 7 天的用药提醒已建立。"); router.refresh(); } catch (e) { setNotice(e instanceof Error ? e.message : "创建失败。"); } }
|
||
return <form className="card card-pad" onSubmit={submit}><div className="button-row"><input className="field" name="name" required placeholder="药品名称" /><input className="field" name="dosage" required placeholder="单次用量,如:1片" /></div><div className="button-row" style={{ marginTop: 8 }}><input className="field" name="times" required placeholder="时间,如:08:00,20:00" /><select className="field" name="source"><option value="PRESCRIPTION">医生处方</option><option value="PACKAGE_LABEL">药盒标签</option><option value="FAMILY_ENTRY">家属录入</option></select></div><input className="field" style={{ marginTop: 8 }} name="instructions" placeholder="饭前/饭后及其他注意事项" /><input className="field" style={{ marginTop: 8 }} name="stockCount" type="number" min="0" placeholder="当前库存(可选)" /><button className="btn primary block" style={{ marginTop: 8 }}>建立提醒</button><Notice text={notice} /></form>;
|
||
}
|
||
|
||
export function CheckInButton({ deviceUuid }: { deviceUuid: string }) {
|
||
const router = useRouter(); const [notice, setNotice] = useState("");
|
||
async function create() { try { await api("/api/check-ins", "POST", { deviceUuid, prompt: "家里人想知道您今天是否安好。" }); setNotice("报平安请求已送到长辈终端。"); router.refresh(); } catch (e) { setNotice(e instanceof Error ? e.message : "发送失败。"); } }
|
||
return <div><button className="btn primary block" onClick={() => void create()}>请长辈报个平安</button><Notice text={notice} /></div>;
|
||
}
|
||
|
||
export function AlertActions({ alertId, status }: { alertId: string; status: string }) {
|
||
const router = useRouter(); const [busy, setBusy] = useState(false);
|
||
async function act(action: string) { setBusy(true); try { let note; if (action !== "claim") { note = window.prompt(action === "resolve" ? "请填写处理结果" : "请填写忽略原因") || ""; if (!note && action === "resolve") return; } await api("/api/alerts", "PATCH", { alertId, action, note }); router.refresh(); } finally { setBusy(false); } }
|
||
if (status === "RESOLVED" || status === "DISMISSED") return <span className="tag">已处理</span>;
|
||
return <div className="button-row"><button className="btn small" disabled={busy} onClick={() => void act("claim")}>我来处理</button><button className="btn primary small" disabled={busy} onClick={() => void act("resolve")}>填写结果</button><button className="btn small" disabled={busy} onClick={() => void act("dismiss")}>误报</button></div>;
|
||
}
|
||
|
||
export function InviteMemberForm({ deviceUuid }: { deviceUuid: string }) {
|
||
const [url, setUrl] = useState(""); const [notice, setNotice] = useState("");
|
||
async function submit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); const form = new FormData(event.currentTarget); try { const result = await api("/api/family-circle/invite", "POST", { deviceUuid, relationLabel: form.get("relationLabel"), role: form.get("role") }); setUrl(result.inviteUrl); setNotice("邀请链接 24 小时内有效。"); } catch (e) { setNotice(e instanceof Error ? e.message : "邀请失败。"); } }
|
||
return <form className="card card-pad" onSubmit={submit}><div className="button-row"><input className="field" name="relationLabel" placeholder="关系,如:儿子" /><select className="field" name="role"><option value="CAREGIVER">照护成员</option><option value="CARING">关怀成员</option></select><button className="btn primary">生成邀请</button></div>{url ? <div className="button-row" style={{ marginTop: 8 }}><input className="field" readOnly value={url} /><button type="button" className="btn" onClick={() => void navigator.clipboard.writeText(url)}>复制</button></div> : null}<Notice text={notice} /></form>;
|
||
}
|
||
|
||
export function JoinFamilyButton({ token }: { token: string }) {
|
||
const router = useRouter(); const [notice, setNotice] = useState("");
|
||
async function join() { try { await api("/api/family-circle/join", "POST", { token }); setNotice("已加入家庭守护圈,正在打开家庭页……"); router.push("/family"); router.refresh(); } catch (e) { setNotice(e instanceof Error ? e.message : "加入失败。"); } }
|
||
return <div><button className="btn primary block" onClick={() => void join()}>接受邀请并加入</button><Notice text={notice} /></div>;
|
||
}
|