import { App, Button, Input, Skeleton } from "antd"; import { Check, ChevronLeft, ChevronRight, CircleAlert, Coins, CreditCard, History, RefreshCw, TicketCheck, WalletCards } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import { useLocation, useNavigate } from "react-router"; import { PaymentCheckoutCode } from "@/components/payment-checkout-code"; import { AppModal } from "@/components/ui/product/app-modal"; import { formatCredits } from "@/constant/credits"; import { closePaymentOrder, createPaymentOrder, getPaymentOrder, listPaymentProviders, listTopupProducts, queryPaymentOrder, refreshPaymentCheckout, type PaymentOrder, type PaymentProvider, type TopupProduct } from "@/services/api/payments"; import { getWallet, redeemCredits, type CreditLedgerEntry, type WalletSummary } from "@/services/api/wallet"; import { cn } from "@/lib/utils"; import { openWorkspaceWallet, WORKSPACE_WALLET_OPEN_EVENT, type WorkspaceWalletOpenDetail } from "@/lib/workspace-wallet"; import { useUserStore } from "@/stores/use-user-store"; type WalletModalTab = "topup" | "redeem" | "history"; export function WorkspaceWalletHost() { const creditsEnabled = useUserStore((state) => state.features.creditsEnabled); const { pathname, search } = useLocation(); const navigate = useNavigate(); const [open, setOpen] = useState(false); const [pendingPaymentOrderId, setPendingPaymentOrderId] = useState(""); const [paymentInvalid, setPaymentInvalid] = useState(false); const applyOpen = (detail: WorkspaceWalletOpenDetail = {}) => { if (!useUserStore.getState().features.creditsEnabled) return; setPendingPaymentOrderId(detail.paymentOrderId || ""); setPaymentInvalid(Boolean(detail.paymentInvalid)); setOpen(true); }; useEffect(() => { const handleOpen = (raw: Event) => { applyOpen((raw as CustomEvent).detail || {}); }; window.addEventListener(WORKSPACE_WALLET_OPEN_EVENT, handleOpen); return () => window.removeEventListener(WORKSPACE_WALLET_OPEN_EVENT, handleOpen); }, []); useEffect(() => { if (pathname !== "/wallet") return; const params = new URLSearchParams(search); openWorkspaceWallet({ paymentOrderId: params.get("paymentOrder") || undefined, paymentInvalid: params.get("payment") === "invalid", }); navigate("/", { replace: true }); }, [navigate, pathname, search]); if (!creditsEnabled) return null; return ( { setOpen(false); setPendingPaymentOrderId(""); setPaymentInvalid(false); }} /> ); } export function WorkspaceWalletModal({ open, onClose, pendingPaymentOrderId, paymentInvalid }: { open: boolean; onClose: () => void; pendingPaymentOrderId?: string; paymentInvalid?: boolean }) { const { message } = App.useApp(); const [tab, setTab] = useState("topup"); const [wallet, setWallet] = useState(null); const [walletLoading, setWalletLoading] = useState(false); const [walletError, setWalletError] = useState(""); const [page, setPage] = useState(1); const [products, setProducts] = useState([]); const [providers, setProviders] = useState([]); const [paymentsLoading, setPaymentsLoading] = useState(false); const [selectedProductId, setSelectedProductId] = useState(""); const [selectedProviderId, setSelectedProviderId] = useState(""); const [code, setCode] = useState(""); const [redeeming, setRedeeming] = useState(false); const [paymentCreating, setPaymentCreating] = useState(false); const [paymentQuerying, setPaymentQuerying] = useState(false); const [paymentOrder, setPaymentOrder] = useState(null); const [paymentOpen, setPaymentOpen] = useState(false); const [clock, setClock] = useState(Date.now()); const idempotencyKey = useRef(""); const completedOrderId = useRef(""); const requestSequence = useRef(0); const selectedProduct = useMemo(() => products.find((item) => item.id === selectedProductId), [products, selectedProductId]); const selectedProvider = useMemo(() => providers.find((item) => item.id === selectedProviderId), [providers, selectedProviderId]); const reloadWallet = async (targetPage = page) => { const sequence = ++requestSequence.current; setWalletLoading(true); setWalletError(""); try { const result = await getWallet(targetPage, 20, "all"); if (sequence === requestSequence.current) setWallet(result); } catch (error) { if (sequence === requestSequence.current) setWalletError(error instanceof Error ? error.message : "读取积分账户失败"); } finally { if (sequence === requestSequence.current) setWalletLoading(false); } }; useEffect(() => { if (!open) return; setPage(1); void reloadWallet(1); setPaymentsLoading(true); Promise.all([listTopupProducts(), listPaymentProviders()]) .then(([productResult, providerResult]) => { setProducts(productResult.products.filter((item) => item.enabled)); setProviders(providerResult.providers.filter((item) => item.enabled && item.pluginEnabled && item.configured)); setSelectedProductId((current) => current || productResult.products.find((item) => item.canPurchase)?.id || productResult.products.find((item) => item.enabled)?.id || ""); setSelectedProviderId((current) => current || providerResult.providers.find((item) => item.enabled && item.pluginEnabled && item.configured)?.id || ""); }) .catch((error) => message.error(error instanceof Error ? error.message : "读取充值配置失败")) .finally(() => setPaymentsLoading(false)); }, [open]); useEffect(() => { if (!open || !paymentInvalid) return; message.error("支付结果无效,未产生积分充值"); }, [open, paymentInvalid]); useEffect(() => { idempotencyKey.current = ""; }, [selectedProductId, selectedProviderId]); useEffect(() => { if (!paymentOpen || !paymentOrder || !["created", "pending", "closing"].includes(paymentOrder.status)) return; const interval = window.setInterval(() => { void refreshPaymentStatus(paymentOrder.id, true); }, 4_000); return () => window.clearInterval(interval); }, [paymentOpen, paymentOrder?.id, paymentOrder?.status]); useEffect(() => { if (!paymentOpen) return; const interval = window.setInterval(() => setClock(Date.now()), 1_000); return () => window.clearInterval(interval); }, [paymentOpen]); const announceWalletUpdated = async (orderId?: string) => { if (orderId && completedOrderId.current === orderId) return; if (orderId) completedOrderId.current = orderId; setPage(1); await reloadWallet(1); window.dispatchEvent(new CustomEvent("wallet:updated")); }; useEffect(() => { if (!open || !pendingPaymentOrderId) return; getPaymentOrder(pendingPaymentOrderId) .then(async ({ order }) => { setPaymentOrder(order); setPaymentOpen(true); if (order.status === "credited") await announceWalletUpdated(order.id); }) .catch((error) => message.error(error instanceof Error ? error.message : "读取支付结果失败")); }, [open, pendingPaymentOrderId]); const redeem = async () => { const normalized = code.trim().toLowerCase(); if (normalized.length !== 32) { message.error("请输入完整的 32 位兑换码"); return; } setRedeeming(true); try { await redeemCredits(normalized); setCode(""); await announceWalletUpdated(); message.success("兑换成功,积分已到账"); } catch (error) { message.error(error instanceof Error ? error.message : "兑换失败"); } finally { setRedeeming(false); } }; const startPayment = async () => { if (!selectedProduct || !selectedProvider) { message.error("请选择充值商品和支付方式"); return; } if (!selectedProduct.canPurchase) { message.error("该充值商品当前不可购买,请刷新后重试"); return; } setPaymentCreating(true); try { if (!idempotencyKey.current) idempotencyKey.current = crypto.randomUUID(); const result = await createPaymentOrder({ productId: selectedProduct.id, providerId: selectedProvider.id, idempotencyKey: idempotencyKey.current }); idempotencyKey.current = ""; setPaymentOrder(result.order); if (result.order.status === "credited") await announceWalletUpdated(result.order.id); if (result.order.checkout.mode === "redirect" && result.order.checkout.url) { window.location.assign(result.order.checkout.url); return; } setPaymentOpen(true); } catch (error) { message.error(error instanceof Error ? error.message : "创建支付订单失败"); } finally { setPaymentCreating(false); } }; async function refreshPaymentStatus(orderId = paymentOrder?.id, silent = false) { if (!orderId || paymentQuerying) return; setPaymentQuerying(true); try { const result = await queryPaymentOrder(orderId); setPaymentOrder(result.order); if (result.order.status === "credited") { await announceWalletUpdated(result.order.id); if (!silent) message.success("支付已确认,积分已到账"); } else if (!silent && result.order.status === "closed") message.warning("订单已关闭,未产生积分充值"); else if (!silent) message.info("渠道尚未确认支付,请稍后再试"); } catch (error) { if (!silent) message.error(error instanceof Error ? error.message : "查询支付结果失败"); } finally { setPaymentQuerying(false); } } const cancelPayment = async () => { if (!paymentOrder) return; setPaymentQuerying(true); try { const result = await closePaymentOrder(paymentOrder.id); setPaymentOrder(result.order); if (result.order.status === "credited") await announceWalletUpdated(result.order.id); else message.success("未支付订单已关闭"); } catch (error) { message.error(error instanceof Error ? error.message : "关闭订单失败"); } finally { setPaymentQuerying(false); } }; const retryCheckout = async () => { if (!paymentOrder) return; setPaymentQuerying(true); try { const result = await refreshPaymentCheckout(paymentOrder.id); setPaymentOrder(result.order); if (result.order.checkout.mode === "redirect" && result.order.checkout.url) window.location.assign(result.order.checkout.url); } catch (error) { message.error(error instanceof Error ? error.message : "刷新支付入口失败"); } finally { setPaymentQuerying(false); } }; const available = wallet?.account.availableMicrocredits ?? 0; const totalPages = Math.max(1, Math.ceil((wallet?.total || 0) / 20)); return ( <>
积分中心

充值、兑换与消费记录

为下一次创作补充积分,随时查看每一笔收支。

可用积分 {wallet ? formatCredits(available, 6) : "--"} 冻结 {wallet ? formatCredits(wallet.account.reservedMicrocredits, 6) : "--"}
{tab === "topup" ? (

在线充值

选择积分套餐和支付方式。

{paymentsLoading ? ( ) : products.length && providers.length ? ( <>
{products.map((product) => { const unavailableLabel = product.saleStatus === "upcoming" ? `发售时间:${product.saleStartAt ? new Date(product.saleStartAt).toLocaleString("zh-CN", { hour12: false }) : "待定"}` : product.saleStatus === "ended" ? "已结束" : product.saleStatus === "sold_out" ? `已售罄(库存 ${product.stockRemaining ?? 0})` : product.saleStrategy === "inventory" ? `库存 ${product.stockRemaining ?? 0}` : product.saleStrategy === "periodic" ? `每 ${product.periodDays} 天限购 ${product.periodPurchaseLimit} 次` : "不限量"; return ( ); })}
{providers.map((provider) => ( ))}
) : (
在线充值暂不可用 当前没有已启用的充值商品或支付渠道,请使用兑换码或联系管理员。
)}
) : tab === "redeem" ? (

兑换码

输入兑换码,将积分存入当前账户。

setCode(event.target.value.replace(/\s/g, ""))} onPressEnter={() => void redeem()} />
) : (

积分消耗历史

包含充值、兑换、生成消费、冻结与退款。

{walletError ? (
记录加载失败 {walletError}
) : walletLoading && !wallet ? ( ) : wallet?.entries.length ? (
{wallet.entries.map((entry) => ( ))}
) : (
还没有积分记录 完成充值、兑换或生成任务后,记录会显示在这里。
)}
第 {page} / {totalPages} 页
)}
setPaymentOpen(false)} footer={paymentFooter(paymentOrder, paymentQuerying, () => setPaymentOpen(false), cancelPayment, refreshPaymentStatus, retryCheckout)} > {paymentOrder ? (
¥ {(paymentOrder.amountFen / 100).toFixed(2)}

{paymentOrder.productName} · {formatCredits(paymentOrder.creditsMicrocredits, 6)} 积分

{paymentOrder.status === "pending" && paymentOrder.checkout.mode === "qr_code" && paymentOrder.checkout.value ? ( <> 请使用支付应用扫码完成支付 ) : null}
) : null}
); } function WalletLedgerRow({ entry }: { entry: CreditLedgerEntry }) { const positive = entry.amountMicrocredits > 0; const title = entry.type === "consume" ? "模型调用" : entry.type === "refund" ? "消费退款" : entry.type === "payment_topup" ? "在线充值" : entry.type === "redeem" ? "兑换码充值" : entry.note || "积分调整"; return (
{positive ? : }
{title} {[entry.scene, entry.model, entry.note].filter(Boolean).join(" · ") || "积分账户变动"}
{positive ? "+" : ""} {formatCredits(entry.amountMicrocredits, 6)}
); } function PaymentStatus({ order, now }: { order: PaymentOrder; now: number }) { if (order.status === "credited") return
支付已确认,积分已经到账
; if (order.status === "closed") return
订单已关闭,未产生积分充值
; if (order.status === "create_failed") return
支付入口创建失败,请重新生成支付入口。
; const remaining = Math.max(0, Math.floor((new Date(order.expiresAt).getTime() - now) / 1000)); const hours = Math.floor(remaining / 3600); const minutes = Math.floor((remaining % 3600) / 60); const seconds = remaining % 60; return (
订单剩余 {String(hours).padStart(2, "0")}:{String(minutes).padStart(2, "0")}:{String(seconds).padStart(2, "0")},将自动确认支付结果
); } function paymentFooter(order: PaymentOrder | null, loading: boolean, close: () => void, cancel: () => Promise, query: (id?: string, silent?: boolean) => Promise, retry: () => Promise) { if (order?.status === "pending") return [ , , ]; if (order?.status === "create_failed") return [ , , ]; return [ , ]; }