mosaicmap/app/admin/common/dynamic-admin-config-form.tsx
2025-08-15 22:31:51 +08:00

338 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Settings,
Globe,
Palette,
Shield,
Users,
Database,
Mail,
FileText,
Server,
HardDrive,
Lock,
User,
ToggleLeft,
RefreshCw,
Download,
Upload,
Save,
CheckCircle,
AlertCircle
} from "lucide-react";
import { SiteOpsConfigType } from "@/types/site-config";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
// 表单验证模式
const configFormSchema = z.object({
'site.name': z.string().min(2, "网站名称至少需要2个字符").max(50, "网站名称不能超过50个字符"),
'site.description': z.string().optional(),
'site.keywords': z.string().optional(),
'site.url': z.string()
.refine((url) => {
if (!url || url === "") return true; // 允许空值
return url.startsWith("http://") || url.startsWith("https://");
}, "无效的URL格式必须以http://或https://开头")
.optional().or(z.literal("")),
'site.logo': z.string().optional(),
'site.color_style': z.enum(["light", "dark"]),
'user.default_role': z.enum(["user", "vip", "admin"]),
'user.register_invite_code': z.boolean(),
'user.register_email_verification': z.boolean(),
'switch.open_register': z.boolean(),
'switch.open_comment': z.boolean(),
});
type ConfigFormValues = z.infer<typeof configFormSchema>;
// 配置管理表单组件
function DynamicAdminConfigForm({
data,
onSave,
onExport,
onImport,
loading = false
}: {
data?: SiteOpsConfigType;
onSave: (values: ConfigFormValues) => Promise<void>;
onExport?: () => Promise<void>;
onImport?: (file: File) => Promise<void>;
loading?: boolean;
}) {
const [saveStatus, setSaveStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [errorMessage, setErrorMessage] = useState<string>('');
// 初始化表单
const form = useForm<ConfigFormValues>({
resolver: zodResolver(configFormSchema),
defaultValues: {
'site.name': data?.site?.info?.name || "MMAP System",
'site.description': "",
'site.keywords': "",
'site.url': "/",
'site.logo': data?.site?.brand?.logo_url || "/images/logo.png",
'site.color_style': (data?.site?.brand?.dark_mode_default ? 'dark' : 'light') as "light" | "dark",
'user.default_role': 'user',
'user.register_invite_code': data?.ops?.features?.invite_code_required ?? false,
'user.register_email_verification': data?.ops?.features?.email_verification ?? false,
'switch.open_register': data?.ops?.features?.registration_enabled ?? true,
'switch.open_comment': true,
}
});
// 处理表单提交
const onSubmit = async (values: ConfigFormValues) => {
setSaveStatus('loading');
setErrorMessage('');
try {
await onSave(values);
setSaveStatus('success');
toast.success("配置保存成功!");
setTimeout(() => setSaveStatus('idle'), 3000);
} catch (error) {
setSaveStatus('error');
setErrorMessage('保存失败,请重试');
toast.error("配置保存失败,请重试");
}
};
return (
<div className="min-h-screen bg-gray-50">
{/* 头部 */}
<div className="bg-white border-b border-gray-200 px-6 py-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900"></h1>
<p className="text-gray-600"></p>
</div>
<div className="flex items-center gap-3">
<Button variant="outline" onClick={() => window.location.reload()}>
<RefreshCw className="h-4 w-4 mr-2" />
</Button>
<Button variant="outline" onClick={onExport}>
<Download className="h-4 w-4 mr-2" />
</Button>
<Button variant="outline" onClick={() => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file && onImport) onImport(file);
};
input.click();
}}>
<Upload className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
</div>
{/* 主要内容 */}
<div className="flex-1 px-6 py-6">
<div className="max-w-7xl mx-auto">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<Tabs defaultValue="content" className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="content"></TabsTrigger>
<TabsTrigger value="user"></TabsTrigger>
<TabsTrigger value="email"></TabsTrigger>
<TabsTrigger value="system"></TabsTrigger>
</TabsList>
{/* 内容配置标签页 */}
<TabsContent value="content" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings className="h-5 w-5" />
</CardTitle>
<p className="text-sm text-gray-500"></p>
</CardHeader>
<CardContent className="space-y-4">
<FormField
control={form.control}
name="site.name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="请输入网站名称" {...field} />
</FormControl>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="site.description"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Textarea placeholder="请输入网站描述" rows={3} {...field} />
</FormControl>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
</TabsContent>
{/* 用户管理标签页 */}
<TabsContent value="user" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="h-5 w-5" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<FormField
control={form.control}
name="user.default_role"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="选择默认角色" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="user"></SelectItem>
<SelectItem value="vip">VIP用户</SelectItem>
<SelectItem value="admin"></SelectItem>
</SelectContent>
</Select>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="switch.open_register"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base"></FormLabel>
<FormDescription></FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</CardContent>
</Card>
</TabsContent>
{/* 其他标签页内容可以继续添加 */}
<TabsContent value="email" className="space-y-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-500">...</p>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="system" className="space-y-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-500">...</p>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* 保存按钮和状态 */}
<div className="flex items-center justify-between pt-6 border-t">
<div className="flex items-center gap-2">
{saveStatus === 'loading' && (
<>
<RefreshCw className="h-4 w-4 animate-spin text-blue-500" />
<span className="text-blue-600">...</span>
</>
)}
{saveStatus === 'success' && (
<>
<CheckCircle className="h-4 w-4 text-green-500" />
<span className="text-green-600"></span>
</>
)}
{saveStatus === 'error' && (
<>
<AlertCircle className="h-4 w-4 text-red-500" />
<span className="text-red-600">{errorMessage || '保存失败'}</span>
</>
)}
</div>
<Button
type="submit"
disabled={loading || saveStatus === 'loading'}
className="flex items-center gap-2"
>
{saveStatus === 'loading' ? (
<>
<RefreshCw className="h-4 w-4 animate-spin" />
...
</>
) : (
<>
<Save className="h-4 w-4" />
</>
)}
</Button>
</div>
</form>
</Form>
</div>
</div>
</div>
);
}