优化路径,添加消息推送
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { PanelShell } from "@/components/panel-shell";
|
||||
import { getCaregiverActivity } from "@/lib/caregiver-panel";
|
||||
import {
|
||||
formatDateTime,
|
||||
getConversationRoleLabel,
|
||||
getDeviceName,
|
||||
getUsageLabel,
|
||||
truncateText,
|
||||
} from "@/lib/panel-format";
|
||||
import { requireCaregiverSession } from "@/lib/page-auth";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function ActivityPage() {
|
||||
const caregiver = await requireCaregiverSession("/activity");
|
||||
const { usageEvents, conversationTurns, toolCalls } = await getCaregiverActivity(
|
||||
caregiver.id,
|
||||
);
|
||||
|
||||
return (
|
||||
<PanelShell
|
||||
currentPath="/activity"
|
||||
title="活动记录独立成页,回看陪伴过程更清楚。"
|
||||
description="这里专门收下老人端的使用事件、最近对话片段和工具调用,不再和留言流混排。"
|
||||
caregiverToken={caregiver.sessionToken}
|
||||
>
|
||||
{usageEvents.length === 0 && conversationTurns.length === 0 && toolCalls.length === 0 ? (
|
||||
<section className="rounded-[32px] border border-dashed border-[var(--line)] bg-white/72 px-6 py-10 text-center shadow-[0_20px_50px_rgba(115,76,42,0.10)]">
|
||||
<h2 className="font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
还没有活动记录
|
||||
</h2>
|
||||
<p className="mx-auto mt-4 max-w-2xl text-base leading-8 text-[var(--muted)]">
|
||||
等老人端开始对话、切换镜头或触发桥接工具后,这里会自动累积活动时间线。
|
||||
</p>
|
||||
</section>
|
||||
) : (
|
||||
<section className="grid gap-6 xl:grid-cols-3">
|
||||
<div className="rounded-[32px] border border-white/75 bg-white/82 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]">
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
使用事件
|
||||
</p>
|
||||
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
老人端操作轨迹
|
||||
</h2>
|
||||
<div className="mt-5 space-y-3">
|
||||
{usageEvents.map((event) => (
|
||||
<div key={event.id} className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
|
||||
<div className="flex items-center justify-between gap-3 text-xs text-[var(--muted)]">
|
||||
<span>{getDeviceName(event.elderDevice.displayName)}</span>
|
||||
<span>{formatDateTime(event.createdAt)}</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm leading-7 text-[var(--ink)]">
|
||||
{getUsageLabel(event.eventType)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[32px] border border-white/75 bg-white/82 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]">
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
对话片段
|
||||
</p>
|
||||
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
最近说了什么
|
||||
</h2>
|
||||
<div className="mt-5 space-y-3">
|
||||
{conversationTurns.map((turn) => (
|
||||
<div key={turn.id} className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
|
||||
<div className="flex items-center justify-between gap-3 text-xs text-[var(--muted)]">
|
||||
<span>{getConversationRoleLabel(turn.role)}</span>
|
||||
<span>{formatDateTime(turn.createdAt)}</span>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-[var(--muted)]">
|
||||
{getDeviceName(turn.elderDevice.displayName)}
|
||||
</p>
|
||||
<p className="mt-2 text-sm leading-7 text-[var(--ink)]">
|
||||
{truncateText(turn.content, 132)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[32px] border border-white/75 bg-white/82 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]">
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
工具调用
|
||||
</p>
|
||||
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
function calling 执行记录
|
||||
</h2>
|
||||
<div className="mt-5 space-y-3">
|
||||
{toolCalls.map((log) => (
|
||||
<div key={log.id} className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
|
||||
<div className="flex items-center justify-between gap-3 text-xs text-[var(--muted)]">
|
||||
<span>{getDeviceName(log.elderDevice.displayName)}</span>
|
||||
<span>{formatDateTime(log.createdAt)}</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm font-semibold text-[var(--ink)]">
|
||||
{log.toolName}
|
||||
</p>
|
||||
<p className="mt-2 text-sm leading-7 text-[var(--muted)]">
|
||||
{truncateText(log.outputText || log.argumentsJson, 136)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</PanelShell>
|
||||
);
|
||||
}
|
||||
@@ -35,7 +35,11 @@ export async function POST(request: Request) {
|
||||
importance: typeof body.importance === "string" ? body.importance : undefined,
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true, messageId: message.id });
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
messageId: message.id,
|
||||
publicId: message.publicId,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { getOrCreateCaregiverSession } from "@/lib/session";
|
||||
import { savePushSubscription } from "@/lib/push";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = (await request.json().catch(() => null)) as
|
||||
| {
|
||||
subscription?: {
|
||||
endpoint?: string;
|
||||
expirationTime?: number | null;
|
||||
keys?: {
|
||||
p256dh?: string;
|
||||
auth?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
| null;
|
||||
|
||||
const rawSubscription = body?.subscription;
|
||||
|
||||
if (
|
||||
!rawSubscription ||
|
||||
typeof rawSubscription.endpoint !== "string" ||
|
||||
!rawSubscription.endpoint.trim()
|
||||
) {
|
||||
return NextResponse.json({ error: "subscription 是必填项。" }, { status: 400 });
|
||||
}
|
||||
|
||||
const subscription = {
|
||||
endpoint: rawSubscription.endpoint,
|
||||
expirationTime:
|
||||
typeof rawSubscription.expirationTime === "number" ||
|
||||
rawSubscription.expirationTime === null
|
||||
? rawSubscription.expirationTime
|
||||
: undefined,
|
||||
keys:
|
||||
rawSubscription.keys && typeof rawSubscription.keys === "object"
|
||||
? {
|
||||
p256dh:
|
||||
typeof rawSubscription.keys.p256dh === "string"
|
||||
? rawSubscription.keys.p256dh
|
||||
: undefined,
|
||||
auth:
|
||||
typeof rawSubscription.keys.auth === "string"
|
||||
? rawSubscription.keys.auth
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
try {
|
||||
const caregiver = await getOrCreateCaregiverSession();
|
||||
await savePushSubscription({
|
||||
caregiverId: caregiver.id,
|
||||
subscription,
|
||||
userAgent: request.headers.get("user-agent"),
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "保存推送订阅失败。",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { removePushSubscription } from "@/lib/push";
|
||||
import { getCaregiverSession } from "@/lib/session";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = (await request.json().catch(() => null)) as
|
||||
| {
|
||||
endpoint?: string;
|
||||
}
|
||||
| null;
|
||||
|
||||
if (!body?.endpoint || typeof body.endpoint !== "string") {
|
||||
return NextResponse.json({ error: "endpoint 是必填项。" }, { status: 400 });
|
||||
}
|
||||
|
||||
const caregiver = await getCaregiverSession();
|
||||
|
||||
if (!caregiver) {
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
await removePushSubscription({
|
||||
caregiverId: caregiver.id,
|
||||
endpoint: body.endpoint,
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
+6
-6
@@ -43,10 +43,10 @@ export default async function BindPage({ searchParams }: BindPageProps) {
|
||||
去扫码
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
href="/devices"
|
||||
className="inline-flex h-12 items-center justify-center rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-5 text-sm font-semibold text-[var(--ink)] transition hover:bg-white"
|
||||
>
|
||||
回到控制台
|
||||
回到设备页
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
@@ -88,16 +88,16 @@ export default async function BindPage({ searchParams }: BindPageProps) {
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/"
|
||||
href={`/devices/${device.deviceUuid}`}
|
||||
className="inline-flex h-12 items-center justify-center rounded-full bg-[var(--copper)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--copper-deep)]"
|
||||
>
|
||||
回到控制台
|
||||
打开这台设备页
|
||||
</Link>
|
||||
<Link
|
||||
href="/scan"
|
||||
href="/devices"
|
||||
className="inline-flex h-12 items-center justify-center rounded-full border border-[var(--line)] bg-white px-5 text-sm font-semibold text-[var(--ink)] transition hover:bg-[var(--paper-soft)]"
|
||||
>
|
||||
继续绑定其他老人
|
||||
查看全部设备
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { SendMessageForm } from "@/components/dashboard-actions";
|
||||
import { PanelShell } from "@/components/panel-shell";
|
||||
import { getCaregiverDeviceDetail } from "@/lib/caregiver-panel";
|
||||
import {
|
||||
formatDateTime,
|
||||
getConversationRoleLabel,
|
||||
getDeviceName,
|
||||
getImportanceLabel,
|
||||
getMessageHref,
|
||||
getUsageLabel,
|
||||
truncateText,
|
||||
} from "@/lib/panel-format";
|
||||
import { requireCaregiverSession } from "@/lib/page-auth";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type DeviceDetailPageProps = {
|
||||
params: Promise<{ deviceUuid: string }>;
|
||||
};
|
||||
|
||||
export default async function DeviceDetailPage({ params }: DeviceDetailPageProps) {
|
||||
const { deviceUuid } = await params;
|
||||
const caregiver = await requireCaregiverSession(`/devices/${deviceUuid}`);
|
||||
const binding = await getCaregiverDeviceDetail(caregiver.id, deviceUuid);
|
||||
|
||||
if (!binding) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const elderMessages = binding.elderDevice.messages.filter(
|
||||
(message) => message.direction === "ELDER_TO_FAMILY",
|
||||
);
|
||||
const familyMessages = binding.elderDevice.messages.filter(
|
||||
(message) => message.direction === "FAMILY_TO_ELDER",
|
||||
);
|
||||
|
||||
return (
|
||||
<PanelShell
|
||||
currentPath="/devices"
|
||||
title={getDeviceName(binding.elderDevice.displayName)}
|
||||
description="这是一台设备的专属页面。给老人写问候、查看这台设备最近的留言、活动和对话,都在这里完成。"
|
||||
caregiverToken={caregiver.sessionToken}
|
||||
>
|
||||
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
|
||||
<p className="text-sm text-[var(--muted)]">老人新留言</p>
|
||||
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
|
||||
{binding.elderUnreadCount}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
|
||||
<p className="text-sm text-[var(--muted)]">待老人查看</p>
|
||||
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
|
||||
{binding.familyUnreadCount}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
|
||||
<p className="text-sm text-[var(--muted)]">最近在线</p>
|
||||
<p className="mt-3 text-lg font-semibold text-[var(--ink)]">
|
||||
{formatDateTime(binding.elderDevice.lastSeenAt)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
|
||||
<p className="text-sm text-[var(--muted)]">绑定链接</p>
|
||||
<p className="mt-3 text-sm leading-7 text-[var(--ink)]">
|
||||
{truncateText(binding.bindUrl, 52)}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-6 xl:grid-cols-[0.95fr_1.05fr]">
|
||||
<div className="space-y-6">
|
||||
<SendMessageForm deviceUuid={binding.elderDevice.deviceUuid} />
|
||||
|
||||
<section className="rounded-[32px] border border-white/75 bg-white/82 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
老人给家里的话
|
||||
</p>
|
||||
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
最近通过桥接工具送出的内容
|
||||
</h2>
|
||||
</div>
|
||||
<Link
|
||||
href="/messages"
|
||||
className="inline-flex h-11 items-center justify-center rounded-full border border-[var(--line)] bg-white px-4 text-sm font-semibold text-[var(--ink)] transition hover:bg-[var(--paper-soft)]"
|
||||
>
|
||||
去留言中心
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 space-y-3">
|
||||
{elderMessages.length === 0 ? (
|
||||
<p className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]">
|
||||
这台设备暂时还没有新的老人留言。
|
||||
</p>
|
||||
) : (
|
||||
elderMessages.map((message) => (
|
||||
<Link
|
||||
key={message.id}
|
||||
href={getMessageHref(message.publicId)}
|
||||
className="block rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 transition hover:bg-white"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-[var(--muted)]">
|
||||
<span className="rounded-full bg-white px-3 py-1 font-semibold text-[var(--ink)]">
|
||||
#{message.publicId}
|
||||
</span>
|
||||
<span>{getImportanceLabel(message.importance)}</span>
|
||||
<span>{formatDateTime(message.createdAt)}</span>
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-7 text-[var(--ink)]">
|
||||
{truncateText(message.content, 112)}
|
||||
</p>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[32px] border border-white/75 bg-white/82 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]">
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
最近活动
|
||||
</p>
|
||||
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
这台设备刚刚发生了什么
|
||||
</h2>
|
||||
|
||||
<div className="mt-5 space-y-3">
|
||||
{binding.elderDevice.usageEvents.length === 0 ? (
|
||||
<p className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]">
|
||||
暂时还没有同步到新的使用事件。
|
||||
</p>
|
||||
) : (
|
||||
binding.elderDevice.usageEvents.map((event) => (
|
||||
<div key={event.id} className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
|
||||
<div className="flex items-center justify-between gap-3 text-xs text-[var(--muted)]">
|
||||
<span>{getUsageLabel(event.eventType)}</span>
|
||||
<span>{formatDateTime(event.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<section className="rounded-[32px] border border-white/75 bg-white/82 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]">
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
家属给老人的问候
|
||||
</p>
|
||||
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
最近写给这位老人的内容
|
||||
</h2>
|
||||
|
||||
<div className="mt-5 space-y-3">
|
||||
{familyMessages.length === 0 ? (
|
||||
<p className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]">
|
||||
还没有发给这位老人的问候。
|
||||
</p>
|
||||
) : (
|
||||
familyMessages.map((message) => (
|
||||
<Link
|
||||
key={message.id}
|
||||
href={getMessageHref(message.publicId)}
|
||||
className="block rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 transition hover:bg-white"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-[var(--muted)]">
|
||||
<span className="rounded-full bg-white px-3 py-1 font-semibold text-[var(--ink)]">
|
||||
#{message.publicId}
|
||||
</span>
|
||||
<span>{getImportanceLabel(message.importance)}</span>
|
||||
<span>{formatDateTime(message.createdAt)}</span>
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-7 text-[var(--ink)]">
|
||||
{truncateText(message.content, 112)}
|
||||
</p>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[32px] border border-white/75 bg-white/82 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]">
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
最近对话片段
|
||||
</p>
|
||||
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
快速回看陪伴内容
|
||||
</h2>
|
||||
|
||||
<div className="mt-5 space-y-3">
|
||||
{binding.elderDevice.conversationTurns.length === 0 ? (
|
||||
<p className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]">
|
||||
这台设备最近还没有同步到对话片段。
|
||||
</p>
|
||||
) : (
|
||||
binding.elderDevice.conversationTurns.map((turn) => (
|
||||
<div key={turn.id} className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
|
||||
<div className="flex items-center justify-between gap-3 text-xs text-[var(--muted)]">
|
||||
<span>{getConversationRoleLabel(turn.role)}</span>
|
||||
<span>{formatDateTime(turn.createdAt)}</span>
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-7 text-[var(--ink)]">
|
||||
{truncateText(turn.content, 124)}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</PanelShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { BindDeviceForm } from "@/components/dashboard-actions";
|
||||
import { PanelShell } from "@/components/panel-shell";
|
||||
import { PwaControls } from "@/components/pwa-controls";
|
||||
import { getCaregiverDevices } from "@/lib/caregiver-panel";
|
||||
import { formatDateTime, getDeviceName, truncateText } from "@/lib/panel-format";
|
||||
import { requireCaregiverSession } from "@/lib/page-auth";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function DevicesPage() {
|
||||
const caregiver = await requireCaregiverSession("/devices");
|
||||
const devices = await getCaregiverDevices(caregiver.id);
|
||||
|
||||
return (
|
||||
<PanelShell
|
||||
currentPath="/devices"
|
||||
title="设备管理和绑定入口单独放在这里。"
|
||||
description="手动粘贴设备码、扫码绑定、查看某台设备的专属页,都统一放在设备页,不再和首页摘要、活动记录混在一起。"
|
||||
caregiverToken={caregiver.sessionToken}
|
||||
actions={<PwaControls />}
|
||||
>
|
||||
<section className="grid gap-6 xl:grid-cols-[0.95fr_1.05fr]">
|
||||
<BindDeviceForm />
|
||||
|
||||
<div className="rounded-[32px] border border-white/75 bg-white/82 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]">
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
设备入口说明
|
||||
</p>
|
||||
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
一台设备一个详情页
|
||||
</h2>
|
||||
<div className="mt-4 space-y-3 rounded-[24px] bg-[var(--paper-soft)] p-5 text-sm leading-7 text-[var(--muted)]">
|
||||
<p>1. 先通过扫码或粘贴设备码建立连接。</p>
|
||||
<p>2. 进入设备详情页给这位老人写留言,不再在首页里混排。</p>
|
||||
<p>3. 每台设备都有自己的最近留言、对话片段和工具调用摘要。</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/scan"
|
||||
className="inline-flex h-12 items-center justify-center rounded-full bg-[var(--olive)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--olive-deep)]"
|
||||
>
|
||||
打开扫码页
|
||||
</Link>
|
||||
<Link
|
||||
href="/messages"
|
||||
className="inline-flex h-12 items-center justify-center rounded-full border border-[var(--line)] bg-white px-5 text-sm font-semibold text-[var(--ink)] transition hover:bg-[var(--paper-soft)]"
|
||||
>
|
||||
去留言中心
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{devices.length === 0 ? (
|
||||
<section className="rounded-[32px] border border-dashed border-[var(--line)] bg-white/72 px-6 py-10 text-center shadow-[0_20px_50px_rgba(115,76,42,0.10)]">
|
||||
<h2 className="font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
还没有绑定设备
|
||||
</h2>
|
||||
<p className="mx-auto mt-4 max-w-2xl text-base leading-8 text-[var(--muted)]">
|
||||
等待绑定成功后,每台设备都会在这里生成自己的专属入口,后续写留言和查看状态都从设备详情页进入。
|
||||
</p>
|
||||
</section>
|
||||
) : (
|
||||
<section className="grid gap-6 lg:grid-cols-2">
|
||||
{devices.map((binding) => {
|
||||
const latestMessage = binding.elderDevice.messages[0];
|
||||
|
||||
return (
|
||||
<article
|
||||
key={binding.id}
|
||||
className="rounded-[32px] border border-white/75 bg-white/82 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]"
|
||||
>
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
设备详情
|
||||
</p>
|
||||
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
{getDeviceName(binding.elderDevice.displayName)}
|
||||
</h2>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-3 text-sm text-[var(--muted)]">
|
||||
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
|
||||
最近在线:{formatDateTime(binding.elderDevice.lastSeenAt)}
|
||||
</span>
|
||||
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
|
||||
老人新留言 {binding.elderUnreadCount}
|
||||
</span>
|
||||
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
|
||||
待老人查看 {binding.familyUnreadCount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-3 sm:grid-cols-3">
|
||||
<div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4">
|
||||
<p className="text-xs text-[var(--muted)]">留言总数</p>
|
||||
<p className="mt-2 text-2xl font-semibold text-[var(--ink)]">
|
||||
{binding.elderDevice._count.messages}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4">
|
||||
<p className="text-xs text-[var(--muted)]">对话条数</p>
|
||||
<p className="mt-2 text-2xl font-semibold text-[var(--ink)]">
|
||||
{binding.elderDevice._count.conversationTurns}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4">
|
||||
<p className="text-xs text-[var(--muted)]">工具调用</p>
|
||||
<p className="mt-2 text-2xl font-semibold text-[var(--ink)]">
|
||||
{binding.elderDevice._count.toolCalls}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
|
||||
<p className="text-sm font-semibold text-[var(--ink)]">最近一条动态</p>
|
||||
<p className="mt-2 text-sm leading-7 text-[var(--muted)]">
|
||||
{latestMessage
|
||||
? truncateText(latestMessage.content, 96)
|
||||
: "这台设备还没有同步到新的留言或问候。"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-3">
|
||||
<Link
|
||||
href={`/devices/${binding.elderDevice.deviceUuid}`}
|
||||
className="inline-flex h-12 items-center justify-center rounded-full bg-[var(--copper)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--copper-deep)]"
|
||||
>
|
||||
打开设备页
|
||||
</Link>
|
||||
<Link
|
||||
href="/messages"
|
||||
className="inline-flex h-12 items-center justify-center rounded-full border border-[var(--line)] bg-white px-5 text-sm font-semibold text-[var(--ink)] transition hover:bg-[var(--paper-soft)]"
|
||||
>
|
||||
去留言中心
|
||||
</Link>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
)}
|
||||
</PanelShell>
|
||||
);
|
||||
}
|
||||
+33
-1
@@ -1,4 +1,4 @@
|
||||
import type { Metadata } from "next";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { Noto_Sans_SC, Noto_Serif_SC } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
@@ -18,6 +18,38 @@ export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://digital-human.xn--876a.net"),
|
||||
title: "家人连线台",
|
||||
description: "给老人设备做绑定、查看留言、追踪陪伴记录的家属端。",
|
||||
manifest: "/manifest.webmanifest",
|
||||
icons: {
|
||||
icon: [
|
||||
{
|
||||
url: "/pwa/icon-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
url: "/pwa/icon-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
apple: [
|
||||
{
|
||||
url: "/pwa/apple-touch-icon.png",
|
||||
sizes: "180x180",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
shortcut: "/pwa/icon-192x192.png",
|
||||
},
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: "default",
|
||||
title: "家人连线台",
|
||||
},
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: "#35548d",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
name: "家人连线台",
|
||||
short_name: "家人连线台",
|
||||
description: "给老人设备做绑定、查看留言、追踪陪伴记录的家属端。",
|
||||
start_url: "/",
|
||||
display: "standalone",
|
||||
background_color: "#f6eee1",
|
||||
theme_color: "#35548d",
|
||||
lang: "zh-CN",
|
||||
categories: ["utilities", "lifestyle", "health"],
|
||||
icons: [
|
||||
{
|
||||
src: "/pwa/icon-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/pwa/icon-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/pwa/icon-maskable-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { PanelShell } from "@/components/panel-shell";
|
||||
import { getCaregiverMessageDetail } from "@/lib/caregiver-panel";
|
||||
import {
|
||||
formatDateTime,
|
||||
getDeviceName,
|
||||
getImportanceLabel,
|
||||
getMessageDirectionLabel,
|
||||
getMessageStatusLabel,
|
||||
} from "@/lib/panel-format";
|
||||
import { requireCaregiverSession } from "@/lib/page-auth";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type MessageDetailPageProps = {
|
||||
params: Promise<{ messageId: string }>;
|
||||
};
|
||||
|
||||
export default async function MessageDetailPage({ params }: MessageDetailPageProps) {
|
||||
const { messageId } = await params;
|
||||
const publicId = Number.parseInt(messageId, 10);
|
||||
|
||||
if (!Number.isFinite(publicId) || publicId <= 0) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const caregiver = await requireCaregiverSession(`/messages/${publicId}`);
|
||||
const message = await getCaregiverMessageDetail(caregiver.id, publicId);
|
||||
|
||||
if (!message) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<PanelShell
|
||||
currentPath="/messages"
|
||||
title={`留言 #${message.publicId}`}
|
||||
description="这是单条留言详情页。无论从通知点击进来,还是从留言中心点开,都能在这里看到完整内容与所属设备信息。"
|
||||
caregiverToken={caregiver.sessionToken}
|
||||
>
|
||||
<section className="grid gap-6 xl:grid-cols-[1.2fr_0.8fr]">
|
||||
<article className="rounded-[32px] border border-white/75 bg-white/82 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]">
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm text-[var(--muted)]">
|
||||
<span className="rounded-full bg-[var(--paper-soft)] px-3 py-1 font-semibold text-[var(--ink)]">
|
||||
#{message.publicId}
|
||||
</span>
|
||||
<span className="rounded-full bg-[var(--paper-soft)] px-3 py-1">
|
||||
{getMessageDirectionLabel(message.direction)}
|
||||
</span>
|
||||
<span className="rounded-full bg-[var(--paper-soft)] px-3 py-1">
|
||||
{getImportanceLabel(message.importance)}
|
||||
</span>
|
||||
<span className="rounded-full bg-[var(--paper-soft)] px-3 py-1">
|
||||
{getMessageStatusLabel(message)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2 className="mt-5 font-[family-name:var(--font-display)] text-4xl leading-[1.2] text-[var(--ink)]">
|
||||
{message.content}
|
||||
</h2>
|
||||
|
||||
{message.recipientRelation ? (
|
||||
<p className="mt-4 text-sm leading-7 text-[var(--muted)]">
|
||||
目标对象:{message.recipientRelation}
|
||||
</p>
|
||||
) : null}
|
||||
</article>
|
||||
|
||||
<aside className="rounded-[32px] border border-white/75 bg-white/82 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]">
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
留言信息
|
||||
</p>
|
||||
<div className="mt-5 space-y-4 text-sm leading-7 text-[var(--muted)]">
|
||||
<div className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
|
||||
<p>所属设备</p>
|
||||
<p className="mt-2 font-semibold text-[var(--ink)]">
|
||||
{getDeviceName(message.elderDevice.displayName)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
|
||||
<p>设备 UUID</p>
|
||||
<p className="mt-2 break-all font-semibold text-[var(--ink)]">
|
||||
{message.elderDevice.deviceUuid}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
|
||||
<p>创建时间</p>
|
||||
<p className="mt-2 font-semibold text-[var(--ink)]">
|
||||
{formatDateTime(message.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
|
||||
<p>最近在线</p>
|
||||
<p className="mt-2 font-semibold text-[var(--ink)]">
|
||||
{formatDateTime(message.elderDevice.lastSeenAt)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/messages"
|
||||
className="inline-flex h-12 items-center justify-center rounded-full bg-[var(--copper)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--copper-deep)]"
|
||||
>
|
||||
回到留言中心
|
||||
</Link>
|
||||
<Link
|
||||
href={`/devices/${message.elderDevice.deviceUuid}`}
|
||||
className="inline-flex h-12 items-center justify-center rounded-full border border-[var(--line)] bg-white px-5 text-sm font-semibold text-[var(--ink)] transition hover:bg-[var(--paper-soft)]"
|
||||
>
|
||||
打开这台设备页
|
||||
</Link>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</PanelShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { PanelShell } from "@/components/panel-shell";
|
||||
import { PwaControls } from "@/components/pwa-controls";
|
||||
import { getCaregiverMessages } from "@/lib/caregiver-panel";
|
||||
import {
|
||||
formatDateTime,
|
||||
getDeviceName,
|
||||
getImportanceLabel,
|
||||
getMessageDirectionLabel,
|
||||
getMessageHref,
|
||||
getMessageStatusLabel,
|
||||
truncateText,
|
||||
} from "@/lib/panel-format";
|
||||
import { requireCaregiverSession } from "@/lib/page-auth";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function MessagesPage() {
|
||||
const caregiver = await requireCaregiverSession("/messages");
|
||||
const { messages, unreadIncomingCount, unreadOutgoingCount } =
|
||||
await getCaregiverMessages(caregiver.id);
|
||||
|
||||
const incomingMessages = messages.filter(
|
||||
(message) => message.direction === "ELDER_TO_FAMILY",
|
||||
);
|
||||
const outgoingMessages = messages.filter(
|
||||
(message) => message.direction === "FAMILY_TO_ELDER",
|
||||
);
|
||||
|
||||
return (
|
||||
<PanelShell
|
||||
currentPath="/messages"
|
||||
title="留言独立成页后,每条消息都能用编号直达。"
|
||||
description="不论是老人留给家里的话,还是家属写给老人的问候,都集中在这里。点击任何一条卡片,都会进入带 URL 编号的详情页。"
|
||||
caregiverToken={caregiver.sessionToken}
|
||||
actions={<PwaControls />}
|
||||
>
|
||||
<section className="grid gap-4 md:grid-cols-3">
|
||||
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
|
||||
<p className="text-sm text-[var(--muted)]">全部留言</p>
|
||||
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
|
||||
{messages.length}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
|
||||
<p className="text-sm text-[var(--muted)]">老人给家里的未读留言</p>
|
||||
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
|
||||
{unreadIncomingCount}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
|
||||
<p className="text-sm text-[var(--muted)]">待老人查看的家属留言</p>
|
||||
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
|
||||
{unreadOutgoingCount}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{messages.length === 0 ? (
|
||||
<section className="rounded-[32px] border border-dashed border-[var(--line)] bg-white/72 px-6 py-10 text-center shadow-[0_20px_50px_rgba(115,76,42,0.10)]">
|
||||
<h2 className="font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
还没有留言记录
|
||||
</h2>
|
||||
<p className="mx-auto mt-4 max-w-2xl text-base leading-8 text-[var(--muted)]">
|
||||
去设备页给老人留一句问候,或等待老人通过桥接工具给家里捎话;新记录会自动出现在这里,并带有可分享的消息编号。
|
||||
</p>
|
||||
<div className="mt-6 flex justify-center gap-3">
|
||||
<Link
|
||||
href="/devices"
|
||||
className="inline-flex h-12 items-center justify-center rounded-full bg-[var(--copper)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--copper-deep)]"
|
||||
>
|
||||
去设备页
|
||||
</Link>
|
||||
<Link
|
||||
href="/scan"
|
||||
className="inline-flex h-12 items-center justify-center rounded-full border border-[var(--line)] bg-white px-5 text-sm font-semibold text-[var(--ink)] transition hover:bg-[var(--paper-soft)]"
|
||||
>
|
||||
去扫码绑定
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
<section className="grid gap-6 xl:grid-cols-2">
|
||||
{[
|
||||
{
|
||||
title: "老人给家里的留言",
|
||||
subtitle: "收到新话时也会从这里直达详情页。",
|
||||
items: incomingMessages,
|
||||
},
|
||||
{
|
||||
title: "家属给老人的问候",
|
||||
subtitle: "写出去的每条问候也有独立 URL,可回看送达状态。",
|
||||
items: outgoingMessages,
|
||||
},
|
||||
].map((section) => (
|
||||
<div
|
||||
key={section.title}
|
||||
className="rounded-[32px] border border-white/75 bg-white/80 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]"
|
||||
>
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
留言列表
|
||||
</p>
|
||||
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
{section.title}
|
||||
</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-[var(--muted)]">{section.subtitle}</p>
|
||||
|
||||
<div className="mt-5 space-y-3">
|
||||
{section.items.length === 0 ? (
|
||||
<p className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]">
|
||||
这一侧暂时还没有留言。
|
||||
</p>
|
||||
) : (
|
||||
section.items.map((message) => (
|
||||
<Link
|
||||
key={message.id}
|
||||
href={getMessageHref(message.publicId)}
|
||||
className="block rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 transition hover:bg-white"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-[var(--muted)]">
|
||||
<span className="rounded-full bg-white px-3 py-1 font-semibold text-[var(--ink)]">
|
||||
#{message.publicId}
|
||||
</span>
|
||||
<span>{getMessageDirectionLabel(message.direction)}</span>
|
||||
<span>{getDeviceName(message.elderDevice.displayName)}</span>
|
||||
<span>{getImportanceLabel(message.importance)}</span>
|
||||
<span>{getMessageStatusLabel(message)}</span>
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-7 text-[var(--ink)]">
|
||||
{truncateText(message.content, 112)}
|
||||
</p>
|
||||
<p className="mt-3 text-xs text-[var(--muted)]">
|
||||
创建于 {formatDateTime(message.createdAt)}
|
||||
</p>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
</PanelShell>
|
||||
);
|
||||
}
|
||||
+208
-408
@@ -1,70 +1,23 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { BindDeviceForm, SendMessageForm } from "@/components/dashboard-actions";
|
||||
import { getCaregiverDashboard } from "@/lib/monitor-data";
|
||||
import { PanelShell } from "@/components/panel-shell";
|
||||
import { PwaControls } from "@/components/pwa-controls";
|
||||
import { getCaregiverOverview } from "@/lib/caregiver-panel";
|
||||
import {
|
||||
buildCaregiverSessionBootstrapPath,
|
||||
getCaregiverSession,
|
||||
} from "@/lib/session";
|
||||
formatDateTime,
|
||||
getDeviceName,
|
||||
getImportanceLabel,
|
||||
getMessageHref,
|
||||
truncateText,
|
||||
} from "@/lib/panel-format";
|
||||
import { requireCaregiverSession } from "@/lib/page-auth";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
function formatTime(date: Date | null | undefined) {
|
||||
if (!date) {
|
||||
return "暂时还没有记录";
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat("zh-CN", {
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function getImportanceLabel(level: string) {
|
||||
switch (level) {
|
||||
case "LOW":
|
||||
return "温和提醒";
|
||||
case "HIGH":
|
||||
return "希望尽快看到";
|
||||
case "URGENT":
|
||||
return "需要尽快联系";
|
||||
default:
|
||||
return "日常问候";
|
||||
}
|
||||
}
|
||||
|
||||
function getUsageLabel(eventType: string) {
|
||||
switch (eventType) {
|
||||
case "APP_OPEN":
|
||||
return "打开了老人端应用";
|
||||
case "SETTINGS_OPENED":
|
||||
return "打开了家人连接页";
|
||||
case "AI_SESSION_STARTED":
|
||||
return "开始了一次陪伴对话";
|
||||
case "AI_SESSION_ENDED":
|
||||
return "结束了一次陪伴对话";
|
||||
case "CAMERA_ENABLED":
|
||||
return "打开了镜头模式";
|
||||
case "CAMERA_DISABLED":
|
||||
return "关闭了镜头模式";
|
||||
case "TOOL_CALLED":
|
||||
return "模型调用了一次桥接工具";
|
||||
default:
|
||||
return eventType;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function Home() {
|
||||
const caregiver = await getCaregiverSession();
|
||||
|
||||
if (!caregiver) {
|
||||
redirect(buildCaregiverSessionBootstrapPath("/"));
|
||||
}
|
||||
|
||||
const dashboard = await getCaregiverDashboard(caregiver.id);
|
||||
const caregiver = await requireCaregiverSession("/");
|
||||
const { dashboard, recentIncomingMessages, recentOutgoingMessages } =
|
||||
await getCaregiverOverview(caregiver.id);
|
||||
|
||||
const totalUnreadForElder = dashboard.devices.reduce(
|
||||
(total, device) => total + device.familyUnreadCount,
|
||||
@@ -80,360 +33,207 @@ export default async function Home() {
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="relative overflow-hidden px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 h-64 bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.78),transparent_68%)]" />
|
||||
<div className="mx-auto max-w-7xl space-y-6">
|
||||
<section className="paper-panel relative overflow-hidden rounded-[36px] border border-white/70 px-6 py-8 shadow-[0_35px_90px_rgba(105,68,41,0.13)] sm:px-8 lg:px-10 lg:py-10">
|
||||
<div className="absolute -right-10 top-[-52px] h-40 w-40 rounded-full bg-[rgba(200,103,51,0.10)] blur-2xl" />
|
||||
<div className="absolute left-10 top-8 h-20 w-20 rounded-full border border-[rgba(118,132,94,0.18)]" />
|
||||
<PanelShell
|
||||
currentPath="/"
|
||||
title="首页只保留摘要,不再把所有操作挤在一处。"
|
||||
description="设备绑定、留言管理、活动回看和扫码入口已经拆到独立路径。这里保留今天最值得先看的摘要,以及最近的新留言。"
|
||||
caregiverToken={dashboard.caregiver.sessionToken}
|
||||
actions={<PwaControls />}
|
||||
>
|
||||
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5 shadow-[0_20px_50px_rgba(115,76,42,0.10)]">
|
||||
<p className="text-sm text-[var(--muted)]">已连接老人设备</p>
|
||||
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
|
||||
{dashboard.devices.length}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5 shadow-[0_20px_50px_rgba(115,76,42,0.10)]">
|
||||
<p className="text-sm text-[var(--muted)]">老人给家里的未读留言</p>
|
||||
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
|
||||
{totalUnreadForCaregiver}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5 shadow-[0_20px_50px_rgba(115,76,42,0.10)]">
|
||||
<p className="text-sm text-[var(--muted)]">待老人查看的家属留言</p>
|
||||
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
|
||||
{totalUnreadForElder}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5 shadow-[0_20px_50px_rgba(115,76,42,0.10)]">
|
||||
<p className="text-sm text-[var(--muted)]">最近存档对话条数</p>
|
||||
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
|
||||
{totalConversations}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="grid gap-8 lg:grid-cols-[1.2fr_0.8fr] lg:items-end">
|
||||
<div>
|
||||
<p className="text-sm font-semibold tracking-[0.24em] text-[var(--copper)] uppercase">
|
||||
Digital Human Bridge
|
||||
</p>
|
||||
<h1 className="mt-4 max-w-3xl font-[family-name:var(--font-display)] text-4xl leading-[1.15] text-[var(--ink)] sm:text-5xl">
|
||||
把老人的陪伴对话、留言和家人问候,收进同一张暖色桌面。
|
||||
</h1>
|
||||
<p className="mt-5 max-w-2xl text-base leading-8 text-[var(--muted)] sm:text-lg">
|
||||
这里是家属端控制台。扫码绑定以后,您能看到老人留给家里的话、老人最近的使用轨迹、重要对话片段,也能反过来给老人写下一句问候。
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-3 text-sm text-[var(--muted)]">
|
||||
<span className="rounded-full border border-[var(--line)] bg-white/70 px-4 py-2">
|
||||
当前家属账号:{dashboard.caregiver.sessionToken.slice(0, 8).toUpperCase()}
|
||||
</span>
|
||||
<span className="rounded-full border border-[var(--line)] bg-white/70 px-4 py-2">
|
||||
已连接 {dashboard.devices.length} 位老人设备
|
||||
</span>
|
||||
</div>
|
||||
<section className="grid gap-4 lg:grid-cols-4">
|
||||
{[
|
||||
{
|
||||
href: "/messages",
|
||||
title: "留言中心",
|
||||
text: "集中看所有老人留言和家属留言,每条消息都有 URL 编号可直接打开。",
|
||||
tone: "bg-[rgba(53,84,141,0.12)]",
|
||||
},
|
||||
{
|
||||
href: "/devices",
|
||||
title: "设备与绑定",
|
||||
text: "去管理设备、扫码绑定、查看某一台设备的专属详情页和留言入口。",
|
||||
tone: "bg-[rgba(118,132,94,0.14)]",
|
||||
},
|
||||
{
|
||||
href: "/activity",
|
||||
title: "活动回看",
|
||||
text: "把使用事件、工具调用和对话片段拆出来,回看时不再和留言混在一起。",
|
||||
tone: "bg-[rgba(199,103,51,0.12)]",
|
||||
},
|
||||
{
|
||||
href: "/scan",
|
||||
title: "扫码绑定",
|
||||
text: "拿起另一台手机时,直接走扫码页,不必再回首页寻找入口。",
|
||||
tone: "bg-[rgba(36,27,20,0.08)]",
|
||||
},
|
||||
].map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="rounded-[28px] border border-white/80 bg-white/84 p-5 shadow-[0_20px_50px_rgba(115,76,42,0.10)] transition hover:-translate-y-0.5"
|
||||
>
|
||||
<div className={`inline-flex rounded-full px-3 py-1 text-xs font-semibold text-[var(--ink)] ${item.tone}`}>
|
||||
跳转路径
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-3 lg:grid-cols-1">
|
||||
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
|
||||
<p className="text-sm text-[var(--muted)]">家属已绑定设备</p>
|
||||
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
|
||||
{dashboard.devices.length}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
|
||||
<p className="text-sm text-[var(--muted)]">老人未读家属留言</p>
|
||||
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
|
||||
{totalUnreadForElder}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
|
||||
<p className="text-sm text-[var(--muted)]">最近存档对话条数</p>
|
||||
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
|
||||
{totalConversations}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<BindDeviceForm />
|
||||
|
||||
<section className="grid gap-6 lg:grid-cols-[0.85fr_1.15fr]">
|
||||
<div className="rounded-[32px] border border-white/70 bg-white/78 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)] backdrop-blur">
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
今日总览
|
||||
</p>
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
|
||||
<p className="text-sm text-[var(--muted)]">老人给家里的新留言</p>
|
||||
<p className="mt-2 text-3xl font-semibold text-[var(--ink)]">
|
||||
{totalUnreadForCaregiver}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
|
||||
<p className="text-sm text-[var(--muted)]">老人待接收的家属留言</p>
|
||||
<p className="mt-2 text-3xl font-semibold text-[var(--ink)]">
|
||||
{totalUnreadForElder}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
|
||||
<p className="text-sm text-[var(--muted)]">扫码绑定更顺手</p>
|
||||
<p className="mt-2 text-sm leading-7 text-[var(--muted)]">
|
||||
如果您正拿着另一台手机,可以直接打开扫码页,对准老人的家人连接二维码。
|
||||
</p>
|
||||
<Link
|
||||
href="/scan"
|
||||
className="mt-4 inline-flex h-11 items-center justify-center rounded-full bg-[var(--olive)] px-4 text-sm font-semibold text-white transition hover:bg-[var(--olive-deep)]"
|
||||
>
|
||||
去扫码
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[32px] border border-white/70 bg-white/78 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)] backdrop-blur">
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
面板说明
|
||||
</p>
|
||||
<div className="mt-4 grid gap-4 sm:grid-cols-3">
|
||||
<div className="rounded-[24px] bg-[var(--paper-soft)] p-5">
|
||||
<h3 className="font-[family-name:var(--font-display)] text-xl text-[var(--ink)]">
|
||||
留言桥接
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-7 text-[var(--muted)]">
|
||||
老人在数字人对话里说“给儿子带句话”时,模型会走 function calling,把内容存到这里。
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[24px] bg-[var(--paper-soft)] p-5">
|
||||
<h3 className="font-[family-name:var(--font-display)] text-xl text-[var(--ink)]">
|
||||
陪伴记录
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-7 text-[var(--muted)]">
|
||||
面板会收下老人端的重要使用事件、最近对话片段和工具调用记录,方便您回看状态。
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[24px] bg-[var(--paper-soft)] p-5">
|
||||
<h3 className="font-[family-name:var(--font-display)] text-xl text-[var(--ink)]">
|
||||
反向留言
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-7 text-[var(--muted)]">
|
||||
家属端写下的留言会进到老人设备的消息列表里,也能被 function calling 工具读取给老人听。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{dashboard.devices.length === 0 ? (
|
||||
<section className="rounded-[32px] border border-dashed border-[var(--line)] bg-white/70 px-6 py-10 text-center shadow-[0_20px_50px_rgba(115,76,42,0.10)]">
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
还没有绑定老人设备
|
||||
</p>
|
||||
<h2 className="mt-3 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
先扫一下老人手机里的二维码
|
||||
<h2 className="mt-4 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
|
||||
{item.title}
|
||||
</h2>
|
||||
<p className="mx-auto mt-4 max-w-2xl text-base leading-8 text-[var(--muted)]">
|
||||
绑定完成后,这里会出现老人的留言、陪伴轨迹、最近对话和工具调用记录。当前页面已经替您准备好扫码与手动绑定入口。
|
||||
</p>
|
||||
</section>
|
||||
) : (
|
||||
<section className="space-y-6">
|
||||
{dashboard.devices.map((binding) => {
|
||||
const familyMessages = binding.elderDevice.messages.filter(
|
||||
(message) => message.direction === "FAMILY_TO_ELDER",
|
||||
);
|
||||
const elderMessages = binding.elderDevice.messages.filter(
|
||||
(message) => message.direction === "ELDER_TO_FAMILY",
|
||||
);
|
||||
<p className="mt-3 text-sm leading-7 text-[var(--muted)]">{item.text}</p>
|
||||
<p className="mt-4 text-sm font-semibold text-[var(--copper)]">前往 {item.href}</p>
|
||||
</Link>
|
||||
))}
|
||||
</section>
|
||||
|
||||
return (
|
||||
<article
|
||||
key={binding.id}
|
||||
className="rounded-[36px] border border-white/75 bg-white/80 p-6 shadow-[0_28px_70px_rgba(108,73,41,0.12)] backdrop-blur lg:p-7"
|
||||
>
|
||||
<div className="flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
老人设备
|
||||
</p>
|
||||
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
{binding.elderDevice.displayName || "未命名陪伴设备"}
|
||||
</h2>
|
||||
<div className="mt-3 flex flex-wrap gap-3 text-sm text-[var(--muted)]">
|
||||
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
|
||||
UUID:{binding.elderDevice.deviceUuid}
|
||||
</span>
|
||||
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
|
||||
最近在线:{formatTime(binding.elderDevice.lastSeenAt)}
|
||||
</span>
|
||||
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
|
||||
绑定链接:{binding.bindUrl}
|
||||
</span>
|
||||
</div>
|
||||
{dashboard.devices.length === 0 ? (
|
||||
<section className="rounded-[32px] border border-dashed border-[var(--line)] bg-white/70 px-6 py-10 text-center shadow-[0_20px_50px_rgba(115,76,42,0.10)]">
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
还没有绑定老人设备
|
||||
</p>
|
||||
<h2 className="mt-3 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
先去设备页或扫码页建立第一条连接
|
||||
</h2>
|
||||
<p className="mx-auto mt-4 max-w-2xl text-base leading-8 text-[var(--muted)]">
|
||||
绑定完成后,留言中心会出现 URL 编号消息卡,活动页会出现陪伴记录,推送也能直达具体留言。
|
||||
</p>
|
||||
<div className="mt-6 flex justify-center gap-3">
|
||||
<Link
|
||||
href="/devices"
|
||||
className="inline-flex h-12 items-center justify-center rounded-full bg-[var(--copper)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--copper-deep)]"
|
||||
>
|
||||
去设备页
|
||||
</Link>
|
||||
<Link
|
||||
href="/scan"
|
||||
className="inline-flex h-12 items-center justify-center rounded-full border border-[var(--line)] bg-white px-5 text-sm font-semibold text-[var(--ink)] transition hover:bg-[var(--paper-soft)]"
|
||||
>
|
||||
直接扫码
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
<section className="grid gap-6 xl:grid-cols-2">
|
||||
<div className="rounded-[32px] border border-white/75 bg-white/80 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
新收到的老人留言
|
||||
</p>
|
||||
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
点一下就能按编号直达
|
||||
</h2>
|
||||
</div>
|
||||
<Link
|
||||
href="/messages"
|
||||
className="inline-flex h-11 items-center justify-center rounded-full border border-[var(--line)] bg-white px-4 text-sm font-semibold text-[var(--ink)] transition hover:bg-[var(--paper-soft)]"
|
||||
>
|
||||
查看全部留言
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 space-y-3">
|
||||
{recentIncomingMessages.length === 0 ? (
|
||||
<p className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]">
|
||||
目前还没有新的老人留言。
|
||||
</p>
|
||||
) : (
|
||||
recentIncomingMessages.map((message) => (
|
||||
<Link
|
||||
key={message.id}
|
||||
href={getMessageHref(message.publicId)}
|
||||
className="block rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 transition hover:bg-white"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-[var(--muted)]">
|
||||
<span className="rounded-full bg-white px-3 py-1 font-semibold text-[var(--ink)]">
|
||||
#{message.publicId}
|
||||
</span>
|
||||
<span>{getDeviceName(message.elderDevice.displayName)}</span>
|
||||
<span>{getImportanceLabel(message.importance)}</span>
|
||||
<span>{formatDateTime(message.createdAt)}</span>
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-7 text-[var(--ink)]">
|
||||
{truncateText(message.content)}
|
||||
</p>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-4">
|
||||
<div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4">
|
||||
<p className="text-xs text-[var(--muted)]">老人新留言</p>
|
||||
<p className="mt-2 text-2xl font-semibold text-[var(--ink)]">
|
||||
{binding.elderUnreadCount}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4">
|
||||
<p className="text-xs text-[var(--muted)]">待老人查看</p>
|
||||
<p className="mt-2 text-2xl font-semibold text-[var(--ink)]">
|
||||
{binding.familyUnreadCount}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4">
|
||||
<p className="text-xs text-[var(--muted)]">对话条数</p>
|
||||
<p className="mt-2 text-2xl font-semibold text-[var(--ink)]">
|
||||
{binding.elderDevice._count.conversationTurns}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4">
|
||||
<p className="text-xs text-[var(--muted)]">工具调用</p>
|
||||
<p className="mt-2 text-2xl font-semibold text-[var(--ink)]">
|
||||
{binding.elderDevice._count.toolCalls}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[32px] border border-white/75 bg-white/80 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
最近发给老人的问候
|
||||
</p>
|
||||
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
发出后也能按编号回看
|
||||
</h2>
|
||||
</div>
|
||||
<Link
|
||||
href="/devices"
|
||||
className="inline-flex h-11 items-center justify-center rounded-full border border-[var(--line)] bg-white px-4 text-sm font-semibold text-[var(--ink)] transition hover:bg-[var(--paper-soft)]"
|
||||
>
|
||||
去设备页留言
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 space-y-3">
|
||||
{recentOutgoingMessages.length === 0 ? (
|
||||
<p className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]">
|
||||
还没有新的家属留言,可以到设备页针对某一台设备写下问候。
|
||||
</p>
|
||||
) : (
|
||||
recentOutgoingMessages.map((message) => (
|
||||
<Link
|
||||
key={message.id}
|
||||
href={getMessageHref(message.publicId)}
|
||||
className="block rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 transition hover:bg-white"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-[var(--muted)]">
|
||||
<span className="rounded-full bg-white px-3 py-1 font-semibold text-[var(--ink)]">
|
||||
#{message.publicId}
|
||||
</span>
|
||||
<span>{getDeviceName(message.elderDevice.displayName)}</span>
|
||||
<span>{getImportanceLabel(message.importance)}</span>
|
||||
<span>{formatDateTime(message.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid gap-6 xl:grid-cols-[0.95fr_1.05fr]">
|
||||
<div className="space-y-6">
|
||||
<section className="rounded-[28px] bg-[var(--paper-soft)] p-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium tracking-[0.16em] text-[var(--copper)] uppercase">
|
||||
老人给家里留的话
|
||||
</p>
|
||||
<h3 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
|
||||
最近想转达给家人的内容
|
||||
</h3>
|
||||
</div>
|
||||
<span className="rounded-full bg-white px-4 py-2 text-sm text-[var(--ink)]">
|
||||
未读 {binding.elderUnreadCount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
{elderMessages.length === 0 ? (
|
||||
<p className="rounded-[22px] bg-white/80 px-4 py-4 text-sm leading-7 text-[var(--muted)]">
|
||||
老人暂时还没有通过大模型给家里捎话。
|
||||
</p>
|
||||
) : (
|
||||
elderMessages.map((message) => (
|
||||
<div key={message.id} className="rounded-[22px] bg-white/88 px-4 py-4">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-[var(--muted)]">
|
||||
<span className="rounded-full bg-[var(--paper-soft)] px-3 py-1">
|
||||
{getImportanceLabel(message.importance)}
|
||||
</span>
|
||||
{message.recipientRelation ? (
|
||||
<span className="rounded-full bg-[var(--paper-soft)] px-3 py-1">
|
||||
目标:{message.recipientRelation}
|
||||
</span>
|
||||
) : null}
|
||||
<span>{formatTime(message.createdAt)}</span>
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-7 text-[var(--ink)]">
|
||||
{message.content}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[28px] bg-[var(--paper-soft)] p-5">
|
||||
<p className="text-sm font-medium tracking-[0.16em] text-[var(--copper)] uppercase">
|
||||
最近活动
|
||||
</p>
|
||||
<h3 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
|
||||
老人端刚刚发生了什么
|
||||
</h3>
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
{binding.elderDevice.usageEvents.length === 0 ? (
|
||||
<p className="rounded-[22px] bg-white/80 px-4 py-4 text-sm leading-7 text-[var(--muted)]">
|
||||
暂时还没有同步到新的使用记录。
|
||||
</p>
|
||||
) : (
|
||||
binding.elderDevice.usageEvents.map((event) => (
|
||||
<div key={event.id} className="rounded-[22px] bg-white/88 px-4 py-4">
|
||||
<div className="flex items-center justify-between gap-3 text-xs text-[var(--muted)]">
|
||||
<span>{getUsageLabel(event.eventType)}</span>
|
||||
<span>{formatTime(event.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<SendMessageForm deviceUuid={binding.elderDevice.deviceUuid} />
|
||||
|
||||
<section className="rounded-[28px] bg-[var(--paper-soft)] p-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium tracking-[0.16em] text-[var(--copper)] uppercase">
|
||||
发给老人的留言
|
||||
</p>
|
||||
<h3 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
|
||||
最近写给老人的问候
|
||||
</h3>
|
||||
</div>
|
||||
<span className="rounded-full bg-white px-4 py-2 text-sm text-[var(--ink)]">
|
||||
待查看 {binding.familyUnreadCount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
{familyMessages.length === 0 ? (
|
||||
<p className="rounded-[22px] bg-white/80 px-4 py-4 text-sm leading-7 text-[var(--muted)]">
|
||||
您还没有给这位老人留下新的问候。
|
||||
</p>
|
||||
) : (
|
||||
familyMessages.map((message) => (
|
||||
<div key={message.id} className="rounded-[22px] bg-white/88 px-4 py-4">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-[var(--muted)]">
|
||||
<span className="rounded-full bg-[var(--paper-soft)] px-3 py-1">
|
||||
{getImportanceLabel(message.importance)}
|
||||
</span>
|
||||
<span>{formatTime(message.createdAt)}</span>
|
||||
<span>
|
||||
{message.readAt ? "老人已看过" : "等待老人查看"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-7 text-[var(--ink)]">
|
||||
{message.content}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[28px] bg-[var(--paper-soft)] p-5">
|
||||
<p className="text-sm font-medium tracking-[0.16em] text-[var(--copper)] uppercase">
|
||||
最近对话片段
|
||||
</p>
|
||||
<h3 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
|
||||
可快速回看的陪伴内容
|
||||
</h3>
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
{binding.elderDevice.conversationTurns.length === 0 ? (
|
||||
<p className="rounded-[22px] bg-white/80 px-4 py-4 text-sm leading-7 text-[var(--muted)]">
|
||||
这位老人最近的实时对话还没有同步到这里。
|
||||
</p>
|
||||
) : (
|
||||
binding.elderDevice.conversationTurns.map((turn) => (
|
||||
<div key={turn.id} className="rounded-[22px] bg-white/88 px-4 py-4">
|
||||
<div className="flex items-center justify-between gap-3 text-xs text-[var(--muted)]">
|
||||
<span>
|
||||
{turn.role === "USER"
|
||||
? "老人说"
|
||||
: turn.role === "ASSISTANT"
|
||||
? "数字人回复"
|
||||
: "工具结果"}
|
||||
</span>
|
||||
<span>{formatTime(turn.createdAt)}</span>
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-7 text-[var(--ink)]">
|
||||
{turn.content}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
<p className="mt-3 text-sm leading-7 text-[var(--ink)]">
|
||||
{truncateText(message.content)}
|
||||
</p>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</PanelShell>
|
||||
);
|
||||
}
|
||||
|
||||
+2
-2
@@ -25,10 +25,10 @@ export default function ScanPage() {
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/"
|
||||
href="/devices"
|
||||
className="inline-flex h-12 items-center justify-center rounded-full border border-[var(--line)] bg-white px-5 text-sm font-semibold text-[var(--ink)] transition hover:bg-[var(--paper-soft)]"
|
||||
>
|
||||
回到控制台
|
||||
回到设备页
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user