556 lines
22 KiB
TypeScript
556 lines
22 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
||
import {
|
||
Button, Card, Form, Input, InputNumber, message, Select, Space, Switch, Tabs, Typography, Upload,
|
||
} from 'antd';
|
||
import {
|
||
SettingOutlined, SaveOutlined, UploadOutlined, FilePdfOutlined, EyeOutlined, DatabaseOutlined, VideoCameraOutlined, RobotOutlined,
|
||
} from '@ant-design/icons';
|
||
import {
|
||
createSystemConfig,
|
||
getGlobalResourceCapacity,
|
||
getSystemConfigs,
|
||
saveGlobalResourceCapacity,
|
||
updateSystemConfig,
|
||
uploadLogo,
|
||
uploadPdf,
|
||
uploadLoginVideo,
|
||
} from '../api';
|
||
import type { ResourceCapacityUnit, SystemConfig } from '../types';
|
||
|
||
const capacityUnitOptions: { value: ResourceCapacityUnit; label: string }[] = [
|
||
{ value: 'MB', label: 'MB(1024 × 1024 字节)' },
|
||
{ value: 'GB', label: 'GB(1024 × 1024 × 1024 字节)' },
|
||
{ value: 'TB', label: 'TB(1024 × 1024 × 1024 × 1024 字节)' },
|
||
];
|
||
|
||
const AdminSettings: React.FC = () => {
|
||
const [configs, setConfigs] = useState<SystemConfig[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [saving, setSaving] = useState(false);
|
||
const [uploading, setUploading] = useState('');
|
||
const [form] = Form.useForm();
|
||
|
||
useEffect(() => {
|
||
load();
|
||
}, []);
|
||
|
||
const load = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const [data, capacity] = await Promise.all([
|
||
getSystemConfigs(),
|
||
getGlobalResourceCapacity(),
|
||
]);
|
||
// 确保 llm_media_as_base64 配置存在
|
||
if (!data.some(c => c.key === 'llm_media_as_base64')) {
|
||
data.push({ id: 'cfg_llm_media_as_base64', key: 'llm_media_as_base64', value: 'true', description: '文字模型请求时图片/视频使用 base64 编码' });
|
||
}
|
||
setConfigs(data);
|
||
const formValues: Record<string, any> = {};
|
||
data.forEach(c => { formValues[c.key] = c.value; });
|
||
// 预扣积分默认值
|
||
if (!formValues.optimize_hold_credits) formValues.optimize_hold_credits = '5';
|
||
formValues.resource_capacity_enabled = capacity.enabled;
|
||
formValues.resource_capacity_limit_value = capacity.limitValue || '1.000';
|
||
formValues.resource_capacity_limit_unit = capacity.limitUnit || 'GB';
|
||
form.setFieldsValue(formValues);
|
||
} catch (e: any) {
|
||
message.error(e?.message || '加载配置失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleSave = async () => {
|
||
try {
|
||
const values = await form.validateFields();
|
||
setSaving(true);
|
||
for (const config of configs) {
|
||
const newVal = values[config.key];
|
||
if (newVal !== undefined && String(newVal) !== config.value) {
|
||
await updateSystemConfig(config.id, String(newVal ?? ''));
|
||
}
|
||
}
|
||
// AI创作预扣积分 - 不存在则创建
|
||
const holdVal = values.optimize_hold_credits;
|
||
if (holdVal !== undefined && holdVal !== null && holdVal !== '') {
|
||
const existing = configs.find(c => c.key === 'optimize_hold_credits');
|
||
if (existing) {
|
||
if (String(holdVal) !== existing.value) {
|
||
await updateSystemConfig(existing.id, String(holdVal));
|
||
}
|
||
} else {
|
||
await createSystemConfig('optimize_hold_credits', String(holdVal), 'AI创作预扣积分数量(防止并发超卖)');
|
||
}
|
||
}
|
||
await saveGlobalResourceCapacity({
|
||
enabled: !!values.resource_capacity_enabled,
|
||
limitValue: String(values.resource_capacity_limit_value ?? '1.000'),
|
||
limitUnit: values.resource_capacity_limit_unit || 'GB',
|
||
});
|
||
message.success('系统配置已保存');
|
||
await load();
|
||
} catch (e: any) {
|
||
message.error(e?.message || '保存失败');
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleUpload = async (file: File, configKey: string) => {
|
||
setUploading(configKey);
|
||
try {
|
||
const res = await uploadPdf(file, configKey);
|
||
setConfigs(prev => prev.map(c => c.key === configKey ? { ...c, value: res.url } : c));
|
||
form.setFieldsValue({ [configKey]: res.url });
|
||
|
||
// Find the config and update to database
|
||
const config = configs.find(c => c.key === configKey);
|
||
if (config) {
|
||
await updateSystemConfig(config.id, res.url);
|
||
}
|
||
|
||
message.success('PDF上传成功并已保存');
|
||
} catch {
|
||
message.error('上传失败');
|
||
} finally {
|
||
setUploading('');
|
||
}
|
||
return false;
|
||
};
|
||
|
||
const handleLogoUpload = async (file: File) => {
|
||
setUploading('site_logo');
|
||
try {
|
||
const res = await uploadLogo(file);
|
||
setConfigs(prev => prev.map(c => c.key === 'site_logo' ? { ...c, value: res.url } : c));
|
||
form.setFieldsValue({ site_logo: res.url });
|
||
const config = configs.find(c => c.key === 'site_logo');
|
||
if (config) {
|
||
await updateSystemConfig(config.id, res.url);
|
||
}
|
||
message.success('Logo上传成功并已保存');
|
||
} catch {
|
||
message.error('上传失败');
|
||
} finally {
|
||
setUploading('');
|
||
}
|
||
return false;
|
||
};
|
||
|
||
const handleLoginVideoUpload = async (file: File) => {
|
||
setUploading('login_bg_video');
|
||
try {
|
||
const res = await uploadLoginVideo(file);
|
||
setConfigs(prev => prev.map(c => c.key === 'login_bg_video' ? { ...c, value: res.url } : c));
|
||
form.setFieldsValue({ login_bg_video: res.url });
|
||
const config = configs.find(c => c.key === 'login_bg_video');
|
||
if (config) {
|
||
await updateSystemConfig(config.id, res.url);
|
||
}
|
||
message.success('登录背景视频上传成功并已保存');
|
||
} catch (e: any) {
|
||
message.error(e?.message || '上传失败');
|
||
} finally {
|
||
setUploading('');
|
||
}
|
||
return false;
|
||
};
|
||
|
||
const handleRemoveLoginVideo = async () => {
|
||
setConfigs(prev => prev.map(c => c.key === 'login_bg_video' ? { ...c, value: '' } : c));
|
||
form.setFieldsValue({ login_bg_video: '' });
|
||
const config = configs.find(c => c.key === 'login_bg_video');
|
||
if (config) {
|
||
await updateSystemConfig(config.id, '');
|
||
}
|
||
message.success('已移除登录背景视频');
|
||
};
|
||
|
||
const handleToggleBase64 = async (checked: boolean) => {
|
||
try {
|
||
let config = configs.find(c => c.key === 'llm_media_as_base64');
|
||
if (config && config.id && !config.id.startsWith('cfg_')) {
|
||
await updateSystemConfig(config.id, checked ? 'true' : 'false');
|
||
} else {
|
||
const res = await createSystemConfig('llm_media_as_base64', checked ? 'true' : 'false', '文字模型请求时图片/视频使用 base64 编码');
|
||
config = res;
|
||
}
|
||
setConfigs(prev => {
|
||
const exists = prev.some(c => c.key === 'llm_media_as_base64');
|
||
if (exists) return prev.map(c => c.key === 'llm_media_as_base64' ? { ...c, value: checked ? 'true' : 'false', id: config!.id } : c);
|
||
return [...prev, config!];
|
||
});
|
||
message.success(`已${checked ? '开启' : '关闭'}文字模型媒体 base64 编码`);
|
||
} catch (e: any) {
|
||
message.error(e?.message || '操作失败');
|
||
}
|
||
};
|
||
|
||
const groupedConfigs: Record<string, SystemConfig[]> = {
|
||
'站点信息': configs.filter(c => c.key.startsWith('site_')),
|
||
'协议配置': configs.filter(c => c.key === 'user_agreement_privacy_url'),
|
||
'SEO 设置': configs.filter(c => c.key.startsWith('seo_')),
|
||
'用户积分配置': configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits')),
|
||
'其他配置': configs.filter(c => c.key === 'operation_manual'),
|
||
'AI创作配置': configs.filter(c => c.key === 'optimize_hold_credits'),
|
||
};
|
||
|
||
const getFieldDescription = (config: SystemConfig): string => {
|
||
const descMap: Record<string, string> = {
|
||
site_name: '平台显示名称,将展示在页面标题和导航栏',
|
||
site_logo: '平台Logo图片URL,建议尺寸 200x40px',
|
||
site_copyright: '显示在前台登录页底部的版权信息,例如:© 2024 民众智创 版权所有',
|
||
user_agreement_privacy_url: '用户登录时需同意的用户协议及隐私政策PDF文件',
|
||
seo_title: '搜索引擎结果中显示的标题',
|
||
seo_description: '搜索引擎结果中显示的描述文字,建议150字以内',
|
||
seo_keywords: '用逗号分隔的关键词列表',
|
||
user_register_credits: '用户注册时赠送的初始积分,默认100',
|
||
user_login_credits: '用户每日登录赠送的积分数量',
|
||
user_login_credits_enabled: '是否启用每日登录赠送积分功能',
|
||
operation_manual: '操作手册链接,前台用户菜单将展示该入口,点击跳转此链接',
|
||
optimize_hold_credits: 'AI创作时预扣积分数量,用于防止并发超卖。预扣后按实际消耗多退少补',
|
||
};
|
||
return descMap[config.key] || config.description || '';
|
||
};
|
||
|
||
const LogoUploadField: React.FC<{ config: SystemConfig }> = ({ config }) => {
|
||
const hasLogo = config.value && config.value.startsWith('/uploads/');
|
||
const baseUrl = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
|
||
const logoUrl = hasLogo ? `${baseUrl}${config.value}` : '';
|
||
|
||
const handleRemove = () => {
|
||
setConfigs(prev => prev.map(c => c.key === 'site_logo' ? { ...c, value: '' } : c));
|
||
form.setFieldsValue({ site_logo: '' });
|
||
message.success('Logo已移除');
|
||
};
|
||
|
||
return (
|
||
<div style={{
|
||
padding: '16px', borderRadius: 10,
|
||
border: '1px solid #f0f0f5', background: '#fafbfc',
|
||
}}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
||
<Space>
|
||
<UploadOutlined style={{ color: '#6366f1', fontSize: 18 }} />
|
||
<Typography.Text strong>网站Logo</Typography.Text>
|
||
</Space>
|
||
<Space>
|
||
{hasLogo && (
|
||
<Button size="small" danger onClick={handleRemove}>
|
||
移除
|
||
</Button>
|
||
)}
|
||
<Upload
|
||
accept="image/*"
|
||
showUploadList={false}
|
||
beforeUpload={handleLogoUpload}
|
||
>
|
||
<Button size="small" type="primary" icon={<UploadOutlined />} loading={uploading === 'site_logo'}>
|
||
{hasLogo ? '重新上传' : '上传Logo'}
|
||
</Button>
|
||
</Upload>
|
||
</Space>
|
||
</div>
|
||
{hasLogo ? (
|
||
<div style={{ textAlign: 'center' }}>
|
||
<img
|
||
src={logoUrl}
|
||
alt="Logo预览"
|
||
style={{
|
||
maxWidth: 200,
|
||
maxHeight: 80,
|
||
objectFit: 'contain',
|
||
border: '1px solid #e2e8f0',
|
||
borderRadius: 8,
|
||
padding: 8,
|
||
}}
|
||
/>
|
||
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 8 }}>
|
||
建议尺寸:200x40px,支持 PNG、JPG 格式
|
||
</Typography.Text>
|
||
</div>
|
||
) : (
|
||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||
尚未上传Logo,将使用系统默认Logo
|
||
</Typography.Text>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const PdfUploadField: React.FC<{ config: SystemConfig }> = ({ config }) => {
|
||
const label = config.key === 'user_agreement_privacy_url' ? '用户协议及隐私政策' : '协议文件';
|
||
const hasFile = config.value && config.value.startsWith('/uploads/');
|
||
const baseUrl = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
|
||
return (
|
||
<div style={{
|
||
padding: '16px', borderRadius: 10,
|
||
border: '1px solid #f0f0f5', background: '#fafbfc',
|
||
marginBottom: 12,
|
||
}}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||
<Space>
|
||
<FilePdfOutlined style={{ color: '#ef4444', fontSize: 18 }} />
|
||
<Typography.Text strong>{label}</Typography.Text>
|
||
</Space>
|
||
<Space>
|
||
{hasFile && (
|
||
<Button size="small" icon={<EyeOutlined />}
|
||
onClick={() => window.open(`${baseUrl}${config.value}`, '_blank')}>
|
||
预览
|
||
</Button>
|
||
)}
|
||
<Upload
|
||
accept=".pdf"
|
||
showUploadList={false}
|
||
beforeUpload={(file) => handleUpload(file, config.key)}
|
||
>
|
||
<Button size="small" type="primary" icon={<UploadOutlined />} loading={uploading === config.key}>
|
||
{hasFile ? '重新上传' : '上传PDF'}
|
||
</Button>
|
||
</Upload>
|
||
</Space>
|
||
</div>
|
||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||
{hasFile ? `已上传: ${config.value}` : '尚未上传,前台将不显示对应链接'}
|
||
</Typography.Text>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const getFieldComponent = (config: SystemConfig) => {
|
||
if (config.key === 'site_logo') {
|
||
return <LogoUploadField config={config} />;
|
||
}
|
||
if (config.key === 'seo_description') {
|
||
return <Input.TextArea rows={3} placeholder={config.description} size="large" />;
|
||
}
|
||
if (config.key === 'seo_keywords') {
|
||
return <Input placeholder="关键词1, 关键词2, 关键词3" size="large" />;
|
||
}
|
||
if (config.key === 'user_login_credits_enabled') {
|
||
return (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||
<Switch defaultChecked={config.value === 'true'} />
|
||
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
|
||
{config.value === 'true' ? '已启用' : '已禁用'}
|
||
</Typography.Text>
|
||
</div>
|
||
);
|
||
}
|
||
if (config.key === 'user_register_credits' || config.key === 'user_login_credits' || config.key === 'optimize_hold_credits') {
|
||
return <Input type="number" min={1} placeholder={config.description} size="large" />;
|
||
}
|
||
return <Input placeholder={config.description} size="large" />;
|
||
};
|
||
|
||
if (loading) {
|
||
return <Card loading variant="outlined" style={{ borderRadius: 12 }} />;
|
||
}
|
||
|
||
const tabItems = [
|
||
{
|
||
key: 'basic',
|
||
label: '网站基础设置',
|
||
children: (
|
||
<Form form={form} layout="vertical">
|
||
{['站点信息', '协议配置', 'SEO 设置'].map(group => (
|
||
<div key={group} style={{ marginBottom: 24 }}>
|
||
<Typography.Text strong style={{ fontSize: 14, display: 'block', marginBottom: 12, paddingBottom: 8, borderBottom: '1px solid #f0f0f5' }}>
|
||
{group}
|
||
</Typography.Text>
|
||
{group === '协议配置' ? (
|
||
groupedConfigs[group]?.map(config => (
|
||
<PdfUploadField key={config.id} config={config} />
|
||
))
|
||
) : (
|
||
groupedConfigs[group]?.map(config => (
|
||
<Form.Item
|
||
key={config.id}
|
||
name={config.key}
|
||
label={<span style={{ fontWeight: 500 }}>{config.description}</span>}
|
||
extra={getFieldDescription(config)}
|
||
>
|
||
{getFieldComponent(config)}
|
||
</Form.Item>
|
||
))
|
||
)}
|
||
</div>
|
||
))}
|
||
</Form>
|
||
),
|
||
},
|
||
{
|
||
key: 'credits',
|
||
label: '用户积分配置',
|
||
children: (
|
||
<Form form={form} layout="vertical">
|
||
<div style={{ marginBottom: 24 }}>
|
||
<Typography.Text strong style={{ fontSize: 14, display: 'block', marginBottom: 12, paddingBottom: 8, borderBottom: '1px solid #f0f0f5' }}>
|
||
用户积分配置
|
||
</Typography.Text>
|
||
{groupedConfigs['用户积分配置']?.map(config => (
|
||
<Form.Item
|
||
key={config.id}
|
||
name={config.key}
|
||
label={<span style={{ fontWeight: 500 }}>{config.description}</span>}
|
||
extra={getFieldDescription(config)}
|
||
>
|
||
{getFieldComponent(config)}
|
||
</Form.Item>
|
||
))}
|
||
{/* AI创作预扣积分 - 固定显示 */}
|
||
<Form.Item
|
||
name="optimize_hold_credits"
|
||
label={<span style={{ fontWeight: 500 }}>AI创作预扣积分数量</span>}
|
||
extra="AI创作时预扣积分数量,用于防止并发超卖。预扣后按实际消耗多退少补"
|
||
>
|
||
<Input type="number" min={1} placeholder="默认5" size="large" />
|
||
</Form.Item>
|
||
</div>
|
||
</Form>
|
||
),
|
||
},
|
||
{
|
||
key: 'other',
|
||
label: '其他配置',
|
||
children: (
|
||
<Form form={form} layout="vertical">
|
||
<div style={{ marginBottom: 24 }}>
|
||
<Typography.Text strong style={{ fontSize: 14, display: 'block', marginBottom: 12, paddingBottom: 8, borderBottom: '1px solid #f0f0f5' }}>
|
||
其他配置
|
||
</Typography.Text>
|
||
{groupedConfigs['其他配置']?.map(config => (
|
||
<Form.Item
|
||
key={config.id}
|
||
name={config.key}
|
||
label={<span style={{ fontWeight: 500 }}>{config.description}</span>}
|
||
extra={getFieldDescription(config)}
|
||
>
|
||
{getFieldComponent(config)}
|
||
</Form.Item>
|
||
))}
|
||
</div>
|
||
|
||
{/* 登录背景视频 */}
|
||
<div style={{ marginBottom: 24 }}>
|
||
<Typography.Text strong style={{ fontSize: 14, display: 'block', marginBottom: 12, paddingBottom: 8, borderBottom: '1px solid #f0f0f5' }}>
|
||
登录背景视频
|
||
</Typography.Text>
|
||
<div style={{ padding: 16, borderRadius: 10, border: '1px solid #f0f0f5', background: '#fafbfc' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||
<Space>
|
||
<VideoCameraOutlined style={{ color: '#6366f1', fontSize: 18 }} />
|
||
<Typography.Text strong>背景视频</Typography.Text>
|
||
</Space>
|
||
<Space>
|
||
{form.getFieldValue('login_bg_video') && (
|
||
<Button size="small" danger onClick={handleRemoveLoginVideo}>
|
||
移除
|
||
</Button>
|
||
)}
|
||
<Upload
|
||
accept="video/*,image/gif,image/webp"
|
||
showUploadList={false}
|
||
beforeUpload={handleLoginVideoUpload}
|
||
>
|
||
<Button size="small" type="primary" icon={<UploadOutlined />} loading={uploading === 'login_bg_video'}>
|
||
上传视频
|
||
</Button>
|
||
</Upload>
|
||
</Space>
|
||
</div>
|
||
{(() => {
|
||
const url = form.getFieldValue('login_bg_video');
|
||
if (!url) {
|
||
return (
|
||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||
未设置,前台将默认使用 backimage.png
|
||
</Typography.Text>
|
||
);
|
||
}
|
||
const fullUrl = url.startsWith('http') ? url : `${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}${url}`;
|
||
const isGif = url.toLowerCase().endsWith('.gif');
|
||
return isGif ? (
|
||
<img src={fullUrl} alt="预览" style={{ width: '100%', maxHeight: 200, borderRadius: 8, background: '#f0f0f5', objectFit: 'contain' }} />
|
||
) : (
|
||
<video
|
||
src={fullUrl}
|
||
controls
|
||
muted
|
||
loop
|
||
playsInline
|
||
style={{ width: '100%', maxHeight: 200, borderRadius: 8, background: '#000' }}
|
||
/>
|
||
);
|
||
})()}
|
||
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 6 }}>
|
||
支持 MP4、WebM、MOV、GIF、WebP,最大 50MB
|
||
</Typography.Text>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 文字模型媒体编码 */}
|
||
<div style={{ marginBottom: 24 }}>
|
||
<Typography.Text strong style={{ fontSize: 14, display: 'block', marginBottom: 12, paddingBottom: 8, borderBottom: '1px solid #f0f0f5' }}>
|
||
文字模型媒体编码
|
||
</Typography.Text>
|
||
<div style={{ padding: 16, borderRadius: 10, border: '1px solid #f0f0f5', background: '#fafbfc' }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||
<Space>
|
||
<RobotOutlined style={{ color: '#6366f1', fontSize: 18 }} />
|
||
<div>
|
||
<Typography.Text strong>图片/视频 base64 编码</Typography.Text>
|
||
<div style={{ color: '#64748b', fontSize: 12, marginTop: 2 }}>
|
||
开启后文字模型请求时将媒体转 base64 发送,而非 URL 链接
|
||
</div>
|
||
</div>
|
||
</Space>
|
||
<Switch
|
||
checked={(configs.find(c => c.key === 'llm_media_as_base64') || {}).value === 'true'}
|
||
onChange={handleToggleBase64}
|
||
checkedChildren="base64"
|
||
unCheckedChildren="链接"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Form>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div style={{ maxWidth: 720 }}>
|
||
<Card variant="outlined" style={{ borderRadius: 12, border: '1px solid #f0f0f5', marginBottom: 16 }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 24 }}>
|
||
<div style={{
|
||
width: 44, height: 44, borderRadius: 10,
|
||
background: 'rgba(99,102,241,0.08)',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
fontSize: 20, color: '#6366f1',
|
||
}}>
|
||
<SettingOutlined />
|
||
</div>
|
||
<div>
|
||
<Typography.Title level={4} style={{ margin: 0 }}>系统设置</Typography.Title>
|
||
<Typography.Text type="secondary">管理站点基础信息、用户积分和系统配置</Typography.Text>
|
||
</div>
|
||
</div>
|
||
|
||
<Tabs items={tabItems} defaultActiveKey="basic" />
|
||
</Card>
|
||
|
||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||
<Button type="primary" icon={<SaveOutlined />} onClick={handleSave} loading={saving}
|
||
size="large" style={{ borderRadius: 8, minWidth: 140 }}>
|
||
保存配置
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default AdminSettings;
|