757 lines
31 KiB
TypeScript
757 lines
31 KiB
TypeScript
"use client";
|
||
|
||
import React, { useState } from "react";
|
||
import { gql, useMutation } from "@apollo/client";
|
||
import { useForm } from "react-hook-form"
|
||
import { z } from "zod"
|
||
import {
|
||
Settings,
|
||
Globe,
|
||
Palette,
|
||
Shield,
|
||
Users,
|
||
Database,
|
||
Mail,
|
||
FileText,
|
||
Server,
|
||
HardDrive,
|
||
Lock,
|
||
User,
|
||
ToggleLeft,
|
||
RefreshCw,
|
||
Download,
|
||
Upload,
|
||
Save,
|
||
CheckCircle,
|
||
AlertCircle,
|
||
Loader2
|
||
} from "lucide-react";
|
||
import { AdminPanelConfig } from "@/types/admin-panel";
|
||
import { commonConfigSchema, zodErrorsToAdminErrors } from "@/lib/config-zod";
|
||
import { SiteOpsConfigType } from "@/types/site-config";
|
||
import { ConfigItemType } from "@/hooks/use-site-config";
|
||
import { UpdateConfig } from "@/types/config";
|
||
import { Button } from "@/components/ui/button";
|
||
import { toast } from "sonner";
|
||
import { zodResolver } from "@hookform/resolvers/zod";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { cn } from "@/lib/utils";
|
||
|
||
|
||
// GraphQL Mutation
|
||
const UPDATE_CONFIG_BATCH = gql`
|
||
mutation UpdateConfigBatch($input: [UpdateConfig!]!) {
|
||
updateConfigBatch(input: $input)
|
||
}
|
||
`;
|
||
|
||
// 创建基于后端数据的动态管理面板配置
|
||
export function createDynamicAdminConfig(
|
||
data?: SiteOpsConfigType,
|
||
onSave?: (values: Record<string, any>) => Promise<void>,
|
||
onExport?: () => Promise<void>,
|
||
onImport?: (file: File) => Promise<void>,
|
||
configs?: ConfigItemType[]
|
||
): AdminPanelConfig {
|
||
|
||
// 从后端数据获取初始值(安全访问嵌套属性)
|
||
const getInitialValues = () => {
|
||
if (!data) return {};
|
||
|
||
return {
|
||
// Site 配置
|
||
'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.copyright': "",
|
||
'site.icp': "",
|
||
'site.icp_url': "",
|
||
'site.color_style': (data.site?.brand?.dark_mode_default ? 'dark' : 'light'),
|
||
|
||
// User 配置
|
||
'user.default_avatar': "/images/avatar.png",
|
||
'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,
|
||
'user.open_login': true,
|
||
'user.open_reset_password': true,
|
||
|
||
// Email 配置
|
||
'email.smtp_host': "",
|
||
'email.smtp_port': 465,
|
||
'email.smtp_user': "",
|
||
'email.smtp_password': "",
|
||
'email.smtp_from': "",
|
||
'email.smtp_from_name': "",
|
||
'email.smtp_from_email': "",
|
||
'email.system_template': "default",
|
||
|
||
// Blog 配置
|
||
'blog.default_author': "",
|
||
'blog.default_category': "",
|
||
'blog.default_tag': "",
|
||
'blog.open_comment': true,
|
||
|
||
// Logging 配置
|
||
'logging.level': 'info',
|
||
'logging.max_files': 10,
|
||
'logging.max_file_size': 10,
|
||
|
||
// Cache 配置
|
||
'cache.ttl': 3600,
|
||
'cache.max_size': 1024,
|
||
|
||
// Switch 配置
|
||
'switch.open_register': data.ops?.features?.registration_enabled ?? true,
|
||
'switch.open_login': true,
|
||
'switch.open_reset_password': true,
|
||
'switch.open_comment': true,
|
||
'switch.open_like': true,
|
||
'switch.open_share': true,
|
||
'switch.open_view': true
|
||
};
|
||
};
|
||
|
||
return {
|
||
header: {
|
||
title: "系统配置管理",
|
||
description: "管理站点配置、运营设置和系统参数",
|
||
breadcrumbs: [
|
||
{ label: "首页", href: "/" },
|
||
{ label: "管理中心", href: "/admin" },
|
||
{ label: "系统配置" }
|
||
],
|
||
actions: [
|
||
{
|
||
id: "refresh",
|
||
label: "刷新数据",
|
||
icon: <RefreshCw className="h-4 w-4" />,
|
||
variant: "outline",
|
||
onClick: () => window.location.reload()
|
||
},
|
||
{
|
||
id: "export",
|
||
label: "导出配置",
|
||
icon: <Download className="h-4 w-4" />,
|
||
variant: "outline",
|
||
onClick: onExport || (() => console.log("导出配置"))
|
||
},
|
||
{
|
||
id: "import",
|
||
label: "导入配置",
|
||
icon: <Upload className="h-4 w-4" />,
|
||
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();
|
||
}
|
||
}
|
||
]
|
||
},
|
||
|
||
tabs: [
|
||
// 内容配置 (Site + Blog)
|
||
{
|
||
id: "content",
|
||
title: "内容配置",
|
||
icon: <Globe className="h-4 w-4" />,
|
||
sections: [
|
||
{
|
||
id: "site-basic",
|
||
title: "站点信息",
|
||
description: "网站基本信息和设置",
|
||
icon: <Settings className="h-5 w-5" />,
|
||
fields: [
|
||
{
|
||
id: "site.name",
|
||
label: "网站名称",
|
||
description: "显示在浏览器标题栏的网站名称",
|
||
type: "input",
|
||
value: data?.site?.info?.name || "MMAP System",
|
||
placeholder: "请输入网站名称",
|
||
validation: {
|
||
required: true,
|
||
minLength: 2,
|
||
maxLength: 50
|
||
}
|
||
},
|
||
{
|
||
id: "site.description",
|
||
label: "网站描述",
|
||
description: "网站的简要描述信息",
|
||
type: "textarea",
|
||
rows: 3,
|
||
value: "",
|
||
placeholder: "请输入网站描述"
|
||
},
|
||
{
|
||
id: "site.keywords",
|
||
label: "网站关键词",
|
||
description: "SEO相关的关键词,用逗号分隔",
|
||
type: "input",
|
||
value: "",
|
||
placeholder: "请输入关键词,用逗号分隔"
|
||
},
|
||
{
|
||
id: "site.url",
|
||
label: "网站地址",
|
||
description: "网站的主域名地址",
|
||
type: "input",
|
||
value: "/",
|
||
placeholder: "请输入网站地址"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: "site-brand",
|
||
title: "品牌设置",
|
||
description: "网站品牌形象和主题配置",
|
||
icon: <Palette className="h-5 w-5" />,
|
||
fields: [
|
||
{
|
||
id: "site.logo",
|
||
label: "Logo地址",
|
||
description: "网站Logo图片的URL地址",
|
||
type: "input",
|
||
value: data?.site?.brand?.logo_url || "/images/logo.png",
|
||
placeholder: "请输入Logo URL"
|
||
},
|
||
{
|
||
id: "site.color_style",
|
||
label: "颜色风格",
|
||
description: "网站的颜色主题风格",
|
||
type: "select",
|
||
value: (data?.site?.brand?.dark_mode_default ? 'dark' : 'light'),
|
||
options: [
|
||
{ label: "浅色主题", value: "light" },
|
||
{ label: "深色主题", value: "dark" }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: "site-legal",
|
||
title: "法律信息",
|
||
description: "网站的法律相关信息",
|
||
icon: <Shield className="h-5 w-5" />,
|
||
fields: [
|
||
{
|
||
id: "site.copyright",
|
||
label: "版权信息",
|
||
description: "网站的版权声明",
|
||
type: "input",
|
||
value: "",
|
||
placeholder: "请输入版权信息"
|
||
},
|
||
{
|
||
id: "site.icp",
|
||
label: "ICP备案号",
|
||
description: "网站的ICP备案号码",
|
||
type: "input",
|
||
value: "",
|
||
placeholder: "请输入ICP备案号"
|
||
},
|
||
{
|
||
id: "site.icp_url",
|
||
label: "ICP备案链接",
|
||
description: "ICP备案查询链接",
|
||
type: "input",
|
||
value: "",
|
||
placeholder: "请输入ICP备案链接"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: "blog-defaults",
|
||
title: "博客设置",
|
||
description: "博客功能的默认配置",
|
||
icon: <FileText className="h-5 w-5" />,
|
||
fields: [
|
||
{
|
||
id: "blog.default_author",
|
||
label: "默认作者",
|
||
description: "博客文章的默认作者",
|
||
type: "input",
|
||
value: "",
|
||
placeholder: "请输入默认作者"
|
||
},
|
||
{
|
||
id: "blog.default_category",
|
||
label: "默认分类",
|
||
description: "博客文章的默认分类",
|
||
type: "input",
|
||
value: "",
|
||
placeholder: "请输入默认分类"
|
||
},
|
||
{
|
||
id: "blog.default_tag",
|
||
label: "默认标签",
|
||
description: "博客文章的默认标签",
|
||
type: "input",
|
||
value: "",
|
||
placeholder: "请输入默认标签"
|
||
},
|
||
{
|
||
id: "blog.open_comment",
|
||
label: "开放评论",
|
||
description: "是否允许用户对博客文章进行评论",
|
||
type: "switch",
|
||
value: true
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
|
||
// 用户管理 (User + Switch)
|
||
{
|
||
id: "user",
|
||
title: "用户管理",
|
||
icon: <Users className="h-4 w-4" />,
|
||
sections: [
|
||
{
|
||
id: "user-defaults",
|
||
title: "默认设置",
|
||
description: "用户相关的默认配置",
|
||
icon: <Settings className="h-5 w-5" />,
|
||
fields: [
|
||
{
|
||
id: "user.default_avatar",
|
||
label: "默认头像",
|
||
description: "新用户的默认头像图片",
|
||
type: "input",
|
||
value: "/images/avatar.png",
|
||
placeholder: "请输入默认头像URL"
|
||
},
|
||
{
|
||
id: "user.default_role",
|
||
label: "默认角色",
|
||
description: "新用户注册后的默认角色",
|
||
type: "select",
|
||
value: 'user',
|
||
options: [
|
||
{ label: "普通用户", value: "user" },
|
||
{ label: "VIP用户", value: "vip" },
|
||
{ label: "管理员", value: "admin" }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: "user-registration",
|
||
title: "注册设置",
|
||
description: "用户注册相关的配置",
|
||
icon: <Shield className="h-5 w-5" />,
|
||
fields: [
|
||
{
|
||
id: "user.register_invite_code",
|
||
label: "需要邀请码",
|
||
description: "注册时是否需要邀请码",
|
||
type: "switch",
|
||
value: data?.ops?.features?.invite_code_required ?? false
|
||
},
|
||
{
|
||
id: "user.register_email_verification",
|
||
label: "需要邮箱验证",
|
||
description: "注册后是否需要验证邮箱",
|
||
type: "switch",
|
||
value: data?.ops?.features?.email_verification ?? false
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: "user-access",
|
||
title: "访问控制",
|
||
description: "用户访问和登录相关设置",
|
||
icon: <Lock className="h-5 w-5" />,
|
||
fields: [
|
||
{
|
||
id: "user.open_login",
|
||
label: "开放登录",
|
||
description: "是否允许用户登录",
|
||
type: "switch",
|
||
value: true
|
||
},
|
||
{
|
||
id: "user.open_reset_password",
|
||
label: "开放重置密码",
|
||
description: "是否允许用户重置密码",
|
||
type: "switch",
|
||
value: true
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: "user-features",
|
||
title: "功能开关",
|
||
description: "用户相关功能的开关配置",
|
||
icon: <ToggleLeft className="h-5 w-5" />,
|
||
fields: [
|
||
{
|
||
id: "switch.open_register",
|
||
label: "开放注册",
|
||
description: "是否允许新用户注册",
|
||
type: "switch",
|
||
value: data?.ops?.features?.registration_enabled ?? true
|
||
},
|
||
{
|
||
id: "switch.open_comment",
|
||
label: "开放评论",
|
||
description: "是否允许用户进行评论",
|
||
type: "switch",
|
||
value: true
|
||
},
|
||
{
|
||
id: "switch.open_like",
|
||
label: "开放点赞",
|
||
description: "是否允许用户进行点赞",
|
||
type: "switch",
|
||
value: true
|
||
},
|
||
{
|
||
id: "switch.open_share",
|
||
label: "开放分享",
|
||
description: "是否允许用户分享内容",
|
||
type: "switch",
|
||
value: true
|
||
},
|
||
{
|
||
id: "switch.open_view",
|
||
label: "开放查看",
|
||
description: "是否允许用户查看内容",
|
||
type: "switch",
|
||
value: true
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
|
||
// 邮件配置
|
||
{
|
||
id: "email",
|
||
title: "邮件配置",
|
||
icon: <Mail className="h-4 w-4" />,
|
||
sections: [
|
||
{
|
||
id: "email-smtp",
|
||
title: "SMTP设置",
|
||
description: "邮件服务器的SMTP配置",
|
||
icon: <Server className="h-5 w-5" />,
|
||
fields: [
|
||
{
|
||
id: "email.smtp_host",
|
||
label: "SMTP主机",
|
||
description: "SMTP服务器地址",
|
||
type: "input",
|
||
value: "",
|
||
placeholder: "请输入SMTP主机地址"
|
||
},
|
||
{
|
||
id: "email.smtp_port",
|
||
label: "SMTP端口",
|
||
description: "SMTP服务器端口号",
|
||
type: "number",
|
||
value: 465,
|
||
validation: {
|
||
required: true,
|
||
min: 1,
|
||
max: 65535
|
||
}
|
||
},
|
||
{
|
||
id: "email.smtp_user",
|
||
label: "SMTP用户名",
|
||
description: "SMTP服务器登录用户名",
|
||
type: "input",
|
||
value: "",
|
||
placeholder: "请输入SMTP用户名"
|
||
},
|
||
{
|
||
id: "email.smtp_password",
|
||
label: "SMTP密码",
|
||
description: "SMTP服务器登录密码",
|
||
type: "password",
|
||
value: "",
|
||
placeholder: "请输入SMTP密码"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: "email-sender",
|
||
title: "发件人设置",
|
||
description: "邮件发件人相关信息",
|
||
icon: <User className="h-5 w-5" />,
|
||
fields: [
|
||
{
|
||
id: "email.smtp_from",
|
||
label: "发件人地址",
|
||
description: "系统发送邮件的发件人地址",
|
||
type: "email",
|
||
value: "",
|
||
placeholder: "请输入发件人邮箱"
|
||
},
|
||
{
|
||
id: "email.smtp_from_name",
|
||
label: "发件人姓名",
|
||
description: "系统发送邮件的发件人姓名",
|
||
type: "input",
|
||
value: "",
|
||
placeholder: "请输入发件人姓名"
|
||
},
|
||
{
|
||
id: "email.smtp_from_email",
|
||
label: "发件人邮箱",
|
||
description: "系统发送邮件的发件人邮箱",
|
||
type: "email",
|
||
value: "",
|
||
placeholder: "请输入发件人邮箱"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: "email-templates",
|
||
title: "邮件模板",
|
||
description: "邮件模板相关配置",
|
||
icon: <FileText className="h-5 w-5" />,
|
||
fields: [
|
||
{
|
||
id: "email.system_template",
|
||
label: "系统模板",
|
||
description: "系统邮件的默认模板",
|
||
type: "select",
|
||
value: "default",
|
||
options: [
|
||
{ label: "默认模板", value: "default" },
|
||
{ label: "简洁模板", value: "simple" },
|
||
{ label: "企业模板", value: "enterprise" }
|
||
]
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
|
||
// 系统配置 (Logging + Cache)
|
||
{
|
||
id: "system",
|
||
title: "系统配置",
|
||
icon: <Database className="h-4 w-4" />,
|
||
sections: [
|
||
{
|
||
id: "logging-level",
|
||
title: "日志级别",
|
||
description: "系统日志的级别设置",
|
||
icon: <Settings className="h-5 w-5" />,
|
||
fields: [
|
||
{
|
||
id: "logging.level",
|
||
label: "日志级别",
|
||
description: "系统记录日志的最低级别",
|
||
type: "select",
|
||
value: 'info',
|
||
options: [
|
||
{ label: "调试", value: "debug" },
|
||
{ label: "信息", value: "info" },
|
||
{ label: "警告", value: "warn" },
|
||
{ label: "错误", value: "error" },
|
||
{ label: "致命", value: "fatal" }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: "logging-files",
|
||
title: "日志文件管理",
|
||
description: "日志文件的管理配置",
|
||
icon: <HardDrive className="h-5 w-5" />,
|
||
fields: [
|
||
{
|
||
id: "logging.max_files",
|
||
label: "最大文件数",
|
||
description: "保留的日志文件最大数量",
|
||
type: "number",
|
||
value: 10,
|
||
validation: {
|
||
required: true,
|
||
min: 1,
|
||
max: 100
|
||
}
|
||
},
|
||
{
|
||
id: "logging.max_file_size",
|
||
label: "最大文件大小(MB)",
|
||
description: "单个日志文件的最大大小",
|
||
type: "number",
|
||
value: 10,
|
||
validation: {
|
||
required: true,
|
||
min: 1,
|
||
max: 10240
|
||
}
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: "cache-settings",
|
||
title: "缓存设置",
|
||
description: "系统缓存的配置参数",
|
||
icon: <Settings className="h-5 w-5" />,
|
||
fields: [
|
||
{
|
||
id: "cache.ttl",
|
||
label: "缓存TTL(秒)",
|
||
description: "缓存的生存时间,单位为秒",
|
||
type: "number",
|
||
value: 3600,
|
||
validation: {
|
||
required: true,
|
||
min: 1,
|
||
max: 86400
|
||
}
|
||
},
|
||
{
|
||
id: "cache.max_size",
|
||
label: "最大缓存大小(MB)",
|
||
description: "缓存的最大内存占用",
|
||
type: "number",
|
||
value: 1024,
|
||
validation: {
|
||
required: true,
|
||
min: 1,
|
||
max: 10000
|
||
}
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
],
|
||
|
||
// 配置选项
|
||
autoSave: false, // 禁用自动保存,使用手动保存
|
||
validateOnChange: true,
|
||
onValidate: (values) => {
|
||
const result = commonConfigSchema.safeParse(values);
|
||
return zodErrorsToAdminErrors(result);
|
||
}
|
||
};
|
||
}
|
||
|
||
// 配置更新Hook
|
||
export function useConfigUpdate() {
|
||
const [updateConfigBatch, { loading, error, data }] = useMutation(UPDATE_CONFIG_BATCH);
|
||
|
||
const updateConfigs = async (configs: Record<string, any>) => {
|
||
try {
|
||
// 将配置对象转换为UpdateConfig数组
|
||
const updateConfigs: UpdateConfig[] = Object.entries(configs).map(([key, value]) => ({
|
||
key,
|
||
value: String(value), // 直接转换为字符串,避免JSON.stringify添加双引号
|
||
description: undefined,
|
||
category: undefined,
|
||
is_editable: true
|
||
}));
|
||
|
||
const result = await updateConfigBatch({
|
||
variables: {
|
||
input: updateConfigs
|
||
}
|
||
});
|
||
|
||
if (result.data?.updateConfigBatch === "successed") {
|
||
toast.success("配置更新成功!");
|
||
return true;
|
||
} else {
|
||
toast.error("配置更新失败");
|
||
return false;
|
||
}
|
||
} catch (err) {
|
||
console.error("更新配置失败:", err);
|
||
toast.error("配置更新失败,请检查网络连接");
|
||
return false;
|
||
}
|
||
};
|
||
|
||
return {
|
||
updateConfigs,
|
||
loading,
|
||
error,
|
||
data
|
||
};
|
||
}
|
||
|
||
// 配置保存按钮组件
|
||
export function ConfigSaveButton({
|
||
onSave,
|
||
loading = false,
|
||
disabled = false
|
||
}: {
|
||
onSave: () => void;
|
||
loading?: boolean;
|
||
disabled?: boolean;
|
||
}) {
|
||
return (
|
||
<Button
|
||
onClick={onSave}
|
||
disabled={disabled || loading}
|
||
className="flex items-center gap-2"
|
||
>
|
||
{loading ? (
|
||
<>
|
||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||
保存中...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Save className="h-4 w-4" />
|
||
保存配置
|
||
</>
|
||
)}
|
||
</Button>
|
||
);
|
||
}
|
||
|
||
// 配置状态指示器
|
||
export function ConfigStatusIndicator({
|
||
status,
|
||
message
|
||
}: {
|
||
status: 'idle' | 'loading' | 'success' | 'error';
|
||
message?: string;
|
||
}) {
|
||
if (status === 'idle') return null;
|
||
|
||
return (
|
||
<div className="flex items-center gap-2 p-2 rounded-md">
|
||
{status === 'loading' && (
|
||
<>
|
||
<RefreshCw className="h-4 w-4 animate-spin text-blue-500" />
|
||
<span className="text-blue-600">保存中...</span>
|
||
</>
|
||
)}
|
||
{status === 'success' && (
|
||
<>
|
||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||
<span className="text-green-600">保存成功</span>
|
||
</>
|
||
)}
|
||
{status === 'error' && (
|
||
<>
|
||
<AlertCircle className="h-4 w-4 text-red-500" />
|
||
<span className="text-red-600">{message || '保存失败'}</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
} |