fix(usage): make model stats card scrollable

This commit is contained in:
Supra4E8C
2026-02-13 16:11:28 +08:00
parent 4dde62ac58
commit 5dbff4c3e0
2 changed files with 100 additions and 78 deletions

View File

@@ -59,95 +59,97 @@ export function ModelStatsCard({ modelStats, loading, hasPrices }: ModelStatsCar
sortKey === key ? (sortDir === 'asc' ? 'ascending' : 'descending') : 'none'; sortKey === key ? (sortDir === 'asc' ? 'ascending' : 'descending') : 'none';
return ( return (
<Card title={t('usage_stats.models')}> <Card title={t('usage_stats.models')} className={styles.detailsFixedCard}>
{loading ? ( {loading ? (
<div className={styles.hint}>{t('common.loading')}</div> <div className={styles.hint}>{t('common.loading')}</div>
) : sorted.length > 0 ? ( ) : sorted.length > 0 ? (
<div className={styles.tableWrapper}> <div className={styles.detailsScroll}>
<table className={styles.table}> <div className={styles.tableWrapper}>
<thead> <table className={styles.table}>
<tr> <thead>
<th className={styles.sortableHeader} aria-sort={ariaSort('model')}> <tr>
<button <th className={styles.sortableHeader} aria-sort={ariaSort('model')}>
type="button"
className={styles.sortHeaderButton}
onClick={() => handleSort('model')}
>
{t('usage_stats.model_name')}{arrow('model')}
</button>
</th>
<th className={styles.sortableHeader} aria-sort={ariaSort('requests')}>
<button
type="button"
className={styles.sortHeaderButton}
onClick={() => handleSort('requests')}
>
{t('usage_stats.requests_count')}{arrow('requests')}
</button>
</th>
<th className={styles.sortableHeader} aria-sort={ariaSort('tokens')}>
<button
type="button"
className={styles.sortHeaderButton}
onClick={() => handleSort('tokens')}
>
{t('usage_stats.tokens_count')}{arrow('tokens')}
</button>
</th>
<th className={styles.sortableHeader} aria-sort={ariaSort('successRate')}>
<button
type="button"
className={styles.sortHeaderButton}
onClick={() => handleSort('successRate')}
>
{t('usage_stats.success_rate')}{arrow('successRate')}
</button>
</th>
{hasPrices && (
<th className={styles.sortableHeader} aria-sort={ariaSort('cost')}>
<button <button
type="button" type="button"
className={styles.sortHeaderButton} className={styles.sortHeaderButton}
onClick={() => handleSort('cost')} onClick={() => handleSort('model')}
> >
{t('usage_stats.total_cost')}{arrow('cost')} {t('usage_stats.model_name')}{arrow('model')}
</button> </button>
</th> </th>
)} <th className={styles.sortableHeader} aria-sort={ariaSort('requests')}>
</tr> <button
</thead> type="button"
<tbody> className={styles.sortHeaderButton}
{sorted.map((stat) => ( onClick={() => handleSort('requests')}
<tr key={stat.model}>
<td className={styles.modelCell}>{stat.model}</td>
<td>
<span className={styles.requestCountCell}>
<span>{stat.requests.toLocaleString()}</span>
<span className={styles.requestBreakdown}>
(<span className={styles.statSuccess}>{stat.successCount.toLocaleString()}</span>{' '}
<span className={styles.statFailure}>{stat.failureCount.toLocaleString()}</span>)
</span>
</span>
</td>
<td>{formatCompactNumber(stat.tokens)}</td>
<td>
<span
className={
stat.successRate >= 95
? styles.statSuccess
: stat.successRate >= 80
? styles.statNeutral
: styles.statFailure
}
> >
{stat.successRate.toFixed(1)}% {t('usage_stats.requests_count')}{arrow('requests')}
</span> </button>
</td> </th>
{hasPrices && <td>{stat.cost > 0 ? formatUsd(stat.cost) : '--'}</td>} <th className={styles.sortableHeader} aria-sort={ariaSort('tokens')}>
<button
type="button"
className={styles.sortHeaderButton}
onClick={() => handleSort('tokens')}
>
{t('usage_stats.tokens_count')}{arrow('tokens')}
</button>
</th>
<th className={styles.sortableHeader} aria-sort={ariaSort('successRate')}>
<button
type="button"
className={styles.sortHeaderButton}
onClick={() => handleSort('successRate')}
>
{t('usage_stats.success_rate')}{arrow('successRate')}
</button>
</th>
{hasPrices && (
<th className={styles.sortableHeader} aria-sort={ariaSort('cost')}>
<button
type="button"
className={styles.sortHeaderButton}
onClick={() => handleSort('cost')}
>
{t('usage_stats.total_cost')}{arrow('cost')}
</button>
</th>
)}
</tr> </tr>
))} </thead>
</tbody> <tbody>
</table> {sorted.map((stat) => (
<tr key={stat.model}>
<td className={styles.modelCell}>{stat.model}</td>
<td>
<span className={styles.requestCountCell}>
<span>{stat.requests.toLocaleString()}</span>
<span className={styles.requestBreakdown}>
(<span className={styles.statSuccess}>{stat.successCount.toLocaleString()}</span>{' '}
<span className={styles.statFailure}>{stat.failureCount.toLocaleString()}</span>)
</span>
</span>
</td>
<td>{formatCompactNumber(stat.tokens)}</td>
<td>
<span
className={
stat.successRate >= 95
? styles.statSuccess
: stat.successRate >= 80
? styles.statNeutral
: styles.statFailure
}
>
{stat.successRate.toFixed(1)}%
</span>
</td>
{hasPrices && <td>{stat.cost > 0 ? formatUsd(stat.cost) : '--'}</td>}
</tr>
))}
</tbody>
</table>
</div>
</div> </div>
) : ( ) : (
<div className={styles.hint}>{t('usage_stats.no_data')}</div> <div className={styles.hint}>{t('usage_stats.no_data')}</div>

View File

@@ -580,6 +580,26 @@
} }
} }
// Fixed-height cards with internal scrolling (API details / model stats)
.detailsFixedCard {
display: flex;
flex-direction: column;
height: 520px;
overflow: hidden;
@include mobile {
height: 420px;
}
}
.detailsScroll {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
}
// Table (80%比例) // Table (80%比例)
.tableWrapper { .tableWrapper {
overflow-x: auto; overflow-x: auto;