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

1156 lines
47 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 { gql, useMutation } from "@apollo/client";
import {
Settings,
Globe,
Palette,
Shield,
Users,
Database,
Mail,
FileText,
Server,
HardDrive,
Lock,
User,
ToggleLeft,
RefreshCw,
Download,
Upload,
Save,
CheckCircle,
AlertCircle
} 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";
// GraphQL Mutation
const UPDATE_CONFIG_BATCH = gql`
mutation UpdateConfigBatch($input: [UpdateConfig!]!) {
updateConfigBatch(input: $input)
}
`;
// 创建基于后端数据的动态管理面板配置带Mutation
export function createDynamicAdminConfigWithMutation(
data?: SiteOpsConfigType,
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>
);
}
// 完整的配置管理面板组件
export function DynamicAdminConfigPanel({
data,
onExport,
onImport
}: {
data?: SiteOpsConfigType;
onExport?: () => Promise<void>;
onImport?: (file: File) => Promise<void>;
}) {
const [formValues, setFormValues] = useState<Record<string, any>>({});
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
const [saveStatus, setSaveStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [errorMessage, setErrorMessage] = useState<string>('');
const { updateConfigs, loading } = useConfigUpdate();
// 生成配置
const config = createDynamicAdminConfigWithMutation(data, onExport, onImport);
// 初始化表单值
React.useEffect(() => {
const initialValues = config.tabs.reduce((acc, tab) => {
tab.sections.forEach(section => {
section.fields.forEach(field => {
acc[field.id] = field.value;
});
});
return acc;
}, {} as Record<string, any>);
setFormValues(initialValues);
}, [config]);
// 处理字段值变化
const handleFieldChange = (fieldId: string, value: any) => {
setFormValues(prev => ({
...prev,
[fieldId]: value
}));
// 清除该字段的验证错误
if (validationErrors[fieldId]) {
setValidationErrors(prev => {
const newErrors = { ...prev };
delete newErrors[fieldId];
return newErrors;
});
}
};
// 验证表单
const validateForm = () => {
const errors: Record<string, string> = {};
config.tabs.forEach(tab => {
tab.sections.forEach(section => {
section.fields.forEach(field => {
const value = formValues[field.id];
// 必填字段验证
if (field.validation?.required && (!value || value === '')) {
errors[field.id] = '此字段为必填项';
return;
}
// 最小长度验证
if (field.validation?.minLength && typeof value === 'string' && value.length < field.validation.minLength) {
errors[field.id] = `最少需要 ${field.validation.minLength} 个字符`;
return;
}
// 最大长度验证
if (field.validation?.maxLength && typeof value === 'string' && value.length > field.validation.maxLength) {
errors[field.id] = `最多允许 ${field.validation.maxLength} 个字符`;
return;
}
// 数值范围验证
if (field.validation?.min !== undefined && typeof value === 'number' && value < field.validation.min) {
errors[field.id] = `最小值不能小于 ${field.validation.min}`;
return;
}
if (field.validation?.max !== undefined && typeof value === 'number' && value > field.validation.max) {
errors[field.id] = `最大值不能大于 ${field.validation.max}`;
return;
}
});
});
});
setValidationErrors(errors);
return Object.keys(errors).length === 0;
};
// 保存配置
const handleSave = async () => {
if (!validateForm()) {
setSaveStatus('error');
setErrorMessage('请检查表单验证错误');
return;
}
setSaveStatus('loading');
setErrorMessage('');
try {
const success = await updateConfigs(formValues);
if (success) {
setSaveStatus('success');
// 3秒后重置状态
setTimeout(() => setSaveStatus('idle'), 3000);
} else {
setSaveStatus('error');
setErrorMessage('保存失败,请重试');
}
} catch (error) {
setSaveStatus('error');
setErrorMessage('保存过程中发生错误');
}
};
// 渲染字段
const renderField = (field: any) => {
const value = formValues[field.id] ?? field.value;
const error = validationErrors[field.id];
const commonProps = {
id: field.id,
value: value,
onChange: (e: any) => handleFieldChange(field.id, e.target.value),
className: `w-full ${error ? 'border-red-500' : ''}`,
placeholder: field.placeholder
};
switch (field.type) {
case 'input':
return (
<div key={field.id} className="space-y-2">
<label htmlFor={field.id} className="text-sm font-medium">
{field.label}
</label>
<input
{...commonProps}
type="text"
/>
{field.description && (
<p className="text-sm text-gray-500">{field.description}</p>
)}
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
</div>
);
case 'textarea':
return (
<div key={field.id} className="space-y-2">
<label htmlFor={field.id} className="text-sm font-medium">
{field.label}
</label>
<textarea
{...commonProps}
rows={field.rows || 3}
onChange={(e) => handleFieldChange(field.id, e.target.value)}
/>
{field.description && (
<p className="text-sm text-gray-500">{field.description}</p>
)}
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
</div>
);
case 'number':
return (
<div key={field.id} className="space-y-2">
<label htmlFor={field.id} className="text-sm font-medium">
{field.label}
</label>
<input
{...commonProps}
type="number"
onChange={(e) => handleFieldChange(field.id, Number(e.target.value))}
/>
{field.description && (
<p className="text-sm text-gray-500">{field.description}</p>
)}
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
</div>
);
case 'email':
return (
<div key={field.id} className="space-y-2">
<label htmlFor={field.id} className="text-sm font-medium">
{field.label}
</label>
<input
{...commonProps}
type="email"
/>
{field.description && (
<p className="text-sm text-gray-500">{field.description}</p>
)}
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
</div>
);
case 'password':
return (
<div key={field.id} className="space-y-2">
<label htmlFor={field.id} className="text-sm font-medium">
{field.label}
</label>
<input
{...commonProps}
type="password"
/>
{field.description && (
<p className="text-sm text-gray-500">{field.description}</p>
)}
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
</div>
);
case 'select':
return (
<div key={field.id} className="space-y-2">
<label htmlFor={field.id} className="text-sm font-medium">
{field.label}
</label>
<select
{...commonProps}
onChange={(e) => handleFieldChange(field.id, e.target.value)}
>
{field.options?.map((option: any) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
{field.description && (
<p className="text-sm text-gray-500">{field.description}</p>
)}
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
</div>
);
case 'switch':
return (
<div key={field.id} className="space-y-2">
<div className="flex items-center justify-between">
<div>
<label htmlFor={field.id} className="text-sm font-medium">
{field.label}
</label>
{field.description && (
<p className="text-sm text-gray-500">{field.description}</p>
)}
</div>
<button
type="button"
onClick={() => handleFieldChange(field.id, !value)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${value ? 'bg-blue-600' : 'bg-gray-200'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${value ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
</div>
);
default:
return null;
}
};
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">{config.header.title}</h1>
<p className="text-gray-600">{config.header.description}</p>
</div>
<div className="flex items-center gap-3">
{config.header.actions?.map((action) => (
<Button
key={action.id}
variant={action.variant as any}
onClick={action.onClick}
className="flex items-center gap-2"
>
{action.icon}
{action.label}
</Button>
))}
</div>
</div>
{/* 面包屑 */}
<nav className="flex mt-4" aria-label="Breadcrumb">
<ol className="flex items-center space-x-2">
{config.header.breadcrumbs?.map((crumb, index) => (
<li key={index} className="flex items-center">
{index > 0 && <span className="mx-2 text-gray-400">/</span>}
{crumb.href ? (
<a
href={crumb.href}
className="text-sm text-blue-600 hover:text-blue-800"
>
{crumb.label}
</a>
) : (
<span className="text-sm text-gray-500">{crumb.label}</span>
)}
</li>
))}
</ol>
</nav>
</div>
{/* 主要内容 */}
<div className="flex-1 px-6 py-6">
<div className="max-w-7xl mx-auto">
{/* 标签页 */}
<div className="bg-white rounded-lg shadow">
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8 px-6" aria-label="Tabs">
{config.tabs.map((tab) => (
<button
key={tab.id}
className="flex items-center gap-2 py-4 px-1 border-b-2 border-transparent text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300"
>
{tab.icon}
{tab.title}
</button>
))}
</nav>
</div>
{/* 标签页内容 */}
<div className="p-6">
{config.tabs.map((tab) => (
<div key={tab.id} className="space-y-8">
{tab.sections.map((section) => (
<div key={section.id} className="space-y-6">
<div className="flex items-center gap-3">
{section.icon}
<div>
<h3 className="text-lg font-medium text-gray-900">
{section.title}
</h3>
<p className="text-sm text-gray-500">
{section.description}
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{section.fields.map((field) => renderField(field))}
</div>
</div>
))}
</div>
))}
</div>
</div>
{/* 保存按钮和状态 */}
<div className="mt-8 flex items-center justify-between">
<ConfigStatusIndicator
status={saveStatus}
message={errorMessage}
/>
<ConfigSaveButton
onSave={handleSave}
loading={loading || saveStatus === 'loading'}
disabled={Object.keys(validationErrors).length > 0}
/>
</div>
</div>
</div>
</div>
);
}