diff --git a/src/features/providers/components/ProviderResourceTable.module.scss b/src/features/providers/components/ProviderResourceTable.module.scss index a056183..02fcc66 100644 --- a/src/features/providers/components/ProviderResourceTable.module.scss +++ b/src/features/providers/components/ProviderResourceTable.module.scss @@ -48,10 +48,48 @@ font-size: 11px; } -.modelsCell { - font-size: 12px; +.metricsCell { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; +} + +.metric { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: var(--radius-md); + border: 1px solid var(--border-color); + background: var(--muted-bg); + font-size: 11px; + line-height: 1.4; + white-space: nowrap; +} + +.metricLabel { color: var(--muted-foreground); - line-height: 1.5; +} + +.metricValue { + color: var(--text-primary); + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.flagTag { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: var(--radius-md); + border: 1px solid var(--primary-30); + background: var(--primary-10); + color: var(--primary-color); + font-size: 11px; + font-weight: 500; + line-height: 1.4; + white-space: nowrap; } .statusBadge { @@ -88,18 +126,15 @@ display: inline-flex; align-items: center; justify-content: center; - gap: 4px; - padding: 4px 8px; + width: 28px; height: 28px; + padding: 0; border: 1px solid transparent; border-radius: var(--radius-md); background: transparent; color: var(--text-secondary); cursor: pointer; - font-size: 12px; - font-weight: 500; transition: background-color $transition-fast, color $transition-fast; - white-space: nowrap; &:hover:not(:disabled) { background: var(--bg-tertiary); @@ -125,9 +160,3 @@ color: var(--destructive-color); } } - -@media (max-width: 768px) { - .iconBtn span:not(.icon) { - display: none; - } -} diff --git a/src/features/providers/components/ProviderResourceTable.tsx b/src/features/providers/components/ProviderResourceTable.tsx index 2d29cc2..c260844 100644 --- a/src/features/providers/components/ProviderResourceTable.tsx +++ b/src/features/providers/components/ProviderResourceTable.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { IconAlertTriangle, @@ -26,7 +27,7 @@ interface ProviderResourceTableProps { onDelete: (resource: ProviderResource) => void; } -const columnWidths = ['20%', '22%', '9%', '14%', '9%', '26%']; +const columnWidths = ['20%', '22%', '8%', '22%', '8%', '20%']; export function ProviderResourceTable({ resources, @@ -38,31 +39,45 @@ export function ProviderResourceTable({ }: ProviderResourceTableProps) { const { t } = useTranslation(); + const renderMetric = (key: string, label: string, value: number) => ( + + {label} + {value} + + ); + + const renderFlagTag = (key: string, label: string) => ( + + {label} + + ); + const renderModelsSummary = (r: ProviderResource) => { + const items: ReactNode[] = []; if (r.brand === 'openaiCompatibility') { - return t('providersPage.table.openaiModelSummary', { - models: r.modelCount, - keys: r.apiKeyEntryCount, - headers: r.headerCount, - }); + items.push( + renderMetric('models', t('providersPage.table.metrics.models'), r.modelCount), + renderMetric('keys', t('providersPage.table.metrics.keys'), r.apiKeyEntryCount), + renderMetric('headers', t('providersPage.table.metrics.headers'), r.headerCount), + ); + } else if (r.brand === 'ampcode') { + items.push( + renderMetric('mappings', t('providersPage.table.metrics.mappings'), r.modelCount), + renderMetric('keys', t('providersPage.table.metrics.keys'), r.apiKeyEntryCount), + ); + } else { + items.push( + renderMetric('models', t('providersPage.table.metrics.models'), r.modelCount), + renderMetric('headers', t('providersPage.table.metrics.headers'), r.headerCount), + ); + if (r.brand === 'codex' && r.flags.websockets) { + items.push(renderFlagTag('ws', t('providersPage.table.websocketsTag'))); + } + if (r.brand === 'claude' && r.flags.cloakEnabled) { + items.push(renderFlagTag('cloak', t('providersPage.table.cloakTag'))); + } } - if (r.brand === 'ampcode') { - return t('providersPage.table.ampcodeSummary', { - mappings: r.modelCount, - keys: r.apiKeyEntryCount, - }); - } - let label = t('providersPage.table.modelSummary', { - models: r.modelCount, - headers: r.headerCount, - }); - if (r.brand === 'codex' && r.flags.websockets) { - label += ` · ${t('providersPage.table.websocketsTag')}`; - } - if (r.brand === 'claude' && r.flags.cloakEnabled) { - label += ` · ${t('providersPage.table.cloakTag')}`; - } - return label; + return
{items}
; }; const renderStatus = (r: ProviderResource) => { @@ -172,9 +187,7 @@ export function ProviderResourceTable({ {t('providersPage.status.none')} )} - - {renderModelsSummary(resource)} - + {renderModelsSummary(resource)} {renderStatus(resource)}
@@ -182,18 +195,19 @@ export function ProviderResourceTable({ type="button" className={styles.iconBtn} aria-label={t('providersPage.actions.view')} + title={t('providersPage.actions.view')} onClick={(e) => { e.stopPropagation(); onView(resource); }} > - {t('providersPage.actions.view')} {isAmpcode ? ( ) : ( )}
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 24a2d2a..ae194cb 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1492,9 +1492,12 @@ "models": "Models/Headers", "status": "Status", "actions": "Actions", - "modelSummary": "{{models}} models · {{headers}} headers", - "openaiModelSummary": "{{models}} models · {{keys}} keys · {{headers}} headers", - "ampcodeSummary": "{{mappings}} mappings · {{keys}} key groups", + "metrics": { + "models": "Models", + "keys": "Keys", + "headers": "Headers", + "mappings": "Mappings" + }, "websocketsTag": "WebSockets", "cloakTag": "Cloak", "noFallbackKey": "No fallback key", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 9aba9d2..0c502f0 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -1489,9 +1489,12 @@ "models": "Модели/Заголовки", "status": "Статус", "actions": "Действия", - "modelSummary": "{{models}} моделей · {{headers}} заголовков", - "openaiModelSummary": "{{models}} моделей · {{keys}} ключей · {{headers}} заголовков", - "ampcodeSummary": "{{mappings}} сопоставлений · {{keys}} групп ключей", + "metrics": { + "models": "Модели", + "keys": "Ключи", + "headers": "Заголовки", + "mappings": "Сопоставления" + }, "websocketsTag": "WebSockets", "cloakTag": "Cloak", "noFallbackKey": "Резервный ключ не задан", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 86d63c8..354a728 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -1492,9 +1492,12 @@ "models": "模型/请求头", "status": "状态", "actions": "操作", - "modelSummary": "{{models}} 个模型 · {{headers}} 个请求头", - "openaiModelSummary": "{{models}} 个模型 · {{keys}} 个密钥 · {{headers}} 个请求头", - "ampcodeSummary": "{{mappings}} 个映射 · {{keys}} 个密钥组", + "metrics": { + "models": "模型", + "keys": "密钥", + "headers": "请求头", + "mappings": "映射" + }, "websocketsTag": "WebSockets", "cloakTag": "Cloak", "noFallbackKey": "未设置兜底密钥", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index ef57226..455dd8e 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -1518,9 +1518,12 @@ "models": "模型/請求標頭", "status": "狀態", "actions": "操作", - "modelSummary": "{{models}} 個模型 · {{headers}} 個請求標頭", - "openaiModelSummary": "{{models}} 個模型 · {{keys}} 個金鑰 · {{headers}} 個請求標頭", - "ampcodeSummary": "{{mappings}} 個映射 · {{keys}} 個金鑰群組", + "metrics": { + "models": "模型", + "keys": "金鑰", + "headers": "請求標頭", + "mappings": "映射" + }, "websocketsTag": "WebSockets", "cloakTag": "Cloak", "noFallbackKey": "未設定備援金鑰",