diff --git a/video-gen-admin/src/api/index.ts b/video-gen-admin/src/api/index.ts index c2b61185..8792901f 100644 --- a/video-gen-admin/src/api/index.ts +++ b/video-gen-admin/src/api/index.ts @@ -13,6 +13,7 @@ import type { VideoPromptSchemaConfigOut, VideoPromptSchemaConfigSavePayload, VideoPromptSchemaPreviewPayload, VideoPromptSchemaPreviewOut, VideoPromptSchemaExportOut, AdminCreditRecordListResponse, AdminCreditRecordQueryParams, + ResourceCapacityConfigOut, ResourceCapacityConfigPayload, AdminUserResourceCapacityOut, } from '../types'; // ── Auth ────────────────────────────────────────────────── @@ -149,6 +150,34 @@ export async function updateSystemConfig(id: string, value: string): Promise { + return api.get('/admin/resource-capacity/global'); +} + +export async function saveGlobalResourceCapacity(payload: ResourceCapacityConfigPayload): Promise { + return api.put('/admin/resource-capacity/global', { + enabled: payload.enabled, + limit_value: payload.limitValue ?? null, + limit_unit: payload.limitUnit ?? null, + }); +} + +export async function getUserResourceCapacity(userId: string): Promise { + return api.get(`/admin/users/${userId}/resource-capacity`); +} + +export async function saveUserResourceCapacity(userId: string, payload: ResourceCapacityConfigPayload): Promise { + return api.put(`/admin/users/${userId}/resource-capacity`, { + enabled: payload.enabled, + limit_value: payload.limitValue ?? null, + limit_unit: payload.limitUnit ?? null, + }); +} + +export async function deleteUserResourceCapacity(userId: string): Promise { + return api.delete(`/admin/users/${userId}/resource-capacity`); +} + export async function uploadPdf(file: File, configKey: string): Promise<{ url: string }> { const formData = new FormData(); formData.append('file', file); diff --git a/video-gen-admin/src/pages/AdminSettings.tsx b/video-gen-admin/src/pages/AdminSettings.tsx index 113b1a27..efe9e8d6 100644 --- a/video-gen-admin/src/pages/AdminSettings.tsx +++ b/video-gen-admin/src/pages/AdminSettings.tsx @@ -1,12 +1,25 @@ import React, { useEffect, useState } from 'react'; import { - Button, Card, Form, Input, message, Space, Switch, Typography, Upload, + Button, Card, Form, Input, InputNumber, message, Select, Space, Switch, Typography, Upload, } from 'antd'; import { - SettingOutlined, SaveOutlined, UploadOutlined, FilePdfOutlined, EyeOutlined, + SettingOutlined, SaveOutlined, UploadOutlined, FilePdfOutlined, EyeOutlined, DatabaseOutlined, } from '@ant-design/icons'; -import { getSystemConfigs, updateSystemConfig, uploadPdf, uploadLogo } from '../api'; -import type { SystemConfig } from '../types'; +import { + getGlobalResourceCapacity, + getSystemConfigs, + saveGlobalResourceCapacity, + updateSystemConfig, + uploadLogo, + uploadPdf, +} from '../api'; +import type { ResourceCapacityUnit, SystemConfig } from '../types'; + +const capacityUnitOptions: { value: ResourceCapacityUnit; label: string }[] = [ + { value: 'MB', label: 'MB(1024 × 1024 字节)' }, + { value: 'GB', label: 'GB(1024 × 1024 × 1024 字节)' }, + { value: 'TB', label: 'TB(1024 × 1024 × 1024 × 1024 字节)' }, +]; const AdminSettings: React.FC = () => { const [configs, setConfigs] = useState([]); @@ -14,7 +27,6 @@ const AdminSettings: React.FC = () => { const [saving, setSaving] = useState(false); const [uploading, setUploading] = useState(''); const [form] = Form.useForm(); - const [logoPreview, setLogoPreview] = useState(''); useEffect(() => { load(); @@ -22,12 +34,23 @@ const AdminSettings: React.FC = () => { const load = async () => { setLoading(true); - const data = await getSystemConfigs(); - setConfigs(data); - const formValues: Record = {}; - data.forEach(c => { formValues[c.key] = c.value; }); - form.setFieldsValue(formValues); - setLoading(false); + try { + const [data, capacity] = await Promise.all([ + getSystemConfigs(), + getGlobalResourceCapacity(), + ]); + setConfigs(data); + const formValues: Record = {}; + data.forEach(c => { formValues[c.key] = c.value; }); + formValues.resource_capacity_enabled = capacity.enabled; + formValues.resource_capacity_limit_value = capacity.limitValue || '1.000'; + formValues.resource_capacity_limit_unit = capacity.limitUnit || 'GB'; + form.setFieldsValue(formValues); + } catch (e: any) { + message.error(e?.message || '加载配置失败'); + } finally { + setLoading(false); + } }; const handleSave = async () => { @@ -36,17 +59,21 @@ const AdminSettings: React.FC = () => { setSaving(true); for (const config of configs) { const newVal = values[config.key]; - if (newVal !== undefined && newVal !== config.value) { - await updateSystemConfig(config.id, newVal ?? ''); + if (newVal !== undefined && String(newVal) !== config.value) { + await updateSystemConfig(config.id, String(newVal ?? '')); } } + await saveGlobalResourceCapacity({ + enabled: !!values.resource_capacity_enabled, + limitValue: String(values.resource_capacity_limit_value ?? '1.000'), + limitUnit: values.resource_capacity_limit_unit || 'GB', + }); message.success('系统配置已保存'); - const data = await getSystemConfigs(); - setConfigs(data); - setSaving(false); + await load(); } catch (e: any) { - setSaving(false); message.error(e?.message || '保存失败'); + } finally { + setSaving(false); } }; @@ -54,7 +81,6 @@ const AdminSettings: React.FC = () => { 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上传成功'); @@ -63,24 +89,22 @@ const AdminSettings: React.FC = () => { } finally { setUploading(''); } - return false; // prevent default upload + return false; }; const handleLogoUpload = async (file: File) => { setUploading('site_logo'); try { const res = await uploadLogo(file); - // Update local state setConfigs(prev => prev.map(c => c.key === 'site_logo' ? { ...c, value: res.url } : c)); form.setFieldsValue({ site_logo: res.url }); - setLogoPreview(res.url); message.success('Logo上传成功'); } catch { message.error('上传失败'); } finally { setUploading(''); } - return false; // prevent default upload + return false; }; const groupedConfigs: Record = { @@ -106,37 +130,11 @@ const AdminSettings: React.FC = () => { return descMap[config.key] || config.description || ''; }; - const getFieldComponent = (config: SystemConfig) => { - if (config.key === 'site_logo') { - return ; - } - if (config.key === 'seo_description') { - return ; - } - if (config.key === 'seo_keywords') { - return ; - } - if (config.key === 'user_login_credits_enabled') { - return ( -
- - - {config.value === 'true' ? '已启用' : '已禁用'} - -
- ); - } - if (config.key === 'user_register_credits' || config.key === 'user_login_credits') { - return ; - } - return ; - }; - const LogoUploadField: React.FC<{ config: SystemConfig }> = ({ config }) => { const hasLogo = config.value && config.value.startsWith('/uploads/'); const baseUrl = import.meta.env.VITE_API_BASE || 'http://localhost:8000'; const logoUrl = hasLogo ? `${baseUrl}${config.value}` : ''; - + const handleRemove = () => { setConfigs(prev => prev.map(c => c.key === 'site_logo' ? { ...c, value: '' } : c)); form.setFieldsValue({ site_logo: '' }); @@ -172,17 +170,17 @@ const AdminSettings: React.FC = () => { {hasLogo ? (
- Logo预览 建议尺寸:200x40px,支持 PNG、JPG 格式 @@ -200,6 +198,7 @@ const AdminSettings: React.FC = () => { const PdfUploadField: React.FC<{ config: SystemConfig }> = ({ config }) => { const label = config.key === 'user_agreement_url' ? '用户协议' : '隐私政策'; const hasFile = config.value && config.value.startsWith('/uploads/'); + const baseUrl = import.meta.env.VITE_API_BASE || 'http://localhost:8000'; return (
{ {hasFile && ( )} @@ -236,6 +235,32 @@ const AdminSettings: React.FC = () => { ); }; + const getFieldComponent = (config: SystemConfig) => { + if (config.key === 'site_logo') { + return ; + } + if (config.key === 'seo_description') { + return ; + } + if (config.key === 'seo_keywords') { + return ; + } + if (config.key === 'user_login_credits_enabled') { + return ( +
+ + + {config.value === 'true' ? '已启用' : '已禁用'} + +
+ ); + } + if (config.key === 'user_register_credits' || config.key === 'user_login_credits') { + return ; + } + return ; + }; + if (loading) { return ; } @@ -282,6 +307,49 @@ const AdminSettings: React.FC = () => { )}
))} + +
+ + 资源空间管控 + +
+ + +
+ 全局生成资源容量上限 +
+ 开启后会按用户当前有效资源占用量进行提交前拦截;用户个人配置存在时优先级高于全局配置。 +
+
+
+ + + +
+ + + + + { showSizeChanger: true, showTotal: (t) => `共 ${t} 个用户`, }} - scroll={{ x: 1000 }} + scroll={{ x: 1280 }} /> - {/* Adjust Credits Modal */} 调整积分 - {creditModal.user?.username}} open={creditModal.open} @@ -428,7 +565,77 @@ const AdminUsers: React.FC = () => { - {/* Create User Modal */} + 容量设置 - {capacityModal.user?.username}} + open={capacityModal.open} + confirmLoading={capacitySaving} + onOk={handleSaveCapacity} + onCancel={() => { setCapacityModal({ open: false, user: null, detail: null }); capacityForm.resetFields(); }} + okText="保存个人配置" cancelText="取消" width={560} + > + + + + 当前生效来源:{capacitySourceLabel(capacityModal.detail?.effective)} + {capacityModal.detail?.effective.hasUserConfig ? '(已单独设置)' : '(未单独设置)'} + + + 已用:{formatBytes(capacityModal.detail?.effective.usedBytes)}; + 总量:{formatBytes(capacityModal.detail?.effective.totalBytes)}; + 可用:{formatBytes(capacityModal.detail?.effective.availableBytes)} + + {capacityModal.detail?.effective.enabled && ( + + )} + + +
+ + + +
+ + + + +