feat: initialize aivideo project
This commit is contained in:
118
frontend-web/src/components/site-shell.tsx
Normal file
118
frontend-web/src/components/site-shell.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
Coins,
|
||||
FolderKanban,
|
||||
ImagePlus,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
Ticket,
|
||||
UserRound,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo } from "react";
|
||||
|
||||
import { api } from "@/lib/api";
|
||||
import type { UserProfile } from "@/lib/types";
|
||||
|
||||
const navigation = [
|
||||
{ href: "/workspace/create", label: "创建任务", icon: Sparkles },
|
||||
{ href: "/workspace/tasks", label: "任务记录", icon: FolderKanban },
|
||||
{ href: "/workspace/assets", label: "素材管理", icon: ImagePlus },
|
||||
{ href: "/wallet", label: "钱包概览", icon: Coins },
|
||||
{ href: "/wallet/recharge", label: "充值中心", icon: Coins },
|
||||
{ href: "/wallet/redeem", label: "兑换密钥", icon: Ticket },
|
||||
{ href: "/invite", label: "邀请中心", icon: Users },
|
||||
{ href: "/profile", label: "个人资料", icon: UserRound },
|
||||
];
|
||||
|
||||
export function SiteShell({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { data, error, isLoading } = useQuery({
|
||||
queryKey: ["web-me"],
|
||||
queryFn: () => api.get<UserProfile>("/api/v1/auth/me"),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
router.replace("/login");
|
||||
}
|
||||
}, [error, router]);
|
||||
|
||||
const title = useMemo(() => {
|
||||
const current = navigation.find((item) => pathname.startsWith(item.href));
|
||||
return current?.label ?? "AIVideo";
|
||||
}, [pathname]);
|
||||
|
||||
if (isLoading || !data) {
|
||||
return (
|
||||
<div className="fullscreen-shell">
|
||||
<div className="pulse-card">正在验证登录态并加载工作台...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="shell-grid">
|
||||
<aside className="shell-sidebar">
|
||||
<div className="brand-block">
|
||||
<span className="brand-kicker">AI VIDEO PLATFORM</span>
|
||||
<h1>AIVideo</h1>
|
||||
<p>把文生视频、图生视频、充值结算和模型路由放进一个工作台。</p>
|
||||
</div>
|
||||
|
||||
<nav className="sidebar-nav">
|
||||
{navigation.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={clsx("nav-item", {
|
||||
active: pathname.startsWith(item.href),
|
||||
})}
|
||||
>
|
||||
<Icon size={18} />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="profile-card">
|
||||
<div>
|
||||
<div className="profile-name">{data.nickname || data.username}</div>
|
||||
<div className="profile-meta">{data.email}</div>
|
||||
</div>
|
||||
<button
|
||||
className="ghost-button"
|
||||
onClick={async () => {
|
||||
await api.post("/api/v1/auth/logout");
|
||||
router.replace("/login");
|
||||
}}
|
||||
>
|
||||
<LogOut size={16} />
|
||||
退出
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="shell-main">
|
||||
<header className="shell-header">
|
||||
<div>
|
||||
<div className="header-kicker">Creative Ops</div>
|
||||
<h2>{title}</h2>
|
||||
</div>
|
||||
<div className="header-chip">{data.publicId}</div>
|
||||
</header>
|
||||
<section className="shell-content">{children}</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user