import React, { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { ChevronDown, ChevronRight } from 'lucide-react'; import { getUserLogs, Q } from '../api'; import { useCurrency } from '../context/SiteContext'; function formatTime(unix) { if (!unix) return '-'; const d = new Date(unix * 1000); const pad = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } function getLogOther(otherStr) { if (!otherStr) return null; try { return JSON.parse(otherStr); } catch (e) { return null; } } function formatAmount(symbol, rate, amount) { return `${symbol}${(Number(amount || 0) * rate).toFixed(6)}`; } function getProviderSummary(other) { if (!other?.provider_name) return ''; if (other.provider_description) { return `${other.provider_name}:${other.provider_description}`; } return other.provider_name; } function getSitePricingDetails(other, symbol, rate, t) { if (!other?.site_billing_mode) return []; if (other.site_billing_mode === 'per_call') { return [ { key: t('分站定价'), value: t('按次计费') }, { key: t('价格'), value: formatAmount(symbol, rate, other.site_fixed_price) }, ]; } const details = [{ key: t('分站定价'), value: t('按量计费') }]; if (Number(other.site_input_price || 0) > 0) { details.push({ key: t('输入价格'), value: `${formatAmount(symbol, rate, other.site_input_price)} / 1M tokens`, }); } if (Number(other.site_output_price || 0) > 0) { details.push({ key: t('输出价格'), value: `${formatAmount(symbol, rate, other.site_output_price)} / 1M tokens`, }); } if (Number(other.site_cache_read_price || 0) > 0) { details.push({ key: t('缓存读取价格'), value: `${formatAmount(symbol, rate, other.site_cache_read_price)} / 1M tokens`, }); } const cacheCreate5m = Number(other.site_cache_creation_price_5m || 0); const cacheCreate1h = Number(other.site_cache_creation_price_1h || 0); const cacheCreate = Number(other.site_cache_creation_price || 0); if (cacheCreate5m > 0 || cacheCreate1h > 0) { const parts = []; if (cacheCreate5m > 0) { parts.push(`5m ${formatAmount(symbol, rate, cacheCreate5m)} / 1M tokens`); } if (cacheCreate1h > 0) { parts.push(`1h ${formatAmount(symbol, rate, cacheCreate1h)} / 1M tokens`); } details.push({ key: t('缓存创建价格'), value: parts.join(' / '), }); } else if (cacheCreate > 0) { details.push({ key: t('缓存创建价格'), value: `${formatAmount(symbol, rate, cacheCreate)} / 1M tokens`, }); } return details; } export default function Logs() { const { t } = useTranslation(); const { symbol, rate } = useCurrency(); const [logs, setLogs] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [loading, setLoading] = useState(true); const [modelFilter, setModelFilter] = useState(''); const [tokenFilter, setTokenFilter] = useState(''); const [expandedRows, setExpandedRows] = useState({}); const pageSize = 20; const load = useCallback(async () => { setLoading(true); try { const params = { p: page, page_size: pageSize, type: 2 }; // type=2 consume only if (modelFilter) params.model_name = modelFilter; if (tokenFilter) params.token_name = tokenFilter; const res = await getUserLogs(params); if (res.data.success) { setLogs(res.data.data?.items || []); setTotal(res.data.data?.total || 0); } } catch (e) { /* interceptor */ } setLoading(false); }, [page, modelFilter, tokenFilter]); useEffect(() => { load(); }, [load]); const totalPages = Math.max(1, Math.ceil(total / pageSize)); const toggleRow = (logId) => { setExpandedRows(prev => ({ ...prev, [logId]: !prev[logId] })); }; const getExpandData = useCallback((log) => { const other = getLogOther(log.other); if (!other) return []; const data = []; const providerSummary = getProviderSummary(other); if (providerSummary) { data.push({ key: t('供应商'), value: providerSummary }); } data.push(...getSitePricingDetails(other, symbol, rate, t)); if (other.cache_tokens > 0) { data.push({ key: t('缓存命中 Tokens'), value: Number(other.cache_tokens).toLocaleString() }); } if (other.cache_creation_tokens > 0) { data.push({ key: t('缓存创建 Tokens'), value: Number(other.cache_creation_tokens).toLocaleString() }); } if (other.cache_creation_tokens_5m > 0) { data.push({ key: t('缓存创建 Tokens (5m)'), value: Number(other.cache_creation_tokens_5m).toLocaleString() }); } if (other.cache_creation_tokens_1h > 0) { data.push({ key: t('缓存创建 Tokens (1h)'), value: Number(other.cache_creation_tokens_1h).toLocaleString() }); } // Request ID if (log.request_id) { data.push({ key: 'Request ID', value: log.request_id }); } // Stream info if (log.is_stream !== undefined) { data.push({ key: t('流式'), value: log.is_stream ? t('是') : t('否') }); } // First response time for streaming if (log.is_stream && other.frt) { const frtSeconds = (parseFloat(other.frt) / 1000.0).toFixed(1); data.push({ key: t('首字时间'), value: `${frtSeconds}s` }); } // Billing source if (other.billing_source === 'subscription') { data.push({ key: t('计费方式'), value: t('订阅抵扣') }); } return data; }, [rate, symbol, t]); return (
{/* Header */}

{t('logs.title')}

{t('logs.subtitle')}

{/* Filters */}
{ setModelFilter(e.target.value); setPage(1); }} className="input w-full" placeholder={t('logs.filterModel')} />
{ setTokenFilter(e.target.value); setPage(1); }} className="input w-full" placeholder={t('logs.filterToken')} />
{/* Table */}
{loading ? (
) : logs.length === 0 ? (

{t('logs.noLogs')}

) : ( <> {/* Desktop table */}
{logs.map((log, i) => { const expandData = getExpandData(log); const hasExpandData = expandData.length > 0; const isExpanded = expandedRows[log.id]; return ( hasExpandData && toggleRow(log.id)} > {isExpanded && hasExpandData && ( )} ); })}
{t('logs.time')} {t('logs.model')} {t('logs.token')} {t('logs.promptTokens')} {t('logs.completionTokens')} {t('logs.cost')} {t('logs.duration')}
{hasExpandData && ( isExpanded ? : )} {formatTime(log.created_at)} {log.model_name || '-'} {log.token_name || '-'} {log.prompt_tokens?.toLocaleString() || '0'} {log.completion_tokens?.toLocaleString() || '0'} {log.quota > 0 ? `${symbol}${(log.quota / Q * rate).toFixed(6)}` : '-'} {log.use_time > 0 ? `${log.use_time}s` : '-'}
{expandData.map((item, idx) => (
{item.key} {item.value}
))}
{/* Mobile cards */}
{logs.map((log, i) => { const expandData = getExpandData(log); const hasExpandData = expandData.length > 0; const isExpanded = expandedRows[log.id]; return (
hasExpandData && toggleRow(log.id)} >
{hasExpandData && ( isExpanded ? : )} {log.model_name || '-'}
{log.quota > 0 ? `${symbol}${(log.quota / Q * rate).toFixed(6)}` : '-'}
{formatTime(log.created_at)} {log.prompt_tokens || 0} / {log.completion_tokens || 0} tokens
{log.token_name && (
{log.token_name}
)} {isExpanded && hasExpandData && (
{expandData.map((item, idx) => (
{item.key} {item.value}
))}
)}
); })}
)}
{/* Pagination */} {total > pageSize && (

{t('logs.showing', { from: (page - 1) * pageSize + 1, to: Math.min(page * pageSize, total), total })}

{page} / {totalPages}
)}
); }