video-flow-b/components/pages/usage-view.tsx
2025-09-15 19:15:51 +08:00

230 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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);