forked from 77media/video-flow
230 lines
8.4 KiB
TypeScript
230 lines
8.4 KiB
TypeScript
"use client";
|
||
|
||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||
import { post } from "@/api/request";
|
||
|
||
/** 接口请求与返回类型定义 */
|
||
interface ConsumptionItem {
|
||
/** 项目名称 */
|
||
project_name: string;
|
||
/** 总消耗(字符串数字) */
|
||
total_consumption: string;
|
||
}
|
||
|
||
interface ConsumptionResponseData {
|
||
consumption_list: ConsumptionItem[];
|
||
page: number;
|
||
page_size: number;
|
||
total: number;
|
||
period_days: 7 | 30 | 90;
|
||
}
|
||
|
||
interface ApiResponse<T = any> {
|
||
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<PeriodDays>(7);
|
||
const [page, setPage] = useState<number>(1);
|
||
const pageSize = 20;
|
||
|
||
const [loading, setLoading] = useState<boolean>(false);
|
||
const [error, setError] = useState<string>("");
|
||
const [items, setItems] = useState<ConsumptionItem[]>([]);
|
||
const [total, setTotal] = useState<number>(0);
|
||
const [serverPeriodDays, setServerPeriodDays] = useState<PeriodDays>(90);
|
||
|
||
/** 读取 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<ApiResponse<ConsumptionResponseData>>(
|
||
"/api/token/consumption",
|
||
body
|
||
);
|
||
if (res && res.successful && res.code === 0) {
|
||
setItems(res.data?.consumption_list || []);
|
||
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]);
|
||
|
||
return (
|
||
<div data-alt="usage-view-container" className="mx-auto max-w-5xl p-6">
|
||
<div data-alt="header" className="mb-6 flex items-center justify-between">
|
||
<h2 className="text-xl font-semibold text-white">Credit Usage Details</h2>
|
||
<div data-alt="period-switch" className="inline-flex rounded-lg bg-white/5 p-1">
|
||
{([7, 30, 90] as PeriodDays[]).map((d) => (
|
||
<button
|
||
key={d}
|
||
type="button"
|
||
data-alt={`period-${d}`}
|
||
onClick={() => handleChangeDays(d)}
|
||
className={
|
||
`px-3 py-1.5 text-sm rounded-md transition-colors ` +
|
||
(days === d
|
||
? "bg-[#C039F6] text-white"
|
||
: "text-white/80 hover:bg-white/10")
|
||
}
|
||
>
|
||
{d}d
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div data-alt="meta" className="mb-3 text-sm text-white/70">
|
||
<span data-alt="meta-period">Period: {periodLabel || "-"} days</span>
|
||
</div>
|
||
|
||
<div data-alt="table-wrapper" className="overflow-hidden rounded-lg border border-white/10">
|
||
<table data-alt="table" className="min-w-full table-fixed">
|
||
<thead className="bg-white/5">
|
||
<tr>
|
||
<th className="w-2/3 px-4 py-2 text-left text-sm font-medium text-white">Project Name</th>
|
||
<th className="w-1/3 px-4 py-2 text-right text-sm font-medium text-white">Total Consumption</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-white/10">
|
||
{loading ? (
|
||
<tr>
|
||
<td className="px-4 py-3 text-white/70" colSpan={2} data-alt="row-loading">Loading...</td>
|
||
</tr>
|
||
) : error ? (
|
||
<tr>
|
||
<td className="px-4 py-3 text-white/70" data-alt="row-error">-</td>
|
||
<td className="px-4 py-3 text-right text-white/70">-</td>
|
||
</tr>
|
||
) : items.length === 0 ? (
|
||
<tr>
|
||
<td className="px-4 py-3 text-white/70" data-alt="row-empty">-</td>
|
||
<td className="px-4 py-3 text-right text-white/70">-</td>
|
||
</tr>
|
||
) : (
|
||
items.map((it, idx) => (
|
||
<tr key={`${it.project_name}-${idx}`}>
|
||
<td className="truncate px-4 py-2 text-white/90" data-alt="cell-project-name">
|
||
{it?.project_name || "-"}
|
||
</td>
|
||
<td className="px-4 py-2 text-right text-white" data-alt="cell-total-consumption">
|
||
{it?.total_consumption ?? "-"}
|
||
</td>
|
||
</tr>
|
||
))
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div data-alt="pagination" className="mt-4 flex items-center justify-between text-sm text-white/80">
|
||
<div data-alt="total-info">
|
||
Total {Number.isFinite(total) ? total : 0}
|
||
</div>
|
||
<div data-alt="pager" className="inline-flex gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={handlePrev}
|
||
disabled={!canPrev}
|
||
className={
|
||
"rounded-md px-3 py-1.5 transition-colors " +
|
||
(canPrev ? "bg-white/10 hover:bg-white/20" : "bg-white/5 text-white/40 cursor-not-allowed")
|
||
}
|
||
data-alt="prev-page"
|
||
>
|
||
Previous
|
||
</button>
|
||
<span data-alt="page-indicator" className="px-1 py-1.5">Page {page}</span>
|
||
<button
|
||
type="button"
|
||
onClick={handleNext}
|
||
disabled={!canNext}
|
||
className={
|
||
"rounded-md px-3 py-1.5 transition-colors " +
|
||
(canNext ? "bg-white/10 hover:bg-white/20" : "bg-white/5 text-white/40 cursor-not-allowed")
|
||
}
|
||
data-alt="next-page"
|
||
>
|
||
Next
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default React.memo(UsageView);
|
||
|
||
|