Round6: 前台图片缩略图+下载/视频修复; 后台连接测试单次+自动消失; 导航重组; 存储中文化; 自动化去掉创建按钮; 短信验证码(注册/重置/充值通知)

This commit is contained in:
QihuAI Design committed 2026-07-14 18:49:27 +08:00
1 parent ba09017884
commit d179cc2f02
14 files changed
+806 -149

No files matched your search

+3 -21
View File
@@ -136,25 +136,7 @@ const Automation: React.FC = () => {
useEffect(() => { loadStats(); }, []);
useEffect(() => { loadTasks(); }, [page, filterTrigger, keyword]);
// Open create modal
const openCreateModal = () => {
setEditingTask(null);
setFormData({
name: '',
description: '',
trigger_type: 'cron',
cron_expression: '*/5 * * * *',
event_condition_str: '',
action_type: 'ai_pipeline',
action_config_str: '{"pipeline_name": "default"}',
is_enabled: true,
max_retries: 3,
timeout_seconds: 300,
});
setModalVisible(true);
};
// Open edit modal
// Open edit modal (tasks are created by AI in chat; admin only edits/enables existing ones)
const openEditModal = async (task: AutomationTask) => {
try {
const result = await adminAPI.getAdminAutomationTask(task.id) as any;
@@ -311,7 +293,7 @@ const Automation: React.FC = () => {
<div className="admin-page">
<div className="admin-page-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "24px", flexWrap: "wrap", gap: "12px" }}>
<h2>自动化任务调度</h2>
<button className="admin-btn admin-btn-primary" onClick={openCreateModal}>+ 创建任务</button>
<span className="admin-page-hint">任务由 AI 在聊天中自动创建,此处仅支持查看、编辑与启停</span>
</div>
{/* Stats Cards */}
@@ -445,7 +427,7 @@ const Automation: React.FC = () => {
{modalVisible && (
<div className="modal-overlay" style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
<div style={{ background: '#fff', borderRadius: '12px', padding: '24px', width: '560px', maxHeight: '90vh', overflow: 'auto', boxShadow: '0 20px 60px rgba(0,0,0,0.2)' }}>
<h3 style={{ margin: '0 0 20px', fontSize: '18px' }}>{editingTask ? '编辑任务' : '创建自动化任务'}</h3>
<h3 style={{ margin: '0 0 20px', fontSize: '18px' }}>编辑任务</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '14px' }}>
<div><label style={labelStyle}>任务名称 *</label>
<input type="text" value={formData.name} onChange={e => setFormData({ ...formData, name: e.target.value })} style={inputStyle} placeholder="如: 每日报表生成" /></div>
+17 -6
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { adminAPI } from '@/api';
import type { ModelProvider, AIModel, ProtocolDef, FetchableModel } from '@/types';
@@ -28,6 +28,7 @@ const capabilityLabels: Record<string, string> = {
// Test result type
interface TestResult {
modelId: string;
success: boolean;
message: string;
latency_ms?: number;
@@ -70,6 +71,12 @@ const Models: React.FC = () => {
// Model test state
const [testingModelId, setTestingModelId] = useState<string | null>(null);
const [testResult, setTestResult] = useState<TestResult | null>(null);
const testTimer = useRef<number | null>(null);
// 测试反馈自动消失(6 秒后)
useEffect(() => {
return () => { if (testTimer.current) clearTimeout(testTimer.current); };
}, []);
// Model editor state
const [showModelEditor, setShowModelEditor] = useState(false);
@@ -118,17 +125,21 @@ const Models: React.FC = () => {
useEffect(() => { loadData(); }, [loadData]);
// Test model connection
// Test model connection (single model only)
const handleTestConnection = async (modelId: string) => {
setTestingModelId(modelId);
setTestResult(null);
if (testTimer.current) { clearTimeout(testTimer.current); testTimer.current = null; }
try {
const result = await adminAPI.testModelConnection(modelId) as any;
setTestResult(result.success !== undefined ? result : (result.data || result as TestResult));
const r = result.success !== undefined ? result : (result.data || result);
setTestResult({ modelId, success: !!r.success, message: r.message || '', latency_ms: r.latency_ms });
} catch (err: any) {
setTestResult({ success: false, message: err.message || '测试失败' });
setTestResult({ modelId, success: false, message: err.message || '测试失败' });
} finally {
setTestingModelId(null);
// 6 秒后自动收起反馈
testTimer.current = window.setTimeout(() => setTestResult(null), 6000);
}
};
@@ -458,8 +469,8 @@ const Models: React.FC = () => {
style={{ ...btnStyle(testingModelId === model.id ? '#9CA3AF' : '#059669', 'small') }}>
{testingModelId === model.id ? '测试中...' : '连接测试'}
</button>
{/* Test Result Popover */}
{testResult && (
{/* Test Result Popover (only for the tested model) */}
{testResult && testResult.modelId === model.id && (
<div style={{ position: 'relative' }}>
<span style={{ fontSize: '11px', padding: '4px 8px', background: testResult.success ? '#DCFCE7' : '#FEE2E2', color: testResult.success ? '#16A34A' : '#DC2626', borderRadius: '4px', display: 'inline-block' }}>
{testResult.success ? '✓ 成功' : '✗ 失败'}
+62 -62
View File
@@ -358,7 +358,7 @@ const Storage: React.FC = () => {
} catch (e: any) {
showToast('Load failed: ' + (e.message || e), 'error');
showToast('加载失败:' + (e.message || e), 'error');
} finally {
@@ -412,7 +412,7 @@ const Storage: React.FC = () => {
setModalVisible(true);
} catch (e: any) { showToast('Failed to get config detail', 'error'); }
} catch (e: any) { showToast('获取配置详情失败', 'error'); }
};
@@ -428,7 +428,7 @@ const Storage: React.FC = () => {
if (!editingConfig && (!formData.access_key.trim() || !formData.secret_key.trim())) {
showToast('Access Key and Secret Key required', 'error'); return;
showToast('请填写 Access Key 和 Secret Key', 'error'); return;
}
@@ -442,7 +442,7 @@ const Storage: React.FC = () => {
try { configObj = JSON.parse(formData.config_str); }
catch { showToast('Invalid JSON config', 'error'); setSubmitting(false); return; }
catch { showToast('JSON 配置格式无效', 'error'); setSubmitting(false); return; }
}
@@ -460,13 +460,13 @@ const Storage: React.FC = () => {
if (formData.secret_key.trim()) data.secret_key = formData.secret_key.trim();
if (editingConfig) { await adminAPI.updateAdminStorageConfig(editingConfig.id, data); showToast('Updated'); }
if (editingConfig) { await adminAPI.updateAdminStorageConfig(editingConfig.id, data); showToast('已更新'); }
else { await adminAPI.createAdminStorageConfig(data); showToast('Created'); }
else { await adminAPI.createAdminStorageConfig(data); showToast('已创建'); }
setModalVisible(false); loadConfigs(); loadStats();
} catch (e: any) { showToast('Failed: ' + (e.message || e), 'error'); }
} catch (e: any) { showToast('失败:' + (e.message || e), 'error'); }
finally { setSubmitting(false); }
@@ -476,7 +476,7 @@ const Storage: React.FC = () => {
const handleDelete = async (config: OSSConfig) => {
if (!window.confirm(`Delete "${config.name}"?`)) return;
if (!window.confirm(`确认删除"${config.name}"吗?`)) return;
try { await adminAPI.deleteAdminStorageConfig(config.id); showToast('已删除'); loadConfigs(); loadStats(); }
@@ -490,7 +490,7 @@ const Storage: React.FC = () => {
try { await adminAPI.toggleAdminStorageConfig(config.id); showToast(config.is_enabled ? '已禁用' : '已启用'); loadConfigs(); loadStats(); }
catch (e: any) { showToast('Failed', 'error'); }
catch (e: any) { showToast('失败', 'error'); }
};
@@ -500,9 +500,9 @@ const Storage: React.FC = () => {
if (config.is_default) return;
try { await adminAPI.setDefaultAdminStorageConfig(config.id); showToast(`Set "${config.name}" as default`); loadConfigs(); loadStats(); }
try { await adminAPI.setDefaultAdminStorageConfig(config.id); showToast(`已将"${config.name}"设为默认`); loadConfigs(); loadStats(); }
catch (e: any) { showToast('Failed', 'error'); }
catch (e: any) { showToast('失败', 'error'); }
};
@@ -518,7 +518,7 @@ const Storage: React.FC = () => {
const data = result.data;
showToast(data.success ? 'Connection OK' : 'Connection failed', data.success ? 'success' : 'error');
showToast(data.success ? '连接成功' : '连接失败', data.success ? 'success' : 'error');
loadConfigs(); loadStats();
@@ -570,7 +570,7 @@ const Storage: React.FC = () => {
setQuotaTotal(data.total || 0);
} catch (e: any) { showToast('Load failed', 'error'); }
} catch (e: any) { showToast('加载失败', 'error'); }
finally { setQuotaLoading(false); }
@@ -592,11 +592,11 @@ const Storage: React.FC = () => {
const result = await adminAPI.initAllStorageQuotas() as any;
showToast(result.message || 'Init complete');
showToast(result.message || '初始化完成');
loadQuotaOverview(); loadQuotaUsers();
} catch (e: any) { showToast('Init failed', 'error'); }
} catch (e: any) { showToast('初始化失败', 'error'); }
};
@@ -612,7 +612,7 @@ const Storage: React.FC = () => {
loadQuotaUsers();
} catch (e: any) { showToast('Recalc failed', 'error'); }
} catch (e: any) { showToast('重新计算失败', 'error'); }
};
@@ -626,7 +626,7 @@ const Storage: React.FC = () => {
setQuotaDetail(result.data || null);
} catch (e: any) { showToast('Failed to load detail', 'error'); }
} catch (e: any) { showToast('加载详情失败', 'error'); }
};
@@ -662,7 +662,7 @@ const Storage: React.FC = () => {
const numVal = parseFloat(adjustValue);
if (isNaN(numVal) || numVal < 0) { showToast('Invalid value', 'error'); return; }
if (isNaN(numVal) || numVal < 0) { showToast('无效的数值', 'error'); return; }
const bytes = adjustUnit === 'GB' ? Math.round(numVal * 1024 * 1024 * 1024) : Math.round(numVal * 1024 * 1024);
@@ -672,13 +672,13 @@ const Storage: React.FC = () => {
await adminAPI.adjustStorageQuota(adjustModal.user_id, { quota_bytes: bytes, quota_type: 'custom' });
showToast('Quota adjusted');
showToast('配额已调整');
setAdjustModal(null);
loadQuotaUsers(); loadQuotaOverview();
} catch (e: any) { showToast('Adjust failed', 'error'); }
} catch (e: any) { showToast('调整失败', 'error'); }
finally { setAdjusting(false); }
@@ -702,7 +702,7 @@ const Storage: React.FC = () => {
{activeTab === 'oss' && (
<button className="admin-btn admin-btn-primary" onClick={openCreateModal}>+ Add Storage</button>
<button className="admin-btn admin-btn-primary" onClick={openCreateModal}>+ 新增存储</button>
)}
@@ -846,7 +846,7 @@ const Storage: React.FC = () => {
<select value={filterProvider} onChange={(e) => { setFilterProvider(e.target.value); setPage(1); }}
style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#F8F9FC', fontSize: '14px' }}>
style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#1F2937', fontSize: '14px' }}>
<option value="">全部类型</option>
@@ -858,7 +858,7 @@ const Storage: React.FC = () => {
onChange={(e) => { setKeyword(e.target.value); setPage(1); }}
style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#F8F9FC', fontSize: '14px', flex: 1, maxWidth: '400px' }} />
style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#1F2937', fontSize: '14px', flex: 1, maxWidth: '400px' }} />
</div>
@@ -906,7 +906,7 @@ const Storage: React.FC = () => {
<tr key={cfg.id} style={{ borderBottom: '1px solid rgba(0,0,0,0.06)' }}>
<td style={{ padding: '12px 16px', fontWeight: 500, color: '#F8F9FC' }}>{cfg.name}</td>
<td style={{ padding: '12px 16px', fontWeight: 500, color: '#1F2937' }}>{cfg.name}</td>
<td style={{ padding: '12px 16px' }}>
@@ -956,7 +956,7 @@ const Storage: React.FC = () => {
style={{ padding: '4px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: 'transparent', cursor: 'pointer', fontSize: '12px', color: cfg.is_enabled ? '#EF4444' : '#10B981' }}>
{cfg.is_enabled ? 'Disable' : 'Enable'}
{cfg.is_enabled ? '禁用' : '启用'}
</button>
@@ -988,13 +988,13 @@ const Storage: React.FC = () => {
<button disabled={page <= 1} onClick={() => setPage(p => p - 1)}
style={{ padding: '6px 16px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: 'transparent', cursor: page <= 1 ? 'not-allowed' : 'pointer', opacity: page <= 1 ? 0.5 : 1, color: '#F8F9FC' }}>上一页</button>
style={{ padding: '6px 16px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: 'transparent', cursor: page <= 1 ? 'not-allowed' : 'pointer', opacity: page <= 1 ? 0.5 : 1, color: '#1F2937' }}>上一页</button>
<span style={{ padding: '6px 12px', color: 'rgba(0,0,0,0.5)', fontSize: '14px' }}>Page {page} / {Math.ceil(total / PAGE_SIZE)} ({total})</span>
<span style={{ padding: '6px 12px', color: 'rgba(0,0,0,0.5)', fontSize: '14px' }}>第 {page} / {Math.ceil(total / PAGE_SIZE)} 页 ({total})</span>
<button disabled={page >= Math.ceil(total / PAGE_SIZE)} onClick={() => setPage(p => p + 1)}
style={{ padding: '6px 16px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: 'transparent', cursor: page >= Math.ceil(total / PAGE_SIZE) ? 'not-allowed' : 'pointer', opacity: page >= Math.ceil(total / PAGE_SIZE) ? 0.5 : 1, color: '#F8F9FC' }}>下一页</button>
style={{ padding: '6px 16px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: 'transparent', cursor: page >= Math.ceil(total / PAGE_SIZE) ? 'not-allowed' : 'pointer', opacity: page >= Math.ceil(total / PAGE_SIZE) ? 0.5 : 1, color: '#1F2937' }}>下一页</button>
</div>
@@ -1078,7 +1078,7 @@ const Storage: React.FC = () => {
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
<span style={{ fontSize: '14px', fontWeight: 600, color: '#F8F9FC' }}>
<span style={{ fontSize: '14px', fontWeight: 600, color: '#1F2937' }}>
{(quotaOverview.tier_labels || {})[level] || `Level ${level}`}
@@ -1086,7 +1086,7 @@ const Storage: React.FC = () => {
<span style={{ padding: '2px 10px', borderRadius: '12px', fontSize: '12px', background: 'rgba(108,99,255,0.15)', color: '#6C63FF' }}>
Default
默认
</span>
@@ -1096,7 +1096,7 @@ const Storage: React.FC = () => {
<div style={{ fontSize: '12px', color: 'rgba(0,0,0,0.4)' }}>
{(quotaOverview.by_level || {})[level] ? `${quotaOverview.by_level[level].user_count} users | Used: ${formatBytes(quotaOverview.by_level[level].total_used_bytes)}` : 'No users'}
{(quotaOverview.by_level || {})[level] ? `${quotaOverview.by_level[level].user_count} 个用户 | 已用:${formatBytes(quotaOverview.by_level[level].total_used_bytes)}` : '暂无用户'}
</div>
@@ -1118,7 +1118,7 @@ const Storage: React.FC = () => {
<select value={quotaLevelFilter} onChange={(e) => { setQuotaLevelFilter(e.target.value === '' ? '' : parseInt(e.target.value)); setQuotaPage(1); }}
style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#F8F9FC', fontSize: '14px' }}>
style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#1F2937', fontSize: '14px' }}>
<option value="">全部等级</option>
@@ -1134,7 +1134,7 @@ const Storage: React.FC = () => {
onChange={(e) => { setQuotaKeyword(e.target.value); setQuotaPage(1); }}
style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#F8F9FC', fontSize: '14px', flex: 1, maxWidth: '400px' }} />
style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#1F2937', fontSize: '14px', flex: 1, maxWidth: '400px' }} />
</div>
@@ -1184,7 +1184,7 @@ const Storage: React.FC = () => {
<td style={{ padding: '12px 16px' }}>
<div style={{ fontWeight: 500, color: '#F8F9FC' }}>{u.username}</div>
<div style={{ fontWeight: 500, color: '#1F2937' }}>{u.username}</div>
<div style={{ fontSize: '12px', color: 'rgba(0,0,0,0.4)' }}>{u.email || '-'}</div>
@@ -1246,7 +1246,7 @@ const Storage: React.FC = () => {
color: u.quota_type === 'custom' ? '#F59E0B' : '#6C63FF',
}}>{u.quota_type === 'custom' ? 'Custom' : 'Plan'}</span>
}}>{u.quota_type === 'custom' ? '自定义' : '套餐'}</span>
</td>
@@ -1284,13 +1284,13 @@ const Storage: React.FC = () => {
<button disabled={quotaPage <= 1} onClick={() => setQuotaPage(p => p - 1)}
style={{ padding: '6px 16px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: 'transparent', cursor: quotaPage <= 1 ? 'not-allowed' : 'pointer', opacity: quotaPage <= 1 ? 0.5 : 1, color: '#F8F9FC' }}>上一页</button>
style={{ padding: '6px 16px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: 'transparent', cursor: quotaPage <= 1 ? 'not-allowed' : 'pointer', opacity: quotaPage <= 1 ? 0.5 : 1, color: '#1F2937' }}>上一页</button>
<span style={{ padding: '6px 12px', color: 'rgba(0,0,0,0.5)', fontSize: '14px' }}>第 {quotaPage} / {Math.ceil(quotaTotal / PAGE_SIZE)} 页 ({quotaTotal})</span>
<button disabled={quotaPage >= Math.ceil(quotaTotal / PAGE_SIZE)} onClick={() => setQuotaPage(p => p + 1)}
style={{ padding: '6px 16px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: 'transparent', cursor: quotaPage >= Math.ceil(quotaTotal / PAGE_SIZE) ? 'not-allowed' : 'pointer', opacity: quotaPage >= Math.ceil(quotaTotal / PAGE_SIZE) ? 0.5 : 1, color: '#F8F9FC' }}>下一页</button>
style={{ padding: '6px 16px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: 'transparent', cursor: quotaPage >= Math.ceil(quotaTotal / PAGE_SIZE) ? 'not-allowed' : 'pointer', opacity: quotaPage >= Math.ceil(quotaTotal / PAGE_SIZE) ? 0.5 : 1, color: '#1F2937' }}>下一页</button>
</div>
@@ -1330,7 +1330,7 @@ const Storage: React.FC = () => {
}}>
<h3 style={{ margin: '0 0 20px', fontSize: '18px', color: '#F8F9FC' }}>
<h3 style={{ margin: '0 0 20px', fontSize: '18px', color: '#1F2937' }}>
{editingConfig ? '编辑存储配置' : '新建存储配置'}
@@ -1346,7 +1346,7 @@ const Storage: React.FC = () => {
placeholder="如:生产环境 MinIO"
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#F8F9FC', fontSize: '14px', boxSizing: 'border-box' }} />
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#1F2937', fontSize: '14px', boxSizing: 'border-box' }} />
</div>
@@ -1356,7 +1356,7 @@ const Storage: React.FC = () => {
<select value={formData.provider} onChange={(e) => setFormData({ ...formData, provider: e.target.value })}
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#F8F9FC', fontSize: '14px', boxSizing: 'border-box' }}>
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#1F2937', fontSize: '14px', boxSizing: 'border-box' }}>
<option value="minio">MinIO</option>
@@ -1376,7 +1376,7 @@ const Storage: React.FC = () => {
placeholder={formData.provider === 'minio' ? 'http://localhost:9000' : 'https://oss-cn-hangzhou.aliyuncs.com'}
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#F8F9FC', fontSize: '14px', boxSizing: 'border-box' }} />
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#1F2937', fontSize: '14px', boxSizing: 'border-box' }} />
</div>
@@ -1388,9 +1388,9 @@ const Storage: React.FC = () => {
<input type="text" value={formData.access_key} onChange={(e) => setFormData({ ...formData, access_key: e.target.value })}
placeholder={editingConfig ? 'Leave empty to keep' : 'Enter Access Key'}
placeholder={editingConfig ? '留空则保持不变' : '请输入 Access Key'}
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#F8F9FC', fontSize: '14px', boxSizing: 'border-box' }} />
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#1F2937', fontSize: '14px', boxSizing: 'border-box' }} />
</div>
@@ -1400,9 +1400,9 @@ const Storage: React.FC = () => {
<input type="password" value={formData.secret_key} onChange={(e) => setFormData({ ...formData, secret_key: e.target.value })}
placeholder={editingConfig ? 'Leave empty to keep' : 'Enter Secret Key'}
placeholder={editingConfig ? '留空则保持不变' : '请输入 Secret Key'}
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#F8F9FC', fontSize: '14px', boxSizing: 'border-box' }} />
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#1F2937', fontSize: '14px', boxSizing: 'border-box' }} />
</div>
@@ -1418,7 +1418,7 @@ const Storage: React.FC = () => {
placeholder="存储桶名称"
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#F8F9FC', fontSize: '14px', boxSizing: 'border-box' }} />
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#1F2937', fontSize: '14px', boxSizing: 'border-box' }} />
</div>
@@ -1430,7 +1430,7 @@ const Storage: React.FC = () => {
placeholder="区域,如 cn-north-1"
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#F8F9FC', fontSize: '14px', boxSizing: 'border-box' }} />
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#1F2937', fontSize: '14px', boxSizing: 'border-box' }} />
</div>
@@ -1444,7 +1444,7 @@ const Storage: React.FC = () => {
placeholder="上传路径,如 aipaaS/uploads/"
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#F8F9FC', fontSize: '14px', boxSizing: 'border-box' }} />
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#1F2937', fontSize: '14px', boxSizing: 'border-box' }} />
</div>
@@ -1452,13 +1452,13 @@ const Storage: React.FC = () => {
<label style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '14px', cursor: 'pointer', color: 'rgba(0,0,0,0.7)' }}>
<input type="checkbox" checked={formData.is_default} onChange={(e) => setFormData({ ...formData, is_default: e.target.checked })} /> Set as default
<input type="checkbox" checked={formData.is_default} onChange={(e) => setFormData({ ...formData, is_default: e.target.checked })} /> 设为默认
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '14px', cursor: 'pointer', color: 'rgba(0,0,0,0.7)' }}>
<input type="checkbox" checked={formData.is_enabled} onChange={(e) => setFormData({ ...formData, is_enabled: e.target.checked })} /> Enabled
<input type="checkbox" checked={formData.is_enabled} onChange={(e) => setFormData({ ...formData, is_enabled: e.target.checked })} /> 启用
</label>
@@ -1466,13 +1466,13 @@ const Storage: React.FC = () => {
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '13px', color: 'rgba(0,0,0,0.7)', fontWeight: 500 }}>Extra Config (JSON, optional)</label>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '13px', color: 'rgba(0,0,0,0.7)', fontWeight: 500 }}>额外配置 (JSON,可选)</label>
<textarea value={formData.config_str} onChange={(e) => setFormData({ ...formData, config_str: e.target.value })}
placeholder='{"use_ssl": true, "timeout": 30}' rows={3}
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#F8F9FC', fontSize: '13px', fontFamily: 'monospace', boxSizing: 'border-box', resize: 'vertical' }} />
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#1F2937', fontSize: '13px', fontFamily: 'monospace', boxSizing: 'border-box', resize: 'vertical' }} />
</div>
@@ -1528,7 +1528,7 @@ const Storage: React.FC = () => {
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h3 style={{ margin: 0, fontSize: '18px', color: '#F8F9FC' }}>存储详情</h3>
<h3 style={{ margin: 0, fontSize: '18px', color: '#1F2937' }}>存储详情</h3>
<button onClick={() => setQuotaDetail(null)} style={{ background: 'transparent', border: 'none', color: 'rgba(0,0,0,0.5)', cursor: 'pointer', fontSize: '20px' }}>x</button>
@@ -1538,7 +1538,7 @@ const Storage: React.FC = () => {
<div style={{ marginBottom: '20px' }}>
<div style={{ fontSize: '20px', fontWeight: 700, color: '#F8F9FC' }}>{quotaDetail.user.username}</div>
<div style={{ fontSize: '20px', fontWeight: 700, color: '#1F2937' }}>{quotaDetail.user.username}</div>
<div style={{ fontSize: '13px', color: 'rgba(0,0,0,0.4)' }}>{quotaDetail.user.email || '-'}</div>
@@ -1558,7 +1558,7 @@ const Storage: React.FC = () => {
<span style={{ fontSize: '13px', color: 'rgba(0,0,0,0.5)' }}>使用量</span>
<span style={{ fontSize: '13px', color: '#fff', fontFamily: 'monospace' }}>
<span style={{ fontSize: '13px', color: '#1F2937', fontFamily: 'monospace' }}>
{formatBytes(quotaDetail.quota.used_bytes)} / {formatBytes(quotaDetail.quota.quota_bytes)}
@@ -1598,7 +1598,7 @@ const Storage: React.FC = () => {
<div style={{ fontSize: '12px', color: 'rgba(0,0,0,0.4)', marginBottom: '4px' }}>文件数</div>
<div style={{ fontSize: '20px', fontWeight: 700, color: '#fff' }}>{quotaDetail.file_count}</div>
<div style={{ fontSize: '20px', fontWeight: 700, color: '#1F2937' }}>{quotaDetail.file_count}</div>
</div>
@@ -1618,7 +1618,7 @@ const Storage: React.FC = () => {
<div>
<div style={{ fontSize: '13px', color: 'rgba(0,0,0,0.5)', marginBottom: '8px', fontWeight: 600 }}>By File Type</div>
<div style={{ fontSize: '13px', color: 'rgba(0,0,0,0.5)', marginBottom: '8px', fontWeight: 600 }}>按文件类型</div>
{Object.entries(quotaDetail.file_type_stats).map(([ftype, stat]) => (
@@ -1626,7 +1626,7 @@ const Storage: React.FC = () => {
<span style={{ fontSize: '13px', color: 'rgba(0,0,0,0.7)' }}>{ftype}</span>
<span style={{ fontSize: '13px', color: 'rgba(0,0,0,0.5)', fontFamily: 'monospace' }}>{stat.count} files / {formatBytes(stat.total_bytes)}</span>
<span style={{ fontSize: '13px', color: 'rgba(0,0,0,0.5)', fontFamily: 'monospace' }}>{stat.count} 个文件 / {formatBytes(stat.total_bytes)}</span>
</div>
@@ -1680,11 +1680,11 @@ const Storage: React.FC = () => {
}}>
<h3 style={{ margin: '0 0 20px', fontSize: '18px', color: '#F8F9FC' }}>调整配额</h3>
<h3 style={{ margin: '0 0 20px', fontSize: '18px', color: '#1F2937' }}>调整配额</h3>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '16px', fontWeight: 600, color: '#fff' }}>{adjustModal.username}</div>
<div style={{ fontSize: '16px', fontWeight: 600, color: '#1F2937' }}>{adjustModal.username}</div>
<div style={{ fontSize: '12px', color: 'rgba(0,0,0,0.4)' }}>
@@ -1704,11 +1704,11 @@ const Storage: React.FC = () => {
step="0.1" min="0"
style={{ flex: 1, padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#F8F9FC', fontSize: '14px' }} />
style={{ flex: 1, padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#1F2937', fontSize: '14px' }} />
<select value={adjustUnit} onChange={(e) => setAdjustUnit(e.target.value as 'GB' | 'MB')}
style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#F8F9FC', fontSize: '14px' }}>
style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.10)', background: '#FFFFFF', color: '#1F2937', fontSize: '14px' }}>
<option value="GB">GB</option>