(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 ? '测试中...' : '连接测试'}
- {/* Test Result Popover */}
- {testResult && (
+ {/* Test Result Popover (only for the tested model) */}
+ {testResult && testResult.modelId === model.id && (
{testResult.success ? '✓ 成功' : '✗ 失败'}
diff --git a/frontend/src/pages/admin/Storage.tsx b/frontend/src/pages/admin/Storage.tsx
index e858932..8c5858e 100644
--- a/frontend/src/pages/admin/Storage.tsx
+++ b/frontend/src/pages/admin/Storage.tsx
@@ -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' && (
-
+
)}
@@ -846,7 +846,7 @@ const Storage: React.FC = () => {
@@ -906,7 +906,7 @@ const Storage: React.FC = () => {
- | {cfg.name} |
+ {cfg.name} |
@@ -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 ? '禁用' : '启用'}
@@ -988,13 +988,13 @@ const Storage: React.FC = () => {
+ 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' }}>上一页
- Page {page} / {Math.ceil(total / PAGE_SIZE)} ({total})
+ 第 {page} / {Math.ceil(total / PAGE_SIZE)} 页 ({total})
+ 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' }}>下一页
@@ -1078,7 +1078,7 @@ const Storage: React.FC = () => {
-
+
{(quotaOverview.tier_labels || {})[level] || `Level ${level}`}
@@ -1086,7 +1086,7 @@ const Storage: React.FC = () => {
- Default
+ 默认
@@ -1096,7 +1096,7 @@ const Storage: React.FC = () => {
- {(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)}` : '暂无用户'}
@@ -1118,7 +1118,7 @@ const Storage: React.FC = () => {
@@ -1184,7 +1184,7 @@ const Storage: React.FC = () => {
|
- {u.username}
+ {u.username}
{u.email || '-'}
@@ -1246,7 +1246,7 @@ const Storage: React.FC = () => {
color: u.quota_type === 'custom' ? '#F59E0B' : '#6C63FF',
- }}>{u.quota_type === 'custom' ? 'Custom' : 'Plan'}
+ }}>{u.quota_type === 'custom' ? '自定义' : '套餐'}
|
@@ -1284,13 +1284,13 @@ const Storage: React.FC = () => {
+ 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' }}>上一页
第 {quotaPage} / {Math.ceil(quotaTotal / PAGE_SIZE)} 页 ({quotaTotal})
+ 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' }}>下一页
@@ -1330,7 +1330,7 @@ const Storage: React.FC = () => {
}}>
-
+
{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' }} />
@@ -1356,7 +1356,7 @@ const Storage: React.FC = () => {