This commit is contained in:
2026-07-16 14:43:20 +08:00
parent 4d872036b2
commit c80e2150f5
6 changed files with 165 additions and 100 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
} }
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-lC1vODuG.js"></script> <script type="module" crossorigin src="/assets/index-DSi2SJq0.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css"> <link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
</head> </head>
<body> <body>
+4
View File
@@ -224,6 +224,10 @@ export async function updateSystemConfig(id: string, value: string): Promise<voi
await api.put(`/admin/system-configs/${id}`, { value }); await api.put(`/admin/system-configs/${id}`, { value });
} }
export async function createSystemConfig(key: string, value: string, description?: string): Promise<SystemConfig> {
return api.post('/admin/system-configs', { key, value, description });
}
export async function getGlobalResourceCapacity(): Promise<ResourceCapacityConfigOut> { export async function getGlobalResourceCapacity(): Promise<ResourceCapacityConfigOut> {
return api.get('/admin/resource-capacity/global'); return api.get('/admin/resource-capacity/global');
} }
+119 -92
View File
@@ -1,11 +1,12 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
Button, Card, Form, Input, InputNumber, message, Select, Space, Switch, Typography, Upload, Button, Card, Form, Input, InputNumber, message, Select, Space, Switch, Tabs, Typography, Upload,
} from 'antd'; } from 'antd';
import { import {
SettingOutlined, SaveOutlined, UploadOutlined, FilePdfOutlined, EyeOutlined, DatabaseOutlined, VideoCameraOutlined, RobotOutlined, SettingOutlined, SaveOutlined, UploadOutlined, FilePdfOutlined, EyeOutlined, DatabaseOutlined, VideoCameraOutlined, RobotOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { import {
createSystemConfig,
getGlobalResourceCapacity, getGlobalResourceCapacity,
getSystemConfigs, getSystemConfigs,
saveGlobalResourceCapacity, saveGlobalResourceCapacity,
@@ -40,6 +41,10 @@ const AdminSettings: React.FC = () => {
getSystemConfigs(), getSystemConfigs(),
getGlobalResourceCapacity(), 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); setConfigs(data);
const formValues: Record<string, any> = {}; const formValues: Record<string, any> = {};
data.forEach(c => { formValues[c.key] = c.value; }); data.forEach(c => { formValues[c.key] = c.value; });
@@ -149,13 +154,22 @@ const AdminSettings: React.FC = () => {
}; };
const handleToggleBase64 = async (checked: boolean) => { const handleToggleBase64 = async (checked: boolean) => {
const existing = configs.find(c => c.key === 'llm_media_as_base64'); try {
if (existing) { let config = configs.find(c => c.key === 'llm_media_as_base64');
await updateSystemConfig(existing.id, checked ? 'true' : 'false'); if (config && config.id && !config.id.startsWith('cfg_')) {
setConfigs(prev => prev.map(c => c.key === 'llm_media_as_base64' ? { ...c, value: checked ? 'true' : 'false' } : c)); 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 编码`); message.success(`${checked ? '开启' : '关闭'}文字模型媒体 base64 编码`);
} else { } catch (e: any) {
message.warning('系统配置项异常,请刷新页面重试'); message.error(e?.message || '操作失败');
} }
}; };
@@ -319,25 +333,82 @@ const AdminSettings: React.FC = () => {
return <Card loading variant="outlined" style={{ borderRadius: 12 }} />; return <Card loading variant="outlined" style={{ borderRadius: 12 }} />;
} }
return ( const tabItems = [
<div style={{ maxWidth: 720 }}> {
<Card variant="outlined" style={{ borderRadius: 12, border: '1px solid #f0f0f5', marginBottom: 16 }}> key: 'basic',
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 24 }}> label: '网站基础设置',
<div style={{ children: (
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"> <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>
))}
</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 }}> <div style={{ marginBottom: 24 }}>
<Typography.Text strong style={{ fontSize: 14, display: 'block', marginBottom: 12, paddingBottom: 8, borderBottom: '1px solid #f0f0f5' }}> <Typography.Text strong style={{ fontSize: 14, display: 'block', marginBottom: 12, paddingBottom: 8, borderBottom: '1px solid #f0f0f5' }}>
@@ -421,74 +492,30 @@ const AdminSettings: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
{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>
))}
<div style={{ marginBottom: 4 }}>
<Typography.Text strong style={{ fontSize: 14, display: 'block', marginBottom: 12, paddingBottom: 8, borderBottom: '1px solid #f0f0f5' }}>
</Typography.Text>
<div style={{ padding: 16, border: '1px solid #f0f0f5', borderRadius: 10, background: '#fafbfc' }}>
<Space align="start" style={{ marginBottom: 16 }}>
<DatabaseOutlined style={{ color: '#6366f1', fontSize: 18, marginTop: 2 }} />
<div>
<Typography.Text strong></Typography.Text>
<div style={{ color: '#64748b', fontSize: 13, marginTop: 4 }}>
</div>
</div>
</Space>
<Form.Item
name="resource_capacity_enabled"
label="启用全局容量管控"
valuePropName="checked"
extra="关闭时全局不限制;若用户设置了个人配置,则仍按用户个人配置优先判断。"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭" />
</Form.Item>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 180px', gap: 16 }}>
<Form.Item
name="resource_capacity_limit_value"
label="容量数值"
extra="最小为1,不能为负数,最多支持3位小数。"
rules={[{ required: true, message: '请输入容量数值' }]}
>
<InputNumber min={1} precision={3} style={{ width: '100%' }} size="large" placeholder="例如 10.500" />
</Form.Item>
<Form.Item
name="resource_capacity_limit_unit"
label="容量单位"
extra="MB / GB / TB 固定枚举"
rules={[{ required: true, message: '请选择容量单位' }]}
>
<Select size="large" options={capacityUnitOptions} />
</Form.Item>
</div>
</div>
</div>
</Form> </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> </Card>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}> <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
+28
View File
@@ -1593,6 +1593,34 @@ async def list_system_configs(
return result.scalars().all() return result.scalars().all()
@router.post("/system-configs", response_model=SystemConfigOut)
async def create_system_config(
req: SystemConfigCreate,
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
from app.utils.id_gen import generate_id
config = SystemConfig(
id=generate_id(),
key=req.key,
value=str(req.value),
description=req.description or "",
)
db.add(config)
await db.flush()
await log_operation(
db,
admin.id,
admin.username,
f"创建系统配置: {config.key}",
"POST",
"/admin/system-configs",
detail=json.dumps({"key": req.key, "value": req.value}, ensure_ascii=False),
)
await db.commit()
return config
@router.put("/system-configs/{config_id}", response_model=SystemConfigOut) @router.put("/system-configs/{config_id}", response_model=SystemConfigOut)
async def update_system_config( async def update_system_config(
config_id: str, config_id: str,
+6
View File
@@ -29,6 +29,12 @@ class ModelConfigOut(ModelConfigCreate):
model_config = {"from_attributes": True} model_config = {"from_attributes": True}
class SystemConfigCreate(BaseModel):
key: str
value: str
description: str | None = None
class SystemConfigUpdate(BaseModel): class SystemConfigUpdate(BaseModel):
value: str | int | float value: str | int | float