/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React, { useMemo, useState, useRef, useEffect } from 'react'; import { TrendingUp, RefreshCw, ChevronDown } from 'lucide-react'; import Sparkline from './Sparkline'; import { renderQuota } from '../../helpers'; const formatMD = (d) => { const m = d.getMonth() + 1; const day = d.getDate(); return `${m}/${day}`; }; const toDateStr = (d) => { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; }; const RANGE_OPTIONS = [ { key: 'today', label: '今天' }, { key: 'yesterday', label: '昨天' }, { key: '24h', label: '近 24 小时' }, { key: '7d', label: '近 7 天' }, { key: '14d', label: '近 14 天' }, { key: '30d', label: '近 30 天' }, { key: 'thisMonth', label: '本月' }, { key: 'lastMonth', label: '上月' }, ]; const getRangeDates = (key) => { const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); switch (key) { case 'today': return { start: today, end: now }; case 'yesterday': { const y = new Date(today.getTime() - 86400000); return { start: y, end: new Date(today.getTime() - 1000) }; } case '24h': return { start: new Date(now.getTime() - 86400000), end: now }; case '7d': return { start: new Date(today.getTime() - 6 * 86400000), end: now }; case '14d': return { start: new Date(today.getTime() - 13 * 86400000), end: now }; case '30d': return { start: new Date(today.getTime() - 29 * 86400000), end: now }; case 'thisMonth': return { start: new Date(now.getFullYear(), now.getMonth(), 1), end: now }; case 'lastMonth': { const lmStart = new Date(now.getFullYear(), now.getMonth() - 1, 1); const lmEnd = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999); return { start: lmStart, end: lmEnd }; } default: return { start: new Date(today.getTime() - 6 * 86400000), end: now }; } }; const BentoTrend = ({ dailyStats = [], topModels = [], className = '', onRefresh, loading = false, }) => { const [rangeOpen, setRangeOpen] = useState(false); const [selectedKey, setSelectedKey] = useState('7d'); const [customStart, setCustomStart] = useState(''); const [customEnd, setCustomEnd] = useState(''); const rangeRef = useRef(null); useEffect(() => { const handler = (e) => { if (rangeRef.current && !rangeRef.current.contains(e.target)) { setRangeOpen(false); } }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, []); const selectedLabel = RANGE_OPTIONS.find((r) => r.key === selectedKey)?.label || '近 7 天'; const handleApply = () => { if (customStart && customEnd) { setSelectedKey('custom'); } setRangeOpen(false); }; const counts = useMemo(() => dailyStats.map((d) => d.count), [dailyStats]); const costs = useMemo(() => dailyStats.map((d) => d.cost), [dailyStats]); const totalCalls = counts.reduce((s, v) => s + v, 0); const totalCost = costs.reduce((s, v) => s + v, 0); const avg = totalCalls > 0 ? (totalCalls / Math.max(counts.length, 1)).toFixed(1) : '0.0'; const peak = counts.length > 0 ? Math.max(...counts) : 0; const topMax = topModels[0]?.count || 0; return (
Token 使用趋势
{selectedLabel} {totalCalls} 次 · 日均{' '} {avg}
花费
{totalCost > 0 ? renderQuota(totalCost) : '—'}
{rangeOpen && (
{RANGE_OPTIONS.map((opt) => ( ))}
setCustomStart(e.target.value)} className='flex-1 rounded-lg border border-slate-200 bg-white px-2 py-1 text-xs text-slate-700 outline-none focus:border-slate-400 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/70 dark:focus:border-white/30' placeholder='开始日期' /> setCustomEnd(e.target.value)} className='flex-1 rounded-lg border border-slate-200 bg-white px-2 py-1 text-xs text-slate-700 outline-none focus:border-slate-400 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/70 dark:focus:border-white/30' placeholder='结束日期' />
)}
{dailyStats.map((d, i) => ( {formatMD(d.date)} ))}
峰值{' '} {peak}
{topModels.length > 0 && (
Top 5 模型 近 30 日
    {topModels.map((m, i) => { const pct = topMax > 0 ? (m.count / topMax) * 100 : 0; return (
  • {i + 1} {m.name}
    {m.count}
  • ); })}
)}
); }; export default BentoTrend;