This commit is contained in:
2026-06-16 13:12:43 +08:00
parent 496759ccdf
commit 1b15561e0a
6 changed files with 156 additions and 7 deletions
+18 -1
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import {
Button, Card, Form, Input, message, Space, Typography, Upload,
Button, Card, Form, Input, message, Space, Switch, Typography, Upload,
} from 'antd';
import {
SettingOutlined, SaveOutlined, UploadOutlined, FilePdfOutlined, EyeOutlined,
@@ -69,6 +69,7 @@ const AdminSettings: React.FC = () => {
'站点信息': 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 => {
@@ -80,6 +81,9 @@ const AdminSettings: React.FC = () => {
seo_title: '搜索引擎结果中显示的标题',
seo_description: '搜索引擎结果中显示的描述文字,建议150字以内',
seo_keywords: '用逗号分隔的关键词列表',
user_register_credits: '用户注册时赠送的初始积分,默认100',
user_login_credits: '用户每日登录赠送的积分数量',
user_login_credits_enabled: '是否启用每日登录赠送积分功能',
};
return descMap[config.key] || config.description || '';
};
@@ -91,6 +95,19 @@ const AdminSettings: React.FC = () => {
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" />;
};
+95 -3
View File
@@ -3,10 +3,10 @@ import {
Button, Card, Checkbox, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Switch, Table, Tabs, Tag, Typography,
} from 'antd';
import {
UserOutlined, WalletOutlined, SearchOutlined, StopOutlined, CheckCircleOutlined, PlusOutlined, MenuOutlined, LockOutlined,
UserOutlined, WalletOutlined, SearchOutlined, StopOutlined, CheckCircleOutlined, PlusOutlined, MenuOutlined, LockOutlined, SettingOutlined, SaveOutlined,
} from '@ant-design/icons';
import { getAdminUsers, adjustCredits, toggleUserStatus, createUser, updateUserMenus, getMenuConfigs, resetUserPassword } from '../api';
import type { AdminUser } from '../types';
import { getAdminUsers, adjustCredits, toggleUserStatus, createUser, updateUserMenus, getMenuConfigs, resetUserPassword, getSystemConfigs, updateSystemConfig } from '../api';
import type { AdminUser, SystemConfig } from '../types';
import { formatDate } from '../utils/formatDate';
const AdminUsers: React.FC = () => {
@@ -29,6 +29,10 @@ const AdminUsers: React.FC = () => {
const [pageSize, setPageSize] = useState(20);
const [total, setTotal] = useState(0);
const [creditConfigs, setCreditConfigs] = useState<SystemConfig[]>([]);
const [configForm] = Form.useForm();
const [configSaving, setConfigSaving] = useState(false);
const load = async () => {
setLoading(true);
try {
@@ -41,6 +45,40 @@ const AdminUsers: React.FC = () => {
useEffect(() => { load(); }, [page, pageSize, search]);
useEffect(() => {
const loadCreditConfigs = async () => {
try {
const configs = await getSystemConfigs();
const credit = configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits'));
setCreditConfigs(credit);
const formValues: Record<string, string> = {};
credit.forEach(c => { formValues[c.key] = c.value; });
configForm.setFieldsValue(formValues);
} catch { /* auth error handled by client */ }
};
loadCreditConfigs();
}, []);
const handleSaveCreditConfigs = async () => {
try {
const values = await configForm.validateFields();
setConfigSaving(true);
for (const config of creditConfigs) {
const newVal = values[config.key];
if (newVal !== undefined && newVal !== config.value) {
await updateSystemConfig(config.id, newVal ?? '');
}
}
message.success('积分配置已保存');
const configs = await getSystemConfigs();
setCreditConfigs(configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits')));
setConfigSaving(false);
} catch (e: any) {
setConfigSaving(false);
message.error(e?.message || '保存失败');
}
};
const handlePageChange = (p: number, ps: number) => {
setPage(p);
setPageSize(ps);
@@ -224,6 +262,60 @@ const AdminUsers: React.FC = () => {
return (
<div>
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5', marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
<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>
<Form form={configForm} layout="vertical">
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 20 }}>
<Form.Item
name="user_register_credits"
label={<span style={{ fontWeight: 500 }}></span>}
extra="用户注册时赠送的初始积分"
>
<InputNumber min={0} style={{ width: '100%' }} size="large" />
</Form.Item>
<Form.Item
name="user_login_credits"
label={<span style={{ fontWeight: 500 }}></span>}
extra="用户每日首次登录赠送的积分"
>
<InputNumber min={0} style={{ width: '100%' }} size="large" />
</Form.Item>
<Form.Item
name="user_login_credits_enabled"
label={<span style={{ fontWeight: 500 }}></span>}
extra="是否开启每日登录赠送积分功能"
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Switch defaultChecked={creditConfigs.find(c => c.key === 'user_login_credits_enabled')?.value === 'true'} />
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
{creditConfigs.find(c => c.key === 'user_login_credits_enabled')?.value === 'true' ? '已启用' : '已禁用'}
</Typography.Text>
</div>
</Form.Item>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16 }}>
<Button type="primary" icon={<SaveOutlined />} onClick={handleSaveCreditConfigs} loading={configSaving} size="large" style={{ borderRadius: 8 }}>
</Button>
</div>
</Form>
</Card>
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
{/* Search bar */}
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>