家庭守护圈:多人加入、角色权限、24 小时邀请链接、旧设备绑定自动兼容。
家庭动态:老人/家属发言、三种可见范围,家庭内部内容不会播给老人。 共同照护:家庭备忘录、任务创建、认领、完成与交接记录。 用药闭环:计划、未来 7 天提醒实例、确认服用、延后、异常反馈。 风险闭环:红橙黄蓝分级、30 分钟去重、推送、认领、处理与误报记录。 报平安:家属发起、老人语音回应、状态同步。 Web 导航升级为守护、家庭、照护、陪伴、我的。 设备接口已升级为设备令牌认证,UUID 不再单独承担认证。
This commit is contained in:
@@ -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<string, unknown>;
|
||||
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 }); }
|
||||
}
|
||||
@@ -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<string, unknown>;
|
||||
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 }); }
|
||||
}
|
||||
@@ -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 }); }
|
||||
}
|
||||
@@ -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 }); }
|
||||
}
|
||||
@@ -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<string, unknown>;
|
||||
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 }); }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { GET } from "@/app/api/device/agenda/route";
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
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 }); }
|
||||
}
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }); }
|
||||
}
|
||||
@@ -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 }); }
|
||||
}
|
||||
@@ -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 }); }
|
||||
}
|
||||
@@ -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 }); }
|
||||
}
|
||||
@@ -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 }); }
|
||||
}
|
||||
@@ -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 <PanelShell currentPath="/care" title="共同照护" description="家人分工、老人确认、结果共享" caregiverLabel={getCaregiverDisplayName(caregiver)}>
|
||||
<div className="segment"><a className={tab === "tasks" ? "active" : ""} href="/care?tab=tasks">任务</a><a className={tab === "medications" ? "active" : ""} href="/care?tab=medications">用药</a><a className={tab === "memos" ? "active" : ""} href="/care?tab=memos">备忘</a></div>
|
||||
{tab === "medications" ? <><MedicationForm deviceUuid={uuid} /><section><div className="section-title"><h2>用药计划</h2><span>仅提醒与记录,不替代医嘱</span></div><div className="card">{workspace.medicationPlans.length ? workspace.medicationPlans.map((plan) => <article className="timeline-item" key={plan.id}><div className="timeline-top">{plan.name}<span className="tag">{plan.active ? "进行中" : "已暂停"}</span></div><p className="timeline-body">每次 {plan.dosage} · {JSON.parse(plan.timesJson).join("、")}{plan.instructions ? ` · ${plan.instructions}` : ""}</p><p className="row-meta">信息来源:{plan.source === "PRESCRIPTION" ? "医生处方" : plan.source === "PACKAGE_LABEL" ? "药盒标签" : "家属录入"}</p></article>) : <div className="empty">还没有用药计划。</div>}</div></section></> : tab === "memos" ? <><MemoForm deviceUuid={uuid} /><section><div className="section-title"><h2>家庭备忘录</h2></div><div className="card">{workspace.memos.length ? workspace.memos.map((memo) => <article className="timeline-item" key={memo.id}><div className="timeline-top">{memo.title}<span className="tag">{memo.priority === "PINNED" ? "长期置顶" : memo.priority === "IMPORTANT" ? "重要" : "普通"}</span></div><p className="timeline-body">{memo.content}</p><p className="row-meta">{memo.visibility === "FAMILY_ONLY" ? "仅家人可见" : "可由数字人告诉长辈"}</p></article>) : <div className="empty">还没有家庭备忘。</div>}</div></section></> : <><TaskForm deviceUuid={uuid} /><section><div className="section-title"><h2>照护交接</h2></div><div className="card">{workspace.tasks.length ? workspace.tasks.map((task) => <article className="timeline-item" key={task.id}><div className="timeline-top">{task.title}<span className="timeline-time">{task.dueAt ? formatDateTime(task.dueAt) : "未设期限"}</span></div><p className="timeline-body">{task.description || "等待家人认领"}</p><p className="row-meta">{task.assignedTo ? `负责人:${task.assignedTo.nickname || task.assignedTo.username || "家人"}` : "尚未认领"}</p><TaskActions taskId={task.id} status={task.status} /></article>) : <div className="empty">还没有照护任务。</div>}</div></section></>}
|
||||
</PanelShell>;
|
||||
}
|
||||
@@ -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 <PanelShell currentPath="/companion" title="温暖陪伴" description="留言、回忆和每一次认真回应" caregiverLabel={getCaregiverDisplayName(caregiver)}><div className="card"><Link className="list-row" href="/messages"><MessageCircle /><span className="row-main"><span className="row-title">双向留言</span><span className="row-meta">家人问候由数字人口播,长辈口述实时送达</span></span></Link><Link className="list-row" href="/activity"><MessagesSquare /><span className="row-main"><span className="row-title">陪伴动态</span><span className="row-meta">查看数字人对话和智能服务记录</span></span></Link><div className="list-row"><BookHeart /><span className="row-main"><span className="row-title">家庭记忆册</span><span className="row-meta">数据结构已预留,作为下一阶段增强功能</span></span></div></div><section className="card card-pad"><h2 style={{ marginTop: 0 }}>本周家庭连接</h2><p className="timeline-body">家庭发布了 {workspace.posts.length} 条动态,留下 {workspace.memos.length} 条共同记忆与备忘。</p></section></PanelShell>; }
|
||||
@@ -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 <main className="app-frame"><div className="app-content app-stack"><section className="card card-pad" style={{ marginTop: 48 }}><h1 style={{ margin: 0 }}>加入家庭守护圈</h1><p className="timeline-body">加入后,你将根据邀请角色参与长辈的家庭动态和共同照护。</p><JoinFamilyButton token={token} /></section></div></main>;
|
||||
}
|
||||
@@ -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 <PanelShell currentPath="/family" title={workspace.name} description={`${workspace.memberships.length} 位家人共同参与`} caregiverLabel={getCaregiverDisplayName(caregiver)}>
|
||||
<FamilyPostForm deviceUuid={deviceUuid} />
|
||||
<section><div className="section-title"><h2>家庭动态</h2></div><div className="card">{workspace.posts.length ? workspace.posts.map((post) => <article key={post.id} className="timeline-item"><div className="timeline-top">{post.elderAuthored ? `${workspace.elderProfile.preferredName}通过安智伴说` : post.author?.nickname || post.author?.username || "家里人"}<span className="timeline-time">{formatDateTime(post.createdAt)}</span></div><p className="timeline-body">{post.content}</p><span className="tag">{post.visibility === "FAMILY_ONLY" ? "仅家人" : post.visibility === "ELDER_ONLY" ? "只给长辈" : "全家可见"}</span></article>) : <div className="empty">发出第一条家庭动态,让关心流动起来。</div>}</div></section>
|
||||
<section><div className="section-title"><h2>家庭成员</h2></div><div className="card">{workspace.memberships.map((member) => <div key={member.id} className="list-row"><span className="avatar">{(member.caregiver.nickname || member.caregiver.username || "家").slice(-1)}</span><span className="row-main"><span className="row-title">{member.caregiver.nickname || member.caregiver.username || "家庭成员"}{member.relationLabel ? ` · ${member.relationLabel}` : ""}</span><span className="row-meta">{roleLabel[member.role]}{member.dutyOrder ? ` · 值守顺序 ${member.dutyOrder}` : ""}</span></span></div>)}</div></section>
|
||||
<section><div className="section-title"><h2>邀请家人</h2><span>链接 24 小时有效</span></div><InviteMemberForm deviceUuid={deviceUuid} /></section>
|
||||
</PanelShell>;
|
||||
}
|
||||
@@ -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 <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>;
|
||||
}
|
||||
+2
-86
@@ -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 (
|
||||
<PanelShell
|
||||
currentPath="/"
|
||||
title={`${greeting()},${name}`}
|
||||
description={`${date} · ${dashboard.devices.length} 台设备已连接`}
|
||||
caregiverLabel={name}
|
||||
unreadCount={unreadIn}
|
||||
>
|
||||
<section className="card">
|
||||
{dashboard.devices.length ? dashboard.devices.map((binding) => (
|
||||
<Link key={binding.id} href={`/devices/${binding.elderDevice.deviceUuid}`} className="list-row" style={{ alignItems: "center" }}>
|
||||
<span className={`dot ${binding.elderDevice.lastSeenAt ? "online" : "offline"}`} style={{ marginTop: 0 }} />
|
||||
<span className="row-title" style={{ flex: 1 }}>{getDeviceName(binding.alias, binding.elderDevice.displayName)}</span>
|
||||
<span className="row-meta" style={{ marginTop: 0 }}>{binding.elderDevice.lastSeenAt ? "在线" : "未连接"}</span>
|
||||
<ChevronRight size={14} color="var(--muted-light)" />
|
||||
</Link>
|
||||
)) : <div className="empty">还没有连接设备,扫码即可开始守护。</div>}
|
||||
</section>
|
||||
|
||||
<section className="stats" style={{ gridTemplateColumns: "repeat(4,1fr)" }}>
|
||||
<Link href="/messages" className="stat accent"><strong>{unreadIn}</strong><span>新留言</span></Link>
|
||||
<Link href="/messages" className="stat"><strong>{unreadOut}</strong><span>待长辈听</span></Link>
|
||||
<Link href="/activity" className="stat"><strong>{conversations}</strong><span>陪伴对话</span></Link>
|
||||
<Link href="/devices" className="stat"><strong>{dashboard.devices.length}</strong><span>设备</span></Link>
|
||||
</section>
|
||||
|
||||
<div className="button-row">
|
||||
<Link href={dashboard.devices[0] ? `/devices/${dashboard.devices[0].elderDevice.deviceUuid}` : "/devices"} className="btn primary" style={{ flex: 1 }}>
|
||||
<PenLine size={15} />给长辈留言
|
||||
</Link>
|
||||
<Link href="/scan" className="btn" style={{ flex: 1 }}><ScanLine size={15} />扫码绑定</Link>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<div className="section-title"><h2>长辈的新留言</h2><Link href="/messages">全部</Link></div>
|
||||
<div className="card">
|
||||
{recentIncomingMessages.length ? recentIncomingMessages.slice(0, 3).map((message) => (
|
||||
<Link key={message.id} href={getMessageHref(message.publicId)} className="list-row">
|
||||
<span className="dot" />
|
||||
<span className="row-main">
|
||||
<span className="row-title">{truncateText(message.content, 62)}</span>
|
||||
<span className="row-meta">{message.importance !== "NORMAL" ? <span className="tag">重要</span> : null}{getDeviceName(message.elderDevice.displayName)} · {formatDateTime(message.createdAt)}</span>
|
||||
</span>
|
||||
</Link>
|
||||
)) : <div className="empty">暂时还没有收到新留言。</div>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className="section-title"><h2>陪伴动态</h2><Link href="/activity">全部</Link></div>
|
||||
<div className="card">
|
||||
<div className="timeline-item"><div className="timeline-top">设备连接情况<span className="timeline-time">现在</span></div><p className="timeline-body">{dashboard.devices.length} 台长辈设备已加入家人守护。</p></div>
|
||||
<div className="timeline-item"><div className="timeline-top">陪伴对话<span className="timeline-time">累计</span></div><p className="timeline-body">数字人已完成 {conversations} 轮陪伴对话。</p></div>
|
||||
</div>
|
||||
</section>
|
||||
</PanelShell>
|
||||
);
|
||||
}
|
||||
import { redirect } from "next/navigation";
|
||||
export default function Home() { redirect("/guard"); }
|
||||
|
||||
Reference in New Issue
Block a user