Files
NianAIGC/components/creation-balance.tsx
T
2026-10-02 19:56:32 +08:00

89 lines
4.6 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { Loader2, RefreshCw, WalletCards } from "lucide-react";
import clsx from "clsx";
import { useBrowserAuth } from "@/components/browser-auth";
import { formatBillingAmount } from "@/lib/billing";
import { fetchOrganizationBalance, type OrganizationBalance } from "@/lib/client/organization-balance";
type BalanceState = {
scope: string;
balance: OrganizationBalance | null;
loading: boolean;
error: string | null;
};
export function CreationBalance({ refreshKey, estimatedAmountFen }: { refreshKey: string; estimatedAmountFen?: number }) {
const { user, status, isSuperAdmin } = useBrowserAuth();
const organizationId = user?.organizationId || "";
const scope = `${user?.id || ""}:${organizationId}:${isSuperAdmin}`;
const [retry, setRetry] = useState(0);
const [state, setState] = useState<BalanceState>({ scope: "", balance: null, loading: false, error: null });
useEffect(() => {
if (status !== "ready" || isSuperAdmin || !organizationId) return;
let active = true;
let controller: AbortController | null = null;
const refresh = async () => {
controller?.abort();
const request = new AbortController();
controller = request;
setState((previous) => ({ scope, balance: previous.scope === scope ? previous.balance : null, loading: true, error: null }));
try {
const balance = await fetchOrganizationBalance(organizationId, request.signal);
if (active && !request.signal.aborted) setState({ scope, balance, loading: false, error: null });
} catch (cause) {
if (active && !request.signal.aborted) {
setState({ scope, balance: null, loading: false, error: cause instanceof Error ? cause.message : "余额读取失败,请重试。" });
}
}
};
const refreshWhenVisible = () => {
if (document.visibilityState === "visible") void refresh();
};
void refresh();
const timer = window.setInterval(refreshWhenVisible, 30000);
window.addEventListener("focus", refreshWhenVisible);
document.addEventListener("visibilitychange", refreshWhenVisible);
return () => {
active = false;
controller?.abort();
window.clearInterval(timer);
window.removeEventListener("focus", refreshWhenVisible);
document.removeEventListener("visibilitychange", refreshWhenVisible);
};
}, [scope, organizationId, status, isSuperAdmin, refreshKey, retry]);
if (status !== "ready") return <span className="creation-balance muted">正在读取余额…</span>;
if (isSuperAdmin) {
return <a className="creation-balance creation-balance-exempt" href="/billing" title="超级管理员生成任务会记录费用,但不扣减组织余额;点击查看计费中心。"><WalletCards size={15} aria-hidden="true" />超管不计额度</a>;
}
if (!organizationId) {
return <span className="creation-balance creation-balance-warning" title="当前账号未绑定组织,请联系管理员绑定后使用共享余额。"><WalletCards size={15} aria-hidden="true" />未绑定组织</span>;
}
const current = state.scope === scope ? state : null;
const balance = current?.balance;
const loading = !current || current.loading;
const error = current?.error;
const insufficient = balance && (balance.wallet.balanceFen <= 0 || estimatedAmountFen !== undefined && balance.wallet.balanceFen < estimatedAmountFen);
const amount = balance ? formatBillingAmount(balance.wallet.balanceFen) : error ? "暂不可用" : "读取中…";
const description = error || (insufficient
? "组织余额不足,请联系管理员充值;点击查看余额明细。"
: `${balance?.organization.name || user?.organizationName || "当前组织"}的共享可用余额,图片和视频共用;点击查看余额明细。`);
return (
<div className={clsx("creation-balance", (insufficient || error) && "creation-balance-warning")}>
<a className="creation-balance-summary" href="/billing" title={description}>
<WalletCards size={15} aria-hidden="true" />
<span>组织余额</span>
<strong aria-live="polite">{amount}</strong>
</a>
<button type="button" className="creation-balance-refresh" onClick={() => setRetry((value) => value + 1)} disabled={loading} aria-label={error ? "重试读取组织余额" : "刷新组织余额"} title={error ? "读取失败,点击重试" : "刷新组织余额"}>
{loading ? <Loader2 size={14} className="spin" /> : <RefreshCw size={14} />}
</button>
{insufficient ? <span className="creation-balance-note">余额不足,请联系管理员充值</span> : null}
</div>
);
}