mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-18 10:40:50 +08:00
fix(usage): make model stats card scrollable
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user