import { useEffect, useState, useCallback } from 'react'; import { adminApi } from '../lib/api'; import type { AdminRecord, Team } from '../types'; import { showToast } from '../components/Toast'; import { DatePicker } from '../components/DatePicker'; import { Select } from '../components/Select'; import { RecordDetailModal } from '../components/RecordDetailModal'; import styles from './RecordsPage.module.css'; export function RecordsPage() { const [records, setRecords] = useState([]); const [detailRecord, setDetailRecord] = useState(null); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [search, setSearch] = useState(''); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [teamFilter, setTeamFilter] = useState(''); const [teams, setTeams] = useState([]); const [loading, setLoading] = useState(true); const pageSize = 20; // Load teams for filter dropdown useEffect(() => { adminApi.getTeams().then(({ data }) => setTeams(data.results)).catch(() => {}); }, []); const fetchRecords = useCallback(async () => { setLoading(true); try { const { data } = await adminApi.getRecords({ page, page_size: pageSize, search, start_date: startDate || undefined, end_date: endDate || undefined, team_id: teamFilter ? Number(teamFilter) : undefined, }); setRecords(data.results); setTotal(data.total); } catch { showToast('加载消费记录失败'); } finally { setLoading(false); } }, [page, search, startDate, endDate, teamFilter]); useEffect(() => { fetchRecords(); }, [fetchRecords]); const handleSearch = () => { setPage(1); fetchRecords(); }; const handleExportCSV = async () => { try { // Fetch all records matching current filters (up to 10000) const { data } = await adminApi.getRecords({ page: 1, page_size: 10000, search, start_date: startDate || undefined, end_date: endDate || undefined, team_id: teamFilter ? Number(teamFilter) : undefined, }); const header = '任务ID,提交时间,完成时间,耗时,团队,用户名,模型,视频时长(秒),模式,比例,分辨率,消费秒数,Tokens,费用(元),成本(元),利润(元),种子值,状态,提示词,失败原因,原始错误,参考素材数\n'; const rows = data.results.map((r) => { const esc = (s: string) => s.replace(/"/g, '""').replace(/^[=+\-@]/, "'$&"); const modeLabel = r.mode === 'universal' ? '全能参考' : '首尾帧'; const modelLabel = r.model === 'seedance_2.0_fast' ? 'AirDrama Fast' : 'AirDrama'; const statusLabel = { queued: '排队中', processing: '生成中', completed: '已完成', failed: '失败' }[r.status]; const profit = ((r.cost_amount || 0) - (r.base_cost_amount || 0)).toFixed(2); const elapsed = r.completed_at ? Math.round((new Date(r.completed_at).getTime() - new Date(r.created_at).getTime()) / 1000) + '秒' : ''; const completedAt = r.completed_at ? new Date(r.completed_at).toLocaleString('zh-CN') : ''; const refCount = (r.reference_urls || []).length; const resolutionLabel = r.resolution ? r.resolution.toUpperCase() : ''; return `"${r.ark_task_id || ''}","${new Date(r.created_at).toLocaleString('zh-CN')}","${completedAt}","${elapsed}","${r.team_name || '-'}","${r.username}","${modelLabel}","${r.duration ?? ''}","${modeLabel}","${r.aspect_ratio || ''}","${resolutionLabel}","${r.seconds_consumed}","${r.tokens_consumed || 0}","${(r.cost_amount || 0).toFixed(2)}","${(r.base_cost_amount || 0).toFixed(2)}","${profit}","${r.seed != null && r.seed !== -1 ? r.seed : ''}","${statusLabel}","${esc(r.prompt || '')}","${esc(r.error_message || '')}","${esc(r.raw_error || '')}","${refCount}"`; }).join('\n'); const blob = new Blob(['\uFEFF' + header + rows], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `消费记录_${new Date().toISOString().slice(0, 10)}.csv`; a.click(); URL.revokeObjectURL(url); showToast('导出成功'); } catch { showToast('导出失败'); } }; const totalPages = Math.ceil(total / pageSize); const statusMap: Record = { queued: '排队中', processing: '生成中', completed: '已完成', failed: '失败' }; const formatElapsed = (r: AdminRecord) => { if (!r.completed_at) return '-'; const ms = new Date(r.completed_at).getTime() - new Date(r.created_at).getTime(); if (ms < 0) return '-'; const totalSec = Math.round(ms / 1000); if (totalSec < 60) return `${totalSec}秒`; const min = Math.floor(totalSec / 60); const sec = totalSec % 60; return `${min}分${sec > 0 ? sec + '秒' : ''}`; }; return (<>

消费记录

setSearch(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} />