105 lines
4.1 KiB
TypeScript
105 lines
4.1 KiB
TypeScript
import Link from "next/link";
|
||
import { Home, MessageCircle, Smartphone, Activity, ScanLine } from "lucide-react";
|
||
import type { ReactNode } from "react";
|
||
|
||
const navigationItems = [
|
||
{ href: "/", label: "首页", icon: Home },
|
||
{ href: "/messages", label: "留言", icon: MessageCircle },
|
||
{ href: "/devices", label: "设备", icon: Smartphone },
|
||
{ href: "/activity", label: "动态", icon: Activity },
|
||
{ href: "/scan", label: "扫码", icon: ScanLine },
|
||
];
|
||
|
||
function isActivePath(currentPath: string, href: string) {
|
||
if (href === "/") {
|
||
return currentPath === "/";
|
||
}
|
||
|
||
return currentPath === href || currentPath.startsWith(`${href}/`);
|
||
}
|
||
|
||
type PanelShellProps = {
|
||
currentPath: string;
|
||
title: string;
|
||
description: string;
|
||
caregiverLabel: string;
|
||
children: ReactNode;
|
||
actions?: ReactNode;
|
||
eyebrow?: string;
|
||
};
|
||
|
||
export function PanelShell({
|
||
currentPath,
|
||
title,
|
||
description,
|
||
caregiverLabel,
|
||
children,
|
||
actions,
|
||
eyebrow = "家人连线",
|
||
}: PanelShellProps) {
|
||
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-72 bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.8),transparent_70%)]" />
|
||
<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">
|
||
<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)] z-0 pointer-events-none" />
|
||
|
||
<div className="flex flex-col gap-6 xl:flex-row xl:items-start xl:justify-between">
|
||
<div className="max-w-4xl">
|
||
<div className="flex flex-wrap gap-2">
|
||
{navigationItems.map((item) => {
|
||
const active = isActivePath(currentPath, item.href);
|
||
const Icon = item.icon;
|
||
|
||
return (
|
||
<Link
|
||
key={item.href}
|
||
href={item.href}
|
||
className={`inline-flex h-11 items-center justify-center gap-2 rounded-full px-4 text-sm font-semibold transition ${
|
||
active
|
||
? "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)]"
|
||
}`}
|
||
>
|
||
<Icon size={16} />
|
||
{item.label}
|
||
</Link>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<p className="mt-6 text-sm font-semibold tracking-[0.24em] text-[var(--copper)] uppercase">
|
||
{eyebrow}
|
||
</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">
|
||
{title}
|
||
</h1>
|
||
<p className="mt-5 max-w-2xl text-base leading-8 text-[var(--muted)] sm:text-lg">
|
||
{description}
|
||
</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/76 px-4 py-2">
|
||
账号:{caregiverLabel}
|
||
</span>
|
||
<form action="/api/account/logout" method="post">
|
||
<button
|
||
type="submit"
|
||
className="inline-flex h-10 items-center justify-center rounded-full border border-[var(--line)] bg-white px-4 font-semibold text-[var(--ink)] transition hover:bg-[var(--paper-soft)]"
|
||
>
|
||
退出登录
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
{actions ? <div className="w-full max-w-md xl:pt-2">{actions}</div> : null}
|
||
</div>
|
||
</header>
|
||
|
||
{children}
|
||
</div>
|
||
</main>
|
||
);
|
||
} |