优化路径,添加消息推送

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
+120
View File
@@ -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>
);
}
+146
View File
@@ -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>
);
}