diff --git a/app/api/alerts/route.ts b/app/api/alerts/route.ts new file mode 100644 index 0000000..95d6b70 --- /dev/null +++ b/app/api/alerts/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server"; +import { updateAlert } from "@/lib/family-guard"; +import { getCaregiverSession } from "@/lib/session"; + +export async function PATCH(request: Request) { + try { + const caregiver = await getCaregiverSession(); + if (!caregiver) return NextResponse.json({ error: "请先登录。" }, { status: 401 }); + const body = await request.json().catch(() => ({})) as Record; + const alert = await updateAlert(caregiver.id, String(body.alertId || ""), String(body.action || ""), body.note); + return NextResponse.json({ ok: true, alert }); + } catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "更新提醒失败。" }, { status: 400 }); } +} diff --git a/app/api/care-tasks/route.ts b/app/api/care-tasks/route.ts new file mode 100644 index 0000000..398380c --- /dev/null +++ b/app/api/care-tasks/route.ts @@ -0,0 +1,22 @@ +import { NextResponse } from "next/server"; +import { createCareTask, updateCareTask } from "@/lib/family-guard"; +import { getCaregiverSession } from "@/lib/session"; + +export async function POST(request: Request) { + try { + const caregiver = await getCaregiverSession(); + if (!caregiver) return NextResponse.json({ error: "请先登录。" }, { status: 401 }); + const task = await createCareTask(caregiver.id, await request.json().catch(() => ({}))); + return NextResponse.json({ ok: true, task }); + } catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "创建任务失败。" }, { status: 400 }); } +} + +export async function PATCH(request: Request) { + try { + const caregiver = await getCaregiverSession(); + if (!caregiver) return NextResponse.json({ error: "请先登录。" }, { status: 401 }); + const body = await request.json().catch(() => ({})) as Record; + const task = await updateCareTask(caregiver.id, String(body.taskId || ""), String(body.action || ""), body.note); + return NextResponse.json({ ok: true, task }); + } catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "更新任务失败。" }, { status: 400 }); } +} diff --git a/app/api/check-ins/route.ts b/app/api/check-ins/route.ts new file mode 100644 index 0000000..8e103a5 --- /dev/null +++ b/app/api/check-ins/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from "next/server"; +import { createCheckIn } from "@/lib/family-guard"; +import { getCaregiverSession } from "@/lib/session"; + +export async function POST(request: Request) { + try { + const caregiver = await getCaregiverSession(); + if (!caregiver) return NextResponse.json({ error: "请先登录。" }, { status: 401 }); + const checkIn = await createCheckIn(caregiver.id, await request.json().catch(() => ({}))); + return NextResponse.json({ ok: true, checkIn }); + } catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "发起报平安失败。" }, { status: 400 }); } +} diff --git a/app/api/device/agenda/route.ts b/app/api/device/agenda/route.ts new file mode 100644 index 0000000..c005ea3 --- /dev/null +++ b/app/api/device/agenda/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from "next/server"; +import { requireDeviceAuth } from "@/lib/device-auth"; +import { getDeviceAgenda } from "@/lib/family-guard"; + +export async function GET(request: Request) { + const deviceUuid = new URL(request.url).searchParams.get("deviceUuid") || ""; + try { + await requireDeviceAuth(request, deviceUuid); + const agenda = await getDeviceAgenda(deviceUuid); + return NextResponse.json({ + circleName: agenda.circle?.name || null, + posts: agenda.posts.map((post) => ({ id: post.id, content: post.content, authorName: post.author?.nickname || post.author?.username || "家里人", createdAt: post.createdAt })), + memos: agenda.memos.map((memo) => ({ id: memo.id, title: memo.title, content: memo.content, priority: memo.priority })), + doses: agenda.doses.map((dose) => ({ id: dose.id, scheduledFor: dose.scheduledFor, status: dose.status, name: dose.medicationPlan.name, dosage: dose.medicationPlan.dosage, instructions: dose.medicationPlan.instructions })), + checkIns: agenda.checkIns, + }); + } catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "读取今日照护事项失败。" }, { status: 401 }); } +} diff --git a/app/api/device/alerts/route.ts b/app/api/device/alerts/route.ts new file mode 100644 index 0000000..29b13ae --- /dev/null +++ b/app/api/device/alerts/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from "next/server"; +import { requireDeviceAuth } from "@/lib/device-auth"; +import { createSafetyAlertForDevice } from "@/lib/family-guard"; +import { sendGuardAlertPush } from "@/lib/push"; + +export async function POST(request: Request) { + const body = await request.json().catch(() => ({})) as Record; + const deviceUuid = typeof body.deviceUuid === "string" ? body.deviceUuid : ""; + try { + await requireDeviceAuth(request, deviceUuid); + const result = await createSafetyAlertForDevice(deviceUuid, body); + if (!result.deduplicated) void sendGuardAlertPush(result.alert.id).catch(() => undefined); + return NextResponse.json({ ok: true, alertId: result.alert.id, deduplicated: result.deduplicated }); + } catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "创建风险提醒失败。" }, { status: 400 }); } +} diff --git a/app/api/device/content/route.ts b/app/api/device/content/route.ts new file mode 100644 index 0000000..9a0d612 --- /dev/null +++ b/app/api/device/content/route.ts @@ -0,0 +1 @@ +export { GET } from "@/app/api/device/agenda/route"; diff --git a/app/api/device/conversations/route.ts b/app/api/device/conversations/route.ts index 3214e5c..7b68921 100644 --- a/app/api/device/conversations/route.ts +++ b/app/api/device/conversations/route.ts @@ -2,6 +2,7 @@ import { ConversationRole } from "@prisma/client"; import { NextResponse } from "next/server"; import { recordConversationTurns } from "@/lib/monitor-data"; +import { requireDeviceAuth } from "@/lib/device-auth"; function toConversationRole(rawRole: unknown) { switch (rawRole) { @@ -55,6 +56,7 @@ export async function POST(request: Request) { } try { + await requireDeviceAuth(request, body.deviceUuid); const result = await recordConversationTurns({ deviceUuid: body.deviceUuid, sessionId: typeof body.sessionId === "string" ? body.sessionId : undefined, @@ -71,4 +73,4 @@ export async function POST(request: Request) { { status: 400 }, ); } -} \ No newline at end of file +} diff --git a/app/api/device/events/route.ts b/app/api/device/events/route.ts new file mode 100644 index 0000000..26b524e --- /dev/null +++ b/app/api/device/events/route.ts @@ -0,0 +1,28 @@ +import { CheckInStatus, MedicationDoseStatus } from "@prisma/client"; +import { NextResponse } from "next/server"; +import { requireDeviceAuth } from "@/lib/device-auth"; +import { respondCheckIn, updateMedicationDose } from "@/lib/family-guard"; + +export async function POST(request: Request) { + const body = await request.json().catch(() => ({})) as Record; + const deviceUuid = typeof body.deviceUuid === "string" ? body.deviceUuid : ""; + try { + await requireDeviceAuth(request, deviceUuid); + if (body.type === "MEDICATION") { + const allowed = Object.values(MedicationDoseStatus); + const status = allowed.includes(body.status as MedicationDoseStatus) ? body.status as MedicationDoseStatus : MedicationDoseStatus.NEEDS_FAMILY_CONFIRMATION; + const dose = await updateMedicationDose(deviceUuid, String(body.doseId || ""), status, body.note, Number(body.snoozeMinutes || 10)); + return NextResponse.json({ ok: true, dose }); + } + if (body.type === "CHECK_IN") { + const status = body.status === CheckInStatus.SAFE + ? CheckInStatus.SAFE + : body.status === CheckInStatus.DECLINED + ? CheckInStatus.DECLINED + : CheckInStatus.NEEDS_CONTACT; + const checkIn = await respondCheckIn(deviceUuid, String(body.checkInId || ""), status, body.responseText); + return NextResponse.json({ ok: true, checkIn }); + } + throw new Error("不支持的设备事件类型。"); + } catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "保存照护反馈失败。" }, { status: 400 }); } +} diff --git a/app/api/device/messages/read/route.ts b/app/api/device/messages/read/route.ts index 081486f..2f9a2a3 100644 --- a/app/api/device/messages/read/route.ts +++ b/app/api/device/messages/read/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from "next/server"; import { markFamilyMessagesAsRead } from "@/lib/monitor-data"; +import { requireDeviceAuth } from "@/lib/device-auth"; export async function POST(request: Request) { const body = (await request.json().catch(() => null)) as @@ -24,6 +25,7 @@ export async function POST(request: Request) { : undefined; try { + await requireDeviceAuth(request, body.deviceUuid); await markFamilyMessagesAsRead(body.deviceUuid, messageIds); return NextResponse.json({ ok: true }); @@ -36,4 +38,4 @@ export async function POST(request: Request) { { status: 400 }, ); } -} \ No newline at end of file +} diff --git a/app/api/device/messages/route.ts b/app/api/device/messages/route.ts index fb04321..e30bce9 100644 --- a/app/api/device/messages/route.ts +++ b/app/api/device/messages/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { listFamilyMessagesForDevice } from "@/lib/monitor-data"; +import { requireDeviceAuth } from "@/lib/device-auth"; export async function GET(request: NextRequest) { const rawDeviceValue = @@ -20,6 +21,7 @@ export async function GET(request: NextRequest) { } try { + await requireDeviceAuth(request, rawDeviceValue); const data = await listFamilyMessagesForDevice(rawDeviceValue, take); return NextResponse.json({ @@ -45,4 +47,4 @@ export async function GET(request: NextRequest) { { status: 400 }, ); } -} \ No newline at end of file +} diff --git a/app/api/device/register/route.ts b/app/api/device/register/route.ts index 6719292..81c6196 100644 --- a/app/api/device/register/route.ts +++ b/app/api/device/register/route.ts @@ -1,6 +1,8 @@ import { NextResponse } from "next/server"; import { createBindUrl, ensureDeviceRegistration } from "@/lib/monitor-data"; +import { createDeviceToken, hashDeviceToken } from "@/lib/device-auth"; +import { prisma } from "@/lib/prisma"; export async function POST(request: Request) { const body = (await request.json().catch(() => null)) as @@ -8,6 +10,7 @@ export async function POST(request: Request) { deviceUuid?: string; displayName?: string; appVersion?: string; + deviceToken?: string; } | null; @@ -26,10 +29,20 @@ export async function POST(request: Request) { appVersion: typeof body.appVersion === "string" ? body.appVersion : undefined, }); + const suppliedToken = typeof body.deviceToken === "string" ? body.deviceToken.trim() : ""; + const tokenMatches = suppliedToken && device.deviceTokenHash === hashDeviceToken(suppliedToken); + if (device.deviceTokenHash && !tokenMatches) { + return NextResponse.json({ error: "设备认证失败,不能覆盖已注册设备。" }, { status: 401 }); + } + const deviceToken = tokenMatches ? suppliedToken : createDeviceToken(); + if (!device.deviceTokenHash) { + await prisma.elderDevice.update({ where: { id: device.id }, data: { deviceTokenHash: hashDeviceToken(deviceToken) } }); + } return NextResponse.json({ deviceUuid: device.deviceUuid, bindUrl: createBindUrl(device.deviceUuid), + deviceToken, }); } catch (error) { return NextResponse.json( @@ -40,4 +53,4 @@ export async function POST(request: Request) { { status: 500 }, ); } -} \ No newline at end of file +} diff --git a/app/api/device/tool-call/route.ts b/app/api/device/tool-call/route.ts index 59b1c72..1b21995 100644 --- a/app/api/device/tool-call/route.ts +++ b/app/api/device/tool-call/route.ts @@ -2,6 +2,7 @@ import { ToolCallStatus } from "@prisma/client"; import { NextResponse } from "next/server"; import { recordToolCall } from "@/lib/monitor-data"; +import { requireDeviceAuth } from "@/lib/device-auth"; function toToolCallStatus(rawStatus: unknown) { return rawStatus === ToolCallStatus.FAILURE @@ -49,6 +50,7 @@ export async function POST(request: Request) { } try { + await requireDeviceAuth(request, body.deviceUuid); const log = await recordToolCall({ deviceUuid: body.deviceUuid, toolName: body.toolName, @@ -70,4 +72,4 @@ export async function POST(request: Request) { { status: 400 }, ); } -} \ No newline at end of file +} diff --git a/app/api/device/usage/route.ts b/app/api/device/usage/route.ts index 36aae3f..2ac8a9d 100644 --- a/app/api/device/usage/route.ts +++ b/app/api/device/usage/route.ts @@ -2,6 +2,7 @@ import { UsageEventType } from "@prisma/client"; import { NextResponse } from "next/server"; import { recordUsageEvent } from "@/lib/monitor-data"; +import { requireDeviceAuth } from "@/lib/device-auth"; function toUsageEventType(rawType: unknown) { switch (rawType) { @@ -46,6 +47,7 @@ export async function POST(request: Request) { } try { + await requireDeviceAuth(request, body.deviceUuid); await recordUsageEvent({ deviceUuid: body.deviceUuid, eventType: toUsageEventType(body.eventType), @@ -62,4 +64,4 @@ export async function POST(request: Request) { { status: 400 }, ); } -} \ No newline at end of file +} diff --git a/app/api/family-circle/invite/route.ts b/app/api/family-circle/invite/route.ts new file mode 100644 index 0000000..2e235a4 --- /dev/null +++ b/app/api/family-circle/invite/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from "next/server"; +import { createFamilyInvitation } from "@/lib/family-guard"; +import { getCaregiverSession } from "@/lib/session"; + +export async function POST(request: Request) { + try { + const caregiver = await getCaregiverSession(); + if (!caregiver) return NextResponse.json({ error: "请先登录。" }, { status: 401 }); + const result = await createFamilyInvitation(caregiver.id, await request.json().catch(() => ({}))); + return NextResponse.json({ ok: true, ...result }); + } catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "创建邀请失败。" }, { status: 400 }); } +} diff --git a/app/api/family-circle/join/route.ts b/app/api/family-circle/join/route.ts new file mode 100644 index 0000000..c253a1b --- /dev/null +++ b/app/api/family-circle/join/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server"; +import { acceptFamilyInvitation } from "@/lib/family-guard"; +import { getCaregiverSession } from "@/lib/session"; + +export async function POST(request: Request) { + try { + const caregiver = await getCaregiverSession(); + if (!caregiver) return NextResponse.json({ error: "请先登录后接受邀请。" }, { status: 401 }); + const body = await request.json().catch(() => ({})) as { token?: string }; + const circle = await acceptFamilyInvitation(caregiver.id, body.token || ""); + return NextResponse.json({ ok: true, circleId: circle.id }); + } catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "加入家庭失败。" }, { status: 400 }); } +} diff --git a/app/api/family-feed/route.ts b/app/api/family-feed/route.ts new file mode 100644 index 0000000..631b455 --- /dev/null +++ b/app/api/family-feed/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server"; +import { createFamilyPost } from "@/lib/family-guard"; +import { getCaregiverSession } from "@/lib/session"; + +export async function POST(request: Request) { + try { + const caregiver = await getCaregiverSession(); + if (!caregiver) return NextResponse.json({ error: "请先登录。" }, { status: 401 }); + const body = await request.json().catch(() => ({})); + const post = await createFamilyPost(caregiver.id, body); + return NextResponse.json({ ok: true, post }); + } catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "发布失败。" }, { status: 400 }); } +} diff --git a/app/api/medications/route.ts b/app/api/medications/route.ts new file mode 100644 index 0000000..dbca30a --- /dev/null +++ b/app/api/medications/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from "next/server"; +import { createMedicationPlan } from "@/lib/family-guard"; +import { getCaregiverSession } from "@/lib/session"; + +export async function POST(request: Request) { + try { + const caregiver = await getCaregiverSession(); + if (!caregiver) return NextResponse.json({ error: "请先登录。" }, { status: 401 }); + const plan = await createMedicationPlan(caregiver.id, await request.json().catch(() => ({}))); + return NextResponse.json({ ok: true, plan }); + } catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "创建用药计划失败。" }, { status: 400 }); } +} diff --git a/app/api/memos/route.ts b/app/api/memos/route.ts new file mode 100644 index 0000000..4ef8969 --- /dev/null +++ b/app/api/memos/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from "next/server"; +import { createFamilyMemo } from "@/lib/family-guard"; +import { getCaregiverSession } from "@/lib/session"; + +export async function POST(request: Request) { + try { + const caregiver = await getCaregiverSession(); + if (!caregiver) return NextResponse.json({ error: "请先登录。" }, { status: 401 }); + const memo = await createFamilyMemo(caregiver.id, await request.json().catch(() => ({}))); + return NextResponse.json({ ok: true, memo }); + } catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "保存失败。" }, { status: 400 }); } +} diff --git a/app/care/page.tsx b/app/care/page.tsx new file mode 100644 index 0000000..c23d71d --- /dev/null +++ b/app/care/page.tsx @@ -0,0 +1,15 @@ +import { MedicationForm, MemoForm, TaskActions, TaskForm } from "@/components/family-guard-actions"; +import { PanelShell } from "@/components/panel-shell"; +import { requireFamilyWorkspace } from "@/lib/family-page"; +import { formatDateTime } from "@/lib/panel-format"; +import { requireCaregiverSession } from "@/lib/page-auth"; +import { getCaregiverDisplayName } from "@/lib/session"; + +export const dynamic = "force-dynamic"; +export default async function CarePage({ searchParams }: PageProps<"/care">) { + const caregiver = await requireCaregiverSession("/care"); const workspace = await requireFamilyWorkspace(caregiver.id); const tab = String((await searchParams).tab || "tasks"); const uuid = workspace.elderProfile.elderDevice.deviceUuid; + return + + {tab === "medications" ? <>

