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} tutorialEnabled={siteSettings.tutorialEnabled ?? false}
rankingEnabled={siteSettings.rankingEnabled ?? true} rankingEnabled={siteSettings.rankingEnabled ?? true}
/> />
<div className="flex-1 pt-16">{children}</div> <div className="flex-1 pr-20">{children}</div>
</div> </div>
); );
} }

View File

@ -63,9 +63,9 @@ async function SidebarWrapper({ children }: { children: React.ReactNode }) {
tutorialEnabled={siteSettings.tutorialEnabled ?? false} tutorialEnabled={siteSettings.tutorialEnabled ?? false}
rankingEnabled={siteSettings.rankingEnabled ?? true} 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} /> <AppSidebar user={session?.user} />
<SidebarInset className="h-[calc(100vh-56px)]">{children}</SidebarInset> {/* Adjust height */} <SidebarInset className="h-full">{children}</SidebarInset>
</div> </div>
</SidebarProvider> </SidebarProvider>
); );

View File

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

View File

@ -59,7 +59,7 @@ export function AppSidebar({ user }: { user: User | undefined }) {
return ( 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> <SidebarHeader>
<SidebarMenu> <SidebarMenu>
<div className="flex flex-row items-center justify-between w-full"> <div className="flex flex-row items-center justify-between w-full">

View File

@ -124,6 +124,25 @@ export function TopNavbar({
// 决定显示哪个 LOGO // 决定显示哪个 LOGO
const displayLogo = (siteLogoDark && siteLogoDark.trim() !== "") ? siteLogoDark : siteLogo; 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 = () => { const handleOpenImageEditor = () => {
setEditorImageUrl(""); // 确保初始为空 setEditorImageUrl(""); // 确保初始为空
@ -182,11 +201,11 @@ export function TopNavbar({
}; };
const navContent = ( 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"> <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 + 网站名称 */} {/* 顶部:LOGO + 网站名称 */}
<a <a
href="/" 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) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
window.location.href = "/"; window.location.href = "/";
@ -196,14 +215,14 @@ export function TopNavbar({
<img <img
src={displayLogo} src={displayLogo}
alt={siteName} 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> </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 (如果启用且有链接) */} {/* 工具箱1 (如果启用且有链接) */}
{toolbox1Enabled && customLinks.filter(l => (l.toolboxGroup || 1) === 1).length > 0 && ( {toolbox1Enabled && customLinks.filter(l => (l.toolboxGroup || 1) === 1).length > 0 && (
<DropdownMenu> <DropdownMenu>
@ -211,31 +230,33 @@ export function TopNavbar({
<Button <Button
variant="ghost" variant="ghost"
size="sm" 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" /> <Briefcase className="h-5 w-5" />
<span className="text-sm font-medium">{toolbox1Name}</span> <span className={navLabelClass}>{getShortLabel(toolbox1Name)}</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[320px] p-2"> <DropdownMenuContent side="left" align="center" className="w-[320px] p-2">
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{customLinks {customLinks
.filter(l => (l.toolboxGroup || 1) === 1) .filter(l => (l.toolboxGroup || 1) === 1)
.sort((a, b) => a.order - b.order) .sort((a, b) => a.order - b.order)
.map((link) => ( .map((link) => (
<DropdownMenuItem key={link.id} asChild className="cursor-pointer"> <DropdownMenuItem key={link.id} asChild className="cursor-pointer">
<a <a
href={link.url} href={link.url}
target="_blank" target="_blank"
rel="noopener noreferrer" 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" 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" /> <ExternalLink className="h-5 w-5 opacity-70" />
<span className="text-xs font-medium line-clamp-2">{link.name}</span> <span className="text-xs font-medium line-clamp-2">{link.name}</span>
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</div> </div>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}
@ -247,31 +268,33 @@ export function TopNavbar({
<Button <Button
variant="ghost" variant="ghost"
size="sm" 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" /> <Briefcase className="h-5 w-5" />
<span className="text-sm font-medium">{toolbox2Name}</span> <span className={navLabelClass}>{getShortLabel(toolbox2Name)}</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[320px] p-2"> <DropdownMenuContent side="left" align="center" className="w-[320px] p-2">
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{customLinks {customLinks
.filter(l => l.toolboxGroup === 2) .filter(l => l.toolboxGroup === 2)
.sort((a, b) => a.order - b.order) .sort((a, b) => a.order - b.order)
.map((link) => ( .map((link) => (
<DropdownMenuItem key={link.id} asChild className="cursor-pointer"> <DropdownMenuItem key={link.id} asChild className="cursor-pointer">
<a <a
href={link.url} href={link.url}
target="_blank" target="_blank"
rel="noopener noreferrer" 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" 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" /> <ExternalLink className="h-5 w-5 opacity-70" />
<span className="text-xs font-medium line-clamp-2">{link.name}</span> <span className="text-xs font-medium line-clamp-2">{link.name}</span>
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</div> </div>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}
@ -279,15 +302,15 @@ export function TopNavbar({
{/* AI智能精选 */} {/* AI智能精选 */}
<a <a
href="/featured" 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" className={navItemBaseClass}
title="AI智能精选" title="AI精选"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
window.location.href = "/featured"; window.location.href = "/featured";
}} }}
> >
<Sparkles className="h-4 w-4 text-yellow-400" /> <Sparkles className="h-5 w-5 text-yellow-400" />
<span>AI精选</span> <span className={navLabelClass}>{getShortLabel("AI精选")}</span>
</a> </a>
{/* 功能教程 */} {/* 功能教程 */}
@ -297,14 +320,15 @@ export function TopNavbar({
<Button <Button
variant="ghost" variant="ghost"
size="sm" 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" type="button"
title="功能教程"
> >
<PlayCircle className="h-4 w-4 text-green-400" /> <PlayCircle className="h-5 w-5 text-green-400" />
<span className="text-sm font-medium">功能教程</span> <span className={navLabelClass}>{getShortLabel("功能教程")}</span>
</Button> </Button>
</DropdownMenuTrigger> </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"> <div className="space-y-1 max-h-[400px] overflow-y-auto">
{tutorialVideos {tutorialVideos
.sort((a, b) => a.order - b.order) .sort((a, b) => a.order - b.order)
@ -355,14 +379,15 @@ export function TopNavbar({
<Button <Button
variant="ghost" variant="ghost"
size="sm" 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" type="button"
title="精选榜单"
> >
<Trophy className="h-4 w-4 text-orange-400" /> <Trophy className="h-5 w-5 text-orange-400" />
<span className="text-sm font-medium">精选榜</span> <span className={navLabelClass}>{getShortLabel("精选榜")}</span>
</Button> </Button>
</DropdownMenuTrigger> </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"> <div className="flex items-center gap-2 mb-3 pb-2 border-b">
<Trophy className="h-5 w-5 text-orange-500" /> <Trophy className="h-5 w-5 text-orange-500" />
<span className="font-semibold">精选榜单 TOP5</span> <span className="font-semibold">精选榜单 TOP5</span>
@ -409,18 +434,18 @@ export function TopNavbar({
<Button <Button
variant="ghost" variant="ghost"
size="sm" 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} onClick={handleOpenImageEditor}
type="button" type="button"
title="图片编辑" title="图片编辑"
> >
<Edit className="h-4 w-4" /> <Edit className="h-5 w-5" />
<span className="text-sm font-medium">图片编辑</span> <span className={navLabelClass}>{getShortLabel("图片编辑")}</span>
</Button> </Button>
{/* 无限画布 */} {/* 无限画布 */}
<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={() => { onClick={() => {
const newId = crypto.randomUUID(); const newId = crypto.randomUUID();
window.location.href = `/canvas/${newId}`; window.location.href = `/canvas/${newId}`;
@ -428,44 +453,42 @@ export function TopNavbar({
type="button" type="button"
title="无限画布" title="无限画布"
> >
<LayoutGrid className="h-4 w-4 text-purple-400" /> <LayoutGrid className="h-5 w-5 text-purple-400" />
<span>无限画布</span> <span className={navLabelClass}>{getShortLabel("无限画布")}</span>
</button> </button>
{/* 我的图册 */} {/* 我的图册 */}
<a <a
href="/gallery" 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="我的图册" title="我的图册"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
window.location.href = "/gallery"; window.location.href = "/gallery";
}} }}
> >
<Images className="h-4 w-4" /> <Images className="h-5 w-5" />
<span>我的图册</span> <span className={navLabelClass}>{getShortLabel("我的图册")}</span>
</a> </a>
</div>
{/* 分隔线 (如果有链接或图册按钮,且是管理员) */} {/* 系统设置(仅管理员可见)*/}
{isAdmin && ( {isAdmin && (
<div className="hidden h-4 w-px bg-white/20 mx-1 sm:block" /> <div className="pb-4">
)}
{/* 系统设置(仅管理员可见)*/}
{isAdmin && (
<a <a
href="/settings" 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="系统设置" title="系统设置"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
window.location.href = "/settings"; window.location.href = "/settings";
}} }}
> >
<Settings className="h-4 w-4" /> <Settings className="h-5 w-5" />
<span className={navLabelClass}>{getShortLabel("系统设置")}</span>
</a> </a>
)} </div>
</div> )}
{/* 图片编辑器 */} {/* 图片编辑器 */}
{showImageEditor && ( {showImageEditor && (