300 lines
12 KiB
TypeScript
300 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { cn } from "@/lib/utils";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Loader2, AlertCircle, CheckCircle } from "lucide-react";
|
|
import { AdminPanelConfig, TabConfig } from "@/types/admin-panel";
|
|
import { useAdminPanel } from "@/hooks/use-admin-panel";
|
|
import { AdminSection } from "./admin-section";
|
|
import { SiteHeader } from "@/app/admin/site-header";
|
|
|
|
interface AdminPanelProps {
|
|
config: AdminPanelConfig;
|
|
initialValues?: Record<string, any>;
|
|
onSubmit?: (values: Record<string, any>) => Promise<void>;
|
|
className?: string;
|
|
// Permission checker function
|
|
hasPermission?: (permission: string) => boolean;
|
|
}
|
|
|
|
export function AdminPanel({
|
|
config,
|
|
initialValues,
|
|
onSubmit,
|
|
className,
|
|
hasPermission = () => true
|
|
}: AdminPanelProps) {
|
|
const [activeTab, setActiveTab] = React.useState(() => {
|
|
// Find first accessible tab
|
|
const firstAccessibleTab = config.tabs.find(tab =>
|
|
!tab.disabled && (!tab.permissions || tab.permissions.some(p => hasPermission(p)))
|
|
);
|
|
return firstAccessibleTab?.id || config.tabs[0]?.id || "";
|
|
});
|
|
|
|
const { state, actions, helpers } = useAdminPanel({
|
|
config,
|
|
initialValues,
|
|
onSubmit
|
|
});
|
|
|
|
// Filter tabs based on permissions
|
|
const visibleTabs = config.tabs.filter(tab =>
|
|
!tab.permissions || tab.permissions.some(p => hasPermission(p))
|
|
);
|
|
|
|
// Get current tab
|
|
const currentTab = visibleTabs.find(tab => tab.id === activeTab);
|
|
|
|
// Filter sections based on permissions
|
|
const getVisibleSections = (tab: TabConfig) => {
|
|
return tab.sections.filter(section =>
|
|
!section.permissions || section.permissions.some(p => hasPermission(p))
|
|
);
|
|
};
|
|
|
|
// Handle save with loading state
|
|
const handleSave = async () => {
|
|
try {
|
|
await actions.save();
|
|
} catch (error) {
|
|
console.error('Save failed:', error);
|
|
// You might want to show a toast notification here
|
|
}
|
|
};
|
|
|
|
// Render header actions
|
|
const renderHeaderActions = () => {
|
|
const actions = config.header.actions || [];
|
|
|
|
return actions
|
|
.filter(action => !action.permissions || action.permissions.some(p => hasPermission(p)))
|
|
.map(action => (
|
|
<Button
|
|
key={action.id}
|
|
variant={action.variant || "default"}
|
|
size={action.size || "default"}
|
|
disabled={action.disabled || state.loading}
|
|
onClick={action.onClick}
|
|
className="gap-2"
|
|
>
|
|
{action.loading && <Loader2 className="h-4 w-4 animate-spin" />}
|
|
{action.icon}
|
|
{action.label}
|
|
</Button>
|
|
));
|
|
};
|
|
|
|
// Render breadcrumbs
|
|
const renderBreadcrumbs = () => {
|
|
if (!config.header.breadcrumbs) return null;
|
|
|
|
return <SiteHeader breadcrumbs={config.header.breadcrumbs.map(crumb => ({
|
|
label: crumb.label,
|
|
href: crumb.href || ""
|
|
}))} />
|
|
|
|
};
|
|
|
|
// Render status indicators
|
|
const renderStatusIndicators = () => {
|
|
const indicators = [];
|
|
|
|
// Loading indicator
|
|
if (state.loading) {
|
|
indicators.push(
|
|
<Badge key="loading" variant="outline" className="gap-1">
|
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
加载中
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
// Saving indicator
|
|
if (state.saving) {
|
|
indicators.push(
|
|
<Badge key="saving" variant="outline" className="gap-1">
|
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
保存中
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
// Dirty state indicator
|
|
if (helpers.isDirty() && !state.saving) {
|
|
indicators.push(
|
|
<Badge key="dirty" variant="destructive" className="gap-1">
|
|
<AlertCircle className="h-3 w-3" />
|
|
有未保存的更改
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
// Valid state indicator
|
|
if (!helpers.isDirty() && helpers.isValid()) {
|
|
indicators.push(
|
|
<Badge key="saved" variant="default" className="gap-1">
|
|
<CheckCircle className="h-3 w-3" />
|
|
已保存
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
// Validation errors indicator
|
|
if (Object.keys(state.errors).length > 0) {
|
|
indicators.push(
|
|
<Badge key="errors" variant="destructive" className="gap-1">
|
|
<AlertCircle className="h-3 w-3" />
|
|
{Object.keys(state.errors).length} 个错误
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
return indicators;
|
|
};
|
|
|
|
// Compute stable grid columns class to avoid Tailwind purge of dynamic classes
|
|
const gridColsMap: Record<number, string> = {
|
|
1: "grid-cols-1",
|
|
2: "grid-cols-2",
|
|
3: "grid-cols-3",
|
|
4: "grid-cols-4",
|
|
5: "grid-cols-5",
|
|
6: "grid-cols-6",
|
|
};
|
|
const gridColsClass = gridColsMap[Math.min(visibleTabs.length, 6)] || "grid-cols-6";
|
|
|
|
return (
|
|
<div className={cn("min-h-screen bg-background", className)}>
|
|
{renderBreadcrumbs()}
|
|
|
|
<div className="flex items-center justify-between mt-8 px-8">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-foreground">
|
|
{config.header.title}
|
|
</h1>
|
|
{config.header.description && (
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
{config.header.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{/* Status indicators */}
|
|
{renderStatusIndicators()}
|
|
|
|
{/* Header actions */}
|
|
{renderHeaderActions()}
|
|
|
|
{/* Save button */}
|
|
{onSubmit && (
|
|
<Button
|
|
onClick={handleSave}
|
|
disabled={state.saving || (!helpers.isDirty() && helpers.isValid())}
|
|
className="gap-2"
|
|
>
|
|
{state.saving && <Loader2 className="h-4 w-4 animate-spin" />}
|
|
保存设置
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="container mx-auto px-6 py-6">
|
|
{config.theme?.layout === "sidebar" ? (
|
|
// Sidebar layout
|
|
<div className="flex gap-6">
|
|
{/* Sidebar */}
|
|
<div className="w-64 space-y-2">
|
|
{visibleTabs.map((tab) => (
|
|
<Button
|
|
key={tab.id}
|
|
variant={activeTab === tab.id ? "default" : "ghost"}
|
|
className="w-full justify-start gap-2"
|
|
onClick={() => setActiveTab(tab.id)}
|
|
disabled={tab.disabled}
|
|
>
|
|
{tab.icon}
|
|
{tab.title}
|
|
{tab.badge && (
|
|
<Badge variant="secondary" className="ml-auto">
|
|
{tab.badge}
|
|
</Badge>
|
|
)}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 space-y-6">
|
|
{currentTab && (
|
|
<div className="space-y-6">
|
|
{getVisibleSections(currentTab).map((section) => (
|
|
<AdminSection
|
|
key={section.id}
|
|
section={section}
|
|
values={state.values}
|
|
errors={state.errors}
|
|
disabled={state.loading}
|
|
onChange={actions.setValue}
|
|
onBlur={() => { }} // Could implement field-level validation
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
// Tabs layout (default)
|
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
|
|
{/* Tab Navigation */}
|
|
<TabsList className={cn(
|
|
"grid w-full gap-2",
|
|
gridColsClass
|
|
)}>
|
|
{visibleTabs.map((tab) => (
|
|
<TabsTrigger
|
|
key={tab.id}
|
|
value={tab.id}
|
|
disabled={tab.disabled}
|
|
className="flex items-center gap-2 text-xs lg:text-sm w-full justify-center"
|
|
>
|
|
{tab.icon}
|
|
<span className="hidden sm:inline truncate">{tab.title}</span>
|
|
{tab.badge && (
|
|
<Badge variant="secondary" className="ml-1 text-xs">
|
|
{tab.badge}
|
|
</Badge>
|
|
)}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
|
|
{/* Tab Content */}
|
|
{visibleTabs.map((tab) => (
|
|
<TabsContent key={tab.id} value={tab.id} className="space-y-6">
|
|
{getVisibleSections(tab).map((section) => (
|
|
<AdminSection
|
|
key={section.id}
|
|
section={section}
|
|
values={state.values}
|
|
errors={state.errors}
|
|
disabled={state.loading}
|
|
onChange={actions.setValue}
|
|
onBlur={() => { }} // Could implement field-level validation
|
|
/>
|
|
))}
|
|
</TabsContent>
|
|
))}
|
|
</Tabs>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
} |