This commit is contained in:
2026-07-19 19:45:32 +08:00
parent b68bfc6287
commit bbfc055c21
12 changed files with 910 additions and 431 deletions
+88 -12
View File
@@ -1,4 +1,5 @@
import { AlertTriangle, CheckCircle2, HeartPulse, PhoneCall } from "lucide-react";
import { AlertActions, CheckInButton } from "@/components/family-guard-actions";
import { PanelShell } from "@/components/panel-shell";
import { requireFamilyWorkspace } from "@/lib/family-page";
@@ -7,17 +8,92 @@ import { requireCaregiverSession } from "@/lib/page-auth";
import { getCaregiverDisplayName } from "@/lib/session";
export const dynamic = "force-dynamic";
export default async function GuardPage() {
const caregiver = await requireCaregiverSession("/guard"); const workspace = await requireFamilyWorkspace(caregiver.id);
const openAlerts = workspace.alerts.filter((a) => a.status === "OPEN" || a.status === "ACKNOWLEDGED");
const todayDoses = workspace.medicationPlans.flatMap((p) => p.doses).filter((d) => d.scheduledFor.toDateString() === new Date().toDateString());
const doneDoses = todayDoses.filter((d) => d.status === "TAKEN").length;
const openTasks = workspace.tasks.filter((t) => t.status !== "COMPLETED" && t.status !== "CANCELLED");
const pendingCheckIns = workspace.checkIns.filter((c) => c.status === "PENDING");
return <PanelShell currentPath="/guard" title="家庭守护" description={`${workspace.elderProfile.preferredName} · 风险线索不是医疗诊断`} caregiverLabel={getCaregiverDisplayName(caregiver)}>
<section className="stats" style={{ gridTemplateColumns: "repeat(4,1fr)" }}><div className="stat accent"><strong>{openAlerts.length}</strong><span>待处理提醒</span></div><div className="stat"><strong>{doneDoses}/{todayDoses.length}</strong><span>今日用药</span></div><div className="stat"><strong>{openTasks.length}</strong><span>照护事项</span></div><div className="stat"><strong>{pendingCheckIns.length}</strong><span>待报平安</span></div></section>
<CheckInButton deviceUuid={workspace.elderProfile.elderDevice.deviceUuid} />
<section><div className="section-title"><h2>风险提醒</h2><span>人工确认后再处置</span></div><div className="card">{workspace.alerts.length ? workspace.alerts.map((alert) => <article id={`alert-${alert.id}`} key={alert.id} className="timeline-item"><div className="timeline-top"><span style={{ color: alert.level === "RED" ? "#b33a2b" : "var(--accent)" }}><AlertTriangle size={15} style={{ verticalAlign: -3, marginRight: 5 }} />{alert.title}</span><span className="timeline-time">{formatDateTime(alert.createdAt)}</span></div><p className="timeline-body">{alert.summary}</p><p className="row-meta">{alert.claimedBy ? `已由 ${alert.claimedBy.nickname || alert.claimedBy.username || "家人"} 接手` : "尚未认领"}</p><AlertActions alertId={alert.id} status={alert.status} /></article>) : <div className="empty"><CheckCircle2 size={26} /><br />当前没有待处理的风险提醒</div>}</div></section>
<section><div className="section-title"><h2>今日状态</h2></div><div className="card"><div className="timeline-item"><div className="timeline-top"><HeartPulse size={15} />设备最近在线<span className="timeline-time">{formatDateTime(workspace.elderProfile.elderDevice.lastSeenAt)}</span></div><p className="timeline-body">仅根据终端在线和主动交互记录展示,不代表生命体征状态。</p></div><div className="timeline-item"><div className="timeline-top"><PhoneCall size={15} />家庭值守</div><p className="timeline-body">{workspace.memberships.filter((m) => m.emergencyPush).map((m) => m.caregiver.nickname || m.caregiver.username || "家人").join("、") || "尚未设置"}</p></div></div></section>
</PanelShell>;
const caregiver = await requireCaregiverSession("/guard");
const workspace = await requireFamilyWorkspace(caregiver.id);
const elder = workspace.elderProfile;
const device = elder.elderDevice;
const lastSeenAt = device.lastSeenAt;
const isOnline = Boolean(lastSeenAt);
const openAlerts = workspace.alerts.filter((alert) => alert.status === "OPEN" || alert.status === "ACKNOWLEDGED");
const today = new Date().toDateString();
const todayDoses = workspace.medicationPlans.flatMap((plan) => plan.doses).filter((dose) => dose.scheduledFor.toDateString() === today);
const doneDoses = todayDoses.filter((dose) => dose.status === "TAKEN").length;
const openTasks = workspace.tasks.filter((task) => task.status !== "COMPLETED" && task.status !== "CANCELLED");
const pendingCheckIns = workspace.checkIns.filter((checkIn) => checkIn.status === "PENDING");
const dutyNames = workspace.memberships
.filter((membership) => membership.emergencyPush)
.map((membership) => membership.caregiver.nickname || membership.caregiver.username || "家人");
return (
<PanelShell
currentPath="/guard"
title="家庭守护"
description={`${elder.preferredName} · 风险线索不是医疗诊断`}
caregiverLabel={getCaregiverDisplayName(caregiver)}
caregiverId={caregiver.id}
>
<section className="elder-status">
<div className="elder-status-main">
<span className="elder-avatar">{elder.preferredName.slice(0, 1)}</span>
<span className="row-main">
<span className="elder-name">
{elder.preferredName}
<span className={`dot ${isOnline ? "online" : "offline"}`} style={{ marginTop: 0 }} />
<span className="online-label" style={{ color: isOnline ? "var(--success)" : "var(--muted-light)" }}>
{isOnline ? "在线" : "离线"}
</span>
</span>
<span className="elder-meta">
{isOnline ? `最近与数字人互动 · ${device.displayName || "长辈终端"}` : `最近在线:${formatDateTime(lastSeenAt)} · ${device.displayName || "长辈终端"}`}
</span>
</span>
</div>
<CheckInButton deviceUuid={device.deviceUuid} />
</section>
<section className="stats" style={{ gridTemplateColumns: "repeat(4, 1fr)" }}>
<div className="stat accent"><strong>{openAlerts.length}</strong><span>待处理提醒</span></div>
<div className="stat"><strong>{doneDoses}/{todayDoses.length}</strong><span>今日用药</span></div>
<div className="stat"><strong>{openTasks.length}</strong><span>照护事项</span></div>
<div className="stat"><strong>{pendingCheckIns.length}</strong><span>待报平安</span></div>
</section>
<section>
<div className="section-title"><h2>风险提醒</h2><span>人工确认后再处置</span></div>
<div className="card">
{workspace.alerts.length ? workspace.alerts.map((alert) => (
<article id={`alert-${alert.id}`} key={alert.id} className="timeline-item">
<div className="timeline-top">
<span className={`alert-title${alert.level === "RED" ? " red" : ""}`}>
<AlertTriangle size={15} strokeWidth={2} />{alert.title}
</span>
<span className="timeline-time">{formatDateTime(alert.createdAt)}</span>
</div>
<p className="timeline-body">{alert.summary}</p>
<p className="row-meta">{alert.claimedBy ? `已由 ${alert.claimedBy.nickname || alert.claimedBy.username || "家人"} 接手` : "尚未认领"}</p>
<AlertActions alertId={alert.id} status={alert.status} />
</article>
)) : (
<div className="empty"><CheckCircle2 size={26} /><br />当前没有待处理的风险提醒</div>
)}
</div>
</section>
<section>
<div className="section-title"><h2>今日状态</h2></div>
<div className="card">
<article className="timeline-item">
<div className="timeline-top"><HeartPulse size={15} />设备最近在线<span className="timeline-time">{formatDateTime(lastSeenAt)}</span></div>
<p className="timeline-body">仅根据终端在线和主动交互记录展示,不代表生命体征状态。</p>
</article>
<article className="timeline-item">
<div className="timeline-top"><PhoneCall size={15} />家庭值守</div>
<p className="timeline-body">{dutyNames.length ? `${dutyNames.join("、")}已开启紧急推送` : "尚未设置家庭值守"}</p>
</article>
</div>
</section>
</PanelShell>
);
}