feat: move navigation to right sidebar

Switch top navigation to a vertical right rail with icon+label and adjust page layouts to reserve sidebar space.
This commit is contained in:
Your Name 2026-02-03 15:15:06 +08:00
parent 33f2a729d6
commit 28d0f445c5
5 changed files with 113 additions and 90 deletions

View File

@ -43,7 +43,7 @@ export default async function AdminLayout({
tutorialEnabled={siteSettings.tutorialEnabled ?? false}
rankingEnabled={siteSettings.rankingEnabled ?? true}
/>
<div className="flex-1 pt-16">{children}</div>
<div className="flex-1 pr-20">{children}</div>
</div>
);
}

View File

@ -63,9 +63,9 @@ async function SidebarWrapper({ children }: { children: React.ReactNode }) {
tutorialEnabled={siteSettings.tutorialEnabled ?? false}
rankingEnabled={siteSettings.rankingEnabled ?? true}
/>
<div className="pt-14 flex h-full w-full overflow-hidden"> {/* Add padding-top for fixed navbar */}
<div className="flex h-svh w-full overflow-hidden pr-20">
<AppSidebar user={session?.user} />
<SidebarInset className="h-[calc(100vh-56px)]">{children}</SidebarInset> {/* Adjust height */}
<SidebarInset className="h-full">{children}</SidebarInset>
</div>
</SidebarProvider>
);

View File

@ -59,7 +59,7 @@ async function CanvasLayoutWrapper({ children }: { children: React.ReactNode })
tutorialEnabled={siteSettings.tutorialEnabled ?? false}
rankingEnabled={siteSettings.rankingEnabled ?? true}
/>
<div className="pt-12 h-full">
<div className="h-full pr-20">
{children}
</div>
</div>

View File

