1
This commit is contained in:
Vendored
+7
-7
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -28,7 +28,7 @@
|
||||
}
|
||||
})();
|
||||
</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">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -224,6 +224,10 @@ export async function updateSystemConfig(id: string, value: string): Promise<voi
|
||||
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> {
|
||||
return api.get('/admin/resource-capacity/global');
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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';
|
||||
import {
|
||||
SettingOutlined, SaveOutlined, UploadOutlined, FilePdfOutlined, EyeOutlined, DatabaseOutlined, VideoCameraOutlined, RobotOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
createSystemConfig,
|
||||
getGlobalResourceCapacity,
|
||||
getSystemConfigs,
|
||||
saveGlobalResourceCapacity,
|
||||
@@ -40,6 +41,10 @@ const AdminSettings: React.FC = () => {
|
||||
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; });
|
||||
@@ -149,13 +154,22 @@ const AdminSettings: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleToggleBase64 = async (checked: boolean) => {
|
||||
const existing = configs.find(c => c.key === 'llm_media_as_base64');
|
||||
if (existing) {
|
||||
await updateSystemConfig(existing.id, checked ? 'true' : 'false');
|
||||
setConfigs(prev => prev.map(c => c.key === 'llm_media_as_base64' ? { ...c, value: checked ? 'true' : 'false' } : c));
|
||||
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 编码`);
|
||||
} else {
|
||||
message.warning('系统配置项异常,请刷新页面重试');
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -319,25 +333,82 @@ const AdminSettings: React.FC = () => {
|
||||
return <Card loading variant="outlined" style={{ borderRadius: 12 }} />;
|
||||
}
|
||||
|
||||
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">管理站点基础信息、协议文件和SEO配置</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
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>
|
||||
))}
|
||||
</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' }}>
|
||||
@@ -421,74 +492,30 @@ const AdminSettings: React.FC = () => {
|
||||
</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>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
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' }}>
|
||||
|
||||
@@ -1593,6 +1593,34 @@ async def list_system_configs(
|
||||
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)
|
||||
async def update_system_config(
|
||||
config_id: str,
|
||||
|
||||
@@ -29,6 +29,12 @@ class ModelConfigOut(ModelConfigCreate):
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class SystemConfigCreate(BaseModel):
|
||||
key: str
|
||||
value: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class SystemConfigUpdate(BaseModel):
|
||||
value: str | int | float
|
||||
|
||||
|
||||
Reference in New Issue
Block a user