优化文案

This commit is contained in:
feie9456 2026-04-17 12:38:59 +08:00
parent 0da5936ce0
commit 032e207888
19 changed files with 6670 additions and 229 deletions

View File

@ -1,4 +1,5 @@
import { PanelShell } from "@/components/panel-shell"; import { PanelShell } from "@/components/panel-shell";
import { ActivityChart } from "@/components/activity-chart";
import { getCaregiverActivity } from "@/lib/caregiver-panel"; import { getCaregiverActivity } from "@/lib/caregiver-panel";
import { import {
formatDateTime, formatDateTime,
@ -11,39 +12,91 @@ import { requireCaregiverSession } from "@/lib/page-auth";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
function aggregateByDate(items: { createdAt: Date | string }[]) {
const counts: Record<string, number> = {};
for (const item of items) {
const date = new Date(item.createdAt);
const key = `${date.getMonth() + 1}/${date.getDate()}`;
counts[key] = (counts[key] || 0) + 1;
}
return Object.entries(counts)
.map(([date, count]) => ({ date, count }))
.reverse();
}
export default async function ActivityPage() { export default async function ActivityPage() {
const caregiver = await requireCaregiverSession("/activity"); const caregiver = await requireCaregiverSession("/activity");
const { usageEvents, conversationTurns, toolCalls } = await getCaregiverActivity( const { usageEvents, conversationTurns, toolCalls } = await getCaregiverActivity(
caregiver.id, caregiver.id,
); );
const usageChartData = aggregateByDate(usageEvents);
const conversationChartData = aggregateByDate(conversationTurns);
return ( return (
<PanelShell <PanelShell
currentPath="/activity" currentPath="/activity"
title="活动记录独立成页,回看陪伴过程更清楚。" title="陪伴动态"
description="这里专门收下老人端的使用事件、最近对话片段和工具调用,不再和留言流混排。" description="了解长辈最近的使用情况、聊天内容和智能服务记录。"
caregiverToken={caregiver.sessionToken} caregiverToken={caregiver.sessionToken}
> >
{usageEvents.length === 0 && conversationTurns.length === 0 && toolCalls.length === 0 ? ( {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)]"> <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 className="font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
</h2> </h2>
<p className="mx-auto mt-4 max-w-2xl text-base leading-8 text-[var(--muted)]"> <p className="mx-auto mt-4 max-w-2xl text-base leading-8 text-[var(--muted)]">
线 使使便
</p> </p>
</section> </section>
) : ( ) : (
<>
{(usageChartData.length > 1 || conversationChartData.length > 1) && (
<section className="grid gap-6 xl:grid-cols-2">
{usageChartData.length > 1 && (
<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-2xl text-[var(--ink)]">
使
</h2>
<div className="mt-4">
<ActivityChart data={usageChartData} color="#c76733" />
</div>
</div>
)}
{conversationChartData.length > 1 && (
<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-2xl text-[var(--ink)]">
</h2>
<div className="mt-4">
<ActivityChart data={conversationChartData} color="#76845e" />
</div>
</div>
)}
</section>
)}
<section className="grid gap-6 xl:grid-cols-3"> <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)]"> <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 className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
使 使
</p> </p>
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]"> <h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
</h2> </h2>
<div className="mt-5 space-y-3"> <div className="mt-5 space-y-3">
{usageEvents.map((event) => ( {usageEvents.length === 0 ? (
<p className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]">
使
</p>
) : (
usageEvents.map((event) => (
<div key={event.id} className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4"> <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)]"> <div className="flex items-center justify-between gap-3 text-xs text-[var(--muted)]">
<span>{getDeviceName(event.elderDevice.displayName)}</span> <span>{getDeviceName(event.elderDevice.displayName)}</span>
@ -53,19 +106,25 @@ export default async function ActivityPage() {
{getUsageLabel(event.eventType)} {getUsageLabel(event.eventType)}
</p> </p>
</div> </div>
))} ))
)}
</div> </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)]"> <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 className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p> </p>
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]"> <h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
</h2> </h2>
<div className="mt-5 space-y-3"> <div className="mt-5 space-y-3">
{conversationTurns.map((turn) => ( {conversationTurns.length === 0 ? (
<p className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]">
</p>
) : (
conversationTurns.map((turn) => (
<div key={turn.id} className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4"> <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)]"> <div className="flex items-center justify-between gap-3 text-xs text-[var(--muted)]">
<span>{getConversationRoleLabel(turn.role)}</span> <span>{getConversationRoleLabel(turn.role)}</span>
@ -78,19 +137,25 @@ export default async function ActivityPage() {
{truncateText(turn.content, 132)} {truncateText(turn.content, 132)}
</p> </p>
</div> </div>
))} ))
)}
</div> </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)]"> <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 className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p> </p>
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]"> <h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
function calling
</h2> </h2>
<div className="mt-5 space-y-3"> <div className="mt-5 space-y-3">
{toolCalls.map((log) => ( {toolCalls.length === 0 ? (
<p className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]">
</p>
) : (
toolCalls.map((log) => (
<div key={log.id} className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4"> <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)]"> <div className="flex items-center justify-between gap-3 text-xs text-[var(--muted)]">
<span>{getDeviceName(log.elderDevice.displayName)}</span> <span>{getDeviceName(log.elderDevice.displayName)}</span>
@ -103,10 +168,12 @@ export default async function ActivityPage() {
{truncateText(log.outputText || log.argumentsJson, 136)} {truncateText(log.outputText || log.argumentsJson, 136)}
</p> </p>
</div> </div>
))} ))
)}
</div> </div>
</div> </div>
</section> </section>
</>
)} )}
</PanelShell> </PanelShell>
); );

View File

