Token 使用趋势添加时间范围控制栏:粒度选择器与刷新按钮

This commit is contained in:
wangxiaoji 2026-06-30 11:04:27 +08:00
parent dd5474f500
commit 6b73f5b6b9
2 changed files with 47 additions and 2 deletions

View File

@ -17,8 +17,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import React, { useMemo } from 'react'; import React, { useMemo, useState } from 'react';
import { TrendingUp } from 'lucide-react'; import { TrendingUp, RefreshCw, ChevronDown } from 'lucide-react';
import Sparkline from './Sparkline'; import Sparkline from './Sparkline';
import { renderQuota } from '../../helpers'; import { renderQuota } from '../../helpers';
@ -28,11 +28,21 @@ const formatMD = (d) => {
return `${m}/${day}`; return `${m}/${day}`;
}; };
const GRANULARITY = [
{ value: 'hour', label: '按小时' },
{ value: 'day', label: '按天' },
{ value: 'week', label: '按周' },
{ value: 'month', label: '按月' },
];
const BentoTrend = ({ const BentoTrend = ({
dailyStats = [], dailyStats = [],
topModels = [], topModels = [],
className = '', className = '',
onRefresh,
loading = false,
}) => { }) => {
const [granularity, setGranularity] = useState('day');
const counts = useMemo(() => dailyStats.map((d) => d.count), [dailyStats]); const counts = useMemo(() => dailyStats.map((d) => d.count), [dailyStats]);
const costs = useMemo(() => dailyStats.map((d) => d.cost), [dailyStats]); const costs = useMemo(() => dailyStats.map((d) => d.cost), [dailyStats]);
@ -71,6 +81,39 @@ const BentoTrend = ({
</div> </div>
</div> </div>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<span className='inline-flex items-center gap-1 rounded-full bg-slate-100 px-3 py-1 text-xs font-medium text-slate-700 dark:bg-white/5 dark:text-white/70'>
7
</span>
<button
onClick={onRefresh}
disabled={loading}
className='flex h-7 w-7 items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 transition-colors hover:border-slate-300 hover:bg-slate-50 hover:text-slate-700 disabled:opacity-50 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/60 dark:hover:bg-white/5 dark:hover:text-white'
title='刷新'
>
<RefreshCw size={12} className={loading ? 'animate-spin' : ''} />
</button>
</div>
<div className='flex items-center gap-1.5'>
<span className='text-[10px] text-slate-400 dark:text-white/40'>粒度</span>
<div className='relative'>
<select
value={granularity}
onChange={(e) => setGranularity(e.target.value)}
className='appearance-none rounded-lg border border-slate-200 bg-white py-1 pl-2.5 pr-7 text-xs text-slate-700 outline-none transition-colors hover:border-slate-300 focus:border-slate-400 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/70 dark:hover:bg-white/5 dark:focus:border-white/30'
>
{GRANULARITY.map((g) => (
<option key={g.value} value={g.value} className='bg-white text-slate-900 dark:bg-neutral-900 dark:text-white'>
{g.label}
</option>
))}
</select>
<ChevronDown size={12} className='pointer-events-none absolute right-1.5 top-1/2 -translate-y-1/2 text-slate-400 dark:text-white/40' />
</div>
</div>
</div>
<div className='flex min-h-[80px] flex-1 items-end text-blue-500'> <div className='flex min-h-[80px] flex-1 items-end text-blue-500'>
<Sparkline data={counts} width={400} height={80} /> <Sparkline data={counts} width={400} height={80} />
</div> </div>

View File

@ -64,6 +64,8 @@ const Dashboard = () => {
<BentoTrend <BentoTrend
dailyStats={dashboard.dailyStats} dailyStats={dashboard.dailyStats}
topModels={dashboard.topModels} topModels={dashboard.topModels}
onRefresh={dashboard.refresh}
loading={dashboard.loading}
className='md:col-span-2' className='md:col-span-2'
/> />
<MonthlyForecast <MonthlyForecast