@ -59,7 +59,7 @@ export function AppSidebar({ user }: { user: User | undefined }) {
return (
<>
<Sidebar className="group-data-[side=left]:border-r-0 !top-14 !h-[calc(100svh-3.5rem)]">
<Sidebar className="group-data-[side=left]:border-r-0">
<SidebarHeader>
<SidebarMenu>
<div className="flex flex-row items-center justify-between w-full">

View File

@ -124,6 +124,25 @@ export function TopNavbar({
// 决定显示哪个 LOGO
const displayLogo = (siteLogoDark && siteLogoDark.trim() !== "") ? siteLogoDark : siteLogo;
const getShortLabel = (label: string) => {
const trimmed = label.trim();
if (!trimmed) return "";
if (trimmed === "AI精选") return "精选";
if (trimmed === "功能教程") return "教程";
if (trimmed === "精选榜") return "榜单";
if (trimmed === "图片编辑") return "编辑";
if (trimmed === "无限画布") return "画布";
if (trimmed === "我的图册") return "图册";
if (trimmed === "系统设置") return "设置";
if (trimmed === "工具箱") return "工具";
if (trimmed === "工具箱2") return "工具";
return Array.from(trimmed).slice(0, 2).join("");
};
const navItemBaseClass =
"flex h-14 w-14 flex-col items-center justify-center gap-1 rounded-xl text-white hover:bg-white/10 transition-colors";
const navLabelClass = "text-[10px] leading-none font-medium";
// 打开图片编辑器 - 直接打开,不触发上传
const handleOpenImageEditor = () => {
setEditorImageUrl(""); // 确保初始为空
@ -182,11 +201,11 @@ export function TopNavbar({
};
const navContent = (
<nav className="fixed top-0 left-0 right-0 z-[100] flex h-12 items-center justify-between bg-black/90 backdrop-blur-sm px-4 text-white border-b border-white/10 shadow-sm">
{/* 左侧:LOGO + 网站名称 */}
<nav className="fixed right-0 top-0 bottom-0 z-[100] flex w-20 flex-col items-center bg-black/90 backdrop-blur-sm text-white border-l border-white/10 shadow-sm">
{/* 顶部:LOGO + 网站名称 */}
<a
href="/"
className="flex items-center gap-2 transition-opacity hover:opacity-80"
className="mt-3 flex flex-col items-center gap-1 transition-opacity hover:opacity-80"
onClick={(e) => {
e.preventDefault();
window.location.href = "/";
@ -196,14 +215,14 @@ export function TopNavbar({
<img
src={displayLogo}
alt={siteName}
className="h-6 w-6 object-contain"
className="h-7 w-7 object-contain"
/>
)}
<span className="text-sm font-semibold">{siteName}</span>
<span className="max-w-[4.5rem] text-center text-[10px] leading-tight line-clamp-2">{siteName}</span>
</a>
{/* 右侧区域:自定义链接 + 系统设置 */}
<div className="flex items-center gap-2">
{/* 侧边功能区 */}
<div className="mt-4 flex w-full flex-1 flex-col items-center gap-3 overflow-y-auto px-2 pb-4">
{/* 工具箱1 (如果启用且有链接) */}
{toolbox1Enabled && customLinks.filter(l => (l.toolboxGroup || 1) === 1).length > 0 && (
<DropdownMenu>
@ -211,31 +230,33 @@ export function TopNavbar({
<Button
variant="ghost"
size="sm"
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors"
className={`${navItemBaseClass} p-0`}
title={toolbox1Name}
type="button"
>
<Briefcase className="h-4 w-4" />
<span className="text-sm font-medium">{toolbox1Name}</span>
<Briefcase className="h-5 w-5" />
<span className={navLabelClass}>{getShortLabel(toolbox1Name)}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[320px] p-2">
<div className="grid grid-cols-2 gap-2">
{customLinks
.filter(l => (l.toolboxGroup || 1) === 1)
.sort((a, b) => a.order - b.order)
.map((link) => (
<DropdownMenuItem key={link.id} asChild className="cursor-pointer">
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="flex flex-col items-center justify-center gap-2 rounded-md bg-muted/50 p-4 hover:bg-accent hover:text-accent-foreground text-center h-20 transition-colors"
>
<ExternalLink className="h-5 w-5 opacity-70" />
<span className="text-xs font-medium line-clamp-2">{link.name}</span>
</a>
</DropdownMenuItem>
))}
</div>
<DropdownMenuContent side="left" align="center" className="w-[320px] p-2">
<div className="grid grid-cols-2 gap-2">
{customLinks
.filter(l => (l.toolboxGroup || 1) === 1)
.sort((a, b) => a.order - b.order)
.map((link) => (
<DropdownMenuItem key={link.id} asChild className="cursor-pointer">
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="flex flex-col items-center justify-center gap-2 rounded-md bg-muted/50 p-4 hover:bg-accent hover:text-accent-foreground text-center h-20 transition-colors"
>
<ExternalLink className="h-5 w-5 opacity-70" />
<span className="text-xs font-medium line-clamp-2">{link.name}</span>
</a>
</DropdownMenuItem>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
)}
@ -247,31 +268,33 @@ export function TopNavbar({
<Button
variant="ghost"
size="sm"
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors"
className={`${navItemBaseClass} p-0`}
title={toolbox2Name}
type="button"
>
<Briefcase className="h-4 w-4" />
<span className="text-sm font-medium">{toolbox2Name}</span>
<Briefcase className="h-5 w-5" />
<span className={navLabelClass}>{getShortLabel(toolbox2Name)}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[320px] p-2">
<div className="grid grid-cols-2 gap-2">
{customLinks
.filter(l => l.toolboxGroup === 2)
.sort((a, b) => a.order - b.order)
.map((link) => (
<DropdownMenuItem key={link.id} asChild className="cursor-pointer">
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="flex flex-col items-center justify-center gap-2 rounded-md bg-muted/50 p-4 hover:bg-accent hover:text-accent-foreground text-center h-20 transition-colors"
>
<ExternalLink className="h-5 w-5 opacity-70" />
<span className="text-xs font-medium line-clamp-2">{link.name}</span>
</a>
</DropdownMenuItem>
))}
</div>
<DropdownMenuContent side="left" align="center" className="w-[320px] p-2">
<div className="grid grid-cols-2 gap-2">
{customLinks
.filter(l => l.toolboxGroup === 2)
.sort((a, b) => a.order - b.order)
.map((link) => (
<DropdownMenuItem key={link.id} asChild className="cursor-pointer">
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="flex flex-col items-center justify-center gap-2 rounded-md bg-muted/50 p-4 hover:bg-accent hover:text-accent-foreground text-center h-20 transition-colors"
>
<ExternalLink className="h-5 w-5 opacity-70" />
<span className="text-xs font-medium line-clamp-2">{link.name}</span>
</a>
</DropdownMenuItem>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
)}
@ -279,15 +302,15 @@ export function TopNavbar({
{/* AI智能精选 */}
<a
href="/featured"
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors text-sm font-medium"
title="AI智能精选"
className={navItemBaseClass}
title="AI精选"
onClick={(e) => {
e.preventDefault();
window.location.href = "/featured";
}}
>
<Sparkles className="h-4 w-4 text-yellow-400" />
<span>AI精选</span>
<Sparkles className="h-5 w-5 text-yellow-400" />
<span className={navLabelClass}>{getShortLabel("AI精选")}</span>
</a>
{/* 功能教程 */}
@ -297,14 +320,15 @@ export function TopNavbar({
<Button
variant="ghost"
size="sm"
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors"
className={`${navItemBaseClass} p-0`}
type="button"
title="功能教程"
>
<PlayCircle className="h-4 w-4 text-green-400" />
<span className="text-sm font-medium">功能教程</span>
<PlayCircle className="h-5 w-5 text-green-400" />
<span className={navLabelClass}>{getShortLabel("功能教程")}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[320px] p-2">
<DropdownMenuContent side="left" align="center" className="w-[320px] p-2">
<div className="space-y-1 max-h-[400px] overflow-y-auto">
{tutorialVideos
.sort((a, b) => a.order - b.order)
@ -355,14 +379,15 @@ export function TopNavbar({
<Button
variant="ghost"
size="sm"
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors"
className={`${navItemBaseClass} p-0`}
type="button"
title="精选榜单"
>
<Trophy className="h-4 w-4 text-orange-400" />
<span className="text-sm font-medium">精选榜</span>
<Trophy className="h-5 w-5 text-orange-400" />
<span className={navLabelClass}>{getShortLabel("精选榜")}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[280px] p-3">
<DropdownMenuContent side="left" align="center" className="w-[280px] p-3">
<div className="flex items-center gap-2 mb-3 pb-2 border-b">
<Trophy className="h-5 w-5 text-orange-500" />
<span className="font-semibold">精选榜单 TOP5</span>
@ -409,18 +434,18 @@ export function TopNavbar({
<Button
variant="ghost"
size="sm"
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors"
className={`${navItemBaseClass} p-0`}
onClick={handleOpenImageEditor}
type="button"
title="图片编辑"
>
<Edit className="h-4 w-4" />
<span className="text-sm font-medium">图片编辑</span>
<Edit className="h-5 w-5" />
<span className={navLabelClass}>{getShortLabel("图片编辑")}</span>
</Button>
{/* 无限画布 */}
<button
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors text-sm font-medium"
className={navItemBaseClass}
onClick={() => {
const newId = crypto.randomUUID();
window.location.href = `/canvas/${newId}`;
@ -428,44 +453,42 @@ export function TopNavbar({
type="button"
title="无限画布"
>
<LayoutGrid className="h-4 w-4 text-purple-400" />
<span>无限画布</span>
<LayoutGrid className="h-5 w-5 text-purple-400" />
<span className={navLabelClass}>{getShortLabel("无限画布")}</span>
</button>
{/* 我的图册 */}
<a
href="/gallery"
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors text-sm font-medium"
className={navItemBaseClass}
title="我的图册"
onClick={(e) => {
e.preventDefault();
window.location.href = "/gallery";
}}
>
<Images className="h-4 w-4" />
<span>我的图册</span>
<Images className="h-5 w-5" />
<span className={navLabelClass}>{getShortLabel("我的图册")}</span>
</a>
</div>
{/* 分隔线 (如果有链接或图册按钮,且是管理员) */}
{isAdmin && (
<div className="hidden h-4 w-px bg-white/20 mx-1 sm:block" />
)}
{/* 系统设置(仅管理员可见)*/}
{isAdmin && (
{/* 系统设置(仅管理员可见)*/}
{isAdmin && (
<div className="pb-4">
<a
href="/settings"
className="flex items-center justify-center h-8 w-8 text-white hover:bg-white/15 hover:text-white rounded-full"
className={navItemBaseClass}
title="系统设置"
onClick={(e) => {
e.preventDefault();
window.location.href = "/settings";
}}
>
<Settings className="h-4 w-4" />
<Settings className="h-5 w-5" />
<span className={navLabelClass}>{getShortLabel("系统设置")}</span>
</a>
)}
</div>
</div>
)}
{/* 图片编辑器 */}
{showImageEditor && (