feat(usage): replace time-range select with custom dropdown

This commit is contained in:
Supra4E8C
2026-02-12 22:25:38 +08:00
parent 887600c03a
commit 50c1b0f4b3
2 changed files with 122 additions and 24 deletions

View File

@@ -44,38 +44,94 @@
align-items: center;
}
.select.timeRangeSelect {
.timeRangeSelect {
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-width: 164px;
height: 40px;
border-color: var(--border-color);
padding: 0 12px;
border: 1px solid var(--border-color);
border-radius: $radius-md;
background-color: var(--bg-primary);
box-shadow: var(--shadow);
color: var(--text-primary);
font-size: 13px;
font-weight: 500;
padding-right: 34px;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
text-align: left;
&:hover {
border-color: var(--border-hover);
}
&:focus {
outline: none;
box-shadow: var(--shadow), 0 0 0 3px rgba(59, 130, 246, 0.15);
}
&[aria-expanded='true'] {
border-color: var(--primary-color);
box-shadow: var(--shadow), 0 0 0 3px rgba(59, 130, 246, 0.15);
}
}
.timeRangeSelectedText {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.timeRangeSelectIcon {
position: absolute;
right: 11px;
top: 50%;
transform: translateY(-50%);
display: inline-flex;
color: var(--text-secondary);
pointer-events: none;
flex-shrink: 0;
transition: transform 0.2s ease;
[aria-expanded='true'] > & {
transform: rotate(180deg);
}
}
.timeRangeDropdown {
position: absolute;
top: calc(100% + 6px);
left: 0;
right: 0;
z-index: 1000;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: $radius-lg;
padding: 6px;
box-shadow: var(--shadow-lg);
display: flex;
flex-direction: column;
gap: 4px;
}
.timeRangeOption {
padding: 8px 12px;
border-radius: $radius-md;
border: 1px solid transparent;
background: transparent;
color: var(--text-primary);
cursor: pointer;
text-align: left;
font-size: 13px;
font-weight: 500;
transition: background-color 0.15s ease, border-color 0.15s ease;
&:hover {
background: var(--bg-secondary);
}
}
.timeRangeOptionActive {
border-color: rgba(59, 130, 246, 0.5);
background: rgba(59, 130, 246, 0.10);
font-weight: 600;
}
.pageTitle {

View File

@@ -1,4 +1,4 @@
import { useState, useMemo, useCallback, useEffect } from 'react';
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import {
Chart as ChartJS,
@@ -54,6 +54,12 @@ const TIME_RANGE_STORAGE_KEY = 'cli-proxy-usage-time-range-v1';
const DEFAULT_CHART_LINES = ['all'];
const DEFAULT_TIME_RANGE: UsageTimeRange = '24h';
const MAX_CHART_LINES = 9;
const TIME_RANGE_OPTIONS: ReadonlyArray<{ value: UsageTimeRange; labelKey: string }> = [
{ value: 'all', labelKey: 'usage_stats.range_all' },
{ value: '7h', labelKey: 'usage_stats.range_7h' },
{ value: '24h', labelKey: 'usage_stats.range_24h' },
{ value: '7d', labelKey: 'usage_stats.range_7d' },
];
const HOUR_WINDOW_BY_TIME_RANGE: Record<Exclude<UsageTimeRange, 'all'>, number> = {
'7h': 7,
'24h': 24,
@@ -110,6 +116,19 @@ export function UsagePage() {
const resolvedTheme = useThemeStore((state) => state.resolvedTheme);
const isDark = resolvedTheme === 'dark';
// Time range dropdown
const [timeRangeOpen, setTimeRangeOpen] = useState(false);
const timeRangeRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!timeRangeOpen) return;
const handleClickOutside = (event: MouseEvent) => {
if (!timeRangeRef.current?.contains(event.target as Node)) setTimeRangeOpen(false);
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [timeRangeOpen]);
// Data hook
const {
usage,
@@ -214,20 +233,43 @@ export function UsagePage() {
<div className={styles.headerActions}>
<div className={styles.timeRangeGroup}>
<span className={styles.timeRangeLabel}>{t('usage_stats.range_filter')}</span>
<div className={styles.timeRangeSelectWrap}>
<select
value={timeRange}
onChange={(event) => setTimeRange(event.target.value as UsageTimeRange)}
className={`${styles.select} ${styles.timeRangeSelect}`}
<div className={styles.timeRangeSelectWrap} ref={timeRangeRef}>
<button
type="button"
className={styles.timeRangeSelect}
onClick={() => setTimeRangeOpen((prev) => !prev)}
aria-haspopup="listbox"
aria-expanded={timeRangeOpen}
>
<option value="all">{t('usage_stats.range_all')}</option>
<option value="7h">{t('usage_stats.range_7h')}</option>
<option value="24h">{t('usage_stats.range_24h')}</option>
<option value="7d">{t('usage_stats.range_7d')}</option>
</select>
<span className={styles.timeRangeSelectIcon} aria-hidden="true">
<IconChevronDown size={14} />
</span>
<span className={styles.timeRangeSelectedText}>
{t(TIME_RANGE_OPTIONS.find((o) => o.value === timeRange)?.labelKey ?? 'usage_stats.range_24h')}
</span>
<span className={styles.timeRangeSelectIcon} aria-hidden="true">
<IconChevronDown size={14} />
</span>
</button>
{timeRangeOpen && (
<div className={styles.timeRangeDropdown} role="listbox" aria-label={t('usage_stats.range_filter')}>
{TIME_RANGE_OPTIONS.map((opt) => {
const active = opt.value === timeRange;
return (
<button
key={opt.value}
type="button"
role="option"
aria-selected={active}
className={`${styles.timeRangeOption} ${active ? styles.timeRangeOptionActive : ''}`}
onClick={() => {
setTimeRange(opt.value);
setTimeRangeOpen(false);
}}
>
{t(opt.labelKey)}
</button>
);
})}
</div>
)}
</div>
</div>
<Button