"use client"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { post } from "@/api/request"; /** 接口请求与返回类型定义 */ type ChangeType = 'INCREASE' | 'DECREASE'; type TransactionType = 'RECHARGE' | 'CONSUME' | 'REFUND' | 'GIFT'; interface ProjectVideoItem { video_name?: string; video_url: string; amount: number; transaction_type: TransactionType; change_type: ChangeType; source_type: string; created_at: string; } interface ProjectInfo { project_id: string; project_name: string; videos: ProjectVideoItem[]; } interface TransactionItem { /** 交易类型 */ transaction_type: TransactionType; /** 增减类型 */ change_type: ChangeType; /** 变动数量 */ amount: number; /** 来源类型 */ source_type: string; /** 项目信息,存在时支持展开展示 */ project_info: ProjectInfo | null; /** 创建时间 */ created_at: string; } interface TransactionResponseData { transaction_records: TransactionItem[]; page: number; page_size: number; total: number; period_days: 7 | 30 | 90; } interface ApiResponse { code: number; message: string; data: T; successful: boolean; } type PeriodDays = 7 | 30 | 90; /** * Credit 使用详情页 * - 周期切换:7 / 30 / 90 * - 分页:上一页/下一页(固定每页20) * - 表格展示:项目名、总消耗;不展示项目ID * - 空/错误状态:使用 “-” 显示 */ const UsageView: React.FC = () => { const [days, setDays] = useState(7); const [page, setPage] = useState(1); const pageSize = 20; const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [serverPeriodDays, setServerPeriodDays] = useState(90); const [expandedKeys, setExpandedKeys] = useState>(new Set()); /** 读取 user_id(参考 dashboard/page.tsx) */ const userId = useMemo(() => { try { const raw = localStorage.getItem("currentUser") || "{}"; const u = JSON.parse(raw); return String(u?.id || ""); } catch { return ""; } }, []); const canPrev = page > 1; const canNext = useMemo(() => { if (total <= 0) return false; const maxPage = Math.ceil(total / pageSize); return page < maxPage; }, [page, total]); const loadData = useCallback(async () => { if (!userId) { setItems([]); setTotal(0); setError("no-user"); return; } setLoading(true); setError(""); try { const body = { user_id: userId, page, page_size: pageSize, days, }; const res = await post>( "/api/token/consumption", body ); if (res && res.successful && res.code === 0) { setItems(res.data?.transaction_records || []); setTotal(res.data?.total || 0); setServerPeriodDays(res.data?.period_days || days); } else { setItems([]); setTotal(0); setError("api-error"); } } catch { setItems([]); setTotal(0); setError("api-error"); } finally { setLoading(false); } }, [userId, page, pageSize, days]); useEffect(() => { loadData(); }, [loadData]); const handleChangeDays = useCallback((d: PeriodDays) => { setDays(d); setPage(1); }, []); const handlePrev = useCallback(() => { if (canPrev) setPage((p) => p - 1); }, [canPrev]); const handleNext = useCallback(() => { if (canNext) setPage((p) => p + 1); }, [canNext]); const periodLabel = useMemo(() => serverPeriodDays || days, [serverPeriodDays, days]); const toggleExpand = useCallback((key: string) => { setExpandedKeys((prev) => { const next = new Set(prev); if (next.has(key)) { next.delete(key); } else { next.add(key); } return next; }); }, []); const dateFormatter = useMemo(() => { try { return new Intl.DateTimeFormat(navigator.languages[0], { dateStyle: 'medium', timeStyle: 'medium', }); } catch { return new Intl.DateTimeFormat(); } }, []); const getAmountColorClass = useCallback((change: ChangeType | undefined) => { if (change === 'INCREASE') return 'text-green-400'; if (change === 'DECREASE') return 'text-rose-400'; return 'text-white'; }, []); const formatSource = useCallback((source: string | undefined) => { if (!source) return '-'; const map: Record = { video_generation: 'Video Generation', manual_admin: 'Manual (Admin)', subscription: 'Subscription', purchase_recharge: 'Purchase', refund: 'Refund', }; return map[source] || source.replace(/_/g, ' ').replace(/\b\w/g, (m) => m.toUpperCase()); }, []); return (

Credit Usage Details

{([7, 30, 90] as PeriodDays[]).map((d) => ( ))}
Period: {periodLabel || "-"} days
{loading ? ( ) : error ? ( ) : items.length === 0 ? ( ) : ( items.map((it, idx) => { const key = `${it.created_at}-${idx}`; const isExpanded = expandedKeys.has(key); return ( {isExpanded && it.project_info && ( )} ); }) )}
Kind Credits From Date
Loading...
-
-
{it?.transaction_type || '-'} {it?.project_info ? ( ) : null}
{Number.isFinite(it?.amount as number) ? `${it.change_type === 'INCREASE' ? '+' : it.change_type === 'DECREASE' ? '-' : ''}${it.amount}` : '-'} {formatSource(it?.source_type)} {it?.created_at ? dateFormatter.format(new Date(it.created_at)) : '-'}
Project: {it.project_info.project_name || '-'}
ID: {it.project_info.project_id || '-'}
{(it.project_info.videos || []).length === 0 ? ( ) : ( it.project_info.videos.map((v, vIdx) => ( )) )}
-
{v.video_name && v.video_name.trim() ? v.video_name : `video${vIdx + 1}`} {Number.isFinite(v.amount as number) ? `${v.change_type === 'INCREASE' ? '+' : v.change_type === 'DECREASE' ? '-' : ''}${v.amount}` : '-'} {formatSource(v.source_type)} {v.created_at ? dateFormatter.format(new Date(v.created_at)) : '-'}
Total {Number.isFinite(total) ? total : 0}
Page {page}
); }; export default React.memo(UsageView);