用药计划

仅提醒与记录,不替代医嘱
{workspace.medicationPlans.length ? workspace.medicationPlans.map((plan) =>
{plan.name}{plan.active ? "进行中" : "已暂停"}

每次 {plan.dosage} · {JSON.parse(plan.timesJson).join("、")}{plan.instructions ? ` · ${plan.instructions}` : ""}

信息来源:{plan.source === "PRESCRIPTION" ? "医生处方" : plan.source === "PACKAGE_LABEL" ? "药盒标签" : "家属录入"}

) :
还没有用药计划。
}
: tab === "memos" ? <>

家庭备忘录

{workspace.memos.length ? workspace.memos.map((memo) =>
{memo.title}{memo.priority === "PINNED" ? "长期置顶" : memo.priority === "IMPORTANT" ? "重要" : "普通"}

{memo.content}

{memo.visibility === "FAMILY_ONLY" ? "仅家人可见" : "可由数字人告诉长辈"}

) :
还没有家庭备忘。
}
: <>

照护交接

{workspace.tasks.length ? workspace.tasks.map((task) =>
{task.title}{task.dueAt ? formatDateTime(task.dueAt) : "未设期限"}

{task.description || "等待家人认领"}

{task.assignedTo ? `负责人:${task.assignedTo.nickname || task.assignedTo.username || "家人"}` : "尚未认领"}

) :
还没有照护任务。
}
} +
; +} diff --git a/app/companion/page.tsx b/app/companion/page.tsx new file mode 100644 index 0000000..16aae14 --- /dev/null +++ b/app/companion/page.tsx @@ -0,0 +1,8 @@ +import Link from "next/link"; +import { BookHeart, MessageCircle, MessagesSquare } from "lucide-react"; +import { PanelShell } from "@/components/panel-shell"; +import { requireFamilyWorkspace as getFamilyWorkspace } from "@/lib/family-page"; +import { requireCaregiverSession } from "@/lib/page-auth"; +import { getCaregiverDisplayName } from "@/lib/session"; + +export default async function CompanionPage() { const caregiver = await requireCaregiverSession("/companion"); const workspace = await getFamilyWorkspace(caregiver.id); return
双向留言家人问候由数字人口播,长辈口述实时送达陪伴动态查看数字人对话和智能服务记录
家庭记忆册数据结构已预留,作为下一阶段增强功能

本周家庭连接

家庭发布了 {workspace.posts.length} 条动态,留下 {workspace.memos.length} 条共同记忆与备忘。

; } diff --git a/app/family/join/page.tsx b/app/family/join/page.tsx new file mode 100644 index 0000000..e2545fd --- /dev/null +++ b/app/family/join/page.tsx @@ -0,0 +1,8 @@ +import { JoinFamilyButton } from "@/components/family-guard-actions"; +import { requireCaregiverSession } from "@/lib/page-auth"; + +export default async function JoinFamilyPage({ searchParams }: PageProps<"/family/join">) { + const token = String((await searchParams).token || ""); + await requireCaregiverSession(`/family/join?token=${encodeURIComponent(token)}`); + return

加入家庭守护圈

加入后,你将根据邀请角色参与长辈的家庭动态和共同照护。

; +} diff --git a/app/family/page.tsx b/app/family/page.tsx new file mode 100644 index 0000000..081b905 --- /dev/null +++ b/app/family/page.tsx @@ -0,0 +1,18 @@ +import { FamilyPostForm, InviteMemberForm } from "@/components/family-guard-actions"; +import { PanelShell } from "@/components/panel-shell"; +import { requireFamilyWorkspace } from "@/lib/family-page"; +import { formatDateTime } from "@/lib/panel-format"; +import { requireCaregiverSession } from "@/lib/page-auth"; +import { getCaregiverDisplayName } from "@/lib/session"; + +export const dynamic = "force-dynamic"; +export default async function FamilyPage() { + const caregiver = await requireCaregiverSession("/family"); const workspace = await requireFamilyWorkspace(caregiver.id); const deviceUuid = workspace.elderProfile.elderDevice.deviceUuid; + const roleLabel = { ADMIN: "管理员", CAREGIVER: "照护成员", CARING: "关怀成员" } as const; + return + +

家庭动态

{workspace.posts.length ? workspace.posts.map((post) =>
{post.elderAuthored ? `${workspace.elderProfile.preferredName}通过安智伴说` : post.author?.nickname || post.author?.username || "家里人"}{formatDateTime(post.createdAt)}

{post.content}

{post.visibility === "FAMILY_ONLY" ? "仅家人" : post.visibility === "ELDER_ONLY" ? "只给长辈" : "全家可见"}
) :
发出第一条家庭动态,让关心流动起来。
}
+

家庭成员

{workspace.memberships.map((member) =>
{(member.caregiver.nickname || member.caregiver.username || "家").slice(-1)}{member.caregiver.nickname || member.caregiver.username || "家庭成员"}{member.relationLabel ? ` · ${member.relationLabel}` : ""}{roleLabel[member.role]}{member.dutyOrder ? ` · 值守顺序 ${member.dutyOrder}` : ""}
)}
+

邀请家人

链接 24 小时有效
+
; +} diff --git a/app/guard/page.tsx b/app/guard/page.tsx new file mode 100644 index 0000000..78d93be --- /dev/null +++ b/app/guard/page.tsx @@ -0,0 +1,23 @@ +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"; +import { formatDateTime } from "@/lib/panel-format"; +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 +
{openAlerts.length}待处理提醒
{doneDoses}/{todayDoses.length}今日用药
{openTasks.length}照护事项
{pendingCheckIns.length}待报平安
+ +

