优化路径,添加消息推送

This commit is contained in:
2026-04-17 12:14:57 +08:00
parent b8a842367b
commit 0da5936ce0
34 changed files with 2472 additions and 435 deletions
+220
View File
@@ -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>
);
}
+146
View File
@@ -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>
);
}