mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 03:10:50 +08:00
feat: add model list viewer for auth file cards
This commit is contained in:
@@ -315,7 +315,12 @@
|
||||
"type_iflow": "iFlow",
|
||||
"type_vertex": "Vertex",
|
||||
"type_empty": "空文件",
|
||||
"type_unknown": "其他"
|
||||
"type_unknown": "其他",
|
||||
"models_button": "模型",
|
||||
"models_title": "支持的模型",
|
||||
"models_loading": "正在加载模型列表...",
|
||||
"models_empty": "该凭证暂无可用模型",
|
||||
"models_empty_desc": "该认证凭证可能尚未被服务器加载或没有绑定任何模型"
|
||||
},
|
||||
"vertex_import": {
|
||||
"title": "Vertex AI 凭证导入",
|
||||
@@ -663,7 +668,7 @@
|
||||
"gemini_api_key": "Gemini API密钥",
|
||||
"codex_api_key": "Codex API密钥",
|
||||
"claude_api_key": "Claude API密钥",
|
||||
"link_copied": "链接已复制到剪贴板"
|
||||
"link_copied": "已复制"
|
||||
},
|
||||
"language": {
|
||||
"switch": "语言",
|
||||
|
||||
@@ -390,3 +390,59 @@
|
||||
text-align: center;
|
||||
padding: $spacing-lg;
|
||||
}
|
||||
|
||||
// 模型列表弹窗
|
||||
.modelsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-sm;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modelItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: $radius-md;
|
||||
border: 1px solid var(--border-color);
|
||||
flex-wrap: wrap;
|
||||
cursor: pointer;
|
||||
transition: all $transition-fast;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-hover);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.modelId {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.modelDisplayName {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modelType {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
background-color: var(--bg-tertiary);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { IconDownload, IconInfo, IconTrash2 } from '@/components/ui/icons';
|
||||
import { IconBot, IconDownload, IconInfo, IconTrash2 } from '@/components/ui/icons';
|
||||
import { useAuthStore, useNotificationStore, useThemeStore } from '@/stores';
|
||||
import { authFilesApi, usageApi } from '@/services/api';
|
||||
import { apiClient } from '@/services/api/client';
|
||||
@@ -140,6 +140,12 @@ export function AuthFilesPage() {
|
||||
const [detailModalOpen, setDetailModalOpen] = useState(false);
|
||||
const [selectedFile, setSelectedFile] = useState<AuthFileItem | null>(null);
|
||||
|
||||
// 模型列表弹窗相关
|
||||
const [modelsModalOpen, setModelsModalOpen] = useState(false);
|
||||
const [modelsLoading, setModelsLoading] = useState(false);
|
||||
const [modelsList, setModelsList] = useState<{ id: string; display_name?: string; type?: string }[]>([]);
|
||||
const [modelsFileName, setModelsFileName] = useState('');
|
||||
|
||||
// OAuth 排除模型相关
|
||||
const [excluded, setExcluded] = useState<Record<string, string[]>>({});
|
||||
const [excludedModalOpen, setExcludedModalOpen] = useState(false);
|
||||
@@ -406,6 +412,23 @@ export function AuthFilesPage() {
|
||||
setDetailModalOpen(true);
|
||||
};
|
||||
|
||||
// 显示模型列表
|
||||
const showModels = async (item: AuthFileItem) => {
|
||||
setModelsFileName(item.name);
|
||||
setModelsList([]);
|
||||
setModelsModalOpen(true);
|
||||
setModelsLoading(true);
|
||||
try {
|
||||
const models = await authFilesApi.getModelsForAuthFile(item.name);
|
||||
setModelsList(models);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '';
|
||||
showNotification(`${t('notification.load_failed')}: ${errorMessage}`, 'error');
|
||||
} finally {
|
||||
setModelsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取类型标签显示文本
|
||||
const getTypeLabel = (type: string): string => {
|
||||
const key = `auth_files.filter_${type}`;
|
||||
@@ -541,6 +564,16 @@ export function AuthFilesPage() {
|
||||
<span className={styles.virtualBadge}>{t('auth_files.type_virtual') || '虚拟认证文件'}</span>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => showModels(item)}
|
||||
className={styles.iconButton}
|
||||
title={t('auth_files.models_button', { defaultValue: '模型' })}
|
||||
disabled={disableControls}
|
||||
>
|
||||
<IconBot className={styles.actionIcon} size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@@ -768,6 +801,49 @@ export function AuthFilesPage() {
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* 模型列表弹窗 */}
|
||||
<Modal
|
||||
open={modelsModalOpen}
|
||||
onClose={() => setModelsModalOpen(false)}
|
||||
title={t('auth_files.models_title', { defaultValue: '支持的模型' }) + ` - ${modelsFileName}`}
|
||||
footer={
|
||||
<Button variant="secondary" onClick={() => setModelsModalOpen(false)}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{modelsLoading ? (
|
||||
<div className={styles.hint}>{t('auth_files.models_loading', { defaultValue: '正在加载模型列表...' })}</div>
|
||||
) : modelsList.length === 0 ? (
|
||||
<EmptyState
|
||||
title={t('auth_files.models_empty', { defaultValue: '该凭证暂无可用模型' })}
|
||||
description={t('auth_files.models_empty_desc', { defaultValue: '该认证凭证可能尚未被服务器加载或没有绑定任何模型' })}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.modelsList}>
|
||||
{modelsList.map((model) => (
|
||||
<div
|
||||
key={model.id}
|
||||
className={styles.modelItem}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(model.id);
|
||||
showNotification(t('notification.link_copied', { defaultValue: '已复制到剪贴板' }), 'success');
|
||||
}}
|
||||
title={t('common.copy', { defaultValue: '点击复制' })}
|
||||
>
|
||||
<span className={styles.modelId}>{model.id}</span>
|
||||
{model.display_name && model.display_name !== model.id && (
|
||||
<span className={styles.modelDisplayName}>{model.display_name}</span>
|
||||
)}
|
||||
{model.type && (
|
||||
<span className={styles.modelType}>{model.type}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* OAuth 排除弹窗 */}
|
||||
<Modal
|
||||
open={excludedModalOpen}
|
||||
|
||||
@@ -29,5 +29,11 @@ export const authFilesApi = {
|
||||
apiClient.patch('/oauth-excluded-models', { provider, models }),
|
||||
|
||||
deleteOauthExcludedEntry: (provider: string) =>
|
||||
apiClient.delete(`/oauth-excluded-models?provider=${encodeURIComponent(provider)}`)
|
||||
apiClient.delete(`/oauth-excluded-models?provider=${encodeURIComponent(provider)}`),
|
||||
|
||||
// 获取认证凭证支持的模型
|
||||
async getModelsForAuthFile(name: string): Promise<{ id: string; display_name?: string; type?: string; owned_by?: string }[]> {
|
||||
const data = await apiClient.get(`/auth-files/models?name=${encodeURIComponent(name)}`);
|
||||
return (data && Array.isArray(data['models'])) ? data['models'] : [];
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user