89 lines
4.6 KiB
TypeScript
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>
|
|
);
|
|
}
|