feat: streamline admin settings layout

This commit is contained in:
Your Name 2026-05-29 14:11:59 +08:00
parent 7fd3feb77c
commit ffccdecc90
4 changed files with 321 additions and 108 deletions

View File

@ -0,0 +1,30 @@
"use client";
import type { ComponentProps, ReactNode } from "react";
import { usePathname } from "next/navigation";
import { TopNavbar } from "@/components/top-navbar";
type AdminLayoutShellProps = ComponentProps<typeof TopNavbar> & {
children: ReactNode;
};
function shouldHideSidebarChrome(pathname: string) {
return pathname === "/settings";
}
export function AdminLayoutShell({
children,
...topNavbarProps
}: AdminLayoutShellProps) {
const pathname = usePathname();
const hideSidebarChrome = shouldHideSidebarChrome(pathname);
return (
<div className="flex min-h-screen flex-col bg-background">
{hideSidebarChrome ? null : <TopNavbar {...topNavbarProps} />}
<div className={hideSidebarChrome ? "flex-1" : "flex-1 pl-16"}>
{children}
</div>
</div>
);
}

View File

@ -1,6 +1,6 @@
import { auth } from "@/app/(auth)/auth"; import { auth } from "@/app/(auth)/auth";
import { TopNavbar } from "@/components/top-navbar";
import { getSiteSettings, getNavLinks, getTutorialVideos } from "@/lib/db/queries"; import { getSiteSettings, getNavLinks, getTutorialVideos } from "@/lib/db/queries";
import { AdminLayoutShell } from "./_components/admin-layout-shell";
export default async function AdminLayout({ export default async function AdminLayout({
children, children,
@ -21,8 +21,7 @@ export default async function AdminLayout({
: null; : null;
return ( return (
<div className="flex min-h-screen flex-col bg-background"> <AdminLayoutShell
<TopNavbar
siteName={siteSettings.siteName} siteName={siteSettings.siteName}
siteLogo={siteSettings.siteLogo || undefined} siteLogo={siteSettings.siteLogo || undefined}
siteLogoDark={siteSettings.siteLogoDark || undefined} siteLogoDark={siteSettings.siteLogoDark || undefined}
@ -39,7 +38,7 @@ export default async function AdminLayout({
toolbox1Enabled={siteSettings.toolbox1Enabled ?? true} toolbox1Enabled={siteSettings.toolbox1Enabled ?? true}
toolbox2Name={siteSettings.toolbox2Name || "工具箱2"} toolbox2Name={siteSettings.toolbox2Name || "工具箱2"}
toolbox2Enabled={siteSettings.toolbox2Enabled ?? false} toolbox2Enabled={siteSettings.toolbox2Enabled ?? false}
tutorialVideos={tutorialVideos.map(v => ({ tutorialVideos={tutorialVideos.map((v) => ({
id: v.id, id: v.id,
title: v.title, title: v.title,
videoUrl: v.videoUrl, videoUrl: v.videoUrl,
@ -51,8 +50,8 @@ export default async function AdminLayout({
rankingEnabled={siteSettings.rankingEnabled ?? true} rankingEnabled={siteSettings.rankingEnabled ?? true}
productPlanningEnabled={(siteSettings as any).productPlanningEnabled ?? true} productPlanningEnabled={(siteSettings as any).productPlanningEnabled ?? true}
videoCanvasEnabled={(siteSettings as any).videoCanvasEnabled ?? true} videoCanvasEnabled={(siteSettings as any).videoCanvasEnabled ?? true}
/> >
<div className="flex-1 pl-16">{children}</div> {children}
</div> </AdminLayoutShell>
); );
} }

View File

@ -1,91 +1,272 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ArrowLeft, ChevronDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { ADMIN_SETTINGS_NAV_GROUPS } from "../_lib/settings-navigation"; import { ADMIN_SETTINGS_NAV_GROUPS } from "../_lib/settings-navigation";
import type { AdminSettingsNavGroup, AdminSettingsNavItem } from "../_lib/types";
import { SettingStatusBadge } from "./setting-status-badge"; import { SettingStatusBadge } from "./setting-status-badge";
export function AdminSettingsNavigation({ activeTab }: { activeTab: string }) { function getActiveGroup(activeTab: string) {
return ( return ADMIN_SETTINGS_NAV_GROUPS.find((group) =>
<div className="rounded-xl border bg-background"> group.items.some((item) => item.value === activeTab)
<div className="border-b bg-muted/20 px-5 py-4"> );
<div className="flex flex-col gap-1"> }
<div className="font-semibold text-muted-foreground text-xs uppercase tracking-[0.16em]">
Admin Console function getActiveItem(activeTab: string) {
</div> return ADMIN_SETTINGS_NAV_GROUPS.flatMap((group) => group.items).find(
<h1 className="font-semibold text-2xl tracking-tight">
系统设置中心
</h1>
<p className="max-w-4xl text-muted-foreground text-sm leading-6">
按后台人员的操作路径重新组织入口:先处理运营数据,再调整站点体验,最后管理模型与基础设施能力。
</p>
</div>
</div>
<div className="grid gap-px bg-border lg:grid-cols-3">
{ADMIN_SETTINGS_NAV_GROUPS.map((group) => {
const hasActiveTab = group.items.some(
(item) => item.value === activeTab (item) => item.value === activeTab
); );
}
function SettingsGroupDropdown({
activeTab,
group,
onTabChange,
}: {
activeTab: string;
group: AdminSettingsNavGroup;
onTabChange: (value: string) => void;
}) {
const hasActiveTab = group.items.some((item) => item.value === activeTab);
return ( return (
<section className="bg-background p-4" key={group.title}> <DropdownMenu>
<div className="mb-3 flex items-start justify-between gap-3"> <DropdownMenuTrigger asChild>
<div className="space-y-1"> <Button
<h2 className="font-semibold text-sm">{group.title}</h2> className="h-10 rounded-full px-4"
<p className="text-muted-foreground text-xs leading-5"> size="sm"
{group.description} variant={hasActiveTab ? "default" : "outline"}
</p> >
</div> <span>{group.title}</span>
{hasActiveTab ? ( <ChevronDown className="h-4 w-4" />
<SettingStatusBadge tone="enabled">当前</SettingStatusBadge> </Button>
) : null} </DropdownMenuTrigger>
</div> <DropdownMenuContent align="start" className="w-80">
<TabsList className="grid h-auto w-full grid-cols-1 gap-1 bg-transparent p-0"> <DropdownMenuLabel>{group.title}</DropdownMenuLabel>
<DropdownMenuSeparator />
{group.items.map((item) => { {group.items.map((item) => {
const Icon = item.icon; const Icon = item.icon;
const triggerClassName = const itemContent = (
"h-auto justify-start gap-2 rounded-md border border-transparent px-3 py-2 text-left data-[state=active]:border-border data-[state=active]:bg-muted/50";
const content = (
<> <>
<Icon className="h-4 w-4 shrink-0 text-muted-foreground" /> <Icon className="h-4 w-4 text-muted-foreground" />
<span className="min-w-0"> <div className="min-w-0 flex-1">
<span className="block truncate font-medium text-sm"> <div className="font-medium text-sm">{item.label}</div>
{item.label} <div className="truncate text-muted-foreground text-xs">
</span>
<span className="block truncate font-normal text-[11px] text-muted-foreground">
{item.description} {item.description}
</span> </div>
</span> </div>
{item.value === activeTab ? (
<SettingStatusBadge tone="enabled">当前</SettingStatusBadge>
) : null}
</> </>
); );
if (item.href) { if (item.href) {
return ( return (
<TabsTrigger <DropdownMenuItem asChild key={item.value}>
asChild <Link href={item.href}>{itemContent}</Link>
className={triggerClassName} </DropdownMenuItem>
key={item.value}
value={item.value}
>
<Link href={item.href}>{content}</Link>
</TabsTrigger>
); );
} }
return ( return (
<TabsTrigger <DropdownMenuItem
className={triggerClassName}
key={item.value} key={item.value}
value={item.value} onSelect={() => onTabChange(item.value)}
> >
{content} {itemContent}
</TabsTrigger> </DropdownMenuItem>
); );
})} })}
</TabsList> </DropdownMenuContent>
</section> </DropdownMenu>
); );
})} }
function SettingsQuickAction({
activeTab,
item,
onTabChange,
}: {
activeTab: string;
item: AdminSettingsNavItem;
onTabChange: (value: string) => void;
}) {
const Icon = item.icon;
const className =
item.value === activeTab
? "border-primary bg-primary text-primary-foreground hover:bg-primary/90"
: "border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground";
if (item.href) {
return (
<Button
asChild
className={`h-10 shrink-0 gap-2 rounded-full border px-4 ${className}`}
size="sm"
variant="ghost"
>
<Link href={item.href}>
<Icon className="h-4 w-4" />
<span>{item.label}</span>
</Link>
</Button>
);
}
return (
<Button
className={`h-10 shrink-0 gap-2 rounded-full border px-4 ${className}`}
onClick={() => onTabChange(item.value)}
size="sm"
variant="ghost"
>
<Icon className="h-4 w-4" />
<span>{item.label}</span>
</Button>
);
}
export function AdminSettingsNavigation({
activeTab,
onTabChange,
}: {
activeTab: string;
onTabChange: (value: string) => void;
}) {
const activeGroup = getActiveGroup(activeTab);
const activeItem = getActiveItem(activeTab);
const ActiveIcon = activeItem?.icon;
return (
<div className="space-y-4 rounded-2xl border bg-background px-5 py-4 shadow-sm">
<div className="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
<div className="space-y-2">
<div className="font-semibold text-muted-foreground text-xs uppercase tracking-[0.16em]">
Admin Console
</div> </div>
<div className="flex flex-wrap items-center gap-3">
<h1 className="font-semibold text-2xl tracking-tight">
系统设置中心
</h1>
{activeGroup ? (
<SettingStatusBadge tone="enabled">{activeGroup.title}</SettingStatusBadge>
) : null}
</div>
<p className="max-w-4xl text-muted-foreground text-sm leading-6">
页面已改成横向工作区:顶部收拢分类入口,正文区域尽量留给配置表单和数据内容。
</p>
</div>
<Button asChild className="h-10 rounded-full px-4" size="sm" variant="outline">
<Link href="/">
<ArrowLeft className="h-4 w-4" />
返回工作台
</Link>
</Button>
</div>
<div className="rounded-2xl border bg-muted/20 p-2">
<div className="flex flex-col gap-3 md:hidden">
<Select onValueChange={onTabChange} value={activeTab}>
<SelectTrigger className="h-11 border-0 bg-background shadow-sm">
<SelectValue placeholder="选择设置项" />
</SelectTrigger>
<SelectContent>
{ADMIN_SETTINGS_NAV_GROUPS.map((group, groupIndex) => (
<SelectGroup key={group.title}>
<SelectLabel>{group.title}</SelectLabel>
{group.items
.filter((item) => !item.href)
.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
{groupIndex < ADMIN_SETTINGS_NAV_GROUPS.length - 1 ? (
<SelectSeparator />
) : null}
</SelectGroup>
))}
</SelectContent>
</Select>
<div className="flex flex-wrap gap-2">
{(activeGroup?.items ?? []).map((item) => (
<SettingsQuickAction
activeTab={activeTab}
item={item}
key={item.value}
onTabChange={onTabChange}
/>
))}
</div>
</div>
<div className="hidden items-center gap-2 md:flex">
{ADMIN_SETTINGS_NAV_GROUPS.map((group) => (
<SettingsGroupDropdown
activeTab={activeTab}
group={group}
key={group.title}
onTabChange={onTabChange}
/>
))}
<Separator className="mx-1 h-7" orientation="vertical" />
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-x-auto pb-1">
{(activeGroup?.items ?? []).map((item) => (
<SettingsQuickAction
activeTab={activeTab}
item={item}
key={item.value}
onTabChange={onTabChange}
/>
))}
</div>
</div>
</div>
{activeItem && ActiveIcon ? (
<div className="flex items-start gap-3 rounded-2xl border bg-muted/10 px-4 py-3">
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-background shadow-sm">
<ActiveIcon className="h-5 w-5 text-muted-foreground" />
</div>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<h2 className="font-semibold text-base">{activeItem.label}</h2>
{activeGroup ? (
<span className="text-muted-foreground text-xs">
{activeGroup.title}
</span>
) : null}
</div>
<p className="mt-1 text-muted-foreground text-sm">
{activeItem.description}
</p>
</div>
</div>
) : null}
</div> </div>
); );
} }

View File

@ -126,7 +126,7 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { import {
defaultAgentWorkflowUiConfig, defaultAgentWorkflowUiConfig,
@ -7996,9 +7996,12 @@ export function AdminSettingsClient() {
}, [expandedLocalAiSections]); }, [expandedLocalAiSections]);
return ( return (
<div className="container mx-auto p-6"> <div className="w-full px-4 py-6 sm:px-6 xl:px-8 2xl:px-10">
<Tabs onValueChange={setActiveTab} value={activeTab}> <Tabs onValueChange={setActiveTab} value={activeTab}>
<AdminSettingsNavigation activeTab={activeTab} /> <AdminSettingsNavigation
activeTab={activeTab}
onTabChange={setActiveTab}
/>
{/* 用户管理 */} {/* 用户管理 */}
<TabsContent className="mt-6" value="users"> <TabsContent className="mt-6" value="users">