@ -27,13 +27,13 @@ export default async function BindPage({ searchParams }: BindPageProps) {
<main className="mx-auto flex min-h-screen max-w-3xl items-center px-4 py-8 sm:px-6"> <main className="mx-auto flex min-h-screen max-w-3xl items-center px-4 py-8 sm:px-6">
<section className="w-full rounded-[36px] border border-white/70 bg-white/82 p-8 text-center shadow-[0_30px_70px_rgba(115,76,42,0.14)] backdrop-blur"> <section className="w-full rounded-[36px] border border-white/70 bg-white/82 p-8 text-center shadow-[0_30px_70px_rgba(115,76,42,0.14)] backdrop-blur">
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase"> <p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p> </p>
<h1 className="mt-4 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]"> <h1 className="mt-4 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
</h1> </h1>
<p className="mt-4 text-base leading-8 text-[var(--muted)]"> <p className="mt-4 text-base leading-8 text-[var(--muted)]">
</p> </p>
<div className="mt-6 flex justify-center gap-3"> <div className="mt-6 flex justify-center gap-3">
<Link <Link
@ -46,7 +46,7 @@ export default async function BindPage({ searchParams }: BindPageProps) {
href="/devices" 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" 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> </Link>
</div> </div>
</section> </section>
@ -70,17 +70,17 @@ export default async function BindPage({ searchParams }: BindPageProps) {
<main className="mx-auto flex min-h-screen max-w-3xl items-center px-4 py-8 sm:px-6"> <main className="mx-auto flex min-h-screen max-w-3xl items-center px-4 py-8 sm:px-6">
<section className="w-full rounded-[36px] border border-white/70 bg-white/84 p-8 shadow-[0_30px_70px_rgba(115,76,42,0.14)] backdrop-blur"> <section className="w-full rounded-[36px] border border-white/70 bg-white/84 p-8 shadow-[0_30px_70px_rgba(115,76,42,0.14)] backdrop-blur">
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase"> <p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p> </p>
<h1 className="mt-4 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]"> <h1 className="mt-4 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
</h1> </h1>
<p className="mt-4 text-base leading-8 text-[var(--muted)]"> <p className="mt-4 text-base leading-8 text-[var(--muted)]">
使
</p> </p>
<div className="mt-6 rounded-[28px] bg-[var(--paper-soft)] p-5"> <div className="mt-6 rounded-[28px] bg-[var(--paper-soft)] p-5">
<p className="text-sm text-[var(--muted)]"> UUID</p> <p className="text-sm text-[var(--muted)]"></p>
<p className="mt-2 break-all text-lg font-semibold text-[var(--ink)]"> <p className="mt-2 break-all text-lg font-semibold text-[var(--ink)]">
{device.deviceUuid} {device.deviceUuid}
</p> </p>
@ -91,13 +91,13 @@ export default async function BindPage({ searchParams }: BindPageProps) {
href={`/devices/${device.deviceUuid}`} 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)]" 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>
<Link <Link
href="/devices" 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)]" 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> </Link>
</div> </div>
</section> </section>

View File

