feat: enhance metrics display in ProviderResourceTable and update localization keys

This commit is contained in:
LTbinglingfeng
2026-05-25 00:10:35 +08:00
Unverified
parent 34a050dffa
commit 1ceb7e15f5
6 changed files with 110 additions and 56 deletions
@@ -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;
}
}
@@ -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) => (
<span key={key} className={styles.metric}>
<span className={styles.metricLabel}>{label}</span>
<span className={styles.metricValue}>{value}</span>
</span>
);
const renderFlagTag = (key: string, label: string) => (
<span key={key} className={styles.flagTag}>
{label}
</span>
);
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 <div className={styles.metricsCell}>{items}</div>;
};
const renderStatus = (r: ProviderResource) => {
@@ -172,9 +187,7 @@ export function ProviderResourceTable({
<span className={styles.baseUrl}>{t('providersPage.status.none')}</span>
)}
</TableCell>
<TableCell>
<span className={styles.modelsCell}>{renderModelsSummary(resource)}</span>
</TableCell>
<TableCell>{renderModelsSummary(resource)}</TableCell>
<TableCell>{renderStatus(resource)}</TableCell>
<TableCell alignRight>
<div className={styles.actions}>
@@ -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);
}}
>
<IconEye size={14} />
<span>{t('providersPage.actions.view')}</span>
</button>
<button
type="button"
className={styles.iconBtn}
aria-label={t('providersPage.actions.edit')}
title={t('providersPage.actions.edit')}
disabled={disableMutations}
onClick={(e) => {
e.stopPropagation();
@@ -201,13 +215,13 @@ export function ProviderResourceTable({
}}
>
<IconPencil size={14} />
<span>{t('providersPage.actions.edit')}</span>
</button>
{isAmpcode ? (
<button
type="button"
className={`${styles.iconBtn} ${styles.iconBtnDanger}`}
aria-label={t('providersPage.actions.clear')}
title={t('providersPage.actions.clear')}
disabled={disableMutations || resource.flags.isPlaceholder}
onClick={(e) => {
e.stopPropagation();
@@ -215,13 +229,13 @@ export function ProviderResourceTable({
}}
>
<IconTrash2 size={14} />
<span>{t('providersPage.actions.clear')}</span>
</button>
) : (
<button
type="button"
className={`${styles.iconBtn} ${styles.iconBtnDanger}`}
aria-label={t('providersPage.actions.delete')}
title={t('providersPage.actions.delete')}
disabled={disableMutations}
onClick={(e) => {
e.stopPropagation();
@@ -229,7 +243,6 @@ export function ProviderResourceTable({
}}
>
<IconTrash2 size={14} />
<span>{t('providersPage.actions.delete')}</span>
</button>
)}
</div>
+6 -3
View File
@@ -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",
+6 -3
View File
@@ -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": "Резервный ключ не задан",
+6 -3
View File
@@ -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": "未设置兜底密钥",
+6 -3
View File
@@ -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": "未設定備援金鑰",