digital-human-monitor-v2/components/family-guard-actions.tsx
feie9456 747dcd359a 家庭守护圈:多人加入、角色权限、24 小时邀请链接、旧设备绑定自动兼容。
家庭动态:老人/家属发言、三种可见范围,家庭内部内容不会播给老人。
共同照护:家庭备忘录、任务创建、认领、完成与交接记录。
用药闭环:计划、未来 7 天提醒实例、确认服用、延后、异常反馈。
风险闭环:红橙黄蓝分级、30 分钟去重、推送、认领、处理与误报记录。
报平安:家属发起、老人语音回应、状态同步。
Web 导航升级为守护、家庭、照护、陪伴、我的。
设备接口已升级为设备令牌认证,UUID 不再单独承担认证。
2026-07-18 19:59:20 +08:00

74 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>;
}