diff --git a/app/usage/page.tsx b/app/usage/page.tsx index 1e96c9a..ef59718 100644 --- a/app/usage/page.tsx +++ b/app/usage/page.tsx @@ -5,7 +5,7 @@ import UsageView from "@/components/pages/usage-view"; const UsagePage: React.FC = () => { return ( -
+
); diff --git a/components/layout/top-bar.tsx b/components/layout/top-bar.tsx index 15c7d7a..1f77356 100644 --- a/components/layout/top-bar.tsx +++ b/components/layout/top-bar.tsx @@ -407,7 +407,7 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe ? "Loading..." : `${credits} credits`} - {/* */} +
{/* Purchase Credits 按钮 */} diff --git a/components/pages/usage-view.tsx b/components/pages/usage-view.tsx index 30b24cc..f97cf71 100644 --- a/components/pages/usage-view.tsx +++ b/components/pages/usage-view.tsx @@ -4,15 +4,42 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { post } from "@/api/request"; /** 接口请求与返回类型定义 */ -interface ConsumptionItem { - /** 项目名称 */ - project_name: string; - /** 总消耗(字符串数字) */ - total_consumption: string; +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 ConsumptionResponseData { - consumption_list: ConsumptionItem[]; +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; @@ -42,9 +69,10 @@ const UsageView: React.FC = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(""); - const [items, setItems] = 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(() => { @@ -80,12 +108,12 @@ const UsageView: React.FC = () => { page_size: pageSize, days, }; - const res = await post>( + const res = await post>( "/api/token/consumption", body ); if (res && res.successful && res.code === 0) { - setItems(res.data?.consumption_list || []); + setItems(res.data?.transaction_records || []); setTotal(res.data?.total || 0); setServerPeriodDays(res.data?.period_days || days); } else { @@ -121,6 +149,52 @@ const UsageView: React.FC = () => { 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(undefined, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }); + } 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 (
@@ -153,36 +227,111 @@ const UsageView: React.FC = () => { - - + + + + {loading ? ( - + ) : error ? ( - - + ) : items.length === 0 ? ( - - + ) : ( - items.map((it, idx) => ( - - - - - )) + items.map((it, idx) => { + const key = `${it.created_at}-${idx}`; + const isExpanded = expandedKeys.has(key); + return ( + + + + + + + + {isExpanded && it.project_info && ( + + + + )} + + ); + }) )}
Project NameTotal ConsumptionKindCreditsFromDate
Loading...Loading...
---
---
- {it?.project_name || "-"} - - {it?.total_consumption ?? "-"} -
+
+ {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)) : '-'} +
+
+