feat(web): 积分中心 - 改为全局弹窗并去掉独立钱包页

顶栏、账户菜单、命令面板、画布余额和 /wallet 回跳都打开同一积分中心弹窗,支付结果不再跳进整页超市。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ddCatandCursor committed 2026-09-17 23:32:19 +08:00
1 parent bb2a79d365
commit 87d92fcf8b
15 files changed
+183 -772

No files matched your search

@@ -1,16 +1,78 @@
import { App, Button, Input, QRCode, Skeleton } from "antd";
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, listPaymentProviders, listTopupProducts, queryPaymentOrder, refreshPaymentCheckout, type PaymentOrder, type PaymentProvider, type TopupProduct } from "@/services/api/payments";
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" | "history";
export function WorkspaceWalletModal({ open, onClose }: { open: boolean; onClose: () => void }) {
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<WorkspaceWalletOpenDetail>).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 (
<WorkspaceWalletModal
open={open}
pendingPaymentOrderId={pendingPaymentOrderId}
paymentInvalid={paymentInvalid}
onClose={() => {
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<WalletModalTab>("topup");
const [wallet, setWallet] = useState<WalletSummary | null>(null);
@@ -66,6 +128,11 @@ export function WorkspaceWalletModal({ open, onClose }: { open: boolean; onClose
.finally(() => setPaymentsLoading(false));
}, [open]);
useEffect(() => {
if (!open || !paymentInvalid) return;
message.error("支付结果无效,未产生积分充值");
}, [open, paymentInvalid]);
useEffect(() => {
idempotencyKey.current = "";
}, [selectedProductId, selectedProviderId]);
@@ -92,6 +159,17 @@ export function WorkspaceWalletModal({ open, onClose }: { open: boolean; onClose
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) {
@@ -257,7 +335,7 @@ export function WorkspaceWalletModal({ open, onClose }: { open: boolean; onClose
<span className="workspace-wallet-payment-icon"><CreditCard /></span>
<strong>¥ {(paymentOrder.amountFen / 100).toFixed(2)}</strong>
<p>{paymentOrder.productName} · {formatCredits(paymentOrder.creditsMicrocredits, 6)} 积分</p>
{paymentOrder.status === "pending" && paymentOrder.checkout.mode === "qr_code" && paymentOrder.checkout.value ? <><QRCode value={paymentOrder.checkout.value} size={208} bordered={false} /><span>请使用支付应用扫码完成支付</span></> : null}
{paymentOrder.status === "pending" && paymentOrder.checkout.mode === "qr_code" && paymentOrder.checkout.value ? <><PaymentCheckoutCode value={paymentOrder.checkout.value} /><span>请使用支付应用扫码完成支付</span></> : null}
<PaymentStatus order={paymentOrder} now={clock} />
</div> : null}
</AppModal>