258 lines
8.9 KiB
TypeScript
258 lines
8.9 KiB
TypeScript
import React, { act, useEffect } from "react";
|
|
import { AdminPanelConfig, TabConfig } from "@/types/admin-panel";
|
|
import { cn } from "@/lib/utils";
|
|
import { SiteHeader } from "../site-header";
|
|
import { Button } from "@/components/ui/button";
|
|
import { AlertCircle, CheckCircle, Loader2 } from "lucide-react";
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
import {
|
|
Tabs,
|
|
TabsList,
|
|
TabsTrigger,
|
|
TabsContent
|
|
} from "@/components/ui/tabs";
|
|
import { useAdminPanel } from "@/hooks/use-admin-panel";
|
|
import { AdminSection } from "@/components/admin";
|
|
import { configFormSchema, ConfigFormValues } from "@/types/config"
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { Form } from "@/components/ui/form";
|
|
import { toast } from "sonner";
|
|
|
|
|
|
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 { state, actions, helpers, form } = useAdminPanel({
|
|
config,
|
|
initialValues,
|
|
onSubmit
|
|
});
|
|
|
|
|
|
const visibleTabs = config.tabs.filter(tab =>
|
|
!tab.permissions || tab.permissions.some(p => hasPermission(p))
|
|
);
|
|
|
|
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 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;
|
|
};
|
|
|
|
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";
|
|
|
|
const getVisibleSections = (tab: TabConfig) => {
|
|
return tab.sections.filter(section =>
|
|
!section.permissions || section.permissions.some(p => hasPermission(p))
|
|
);
|
|
};
|
|
|
|
|
|
|
|
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={async () => await actions.save()}
|
|
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">
|
|
|
|
<Form {...form}>
|
|
<form>
|
|
<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>
|
|
|
|
{visibleTabs.map((tab) => (
|
|
<TabsContent key={tab.id} value={tab.id} className="space-y-6">
|
|
{getVisibleSections(tab).map((section) => (
|
|
<AdminSection
|
|
key={section.id}
|
|
section={section}
|
|
disabled={state.loading}
|
|
onChange={actions.setValue}
|
|
onBlur={() => { }} // Could implement field-level validation
|
|
form={form}
|
|
/>
|
|
))}
|
|
</TabsContent>
|
|
))}
|
|
</Tabs>
|
|
</form>
|
|
|
|
</Form>
|
|
|
|
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
} |