273 lines
8.4 KiB
TypeScript
273 lines
8.4 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
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 type { AdminSettingsNavGroup, AdminSettingsNavItem } from "../_lib/types";
|
||
import { SettingStatusBadge } from "./setting-status-badge";
|
||
|
||
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 (
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<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]">
|
||
Admin Console
|
||
</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>
|
||
);
|
||
}
|