feat: streamline admin settings layout
This commit is contained in:
parent
7fd3feb77c
commit
ffccdecc90
30
app/(admin)/_components/admin-layout-shell.tsx
Normal file
30
app/(admin)/_components/admin-layout-shell.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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,38 +21,37 @@ 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}
|
isAdmin={session?.user?.type === "admin" || session?.user?.type === "agent_admin"}
|
||||||
isAdmin={session?.user?.type === "admin" || session?.user?.type === "agent_admin"}
|
settingsHref={settingsHref}
|
||||||
settingsHref={settingsHref}
|
customLinks={navLinks.map((link) => ({
|
||||||
customLinks={navLinks.map((link) => ({
|
id: link.id,
|
||||||
id: link.id,
|
name: link.name,
|
||||||
name: link.name,
|
url: link.url,
|
||||||
url: link.url,
|
order: link.order,
|
||||||
order: link.order,
|
toolboxGroup: link.toolboxGroup || 1,
|
||||||
toolboxGroup: link.toolboxGroup || 1,
|
}))}
|
||||||
}))}
|
toolbox1Name={siteSettings.toolbox1Name || "工具箱"}
|
||||||
toolbox1Name={siteSettings.toolbox1Name || "工具箱"}
|
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,
|
coverUrl: v.coverUrl,
|
||||||
coverUrl: v.coverUrl,
|
description: v.description,
|
||||||
description: v.description,
|
order: v.order,
|
||||||
order: v.order,
|
}))}
|
||||||
}))}
|
tutorialEnabled={siteSettings.tutorialEnabled ?? false}
|
||||||
tutorialEnabled={siteSettings.tutorialEnabled ?? false}
|
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}
|
>
|
||||||
/>
|
{children}
|
||||||
<div className="flex-1 pl-16">{children}</div>
|
</AdminLayoutShell>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 ADMIN_SETTINGS_NAV_GROUPS.find((group) =>
|
||||||
|
group.items.some((item) => item.value === activeTab)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveItem(activeTab: string) {
|
||||||
|
return ADMIN_SETTINGS_NAV_GROUPS.flatMap((group) => group.items).find(
|
||||||
|
(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 (
|
||||||
<div className="rounded-xl border bg-background">
|
<DropdownMenu>
|
||||||
<div className="border-b bg-muted/20 px-5 py-4">
|
<DropdownMenuTrigger asChild>
|
||||||
<div className="flex flex-col gap-1">
|
<Button
|
||||||
|
className="h-10 rounded-full px-4"
|
||||||
|
size="sm"
|
||||||
|
variant={hasActiveTab ? "default" : "outline"}
|
||||||
|
>
|
||||||
|
<span>{group.title}</span>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="w-80">
|
||||||
|
<DropdownMenuLabel>{group.title}</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{group.items.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
const itemContent = (
|
||||||
|
<>
|
||||||
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="font-medium text-sm">{item.label}</div>
|
||||||
|
<div className="truncate text-muted-foreground text-xs">
|
||||||
|
{item.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{item.value === activeTab ? (
|
||||||
|
<SettingStatusBadge tone="enabled">当前</SettingStatusBadge>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (item.href) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem asChild key={item.value}>
|
||||||
|
<Link href={item.href}>{itemContent}</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={item.value}
|
||||||
|
onSelect={() => onTabChange(item.value)}
|
||||||
|
>
|
||||||
|
{itemContent}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</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]">
|
<div className="font-semibold text-muted-foreground text-xs uppercase tracking-[0.16em]">
|
||||||
Admin Console
|
Admin Console
|
||||||
</div>
|
</div>
|
||||||
<h1 className="font-semibold text-2xl tracking-tight">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
系统设置中心
|
<h1 className="font-semibold text-2xl tracking-tight">
|
||||||
</h1>
|
系统设置中心
|
||||||
|
</h1>
|
||||||
|
{activeGroup ? (
|
||||||
|
<SettingStatusBadge tone="enabled">{activeGroup.title}</SettingStatusBadge>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<p className="max-w-4xl text-muted-foreground text-sm leading-6">
|
<p className="max-w-4xl text-muted-foreground text-sm leading-6">
|
||||||
按后台人员的操作路径重新组织入口:先处理运营数据,再调整站点体验,最后管理模型与基础设施能力。
|
页面已改成横向工作区:顶部收拢分类入口,正文区域尽量留给配置表单和数据内容。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
<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
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<section className="bg-background p-4" key={group.title}>
|
|
||||||
<div className="mb-3 flex items-start justify-between gap-3">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<h2 className="font-semibold text-sm">{group.title}</h2>
|
|
||||||
<p className="text-muted-foreground text-xs leading-5">
|
|
||||||
{group.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{hasActiveTab ? (
|
|
||||||
<SettingStatusBadge tone="enabled">当前</SettingStatusBadge>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<TabsList className="grid h-auto w-full grid-cols-1 gap-1 bg-transparent p-0">
|
|
||||||
{group.items.map((item) => {
|
|
||||||
const Icon = item.icon;
|
|
||||||
const triggerClassName =
|
|
||||||
"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" />
|
|
||||||
<span className="min-w-0">
|
|
||||||
<span className="block truncate font-medium text-sm">
|
|
||||||
{item.label}
|
|
||||||
</span>
|
|
||||||
<span className="block truncate font-normal text-[11px] text-muted-foreground">
|
|
||||||
{item.description}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (item.href) {
|
<div className="rounded-2xl border bg-muted/20 p-2">
|
||||||
return (
|
<div className="flex flex-col gap-3 md:hidden">
|
||||||
<TabsTrigger
|
<Select onValueChange={onTabChange} value={activeTab}>
|
||||||
asChild
|
<SelectTrigger className="h-11 border-0 bg-background shadow-sm">
|
||||||
className={triggerClassName}
|
<SelectValue placeholder="选择设置项" />
|
||||||
key={item.value}
|
</SelectTrigger>
|
||||||
value={item.value}
|
<SelectContent>
|
||||||
>
|
{ADMIN_SETTINGS_NAV_GROUPS.map((group, groupIndex) => (
|
||||||
<Link href={item.href}>{content}</Link>
|
<SelectGroup key={group.title}>
|
||||||
</TabsTrigger>
|
<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>
|
||||||
|
|
||||||
return (
|
<div className="flex flex-wrap gap-2">
|
||||||
<TabsTrigger
|
{(activeGroup?.items ?? []).map((item) => (
|
||||||
className={triggerClassName}
|
<SettingsQuickAction
|
||||||
key={item.value}
|
activeTab={activeTab}
|
||||||
value={item.value}
|
item={item}
|
||||||
>
|
key={item.value}
|
||||||
{content}
|
onTabChange={onTabChange}
|
||||||
</TabsTrigger>
|
/>
|
||||||
);
|
))}
|
||||||
})}
|
</div>
|
||||||
</TabsList>
|
</div>
|
||||||
</section>
|
|
||||||
);
|
<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>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user