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>
|
||||||
<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>
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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' }}>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user