Files
NianAIGC/components/app-shell.tsx
2026-06-03 12:03:14 +08:00

100 lines
3.4 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
Archive,
LogIn,
LogOut,
ScrollText,
Settings,
Sparkles,
UserCircle
} from "lucide-react";
import clsx from "clsx";
import { revealChildren, runScopedMotion } from "@/lib/ui/motion";
import type { AuthUser } from "@/lib/auth/session";
const nav = [
{ href: "/create", label: "创作", icon: Sparkles },
{ href: "/assets", label: "结果", icon: Archive },
{ href: "/logs", label: "日志", icon: ScrollText },
{ href: "/settings", label: "设置", icon: Settings }
];
export function AppShell({
children,
user,
authRequired
}: {
children: React.ReactNode;
user?: AuthUser | null;
authRequired?: boolean;
}) {
const pathname = usePathname();
const shellRef = useRef<HTMLDivElement | null>(null);
const isAuthPage = pathname.startsWith("/auth");
useEffect(() => {
return runScopedMotion(shellRef, (scope) => revealChildren(scope, "[data-shell-animate]"));
}, []);
return (
<div className={clsx("app-shell", isAuthPage && "auth-shell")} ref={shellRef}>
{!isAuthPage ? <a className="skip-link" href="#main-content"></a> : null}
{!isAuthPage ? (
<header className="topbar" data-shell-animate>
<Link className="brand" href="/create" aria-label="智念AIGC平台">
<span className="brand-mark" aria-hidden="true">
<Image className="brand-logo-img" src="/logo/zhinian-logo.png" alt="" width={124} height={28} priority />
</span>
<div>
<div className="brand-title">AIGC平台</div>
</div>
</Link>
<nav className="nav top-nav" aria-label="主导航">
{nav.map((item) => {
const Icon = item.icon;
const active = item.href === "/" ? pathname === "/" : pathname.startsWith(item.href);
return (
<Link
key={item.href}
href={item.href}
className={clsx("nav-link", active && "active")}
aria-current={active ? "page" : undefined}
>
<Icon aria-hidden="true" />
<span>{item.label}</span>
</Link>
);
})}
</nav>
<div className="topbar-account">
{user ? (
<>
<span className="account-chip" title={user.username || user.subject}>
<UserCircle aria-hidden="true" />
<span>{user.displayName}</span>
</span>
<form action="/api/auth/logout" method="post">
<button className="icon-button logout-button" type="submit" aria-label="退出登录" title="退出登录">
<LogOut aria-hidden="true" />
</button>
</form>
</>
) : authRequired ? (
<Link className="nav-link login-link" href="/auth/login">
<LogIn aria-hidden="true" />
<span></span>
</Link>
) : null}
</div>
</header>
) : null}
<main className={clsx("main", isAuthPage && "auth-main")} id="main-content" tabIndex={-1}>{children}</main>
</div>
);
}