338 lines
17 KiB
TypeScript
338 lines
17 KiB
TypeScript
"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>
|
||
);
|
||
}
|