diff --git a/src/pages/AuthFilesPage.module.scss b/src/pages/AuthFilesPage.module.scss index 6f09678..f7d9ab7 100644 --- a/src/pages/AuthFilesPage.module.scss +++ b/src/pages/AuthFilesPage.module.scss @@ -250,6 +250,78 @@ border-color: var(--failure-badge-border, #fca5a5); } +// 状态监测栏 +.statusBar { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; + max-width: 280px; +} + +.statusBlocks { + display: flex; + gap: 2px; + flex: 1; + min-width: 180px; +} + +.statusBlock { + flex: 1; + height: 8px; + border-radius: 2px; + min-width: 6px; + transition: transform 0.15s ease, opacity 0.15s ease; + + &:hover { + transform: scaleY(1.5); + opacity: 0.85; + } +} + +.statusBlockSuccess { + background-color: var(--success-color, #22c55e); +} + +.statusBlockFailure { + background-color: var(--danger-color, #ef4444); +} + +.statusBlockMixed { + background-color: var(--warning-color, #f59e0b); +} + +.statusBlockIdle { + background-color: var(--border-secondary, #e5e7eb); +} + +.statusRate { + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + font-weight: 600; + white-space: nowrap; + padding: 4px 8px; + border-radius: 6px; + background: var(--bg-tertiary); +} + +.statusRateHigh { + color: var(--success-badge-text, #065f46); + background: var(--success-badge-bg, #d1fae5); +} + +.statusRateMedium { + color: var(--warning-text, #92400e); + background: var(--warning-bg, #fef3c7); +} + +.statusRateLow { + color: var(--failure-badge-text, #991b1b); + background: var(--failure-badge-bg, #fee2e2); +} + .cardActions { display: flex; gap: $spacing-xs; diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index 232aee1..87f8263 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -11,7 +11,8 @@ import { useAuthStore, useNotificationStore, useThemeStore } from '@/stores'; import { authFilesApi, usageApi } from '@/services/api'; import { apiClient } from '@/services/api/client'; import type { AuthFileItem } from '@/types'; -import type { KeyStats, KeyStatBucket } from '@/utils/usage'; +import type { KeyStats, KeyStatBucket, UsageDetail } from '@/utils/usage'; +import { collectUsageDetails, calculateStatusBarData } from '@/utils/usage'; import { formatFileSize } from '@/utils/format'; import styles from './AuthFilesPage.module.scss'; @@ -143,6 +144,7 @@ export function AuthFilesPage() { const [deleting, setDeleting] = useState(null); const [deletingAll, setDeletingAll] = useState(false); const [keyStats, setKeyStats] = useState({ bySource: {}, byAuthIndex: {} }); + const [usageDetails, setUsageDetails] = useState([]); // 详情弹窗相关 const [detailModalOpen, setDetailModalOpen] = useState(false); @@ -195,11 +197,16 @@ export function AuthFilesPage() { } }, [t]); - // 加载 key 统计 + // 加载 key 统计和 usage 明细 const loadKeyStats = useCallback(async () => { try { - const stats = await usageApi.getKeyStats(); + const usageResponse = await usageApi.getUsage(); + const usageData = usageResponse?.usage ?? usageResponse; + const stats = await usageApi.getKeyStats(usageData); setKeyStats(stats); + // 收集 usage 明细用于状态栏 + const details = collectUsageDetails(usageData); + setUsageDetails(details); } catch { // 静默失败 } @@ -570,6 +577,50 @@ export function AuthFilesPage() { ); + // 渲染状态监测栏 + const renderStatusBar = (item: AuthFileItem) => { + // 认证文件使用 authIndex 来匹配 usage 数据 + const rawAuthIndex = item['auth_index'] ?? item.authIndex; + const authIndexKey = normalizeAuthIndexValue(rawAuthIndex); + + // 过滤出属于该认证文件的 usage 明细 + const filteredDetails = usageDetails.filter((detail) => { + const detailAuthIndex = normalizeAuthIndexValue(detail.auth_index); + return detailAuthIndex !== null && detailAuthIndex === authIndexKey; + }); + + const statusData = calculateStatusBarData(filteredDetails); + const hasData = statusData.totalSuccess + statusData.totalFailure > 0; + const rateClass = !hasData + ? '' + : statusData.successRate >= 90 + ? styles.statusRateHigh + : statusData.successRate >= 50 + ? styles.statusRateMedium + : styles.statusRateLow; + + return ( +
+
+ {statusData.blocks.map((state, idx) => { + const blockClass = + state === 'success' + ? styles.statusBlockSuccess + : state === 'failure' + ? styles.statusBlockFailure + : state === 'mixed' + ? styles.statusBlockMixed + : styles.statusBlockIdle; + return
; + })} +
+ + {hasData ? `${statusData.successRate.toFixed(1)}%` : '--'} + +
+ ); + }; + // 渲染单个认证文件卡片 const renderFileCard = (item: AuthFileItem) => { const fileStats = resolveAuthFileStats(item, keyStats); @@ -606,6 +657,9 @@ export function AuthFilesPage() {
+ {/* 状态监测栏 */} + {renderStatusBar(item)} +
{isRuntimeOnly ? (
{t('auth_files.type_virtual') || '虚拟认证文件'}