213 lines
8.1 KiB
TypeScript
213 lines
8.1 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import {
|
|
Button, Card, Form, Input, message, Space, Switch, Typography, Upload,
|
|
} from 'antd';
|
|
import {
|
|
SettingOutlined, SaveOutlined, UploadOutlined, FilePdfOutlined, EyeOutlined,
|
|
} from '@ant-design/icons';
|
|
import { getSystemConfigs, updateSystemConfig, uploadPdf } from '../api';
|
|
import type { SystemConfig } from '../types';
|
|
|
|
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);
|
|
const data = await getSystemConfigs();
|
|
setConfigs(data);
|
|
const formValues: Record<string, string> = {};
|
|
data.forEach(c => { formValues[c.key] = c.value; });
|
|
form.setFieldsValue(formValues);
|
|
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 && newVal !== config.value) {
|
|
await updateSystemConfig(config.id, newVal ?? '');
|
|
}
|
|
}
|
|
message.success('系统配置已保存');
|
|
const data = await getSystemConfigs();
|
|
setConfigs(data);
|
|
setSaving(false);
|
|
} catch (e: any) {
|
|
setSaving(false);
|
|
message.error(e?.message || '保存失败');
|
|
}
|
|
};
|
|
|
|
const handleUpload = async (file: File, configKey: string) => {
|
|
setUploading(configKey);
|
|
try {
|
|
const res = await uploadPdf(file, configKey);
|
|
// Update local state
|
|
setConfigs(prev => prev.map(c => c.key === configKey ? { ...c, value: res.url } : c));
|
|
form.setFieldsValue({ [configKey]: res.url });
|
|
message.success('PDF上传成功');
|
|
} catch {
|
|
message.error('上传失败');
|
|
} finally {
|
|
setUploading('');
|
|
}
|
|
return false; // prevent default upload
|
|
};
|
|
|
|
const groupedConfigs: Record<string, SystemConfig[]> = {
|
|
'站点信息': configs.filter(c => c.key.startsWith('site_')),
|
|
'协议配置': configs.filter(c => c.key === 'user_agreement_url' || c.key === 'privacy_policy_url'),
|
|
'SEO 设置': configs.filter(c => c.key.startsWith('seo_')),
|
|
'用户积分配置': configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits')),
|
|
};
|
|
|
|
const getFieldDescription = (config: SystemConfig): string => {
|
|
const descMap: Record<string, string> = {
|
|
site_name: '平台显示名称,将展示在页面标题和导航栏',
|
|
site_logo: '平台Logo图片URL,建议尺寸 200x40px',
|
|
user_agreement_url: '用户注册/登录时需同意的用户协议PDF文件',
|
|
privacy_policy_url: '用户注册/登录时需同意的隐私政策PDF文件',
|
|
seo_title: '搜索引擎结果中显示的标题',
|
|
seo_description: '搜索引擎结果中显示的描述文字,建议150字以内',
|
|
seo_keywords: '用逗号分隔的关键词列表',
|
|
user_register_credits: '用户注册时赠送的初始积分,默认100',
|
|
user_login_credits: '用户每日登录赠送的积分数量',
|
|
user_login_credits_enabled: '是否启用每日登录赠送积分功能',
|
|
};
|
|
return descMap[config.key] || config.description || '';
|
|
};
|
|
|
|
const getFieldComponent = (config: SystemConfig) => {
|
|
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') {
|
|
return <Input type="number" min={0} placeholder={config.description} size="large" />;
|
|
}
|
|
return <Input placeholder={config.description} size="large" />;
|
|
};
|
|
|
|
const PdfUploadField: React.FC<{ config: SystemConfig }> = ({ config }) => {
|
|
const label = config.key === 'user_agreement_url' ? '用户协议' : '隐私政策';
|
|
const hasFile = config.value && config.value.startsWith('/uploads/');
|
|
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(`http://localhost:8000${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>
|
|
);
|
|
};
|
|
|
|
if (loading) {
|
|
return <Card loading bordered={false} style={{ borderRadius: 12 }} />;
|
|
}
|
|
|
|
return (
|
|
<div style={{ maxWidth: 720 }}>
|
|
<Card bordered={false} 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">管理站点基础信息、协议文件和SEO配置</Typography.Text>
|
|
</div>
|
|
</div>
|
|
|
|
<Form form={form} layout="vertical">
|
|
{Object.entries(groupedConfigs).map(([group, items]) => (
|
|
<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 === '协议配置' ? (
|
|
items.map(config => (
|
|
<PdfUploadField key={config.id} config={config} />
|
|
))
|
|
) : (
|
|
items.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>
|
|
</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;
|