@ -41,18 +41,18 @@ export default async function DeviceDetailPage({ params }: DeviceDetailPageProps
<PanelShell <PanelShell
currentPath="/devices" currentPath="/devices"
title={getDeviceName(binding.elderDevice.displayName)} title={getDeviceName(binding.elderDevice.displayName)}
description="这是一台设备的专属页面。给老人写问候、查看这台设备最近的留言、活动和对话,都在这里完成。" description="给长辈写问候、查看留言和最近的使用动态。"
caregiverToken={caregiver.sessionToken} caregiverToken={caregiver.sessionToken}
> >
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4"> <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"> <div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
<p className="text-sm text-[var(--muted)]"></p> <p className="text-sm text-[var(--muted)]"></p>
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]"> <p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
{binding.elderUnreadCount} {binding.elderUnreadCount}
</p> </p>
</div> </div>
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5"> <div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
<p className="text-sm text-[var(--muted)]"></p> <p className="text-sm text-[var(--muted)]"></p>
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]"> <p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
{binding.familyUnreadCount} {binding.familyUnreadCount}
</p> </p>
@ -64,7 +64,7 @@ export default async function DeviceDetailPage({ params }: DeviceDetailPageProps
</p> </p>
</div> </div>
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5"> <div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
<p className="text-sm text-[var(--muted)]"></p> <p className="text-sm text-[var(--muted)]"></p>
<p className="mt-3 text-sm leading-7 text-[var(--ink)]"> <p className="mt-3 text-sm leading-7 text-[var(--ink)]">
{truncateText(binding.bindUrl, 52)} {truncateText(binding.bindUrl, 52)}
</p> </p>
@ -79,24 +79,24 @@ export default async function DeviceDetailPage({ params }: DeviceDetailPageProps
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div> <div>
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase"> <p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p> </p>
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]"> <h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
</h2> </h2>
</div> </div>
<Link <Link
href="/messages" 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)]" 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> </Link>
</div> </div>
<div className="mt-5 space-y-3"> <div className="mt-5 space-y-3">
{elderMessages.length === 0 ? ( {elderMessages.length === 0 ? (
<p className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]"> <p className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]">
</p> </p>
) : ( ) : (
elderMessages.map((message) => ( elderMessages.map((message) => (
@ -126,13 +126,13 @@ export default async function DeviceDetailPage({ params }: DeviceDetailPageProps
</p> </p>
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]"> <h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
使
</h2> </h2>
<div className="mt-5 space-y-3"> <div className="mt-5 space-y-3">
{binding.elderDevice.usageEvents.length === 0 ? ( {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 className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]">
使 使
</p> </p>
) : ( ) : (
binding.elderDevice.usageEvents.map((event) => ( binding.elderDevice.usageEvents.map((event) => (
@ -151,16 +151,16 @@ export default async function DeviceDetailPage({ params }: DeviceDetailPageProps
<div className="space-y-6"> <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)]"> <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 className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p> </p>
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]"> <h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
</h2> </h2>
<div className="mt-5 space-y-3"> <div className="mt-5 space-y-3">
{familyMessages.length === 0 ? ( {familyMessages.length === 0 ? (
<p className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]"> <p className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]">
</p> </p>
) : ( ) : (
familyMessages.map((message) => ( familyMessages.map((message) => (
@ -190,13 +190,13 @@ export default async function DeviceDetailPage({ params }: DeviceDetailPageProps
</p> </p>
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]"> <h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
</h2> </h2>
<div className="mt-5 space-y-3"> <div className="mt-5 space-y-3">
{binding.elderDevice.conversationTurns.length === 0 ? ( {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 className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]">
</p> </p>
) : ( ) : (
binding.elderDevice.conversationTurns.map((turn) => ( binding.elderDevice.conversationTurns.map((turn) => (

View File

@ -16,8 +16,8 @@ export default async function DevicesPage() {
return ( return (
<PanelShell <PanelShell
currentPath="/devices" currentPath="/devices"
title="设备管理和绑定入口单独放在这里。" title="我的设备"
description="手动粘贴设备码、扫码绑定、查看某台设备的专属页,都统一放在设备页,不再和首页摘要、活动记录混在一起。" description="管理已连接的长辈设备,点击进入设备详情查看留言和使用动态。"
caregiverToken={caregiver.sessionToken} caregiverToken={caregiver.sessionToken}
actions={<PwaControls />} actions={<PwaControls />}
> >
@ -26,15 +26,15 @@ export default async function DevicesPage() {
<div className="rounded-[32px] border border-white/75 bg-white/82 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]"> <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 className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
使
</p> </p>
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]"> <h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
</h2> </h2>
<div className="mt-4 space-y-3 rounded-[24px] bg-[var(--paper-soft)] p-5 text-sm leading-7 text-[var(--muted)]"> <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>1. </p>
<p>2. </p> <p>2. </p>
<p>3. </p> <p>3. 使</p>
</div> </div>
<div className="mt-6 flex flex-wrap gap-3"> <div className="mt-6 flex flex-wrap gap-3">
@ -42,13 +42,13 @@ export default async function DevicesPage() {
href="/scan" 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)]" 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>
<Link <Link
href="/messages" 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)]" 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> </Link>
</div> </div>
</div> </div>
@ -57,10 +57,10 @@ export default async function DevicesPage() {
{devices.length === 0 ? ( {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)]"> <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 className="font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
</h2> </h2>
<p className="mx-auto mt-4 max-w-2xl text-base leading-8 text-[var(--muted)]"> <p className="mx-auto mt-4 max-w-2xl text-base leading-8 text-[var(--muted)]">
</p> </p>
</section> </section>
) : ( ) : (
@ -74,7 +74,7 @@ export default async function DevicesPage() {
className="rounded-[32px] border border-white/75 bg-white/82 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]" 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 className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p> </p>
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]"> <h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
{getDeviceName(binding.elderDevice.displayName)} {getDeviceName(binding.elderDevice.displayName)}
@ -85,28 +85,28 @@ export default async function DevicesPage() {
线{formatDateTime(binding.elderDevice.lastSeenAt)} 线{formatDateTime(binding.elderDevice.lastSeenAt)}
</span> </span>
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2"> <span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
{binding.elderUnreadCount} {binding.elderUnreadCount}
</span> </span>
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2"> <span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
{binding.familyUnreadCount} {binding.familyUnreadCount}
</span> </span>
</div> </div>
<div className="mt-5 grid gap-3 sm:grid-cols-3"> <div className="mt-5 grid gap-3 sm:grid-cols-3">
<div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4"> <div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4">
<p className="text-xs text-[var(--muted)]"></p> <p className="text-xs text-[var(--muted)]"></p>
<p className="mt-2 text-2xl font-semibold text-[var(--ink)]"> <p className="mt-2 text-2xl font-semibold text-[var(--ink)]">
{binding.elderDevice._count.messages} {binding.elderDevice._count.messages}
</p> </p>
</div> </div>
<div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4"> <div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4">
<p className="text-xs text-[var(--muted)]"></p> <p className="text-xs text-[var(--muted)]"></p>
<p className="mt-2 text-2xl font-semibold text-[var(--ink)]"> <p className="mt-2 text-2xl font-semibold text-[var(--ink)]">
{binding.elderDevice._count.conversationTurns} {binding.elderDevice._count.conversationTurns}
</p> </p>
</div> </div>
<div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4"> <div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4">
<p className="text-xs text-[var(--muted)]"></p> <p className="text-xs text-[var(--muted)]"></p>
<p className="mt-2 text-2xl font-semibold text-[var(--ink)]"> <p className="mt-2 text-2xl font-semibold text-[var(--ink)]">
{binding.elderDevice._count.toolCalls} {binding.elderDevice._count.toolCalls}
</p> </p>
@ -114,11 +114,11 @@ export default async function DevicesPage() {
</div> </div>
<div className="mt-5 rounded-[24px] bg-[var(--paper-soft)] px-5 py-4"> <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="text-sm font-semibold text-[var(--ink)]"></p>
<p className="mt-2 text-sm leading-7 text-[var(--muted)]"> <p className="mt-2 text-sm leading-7 text-[var(--muted)]">
{latestMessage {latestMessage
? truncateText(latestMessage.content, 96) ? truncateText(latestMessage.content, 96)
: "这台设备还没有同步到新的留言或问候。"} : "还没有收到新的留言或问候。"}
</p> </p>
</div> </div>
@ -127,13 +127,13 @@ export default async function DevicesPage() {
href={`/devices/${binding.elderDevice.deviceUuid}`} 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)]" 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>
<Link <Link
href="/messages" 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)]" 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> </Link>
</div> </div>
</article> </article>

View File

@ -38,7 +38,6 @@ body {
} }
a { a {
color: inherit;
text-decoration: none; text-decoration: none;
} }

View File

@ -17,7 +17,7 @@ const displayFont = Noto_Serif_SC({
export const metadata: Metadata = { export const metadata: Metadata = {
metadataBase: new URL("https://digital-human.xn--876a.net"), metadataBase: new URL("https://digital-human.xn--876a.net"),
title: "家人连线台", title: "家人连线台",
description: "给老人设备做绑定、查看留言、追踪陪伴记录的家属端。", description: "随时了解长辈的近况,查看留言、使用动态,给长辈写问候。",
manifest: "/manifest.webmanifest", manifest: "/manifest.webmanifest",
icons: { icons: {
icon: [ icon: [

View File

@ -4,7 +4,7 @@ export default function manifest(): MetadataRoute.Manifest {
return { return {
name: "家人连线台", name: "家人连线台",
short_name: "家人连线台", short_name: "家人连线台",
description: "给老人设备做绑定、查看留言、追踪陪伴记录的家属端。", description: "随时了解长辈的近况,查看留言、使用动态,给长辈写问候。",
start_url: "/", start_url: "/",
display: "standalone", display: "standalone",
background_color: "#f6eee1", background_color: "#f6eee1",

View File

@ -37,7 +37,7 @@ export default async function MessageDetailPage({ params }: MessageDetailPagePro
<PanelShell <PanelShell
currentPath="/messages" currentPath="/messages"
title={`留言 #${message.publicId}`} title={`留言 #${message.publicId}`}
description="这是单条留言详情页。无论从通知点击进来,还是从留言中心点开,都能在这里看到完整内容与所属设备信息。" description="查看这条留言的完整内容和设备信息。"
caregiverToken={caregiver.sessionToken} caregiverToken={caregiver.sessionToken}
> >
<section className="grid gap-6 xl:grid-cols-[1.2fr_0.8fr]"> <section className="grid gap-6 xl:grid-cols-[1.2fr_0.8fr]">
@ -70,7 +70,7 @@ export default async function MessageDetailPage({ params }: MessageDetailPagePro
<aside className="rounded-[32px] border border-white/75 bg-white/82 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]"> <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 className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p> </p>
<div className="mt-5 space-y-4 text-sm leading-7 text-[var(--muted)]"> <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"> <div className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
@ -80,7 +80,7 @@ export default async function MessageDetailPage({ params }: MessageDetailPagePro
</p> </p>
</div> </div>
<div className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4"> <div className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
<p> UUID</p> <p></p>
<p className="mt-2 break-all font-semibold text-[var(--ink)]"> <p className="mt-2 break-all font-semibold text-[var(--ink)]">
{message.elderDevice.deviceUuid} {message.elderDevice.deviceUuid}
</p> </p>
@ -104,13 +104,13 @@ export default async function MessageDetailPage({ params }: MessageDetailPagePro
href="/messages" 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)]" 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>
<Link <Link
href={`/devices/${message.elderDevice.deviceUuid}`} 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)]" 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> </Link>
</div> </div>
</aside> </aside>

View File

@ -31,8 +31,8 @@ export default async function MessagesPage() {
return ( return (
<PanelShell <PanelShell
currentPath="/messages" currentPath="/messages"
title="留言独立成页后,每条消息都能用编号直达。" title="留言"
description="不论是老人留给家里的话,还是家属写给老人的问候,都集中在这里。点击任何一条卡片,都会进入带 URL 编号的详情页。" description="长辈捆来的话和您发出的问候,都在这里。"
caregiverToken={caregiver.sessionToken} caregiverToken={caregiver.sessionToken}
actions={<PwaControls />} actions={<PwaControls />}
> >
@ -44,13 +44,13 @@ export default async function MessagesPage() {
</p> </p>
</div> </div>
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5"> <div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
<p className="text-sm text-[var(--muted)]"></p> <p className="text-sm text-[var(--muted)]"></p>
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]"> <p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
{unreadIncomingCount} {unreadIncomingCount}
</p> </p>
</div> </div>
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5"> <div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
<p className="text-sm text-[var(--muted)]"></p> <p className="text-sm text-[var(--muted)]"></p>
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]"> <p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
{unreadOutgoingCount} {unreadOutgoingCount}
</p> </p>
@ -60,23 +60,23 @@ export default async function MessagesPage() {
{messages.length === 0 ? ( {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)]"> <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 className="font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
</h2> </h2>
<p className="mx-auto mt-4 max-w-2xl text-base leading-8 text-[var(--muted)]"> <p className="mx-auto mt-4 max-w-2xl text-base leading-8 text-[var(--muted)]">
</p> </p>
<div className="mt-6 flex justify-center gap-3"> <div className="mt-6 flex justify-center gap-3">
<Link <Link
href="/devices" 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)]" 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>
<Link <Link
href="/scan" 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)]" 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> </Link>
</div> </div>
</section> </section>
@ -84,13 +84,13 @@ export default async function MessagesPage() {
<section className="grid gap-6 xl:grid-cols-2"> <section className="grid gap-6 xl:grid-cols-2">
{[ {[
{ {
title: "老人给家里的留言", title: "长辈的留言",
subtitle: "收到新话时也会从这里直达详情页。", subtitle: "长辈通过智能助理捆来的话。",
items: incomingMessages, items: incomingMessages,
}, },
{ {
title: "家属给老人的问候", title: "我发出的问候",
subtitle: "写出去的每条问候也有独立 URL可回看送达状态。", subtitle: "您写给长辈的每一条问候。",
items: outgoingMessages, items: outgoingMessages,
}, },
].map((section) => ( ].map((section) => (
@ -99,7 +99,7 @@ export default async function MessagesPage() {
className="rounded-[32px] border border-white/75 bg-white/80 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]" 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 className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
{section.title === "长辈的留言" ? "收到的" : "发出的"}
</p> </p>
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]"> <h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
{section.title} {section.title}

View File

@ -1,4 +1,5 @@
import Link from "next/link"; import Link from "next/link";
import { MessageCircle, Smartphone, Activity, ScanLine } from "lucide-react";
import { PanelShell } from "@/components/panel-shell"; import { PanelShell } from "@/components/panel-shell";
import { PwaControls } from "@/components/pwa-controls"; import { PwaControls } from "@/components/pwa-controls";
@ -35,32 +36,32 @@ export default async function Home() {
return ( return (
<PanelShell <PanelShell
currentPath="/" currentPath="/"
title="首页只保留摘要,不再把所有操作挤在一处。" title="今日概览"
description="设备绑定、留言管理、活动回看和扫码入口已经拆到独立路径。这里保留今天最值得先看的摘要,以及最近的新留言。" description="随时了解长辈的近况,留言和动态一目了然。"
caregiverToken={dashboard.caregiver.sessionToken} caregiverToken={dashboard.caregiver.sessionToken}
actions={<PwaControls />} actions={<PwaControls />}
> >
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4"> <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)]"> <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="text-sm text-[var(--muted)]"></p>
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]"> <p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
{dashboard.devices.length} {dashboard.devices.length}
</p> </p>
</div> </div>
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5 shadow-[0_20px_50px_rgba(115,76,42,0.10)]"> <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="text-sm text-[var(--muted)]"></p>
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]"> <p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
{totalUnreadForCaregiver} {totalUnreadForCaregiver}
</p> </p>
</div> </div>
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5 shadow-[0_20px_50px_rgba(115,76,42,0.10)]"> <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="text-sm text-[var(--muted)]"></p>
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]"> <p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
{totalUnreadForElder} {totalUnreadForElder}
</p> </p>
</div> </div>
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5 shadow-[0_20px_50px_rgba(115,76,42,0.10)]"> <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="text-sm text-[var(--muted)]"></p>
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]"> <p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
{totalConversations} {totalConversations}
</p> </p>
@ -71,69 +72,77 @@ export default async function Home() {
{[ {[
{ {
href: "/messages", href: "/messages",
title: "留言中心", title: "留言",
text: "集中看所有老人留言和家属留言,每条消息都有 URL 编号可直接打开。", text: "查看长辈捎来的话,也能回看您发出的每一条问候。",
tone: "bg-[rgba(53,84,141,0.12)]", tone: "bg-[rgba(53,84,141,0.12)]",
icon: MessageCircle,
}, },
{ {
href: "/devices", href: "/devices",
title: "设备与绑定", title: "我的设备",
text: "去管理设备、扫码绑定、查看某一台设备的专属详情页和留言入口。", text: "管理已绑定的长辈设备,进入设备详情给长辈写留言。",
tone: "bg-[rgba(118,132,94,0.14)]", tone: "bg-[rgba(118,132,94,0.14)]",
icon: Smartphone,
}, },
{ {
href: "/activity", href: "/activity",
title: "活动回看", title: "陪伴动态",
text: "把使用事件、工具调用和对话片段拆出来,回看时不再和留言混在一起。", text: "了解长辈最近的使用情况和聊天记录,安心守护。",
tone: "bg-[rgba(199,103,51,0.12)]", tone: "bg-[rgba(199,103,51,0.12)]",
icon: Activity,
}, },
{ {
href: "/scan", href: "/scan",
title: "扫码绑定", title: "扫码绑定",
text: "拿起另一台手机时,直接走扫码页,不必再回首页寻找入口。", text: "扫描长辈手机上的二维码,一步完成设备连接。",
tone: "bg-[rgba(36,27,20,0.08)]", tone: "bg-[rgba(36,27,20,0.08)]",
icon: ScanLine,
}, },
].map((item) => ( ].map((item) => {
const Icon = item.icon;
return (
<Link <Link
key={item.href} key={item.href}
href={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" 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 className={`inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs font-semibold text-[var(--ink)] ${item.tone}`}>
<Icon size={14} />
{item.title}
</div> </div>
<h2 className="mt-4 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]"> <h2 className="mt-4 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
{item.title} {item.title}
</h2> </h2>
<p className="mt-3 text-sm leading-7 text-[var(--muted)]">{item.text}</p> <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> <p className="mt-4 text-sm font-semibold text-[var(--copper)]"> </p>
</Link> </Link>
))} );
})}
</section> </section>
{dashboard.devices.length === 0 ? ( {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)]"> <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 className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
使
</p> </p>
<h2 className="mt-3 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]"> <h2 className="mt-3 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
</h2> </h2>
<p className="mx-auto mt-4 max-w-2xl text-base leading-8 text-[var(--muted)]"> <p className="mx-auto mt-4 max-w-2xl text-base leading-8 text-[var(--muted)]">
URL 使
</p> </p>
<div className="mt-6 flex justify-center gap-3"> <div className="mt-6 flex justify-center gap-3">
<Link <Link
href="/devices" href="/scan"
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)]" 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>
<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)]" 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> </Link>
</div> </div>
</section> </section>
@ -143,24 +152,24 @@ export default async function Home() {
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div> <div>
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase"> <p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p> </p>
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]"> <h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
</h2> </h2>
</div> </div>
<Link <Link
href="/messages" 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)]" 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> </Link>
</div> </div>
<div className="mt-5 space-y-3"> <div className="mt-5 space-y-3">
{recentIncomingMessages.length === 0 ? ( {recentIncomingMessages.length === 0 ? (
<p className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]"> <p className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]">
</p> </p>
) : ( ) : (
recentIncomingMessages.map((message) => ( recentIncomingMessages.map((message) => (
@ -190,24 +199,24 @@ export default async function Home() {
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div> <div>
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase"> <p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p> </p>
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]"> <h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
</h2> </h2>
</div> </div>
<Link <Link
href="/devices" 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)]" 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> </Link>
</div> </div>
<div className="mt-5 space-y-3"> <div className="mt-5 space-y-3">
{recentOutgoingMessages.length === 0 ? ( {recentOutgoingMessages.length === 0 ? (
<p className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]"> <p className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4 text-sm leading-7 text-[var(--muted)]">
</p> </p>
) : ( ) : (
recentOutgoingMessages.map((message) => ( recentOutgoingMessages.map((message) => (

View File

@ -8,19 +8,19 @@ export default function ScanPage() {
<section className="grid gap-6 lg:grid-cols-[0.82fr_1.18fr]"> <section className="grid gap-6 lg:grid-cols-[0.82fr_1.18fr]">
<div className="rounded-[36px] border border-white/70 bg-white/78 p-6 shadow-[0_30px_70px_rgba(115,76,42,0.14)] backdrop-blur"> <div className="rounded-[36px] border border-white/70 bg-white/78 p-6 shadow-[0_30px_70px_rgba(115,76,42,0.14)] backdrop-blur">
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase"> <p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p> </p>
<h1 className="mt-4 font-[family-name:var(--font-display)] text-4xl leading-[1.15] text-[var(--ink)]"> <h1 className="mt-4 font-[family-name:var(--font-display)] text-4xl leading-[1.15] text-[var(--ink)]">
</h1> </h1>
<p className="mt-4 text-base leading-8 text-[var(--muted)]"> <p className="mt-4 text-base leading-8 text-[var(--muted)]">
UUID 使 使
</p> </p>
<div className="mt-6 space-y-3 rounded-[28px] bg-[var(--paper-soft)] p-5 text-sm leading-7 text-[var(--muted)]"> <div className="mt-6 space-y-3 rounded-[28px] bg-[var(--paper-soft)] p-5 text-sm leading-7 text-[var(--muted)]">
<p>1. </p> <p>1. </p>
<p>2. </p> <p>2. </p>
<p>3. </p> <p>3. </p>
</div> </div>
<div className="mt-6 flex flex-wrap gap-3"> <div className="mt-6 flex flex-wrap gap-3">
@ -28,7 +28,7 @@ export default function ScanPage() {
href="/devices" 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)]" 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> </Link>
</div> </div>
</div> </div>

View File

@ -0,0 +1,65 @@
"use client";
import {
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
CartesianGrid,
} from "recharts";
type DailyCount = {
date: string;
count: number;
};
type ActivityChartProps = {
data: DailyCount[];
color?: string;
};
export function ActivityChart({ data, color = "var(--copper)" }: ActivityChartProps) {
if (data.length === 0) {
return null;
}
return (
<div className="h-48 w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data} margin={{ top: 4, right: 4, bottom: 0, left: -20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(85,60,42,0.08)" />
<XAxis
dataKey="date"
tick={{ fontSize: 11, fill: "#726153" }}
tickLine={false}
axisLine={false}
/>
<YAxis
tick={{ fontSize: 11, fill: "#726153" }}
tickLine={false}
axisLine={false}
allowDecimals={false}
/>
<Tooltip
contentStyle={{
borderRadius: 16,
border: "1px solid rgba(85,60,42,0.12)",
background: "rgba(255,253,248,0.96)",
fontSize: 13,
}}
labelStyle={{ color: "#241b14", fontWeight: 600 }}
formatter={(value: number) => [`${value}`, "次数"]}
/>
<Bar
dataKey="count"
fill={color}
radius={[6, 6, 0, 0]}
maxBarSize={32}
/>
</BarChart>
</ResponsiveContainer>
</div>
);
}

View File

@ -14,7 +14,7 @@ export function BindDeviceForm() {
event.preventDefault(); event.preventDefault();
if (!rawCode.trim()) { if (!rawCode.trim()) {
setNotice("先把老人二维码里的设备码贴进来。\n"); setNotice("请先粘贴长辈手机上的设备码。\n");
return; return;
} }
@ -38,7 +38,7 @@ export function BindDeviceForm() {
throw new Error(payload?.error || "连接失败,请稍后再试。"); throw new Error(payload?.error || "连接失败,请稍后再试。");
} }
setNotice("已经连上了,这位老人的留言和陪伴记录会出现在下方。\n"); setNotice("连接成功!现在可以查看长辈的留言和动态了。\n");
setRawCode(""); setRawCode("");
startTransition(() => { startTransition(() => {
router.refresh(); router.refresh();
@ -57,20 +57,20 @@ export function BindDeviceForm() {
<div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between"> <div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-xl"> <div className="max-w-xl">
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase"> <p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p> </p>
<h3 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]"> <h3 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
</h3> </h3>
<p className="mt-2 text-sm leading-7 text-[var(--muted)]"> <p className="mt-2 text-sm leading-7 text-[var(--muted)]">
</p> </p>
</div> </div>
<Link <Link
href="/scan" href="/scan"
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)]" 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>
</div> </div>
@ -78,7 +78,7 @@ export function BindDeviceForm() {
<input <input
value={rawCode} value={rawCode}
onChange={(event) => setRawCode(event.target.value)} onChange={(event) => setRawCode(event.target.value)}
placeholder="粘贴二维码链接,或者直接贴设备 UUID" placeholder="粘贴设备码或二维码链接"
className="h-13 flex-1 rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-5 text-sm text-[var(--ink)] outline-none transition focus:border-[var(--copper)] focus:bg-white" className="h-13 flex-1 rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-5 text-sm text-[var(--ink)] outline-none transition focus:border-[var(--copper)] focus:bg-white"
/> />
<button <button
@ -115,7 +115,7 @@ export function SendMessageForm({ deviceUuid }: SendMessageFormProps) {
event.preventDefault(); event.preventDefault();
if (!content.trim()) { if (!content.trim()) {
setNotice("先写下想对老人说的话。\n"); setNotice("先写下想说的话。\n");
setCreatedMessagePublicId(null); setCreatedMessagePublicId(null);
return; return;
} }
@ -144,7 +144,7 @@ export function SendMessageForm({ deviceUuid }: SendMessageFormProps) {
throw new Error(payload?.error || "留言发送失败,请稍后再试。"); throw new Error(payload?.error || "留言发送失败,请稍后再试。");
} }
setNotice("留言已经写进家人的收件盒,老人下次打开或询问时就能看到。\n"); setNotice("留言已发出,长辈下次使用时就能看到。\n");
setCreatedMessagePublicId( setCreatedMessagePublicId(
typeof payload?.publicId === "number" ? payload.publicId : null, typeof payload?.publicId === "number" ? payload.publicId : null,
); );
@ -168,10 +168,10 @@ export function SendMessageForm({ deviceUuid }: SendMessageFormProps) {
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div> <div>
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase"> <p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p> </p>
<h4 className="mt-2 font-[family-name:var(--font-display)] text-xl text-[var(--ink)]"> <h4 className="mt-2 font-[family-name:var(--font-display)] text-xl text-[var(--ink)]">
</h4> </h4>
</div> </div>
<select <select
@ -196,7 +196,7 @@ export function SendMessageForm({ deviceUuid }: SendMessageFormProps) {
<div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm leading-7 text-[var(--muted)]"> <p className="text-sm leading-7 text-[var(--muted)]">
function calling
</p> </p>
<button <button
type="submit" type="submit"

View File

@ -1,12 +1,13 @@
import Link from "next/link"; import Link from "next/link";
import { Home, MessageCircle, Smartphone, Activity, ScanLine } from "lucide-react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
const navigationItems = [ const navigationItems = [
{ href: "/", label: "总览" }, { href: "/", label: "首页", icon: Home },
{ href: "/messages", label: "留言" }, { href: "/messages", label: "留言", icon: MessageCircle },
{ href: "/devices", label: "设备" }, { href: "/devices", label: "设备", icon: Smartphone },
{ href: "/activity", label: "动" }, { href: "/activity", label: "", icon: Activity },
{ href: "/scan", label: "扫码" }, { href: "/scan", label: "扫码", icon: ScanLine },
]; ];
function isActivePath(currentPath: string, href: string) { function isActivePath(currentPath: string, href: string) {
@ -34,7 +35,7 @@ export function PanelShell({
caregiverToken, caregiverToken,
children, children,
actions, actions,
eyebrow = "Digital Human Bridge", eyebrow = "家人连线",
}: PanelShellProps) { }: PanelShellProps) {
return ( return (
<main className="relative overflow-hidden px-4 py-6 sm:px-6 lg:px-8 lg:py-8"> <main className="relative overflow-hidden px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
@ -42,24 +43,26 @@ export function PanelShell({
<div className="mx-auto max-w-7xl space-y-6"> <div className="mx-auto max-w-7xl space-y-6">
<header className="paper-panel relative overflow-hidden rounded-[36px] border border-white/70 px-6 py-7 shadow-[0_35px_90px_rgba(105,68,41,0.13)] sm:px-8 lg:px-10 lg:py-8"> <header className="paper-panel relative overflow-hidden rounded-[36px] border border-white/70 px-6 py-7 shadow-[0_35px_90px_rgba(105,68,41,0.13)] sm:px-8 lg:px-10 lg:py-8">
<div className="absolute -right-12 top-[-50px] h-44 w-44 rounded-full bg-[rgba(53,84,141,0.14)] blur-2xl" /> <div className="absolute -right-12 top-[-50px] h-44 w-44 rounded-full bg-[rgba(53,84,141,0.14)] blur-2xl" />
<div className="absolute left-10 top-10 h-24 w-24 rounded-full border border-[rgba(118,132,94,0.18)]" /> <div className="absolute left-10 top-10 h-24 w-24 rounded-full border border-[rgba(118,132,94,0.18)] z-0 pointer-events-none" />
<div className="flex flex-col gap-6 xl:flex-row xl:items-start xl:justify-between"> <div className="flex flex-col gap-6 xl:flex-row xl:items-start xl:justify-between">
<div className="max-w-4xl"> <div className="max-w-4xl">
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-2">
{navigationItems.map((item) => { {navigationItems.map((item) => {
const active = isActivePath(currentPath, item.href); const active = isActivePath(currentPath, item.href);
const Icon = item.icon;
return ( return (
<Link <Link
key={item.href} key={item.href}
href={item.href} href={item.href}
className={`inline-flex h-11 items-center justify-center rounded-full px-4 text-sm font-semibold transition ${ className={`inline-flex h-11 items-center justify-center gap-2 rounded-full px-4 text-sm font-semibold transition ${
active active
? "bg-[var(--ink)] text-white shadow-[0_12px_30px_rgba(36,27,20,0.18)]" ? "bg-[var(--ink)] text-white shadow-[0_12px_30px_rgba(36,27,20,0.18)]"
: "border border-[var(--line)] bg-white/82 text-[var(--ink)] hover:bg-[var(--paper-soft)]" : "border border-[var(--line)] bg-white/82 text-[var(--ink)] hover:bg-[var(--paper-soft)]"
}`} }`}
> >
<Icon size={16} />
{item.label} {item.label}
</Link> </Link>
); );
@ -78,10 +81,7 @@ export function PanelShell({
<div className="mt-6 flex flex-wrap gap-3 text-sm text-[var(--muted)]"> <div className="mt-6 flex flex-wrap gap-3 text-sm text-[var(--muted)]">
<span className="rounded-full border border-[var(--line)] bg-white/76 px-4 py-2"> <span className="rounded-full border border-[var(--line)] bg-white/76 px-4 py-2">
{caregiverToken.slice(0, 8).toUpperCase()} {caregiverToken.slice(0, 8).toUpperCase()}
</span>
<span className="rounded-full border border-[var(--line)] bg-white/76 px-4 py-2">
</span> </span>
</div> </div>
</div> </div>

View File

@ -119,7 +119,7 @@ export function PwaControls() {
async function enableNotifications() { async function enableNotifications() {
if (!vapidPublicKey) { if (!vapidPublicKey) {
setNotice("当前环境还没配置推送密钥,先把面板安装到桌面即可。\n"); setNotice("当前还没有配置推送服务,先将应用安装到桌面即可。\n");
return; return;
} }
@ -160,7 +160,7 @@ export function PwaControls() {
}); });
setNotificationState("enabled"); setNotificationState("enabled");
setNotice("新留言会直接推到这台设备上,点开即可直达对应留言。\n"); setNotice("已开启消息提醒,长辈的新留言会直接推送到您的设备。\n");
} catch { } catch {
setNotificationState("idle"); setNotificationState("idle");
setNotice("打开消息提醒失败,请稍后再试。\n"); setNotice("打开消息提醒失败,请稍后再试。\n");
@ -219,18 +219,18 @@ export function PwaControls() {
return ( return (
<section className="rounded-[28px] border border-white/80 bg-white/88 p-5 shadow-[0_22px_55px_rgba(53,84,141,0.12)]"> <section className="rounded-[28px] border border-white/80 bg-white/88 p-5 shadow-[0_22px_55px_rgba(53,84,141,0.12)]">
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase"> <p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p> </p>
<h2 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]"> <h2 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
</h2> </h2>
<p className="mt-3 text-sm leading-7 text-[var(--muted)]"> <p className="mt-3 text-sm leading-7 text-[var(--muted)]">
</p> </p>
<div className="mt-4 flex flex-wrap gap-3 text-sm text-[var(--muted)]"> <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"> <span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
{isStandalone ? "当前已可从桌面直接打开" : "浏览器内访问中"} {isStandalone ? "已安装到桌面" : "当前在浏览器中打开"}
</span> </span>
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2"> <span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
{notificationState === "enabled" {notificationState === "enabled"

View File

@ -45,7 +45,7 @@ export function ScanClient() {
scannerRef.current.stop(); scannerRef.current.stop();
setErrorText(null); setErrorText(null);
setStatusText("识别到了设备,正在建立连接……"); setStatusText("识别到了设备,正在连接……");
try { try {
const response = await fetch("/api/family/bind", { const response = await fetch("/api/family/bind", {
@ -62,7 +62,7 @@ export function ScanClient() {
const deviceUuid = payload?.deviceUuid; const deviceUuid = payload?.deviceUuid;
if (!response.ok || !deviceUuid) { if (!response.ok || !deviceUuid) {
throw new Error(payload?.error || "绑定失败,请重新扫描一次。"); throw new Error(payload?.error || "连接失败,请重新扫描。");
} }
startTransition(() => { startTransition(() => {
@ -72,9 +72,9 @@ export function ScanClient() {
}); });
} catch (error) { } catch (error) {
setErrorText( setErrorText(
error instanceof Error ? error.message : "绑定失败,请重新扫描一次。", error instanceof Error ? error.message : "连接失败,请重新扫描。",
); );
setStatusText("请再把二维码对准镜头中央。"); setStatusText("请再次将二维码对准镜头。");
await scannerRef.current.start().catch(() => null); await scannerRef.current.start().catch(() => null);
} }
}); });
@ -91,7 +91,7 @@ export function ScanClient() {
const hasCamera = await QrScanner.hasCamera(); const hasCamera = await QrScanner.hasCamera();
if (!hasCamera) { if (!hasCamera) {
setErrorText("当前设备没有可用摄像头,请改用系统扫码或手动粘贴设备码。"); setErrorText("当前设备没有可用摄像头,请改用手动粘贴设备码。");
setStatusText("无法打开镜头"); setStatusText("无法打开镜头");
return; return;
} }
@ -114,7 +114,7 @@ export function ScanClient() {
await scanner.start(); await scanner.start();
if (!cancelled) { if (!cancelled) {
setStatusText("请把老人的二维码放到取景框中央。"); setStatusText("请将长辈手机上的二维码放到取景框内。");
} }
} catch (error) { } catch (error) {
setErrorText( setErrorText(
@ -151,7 +151,7 @@ export function ScanClient() {
<div className="mt-4 space-y-2"> <div className="mt-4 space-y-2">
<p className="text-base font-semibold text-[var(--ink)]">{statusText}</p> <p className="text-base font-semibold text-[var(--ink)]">{statusText}</p>
<p className="text-sm leading-7 text-[var(--muted)]"> <p className="text-sm leading-7 text-[var(--muted)]">
</p> </p>
{errorText ? ( {errorText ? (
<p className="rounded-[20px] bg-[var(--paper-soft)] px-4 py-3 text-sm leading-7 text-[var(--copper-deep)]"> <p className="rounded-[20px] bg-[var(--paper-soft)] px-4 py-3 text-sm leading-7 text-[var(--copper-deep)]">

View File

@ -39,7 +39,7 @@ export function getUsageLabel(eventType: UsageEventType | string) {
case UsageEventType.APP_OPEN: case UsageEventType.APP_OPEN:
return "打开了老人端应用"; return "打开了老人端应用";
case UsageEventType.SETTINGS_OPENED: case UsageEventType.SETTINGS_OPENED:
return "打开了家人连接页"; return "查看了设置页面";
case UsageEventType.AI_SESSION_STARTED: case UsageEventType.AI_SESSION_STARTED:
return "开始了一次陪伴对话"; return "开始了一次陪伴对话";
case UsageEventType.AI_SESSION_ENDED: case UsageEventType.AI_SESSION_ENDED:
@ -49,7 +49,7 @@ export function getUsageLabel(eventType: UsageEventType | string) {
case UsageEventType.CAMERA_DISABLED: case UsageEventType.CAMERA_DISABLED:
return "关闭了镜头模式"; return "关闭了镜头模式";
case UsageEventType.TOOL_CALLED: case UsageEventType.TOOL_CALLED:
return "模型调用了一次桥接工具"; return "使用了一项智能服务";
default: default:
return eventType; return eventType;
} }
@ -60,7 +60,7 @@ export function getConversationRoleLabel(role: ConversationRole | string) {
case ConversationRole.USER: case ConversationRole.USER:
return "老人说"; return "老人说";
case ConversationRole.TOOL: case ConversationRole.TOOL:
return "工具结果"; return "智能助理";
default: default:
return "数字人回复"; return "数字人回复";
} }
@ -110,5 +110,5 @@ export function getMessageHref(publicId: number) {
} }
export function getDeviceName(displayName?: string | null) { export function getDeviceName(displayName?: string | null) {
return displayName?.trim() || "未命名陪伴设备"; return displayName?.trim() || "长辈的设备";
} }

6299
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -13,11 +13,13 @@
"@prisma/adapter-pg": "^7.7.0", "@prisma/adapter-pg": "^7.7.0",
"@prisma/client": "^7.7.0", "@prisma/client": "^7.7.0",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"lucide-react": "^1.8.0",
"next": "16.2.4", "next": "16.2.4",
"pg": "^8.20.0", "pg": "^8.20.0",
"qr-scanner": "^1.4.2", "qr-scanner": "^1.4.2",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",
"recharts": "^3.8.1",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"web-push": "^3.6.7" "web-push": "^3.6.7"
}, },