import { useCallback, useEffect, useState } from "react"; import { LocalOnlyFooter, ProductHeader } from "./project-pages.js"; import "./credits-page.css"; type EntryType = "registration_grant" | "generation_reserve" | "generation_commit" | "generation_release" | "admin_adjustment"; interface CreditBalance { available_balance: number; reserved_balance: number; updated_at: string; } interface LedgerEntry { amount: number; available_after: number; available_before: number; created_at: string; entry_id: string; entry_type: EntryType; model_id: string | null; reason: string | null; reference_id: string | null; reference_type: "registration" | "generation" | "admin_adjustment" | null; reserved_after: number; reserved_before: number; status: "succeeded" | "frozen" | "committed" | "released"; } interface LedgerResponse { credits: Pick; entries: LedgerEntry[]; next_cursor: string | null; updated_at: string; } const entryLabels: Record = { admin_adjustment: "管理员调整", generation_commit: "生成结算", generation_release: "生成释放", generation_reserve: "生成冻结", registration_grant: "注册赠点", }; const statusLabels = { committed: "已结算", frozen: "已冻结", released: "已释放", succeeded: "已完成", } as const; async function readJson(url: string) { const response = await fetch(url, { credentials: "same-origin" }); if (response.status === 401) { window.dispatchEvent(new Event("dada:session-invalid")); throw new Error("session_invalid"); } if (!response.ok) throw new Error("request_failed"); return response.json() as Promise; } function localTime(value: string) { return new Intl.DateTimeFormat("zh-CN", { dateStyle: "medium", timeStyle: "short" }).format(new Date(value)); } export function CreditsPage() { const [balance, setBalance] = useState(); const [entries, setEntries] = useState([]); const [nextCursor, setNextCursor] = useState(null); const [eventType, setEventType] = useState(""); const [from, setFrom] = useState(""); const [to, setTo] = useState(""); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [failed, setFailed] = useState(false); const load = useCallback(async (cursor?: string) => { const query = new URLSearchParams({ limit: "20" }); if (cursor) query.set("cursor", cursor); if (eventType) query.set("event_type", eventType); if (from) query.set("from", new Date(`${from}T00:00:00`).toISOString()); if (to) query.set("to", new Date(`${to}T23:59:59.999`).toISOString()); if (cursor) setLoadingMore(true); else setLoading(true); setFailed(false); try { const [nextBalance, ledger] = await Promise.all([ readJson("/api/v1/me/credits"), readJson(`/api/v1/me/credit-ledger?${query}`), ]); setBalance(nextBalance); setEntries((current) => cursor ? [...current, ...ledger.entries] : ledger.entries); setNextCursor(ledger.next_cursor); } catch (error) { if (!(error instanceof Error) || error.message !== "session_invalid") setFailed(true); } finally { setLoading(false); setLoadingMore(false); } }, [eventType, from, to]); useEffect(() => { void load(); }, [load]); return (

ACCOUNT CREDIT

点数明细

{balance ? : null}
{balance ? (
当前可用点数{balance.available_balance}
冻结点数{balance.reserved_balance}已冻结 {balance.reserved_balance} 点

可用点数不足时,请联系管理员调整点数。

) : null} {failed ?

点数读取失败。{balance ? "当前保留上次余额,提交时仍以服务端实时校验为准。" : "请重新读取。"}

: null}

变动记录

{loading && entries.length === 0 ?

正在读取点数记录

: null} {!loading && entries.length === 0 && !failed ?

暂无点数变动

: null} {entries.length > 0 ? (
{entries.map((entry) => ( ))}
时间事件状态变动关联余额
{entryLabels[entry.entry_type]}{entry.reason ? {entry.reason} : null} {statusLabels[entry.status]} 变动 {entry.amount > 0 ? `+${entry.amount}` : entry.amount} {entry.reference_type === "generation" ? "生成任务" : entry.reference_type === "admin_adjustment" ? "管理操作" : "注册"} 可用 {entry.available_after} / 冻结 {entry.reserved_after}
) : null} {nextCursor ? : null} {failed ? : null}
); }