mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 11:10:49 +08:00
fix(usage): make sorting and api expansion keyboard accessible
This commit is contained in:
@@ -74,6 +74,7 @@ export function ApiDetailsCard({ apiStats, loading, hasPrices }: ApiDetailsCardP
|
|||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-pressed={sortKey === key}
|
||||||
className={`${styles.apiSortBtn} ${sortKey === key ? styles.apiSortBtnActive : ''}`}
|
className={`${styles.apiSortBtn} ${sortKey === key ? styles.apiSortBtnActive : ''}`}
|
||||||
onClick={() => handleSort(key)}
|
onClick={() => handleSort(key)}
|
||||||
>
|
>
|
||||||
@@ -82,9 +83,19 @@ export function ApiDetailsCard({ apiStats, loading, hasPrices }: ApiDetailsCardP
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.apiList}>
|
<div className={styles.apiList}>
|
||||||
{sorted.map((api) => (
|
{sorted.map((api, index) => {
|
||||||
|
const isExpanded = expandedApis.has(api.endpoint);
|
||||||
|
const panelId = `api-models-${index}`;
|
||||||
|
|
||||||
|
return (
|
||||||
<div key={api.endpoint} className={styles.apiItem}>
|
<div key={api.endpoint} className={styles.apiItem}>
|
||||||
<div className={styles.apiHeader} onClick={() => toggleExpand(api.endpoint)}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.apiHeader}
|
||||||
|
onClick={() => toggleExpand(api.endpoint)}
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
aria-controls={panelId}
|
||||||
|
>
|
||||||
<div className={styles.apiInfo}>
|
<div className={styles.apiInfo}>
|
||||||
<span className={styles.apiEndpoint}>{api.endpoint}</span>
|
<span className={styles.apiEndpoint}>{api.endpoint}</span>
|
||||||
<div className={styles.apiStats}>
|
<div className={styles.apiStats}>
|
||||||
@@ -110,11 +121,11 @@ export function ApiDetailsCard({ apiStats, loading, hasPrices }: ApiDetailsCardP
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className={styles.expandIcon}>
|
<span className={styles.expandIcon}>
|
||||||
{expandedApis.has(api.endpoint) ? '▼' : '▶'}
|
{isExpanded ? '▼' : '▶'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
{expandedApis.has(api.endpoint) && (
|
{isExpanded && (
|
||||||
<div className={styles.apiModels}>
|
<div id={panelId} className={styles.apiModels}>
|
||||||
{Object.entries(api.models).map(([model, stats]) => (
|
{Object.entries(api.models).map(([model, stats]) => (
|
||||||
<div key={model} className={styles.modelRow}>
|
<div key={model} className={styles.modelRow}>
|
||||||
<span className={styles.modelName}>{model}</span>
|
<span className={styles.modelName}>{model}</span>
|
||||||
@@ -133,7 +144,8 @@ export function ApiDetailsCard({ apiStats, loading, hasPrices }: ApiDetailsCardP
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ export function ModelStatsCard({ modelStats, loading, hasPrices }: ModelStatsCar
|
|||||||
|
|
||||||
const arrow = (key: SortKey) =>
|
const arrow = (key: SortKey) =>
|
||||||
sortKey === key ? (sortDir === 'asc' ? ' ▲' : ' ▼') : '';
|
sortKey === key ? (sortDir === 'asc' ? ' ▲' : ' ▼') : '';
|
||||||
|
const ariaSort = (key: SortKey): 'none' | 'ascending' | 'descending' =>
|
||||||
|
sortKey === key ? (sortDir === 'asc' ? 'ascending' : 'descending') : 'none';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title={t('usage_stats.models')}>
|
<Card title={t('usage_stats.models')}>
|
||||||
@@ -65,21 +67,51 @@ export function ModelStatsCard({ modelStats, loading, hasPrices }: ModelStatsCar
|
|||||||
<table className={styles.table}>
|
<table className={styles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className={styles.sortableHeader} onClick={() => handleSort('model')}>
|
<th className={styles.sortableHeader} aria-sort={ariaSort('model')}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.sortHeaderButton}
|
||||||
|
onClick={() => handleSort('model')}
|
||||||
|
>
|
||||||
{t('usage_stats.model_name')}{arrow('model')}
|
{t('usage_stats.model_name')}{arrow('model')}
|
||||||
|
</button>
|
||||||
</th>
|
</th>
|
||||||
<th className={styles.sortableHeader} onClick={() => handleSort('requests')}>
|
<th className={styles.sortableHeader} aria-sort={ariaSort('requests')}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.sortHeaderButton}
|
||||||
|
onClick={() => handleSort('requests')}
|
||||||
|
>
|
||||||
{t('usage_stats.requests_count')}{arrow('requests')}
|
{t('usage_stats.requests_count')}{arrow('requests')}
|
||||||
|
</button>
|
||||||
</th>
|
</th>
|
||||||
<th className={styles.sortableHeader} onClick={() => handleSort('tokens')}>
|
<th className={styles.sortableHeader} aria-sort={ariaSort('tokens')}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.sortHeaderButton}
|
||||||
|
onClick={() => handleSort('tokens')}
|
||||||
|
>
|
||||||
{t('usage_stats.tokens_count')}{arrow('tokens')}
|
{t('usage_stats.tokens_count')}{arrow('tokens')}
|
||||||
|
</button>
|
||||||
</th>
|
</th>
|
||||||
<th className={styles.sortableHeader} onClick={() => handleSort('successRate')}>
|
<th className={styles.sortableHeader} aria-sort={ariaSort('successRate')}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.sortHeaderButton}
|
||||||
|
onClick={() => handleSort('successRate')}
|
||||||
|
>
|
||||||
{t('usage_stats.success_rate')}{arrow('successRate')}
|
{t('usage_stats.success_rate')}{arrow('successRate')}
|
||||||
|
</button>
|
||||||
</th>
|
</th>
|
||||||
{hasPrices && (
|
{hasPrices && (
|
||||||
<th className={styles.sortableHeader} onClick={() => handleSort('cost')}>
|
<th className={styles.sortableHeader} aria-sort={ariaSort('cost')}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.sortHeaderButton}
|
||||||
|
onClick={() => handleSort('cost')}
|
||||||
|
>
|
||||||
{t('usage_stats.total_cost')}{arrow('cost')}
|
{t('usage_stats.total_cost')}{arrow('cost')}
|
||||||
|
</button>
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -451,6 +451,11 @@
|
|||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--primary-color);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.apiSortBtnActive {
|
.apiSortBtnActive {
|
||||||
@@ -474,16 +479,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.apiHeader {
|
.apiHeader {
|
||||||
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.15s ease;
|
transition: background-color 0.15s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--bg-tertiary);
|
background-color: var(--bg-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--primary-color);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.apiInfo {
|
.apiInfo {
|
||||||
@@ -588,7 +604,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
th.sortableHeader {
|
th.sortableHeader {
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: color 0.15s ease;
|
transition: color 0.15s ease;
|
||||||
|
|
||||||
@@ -606,6 +621,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sortHeaderButton {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--primary-color);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.modelCell {
|
.modelCell {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
max-width: 240px;
|
max-width: 240px;
|
||||||
|
|||||||
Reference in New Issue
Block a user