风险提醒

人工确认后再处置
{workspace.alerts.length ? workspace.alerts.map((alert) =>
{alert.title}{formatDateTime(alert.createdAt)}

{alert.summary}

{alert.claimedBy ? `已由 ${alert.claimedBy.nickname || alert.claimedBy.username || "家人"} 接手` : "尚未认领"}

) :

当前没有待处理的风险提醒
}
+

今日状态

设备最近在线{formatDateTime(workspace.elderProfile.elderDevice.lastSeenAt)}

仅根据终端在线和主动交互记录展示,不代表生命体征状态。

家庭值守

{workspace.memberships.filter((m) => m.emergencyPush).map((m) => m.caregiver.nickname || m.caregiver.username || "家人").join("、") || "尚未设置"}

+
; +} diff --git a/app/page.tsx b/app/page.tsx index 8d5658b..ca3ccee 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,86 +1,2 @@ -import Link from "next/link"; -import { ChevronRight, PenLine, ScanLine } from "lucide-react"; - -import { PanelShell } from "@/components/panel-shell"; -import { getCaregiverOverview } from "@/lib/caregiver-panel"; -import { formatDateTime, getDeviceName, getMessageHref, truncateText } from "@/lib/panel-format"; -import { requireCaregiverSession } from "@/lib/page-auth"; -import { getCaregiverDisplayName } from "@/lib/session"; - -export const dynamic = "force-dynamic"; - -function greeting() { - const hour = new Date().getHours(); - if (hour < 11) return "早上好"; - if (hour < 14) return "中午好"; - if (hour < 18) return "下午好"; - return "晚上好"; -} - -export default async function Home() { - const caregiver = await requireCaregiverSession("/"); - const { dashboard, recentIncomingMessages } = await getCaregiverOverview(caregiver.id); - const name = getCaregiverDisplayName(dashboard.caregiver); - const unreadIn = dashboard.devices.reduce((n, d) => n + d.elderUnreadCount, 0); - const unreadOut = dashboard.devices.reduce((n, d) => n + d.familyUnreadCount, 0); - const conversations = dashboard.devices.reduce((n, d) => n + d.elderDevice._count.conversationTurns, 0); - const date = new Intl.DateTimeFormat("zh-CN", { month: "numeric", day: "numeric", weekday: "short" }).format(new Date()); - - return ( - -
- {dashboard.devices.length ? dashboard.devices.map((binding) => ( - - - {getDeviceName(binding.alias, binding.elderDevice.displayName)} - {binding.elderDevice.lastSeenAt ? "在线" : "未连接"} - - - )) :
还没有连接设备,扫码即可开始守护。
} -
- -
- {unreadIn}新留言 - {unreadOut}待长辈听 - {conversations}陪伴对话 - {dashboard.devices.length}设备 -
- -
- - 给长辈留言 - - 扫码绑定 -
- -
-

长辈的新留言

全部
-
- {recentIncomingMessages.length ? recentIncomingMessages.slice(0, 3).map((message) => ( - - - - {truncateText(message.content, 62)} - {message.importance !== "NORMAL" ? 重要 : null}{getDeviceName(message.elderDevice.displayName)} · {formatDateTime(message.createdAt)} - - - )) :
暂时还没有收到新留言。
} -
-
- -
-

陪伴动态

全部
-
-
设备连接情况现在

{dashboard.devices.length} 台长辈设备已加入家人守护。

-
陪伴对话累计

数字人已完成 {conversations} 轮陪伴对话。

-
-
-
- ); -} +import { redirect } from "next/navigation"; +export default function Home() { redirect("/guard"); } diff --git a/components/family-guard-actions.tsx b/components/family-guard-actions.tsx new file mode 100644 index 0000000..4ef16a2 --- /dev/null +++ b/components/family-guard-actions.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { FormEvent, useState } from "react"; + +async function api(path: string, method: string, body: Record) { + 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 ?

{text}

: null; } + +export function FamilyPostForm({ deviceUuid }: { deviceUuid: string }) { + const router = useRouter(); const [notice, setNotice] = useState(""); const [busy, setBusy] = useState(false); + async function submit(event: FormEvent) { + 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