feat: initialize aivideo project

This commit is contained in:
2026-04-17 18:33:05 +08:00
commit 14b18d67fe
162 changed files with 26251 additions and 0 deletions

View 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>
);
}