Ver código fonte

feat: 对接支付逻辑

BaiLuoYan 4 semanas atrás
pai
commit
2fa0412358

+ 1 - 0
package.json

@@ -51,6 +51,7 @@
     "less": "^4.2.2",
     "lint-staged": "^15.5.0",
     "lodash-es": "^4.17.21",
+    "qrcode.react": "4.2.0",
     "qs": "^6.14.0",
     "ramda": "^0.30.1",
     "react": "^18.2.0",

+ 12 - 0
pnpm-lock.yaml

@@ -89,6 +89,9 @@ importers:
       lodash-es:
         specifier: ^4.17.21
         version: 4.17.21
+      qrcode.react:
+        specifier: 4.2.0
+        version: 4.2.0([email protected])
       qs:
         specifier: ^6.14.0
         version: 6.14.0
@@ -3722,6 +3725,11 @@ packages:
     resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
     engines: {node: '>=6'}
 
+  [email protected]:
+    resolution: {integrity: sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
   [email protected]:
     resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
     engines: {node: '>=0.6'}
@@ -8859,6 +8867,10 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]([email protected]):
+    dependencies:
+      react: 18.3.1
+
   [email protected]:
     dependencies:
       side-channel: 1.1.0

+ 132 - 82
src/components/Dialog/index.tsx

@@ -4,6 +4,9 @@ import { Icon, IconifyIcon } from '@iconify/react';
 
 import { useResponsive } from '@/hooks/useSize';
 import { dialogModel } from '@/models/dialogModel';
+
+import closeIcon from '@/assets/iconify/single-color/close.svg';
+
 import { useAction } from './useAction';
 
 export interface DialogButton {
@@ -21,99 +24,146 @@ export interface DialogProps {
     buttons?: DialogButton[];
     zIndex: number;
     maskClosable?: boolean;
+    closeable?: boolean;
 }
 
-const Dialog = memo(({ open, id, icon, title, content, buttons, zIndex, maskClosable = true }: DialogProps) => {
-    const { isMobile } = useResponsive();
-    const { closeDialog } = dialogModel.useModel();
-    const { handleOverlayClick, handleButtonClick } = useAction({ id, closeDialog, maskClosable });
+const Dialog = memo(
+    ({
+        open,
+        id,
+        icon,
+        title,
+        content,
+        buttons,
+        zIndex,
+        maskClosable = true,
+        closeable = false,
+    }: DialogProps) => {
+        const { isMobile } = useResponsive();
+        const { closeDialog } = dialogModel.useModel();
+        const { handleOverlayClick, handleButtonClick } = useAction({
+            id,
+            closeDialog,
+            maskClosable,
+        });
 
-    if (!open) {
-        return null;
-    }
+        if (!open) {
+            return null;
+        }
 
-    const renderIcon = () => {
-        if (!icon) return null;
-        return <Icon icon={icon} className="w-10 h-10 flex-shrink-0 text-white" />;
-    };
+        const renderIcon = () => {
+            if (!icon) return null;
+            return <Icon icon={icon} className="w-10 h-10 flex-shrink-0 text-white" />;
+        };
 
-    return (
-        <Fragment>
-            <div
-                className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center p-4"
-                style={{ zIndex }}
-                onClick={handleOverlayClick}
-            >
+        return (
+            <Fragment>
                 <div
-                    className={`bg-[#272A33] flex flex-col max-w-full ${
-                        isMobile
-                            ? 'w-[328px] rounded-2xl shadow-[0px_8px_36px_0px_rgba(0,0,0,0.16)] pt-2'
-                            : 'w-[544px] rounded-xl shadow-[0px_8px_8px_-4px_rgba(16,24,40,0.04),0px_20px_24px_-4px_rgba(16,24,40,0.1)]'
-                    }`}
-                    onClick={(e) => e.stopPropagation()}
+                    className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center p-4"
+                    style={{ zIndex }}
+                    onClick={handleOverlayClick}
                 >
-                    <div className={`flex flex-col ${isMobile ? 'gap-2.5 p-[10px_24px]' : 'gap-5 p-6 pt-6'}`}>
-                        {isMobile ? (
-                            <>
-                                {icon && <div className="flex justify-start">{renderIcon()}</div>}
-                                {title && (
-                                    <h2 className="text-white text-[22px] leading-[1.27] font-[510] text-start">
-                                        {title}
-                                    </h2>
-                                )}
-                                {content && (
-                                    <div className="text-white px-0 py-2 text-sm leading-[1.43] text-start">
-                                        {content}
-                                    </div>
-                                )}
-                            </>
-                        ) : (
-                            <>
-                                {(icon || title) && (
-                                    <div className="flex items-center gap-[14px]">
-                                        {renderIcon()}
-                                        {title && (
-                                            <h2 className="flex-1 text-white text-2xl leading-[1.17] font-semibold">
-                                                {title}
-                                            </h2>
-                                        )}
-                                    </div>
-                                )}
-                                {content && (
-                                    <div className="text-white text-sm leading-[1.43]">{content}</div>
-                                )}
-                            </>
-                        )}
-                    </div>
-                    {buttons && buttons.length > 0 && (
+                    <div
+                        className={`bg-[#272A33] flex flex-col max-w-full [&_button]:select-none ${
+                            isMobile
+                                ? 'w-[328px] rounded-2xl shadow-[0px_8px_36px_0px_rgba(0,0,0,0.16)] pt-2'
+                                : 'w-[544px] rounded-xl shadow-[0px_8px_8px_-4px_rgba(16,24,40,0.04),0px_20px_24px_-4px_rgba(16,24,40,0.1)]'
+                        }`}
+                        onClick={(e) => e.stopPropagation()}
+                    >
                         <div
-                            className={`flex ${
-                                buttons.length === 1 ? 'flex-col' : 'flex-row'
-                            } ${isMobile ? 'gap-2.5 px-6 pt-4 pb-6' : 'gap-6 p-6'}`}
+                            className={`relative flex flex-col ${isMobile ? 'gap-2.5 p-[10px_24px]' : 'gap-5 p-6'}`}
                         >
-                            {buttons.map((button, index) => (
-                                <button
-                                    key={index}
-                                    type="button"
-                                    onClick={(e) => handleButtonClick(button, e, id)}
-                                    className={`flex-1 rounded-lg flex items-center justify-center gap-2.5 px-5 ${
-                                        isMobile ? 'h-[42px] py-2.5' : 'h-11 py-3.5'
-                                    } ${
-                                        button.variant === 'secondary'
-                                            ? 'bg-[#1C1E25] text-[#646776]'
-                                            : 'bg-[#0EA5E9] text-[#F5F5F5]'
-                                    } text-sm leading-[1.21] font-medium`}
-                                >
-                                    {button.label}
-                                </button>
-                            ))}
+                            {isMobile ? (
+                                <>
+                                    {icon && (
+                                        <div className="flex justify-start">{renderIcon()}</div>
+                                    )}
+                                    {title && (
+                                        <h2 className="text-white text-[22px] leading-[1.27] font-[510] text-start">
+                                            {title}
+                                        </h2>
+                                    )}
+                                    {closeable && (
+                                        <div className="absolute top-[12px] left-0 right-0 h-6 px-6 flex justify-end">
+                                            <button
+                                                type="button"
+                                                onClick={() => closeDialog(id)}
+                                                className="p-0 rounded-lg bg-transparent border-none text-white outline-none focus:outline-none focus:bg-transparent active:bg-transparent hover:text-[#0EA5E9]/80 active:text-[#0EA5E9]/60 [-webkit-tap-highlight-color:transparent] transition-colors"
+                                                aria-label="Close menu"
+                                            >
+                                                <Icon icon={closeIcon} className="w-6 h-6" />
+                                            </button>
+                                        </div>
+                                    )}
+                                    {content && (
+                                        <div className="text-white px-0 py-2 text-sm leading-[1.43] text-start">
+                                            {content}
+                                        </div>
+                                    )}
+                                </>
+                            ) : (
+                                <>
+                                    {(icon || title) && (
+                                        <div className="flex items-center gap-[14px]">
+                                            {renderIcon()}
+                                            {title && (
+                                                <h2 className="flex-1 text-white text-2xl leading-[1.17] font-semibold">
+                                                    {title}
+                                                </h2>
+                                            )}
+                                        </div>
+                                    )}
+                                    {closeable && (
+                                        <div className="absolute top-[26px] left-0 right-0 h-6 px-6 flex justify-end">
+                                            <button
+                                                type="button"
+                                                onClick={() => closeDialog(id)}
+                                                className="p-0 rounded-lg bg-transparent border-none text-white outline-none focus:outline-none focus:bg-transparent active:bg-transparent hover:text-[#0EA5E9]/80 active:text-[#0EA5E9]/60 [-webkit-tap-highlight-color:transparent] transition-colors"
+                                                aria-label="Close menu"
+                                            >
+                                                <Icon icon={closeIcon} className="w-6 h-6" />
+                                            </button>
+                                        </div>
+                                    )}
+                                    {content && (
+                                        <div className="text-white text-sm leading-[1.43]">
+                                            {content}
+                                        </div>
+                                    )}
+                                </>
+                            )}
                         </div>
-                    )}
+                        {buttons && buttons.length > 0 && (
+                            <div
+                                className={`flex ${
+                                    buttons.length === 1 ? 'flex-col' : 'flex-row'
+                                } ${isMobile ? 'gap-2.5 px-6 pt-4 pb-6' : 'gap-6 p-6'}`}
+                            >
+                                {buttons.map((button, index) => (
+                                    <button
+                                        key={index}
+                                        type="button"
+                                        onClick={(e) => handleButtonClick(button, e, id)}
+                                        className={`flex-1 rounded-lg flex items-center justify-center gap-2.5 px-5 ${
+                                            isMobile ? 'h-[42px] py-2.5' : 'h-11 py-3.5'
+                                        } ${
+                                            button.variant === 'secondary'
+                                                ? 'bg-[#1C1E25] text-[#646776]'
+                                                : 'bg-[#0EA5E9] text-[#F5F5F5]'
+                                        } text-sm leading-[1.21] font-medium`}
+                                    >
+                                        {button.label}
+                                    </button>
+                                ))}
+                            </div>
+                        )}
+                    </div>
                 </div>
-            </div>
-        </Fragment>
-    );
-});
+            </Fragment>
+        );
+    }
+);
 
 Dialog.displayName = 'Dialog';
 

+ 2 - 1
src/defines/index.ts

@@ -2,4 +2,5 @@ export * from './docLastUpdated';
 export * from './errorShowType';
 export * from './planTagType';
 export * from './payMethodType';
-export * from './payOrderStatus';
+export * from './payOrderStatus';
+export * from './payUrlShowType';

+ 2 - 2
src/defines/payOrderStatus.ts

@@ -1,6 +1,6 @@
 export enum PayOrderStatus {
-    PENDING = 0,
     PAID = 1,  // 已支付
     FAILED = 2,  // 支付失败
-    OTHER = 3,  // 其他状态
+    PENDING = 3,  // 待支付
+    OTHER = 4,  // 其他状态
 }

+ 4 - 0
src/defines/payUrlShowType.ts

@@ -0,0 +1,4 @@
+export enum PayUrlShowType {
+    JUMP_TO_URL = 1,
+    SHOW_QR_CODE = 2,
+}

+ 4 - 1
src/locales/en-US/pages.ts

@@ -53,7 +53,10 @@ export default {
             loginRequired: 'Please enter username and password',
             loginError: 'Login failed',
             goToPayTitle: 'Go to payment',
-            goToPayDesc: 'Click the button below to go to the payment page',
+            goToPayDesc:
+                'Payment order has been generated for you, click the button below to go to the payment page',
+            goToPayQrDesc:
+                'Your payment order has been created. Please scan the QR code below to complete payment',
             goToPayButton: 'Go to pay',
             waitingTitle: 'Waiting for payment',
             waitingDesc:

+ 4 - 1
src/locales/fa-IR/pages.ts

@@ -53,7 +53,10 @@ export default {
             loginRequired: 'لطفاً نام کاربری و رمز عبور را وارد کنید',
             loginError: 'ورود ناموفق',
             goToPayTitle: 'رفتن به پرداخت',
-            goToPayDesc: 'برای رفتن به صفحه پرداخت دکمه زیر را بزنید',
+            goToPayDesc:
+                'سفارش پرداخت برای شما ایجاد شده، برای رفتن به صفحه پرداخت دکمه زیر را بزنید',
+            goToPayQrDesc:
+                'سفارش پرداخت برای شما ایجاد شده، لطفاً کد QR زیر را اسکن کنید تا پرداخت تکمیل شود',
             goToPayButton: 'پرداخت',
             waitingTitle: 'در انتظار پرداخت',
             waitingDesc:

+ 2 - 1
src/locales/zh-CN/pages.ts

@@ -53,7 +53,8 @@ export default {
             loginRequired: '请输入用户名和密码',
             loginError: '登录失败',
             goToPayTitle: '跳转支付',
-            goToPayDesc: '点击下方按钮跳转到支付页面完成支付',
+            goToPayDesc: '已为您生成订单,点击下方按钮跳转到支付页面完成支付',
+            goToPayQrDesc: '已为您生成订单,请扫描下方二维码完成支付',
             goToPayButton: '去支付',
             waitingTitle: '等待支付',
             waitingDesc: '请在支付页面完成支付,支付完成后将自动关闭',

+ 1 - 0
src/models/dialogModel.ts

@@ -23,6 +23,7 @@ interface DialogModel extends DialogState {
      *   - icon: 可选,标题前图标(string 或 IconifyIcon)
      *   - buttons: 可选,底部按钮列表,每项为 { label, onClick?(event, dialogId), variant?: 'primary'|'secondary' }
      *   - maskClosable: 可选,点击遮罩是否关闭,默认 true
+     *   - closeable: 可选,是否显示关闭按钮,默认 false
      * @returns 该弹窗的唯一 id,用于 closeDialog
      */
     openDialog: (props: Omit<DialogProps, 'open' | 'id' | 'zIndex'>) => string;

+ 58 - 0
src/pages/pricing/components/OrderSummary/PayQrContent.tsx

@@ -0,0 +1,58 @@
+import { useEffect, useRef } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import { QRCodeSVG } from 'qrcode.react';
+
+import { PayOrderStatus } from '@/defines';
+import { fetchPayOrderStatus } from '@/services/config';
+
+const POLL_INTERVAL_MS = 2500;
+const QR_SIZE = 200;
+
+export interface PayQrContentProps {
+    orderId: string;
+    payUrl: string;
+    onClose: () => void;
+}
+
+export function PayQrContent({ orderId, payUrl, onClose }: PayQrContentProps) {
+    const { t } = useTranslation();
+    const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
+
+    useEffect(() => {
+        const poll = () => {
+            fetchPayOrderStatus({ orderId })
+                .then((res) => {
+                    const state = res?.data?.orderState;
+                    if (state === PayOrderStatus.PAID || state === PayOrderStatus.FAILED) {
+                        if (timerRef.current) {
+                            clearInterval(timerRef.current);
+                            timerRef.current = null;
+                        }
+                        onClose();
+                    }
+                })
+                .catch(() => {});
+        };
+
+        poll();
+        timerRef.current = setInterval(poll, POLL_INTERVAL_MS);
+
+        return () => {
+            if (timerRef.current) {
+                clearInterval(timerRef.current);
+            }
+        };
+    }, [orderId, onClose]);
+
+    return (
+        <div className="flex flex-col items-center gap-4">
+            <p className="text-white/90 text-sm leading-[1.43] text-center">
+                {t('pages.pricing.payFlow.goToPayQrDesc')}
+            </p>
+            <div className="bg-white rounded-lg p-2">
+                <QRCodeSVG value={payUrl} size={QR_SIZE} />
+            </div>
+        </div>
+    );
+}

+ 173 - 102
src/pages/pricing/useAction.tsx

@@ -3,91 +3,199 @@ import { useCallback } from 'react';
 import { Trans, useTranslation } from 'react-i18next';
 
 import LoginForm from '@/components/LoginForm';
+import { PayUrlShowType } from '@/defines';
 import { useAppUrls } from '@/hooks/useAppUrls';
 import { dialogModel } from '@/models/dialogModel';
 import { fetchPayOrderCreate } from '@/services/config';
 import { getToken } from '@/utils/authUtils';
 import { currentUnixTimestamp } from '@/utils/timeUtils';
 
+import { PayQrContent } from './components/OrderSummary/PayQrContent';
 import { PayWaitingContent } from './components/OrderSummary/PayWaitingContent';
 import type { Plan } from './useService';
 
 const ORDER_TYPE_PLAN = 1;
 
+type PayOrder = { orderId: string; payUrl: string; showType: PayUrlShowType };
+
 export interface UseActionReturn {
     handlePayNow: (plan: Plan, payMethod: string) => void;
 }
 
-export function useAction(): UseActionReturn {
+function useLoginDialog() {
     const { t } = useTranslation();
     const { openDialog, closeDialog } = dialogModel.useModel();
     const { deeplinkUrl, downloadUrlByPlatform } = useAppUrls();
 
+    return useCallback(() => {
+        const id = openDialog({
+            title: t('pages.pricing.payFlow.loginTitle'),
+            content: (
+                <div className="flex flex-col gap-2">
+                    <p className="text-white/80 text-sm mb-2">
+                        <Trans
+                            i18nKey="pages.pricing.payFlow.loginPrompt"
+                            components={{
+                                linkText: (() => {
+                                    const Wrap = ({
+                                        children,
+                                    }: {
+                                        children?: React.ReactNode;
+                                    }) =>
+                                        deeplinkUrl ? (
+                                            <a
+                                                href={deeplinkUrl}
+                                                className="text-[#0EA5E9] hover:underline"
+                                                target="_blank"
+                                                rel="noopener noreferrer"
+                                            >
+                                                {children}
+                                            </a>
+                                        ) : (
+                                            <span>{children}</span>
+                                        );
+                                    return <Wrap />;
+                                })(),
+                                downloadLink: (() => {
+                                    const Wrap = ({
+                                        children,
+                                    }: {
+                                        children?: React.ReactNode;
+                                    }) =>
+                                        downloadUrlByPlatform ? (
+                                            <a
+                                                href={downloadUrlByPlatform}
+                                                className="text-[#0EA5E9] hover:underline"
+                                                target="_blank"
+                                                rel="noopener noreferrer"
+                                            >
+                                                {children}
+                                            </a>
+                                        ) : (
+                                            <span>{children}</span>
+                                        );
+                                    return <Wrap />;
+                                })(),
+                            }}
+                        />
+                    </p>
+                    <LoginForm onSuccess={() => closeDialog(id)} />
+                </div>
+            ),
+            maskClosable: false,
+            closeable: true,
+        });
+    }, [t, openDialog, closeDialog, deeplinkUrl, downloadUrlByPlatform]);
+}
+
+function useQrPayDialog() {
+    const { t } = useTranslation();
+    const { openDialog, closeDialog } = dialogModel.useModel();
+
+    return useCallback(
+        (order: PayOrder) => {
+            const qrDialogId = openDialog({
+                title: t('pages.pricing.payFlow.waitingTitle'),
+                content: (
+                    <PayQrContent
+                        orderId={order.orderId}
+                        payUrl={order.payUrl}
+                        onClose={() => closeDialog(qrDialogId)}
+                    />
+                ),
+                buttons: [
+                    {
+                        label: t('pages.pricing.payFlow.closeWaiting'),
+                        variant: 'secondary' as const,
+                        onClick: (
+                            _e: React.MouseEvent<HTMLButtonElement>,
+                            dialogId: string
+                        ) => {
+                            closeDialog(dialogId);
+                        },
+                    },
+                ],
+                maskClosable: false,
+                closeable: true,
+            });
+        },
+        [t, openDialog, closeDialog],
+    );
+}
+
+function useJumpToPayDialog() {
+    const { t } = useTranslation();
+    const { openDialog, closeDialog } = dialogModel.useModel();
+
+    return useCallback(
+        (order: PayOrder) => {
+            openDialog({
+                title: t('pages.pricing.payFlow.goToPayTitle'),
+                content: (
+                    <p className="text-white/90 text-sm leading-[1.43]">
+                        {t('pages.pricing.payFlow.goToPayDesc')}
+                    </p>
+                ),
+                buttons: [
+                    {
+                        label: t('pages.pricing.payFlow.goToPayButton'),
+                        variant: 'primary' as const,
+                        onClick: (
+                            _e: React.MouseEvent<HTMLButtonElement>,
+                            dialogId: string
+                        ) => {
+                            window.open(order.payUrl, '_blank');
+                            closeDialog(dialogId);
+                            const waitId = openDialog({
+                                title: t('pages.pricing.payFlow.waitingTitle'),
+                                content: (
+                                    <PayWaitingContent
+                                        orderId={order.orderId}
+                                        onClose={() => closeDialog(waitId)}
+                                    />
+                                ),
+                                buttons: [
+                                    {
+                                        label: t('pages.pricing.payFlow.closeWaiting'),
+                                        variant: 'secondary' as const,
+                                        onClick: (
+                                            _e: React.MouseEvent<HTMLButtonElement>,
+                                            d: string
+                                        ) => {
+                                            closeDialog(d);
+                                        },
+                                    },
+                                ],
+                                maskClosable: false,
+                            });
+                        },
+                    },
+                ],
+                maskClosable: false,
+                closeable: true,
+            });
+        },
+        [t, openDialog, closeDialog],
+    );
+}
+
+export function useAction(): UseActionReturn {
+    const openLoginDialog = useLoginDialog();
+    const openQrPayDialog = useQrPayDialog();
+    const openJumpToPayDialog = useJumpToPayDialog();
+
     const handlePayNow = useCallback(
         (selectedPlan: Plan, selectedPayMethod: string) => {
             const token = getToken();
             const expired =
                 !token?.accessExpires || (token.accessExpires ?? 0) - currentUnixTimestamp() <= 0;
             if (!token?.accessToken || expired) {
-                const id = openDialog({
-                    title: t('pages.pricing.payFlow.loginTitle'),
-                    content: (
-                        <div className="flex flex-col gap-2">
-                            <p className="text-white/80 text-sm mb-2">
-                                <Trans
-                                    i18nKey="pages.pricing.payFlow.loginPrompt"
-                                    components={{
-                                        linkText: (() => {
-                                            const Wrap = ({
-                                                children,
-                                            }: {
-                                                children?: React.ReactNode;
-                                            }) =>
-                                                deeplinkUrl ? (
-                                                    <a
-                                                        href={deeplinkUrl}
-                                                        className="text-[#0EA5E9] hover:underline"
-                                                        target="_blank"
-                                                        rel="noopener noreferrer"
-                                                    >
-                                                        {children}
-                                                    </a>
-                                                ) : (
-                                                    <span>{children}</span>
-                                                );
-                                            return <Wrap />;
-                                        })(),
-                                        downloadLink: (() => {
-                                            const Wrap = ({
-                                                children,
-                                            }: {
-                                                children?: React.ReactNode;
-                                            }) =>
-                                                downloadUrlByPlatform ? (
-                                                    <a
-                                                        href={downloadUrlByPlatform}
-                                                        className="text-[#0EA5E9] hover:underline"
-                                                        target="_blank"
-                                                        rel="noopener noreferrer"
-                                                    >
-                                                        {children}
-                                                    </a>
-                                                ) : (
-                                                    <span>{children}</span>
-                                                );
-                                            return <Wrap />;
-                                        })(),
-                                    }}
-                                />
-                            </p>
-                            <LoginForm onSuccess={() => closeDialog(id)} />
-                        </div>
-                    ),
-                    maskClosable: true,
-                });
+                // 如果用户未登录,则打开登录对话框
+                openLoginDialog();
                 return;
             }
 
+            // 创建订单
             fetchPayOrderCreate({
                 orderType: ORDER_TYPE_PLAN,
                 payType: selectedPayMethod,
@@ -97,54 +205,17 @@ export function useAction(): UseActionReturn {
                     const order = res?.data?.userPayOrder;
                     if (!order?.payUrl || !order?.orderId) return;
 
-                    openDialog({
-                        title: t('pages.pricing.payFlow.goToPayTitle'),
-                        content: (
-                            <p className="text-white/90 text-sm leading-[1.43]">
-                                {t('pages.pricing.payFlow.goToPayDesc')}
-                            </p>
-                        ),
-                        buttons: [
-                            {
-                                label: t('pages.pricing.payFlow.goToPayButton'),
-                                variant: 'primary',
-                                onClick: (
-                                    _e: React.MouseEvent<HTMLButtonElement>,
-                                    dialogId: string
-                                ) => {
-                                    window.open(order.payUrl, '_blank');
-                                    closeDialog(dialogId);
-                                    const waitId = openDialog({
-                                        title: t('pages.pricing.payFlow.waitingTitle'),
-                                        content: (
-                                            <PayWaitingContent
-                                                orderId={order.orderId}
-                                                onClose={() => closeDialog(waitId)}
-                                            />
-                                        ),
-                                        buttons: [
-                                            {
-                                                label: t('pages.pricing.payFlow.closeWaiting'),
-                                                variant: 'secondary',
-                                                onClick: (
-                                                    _e: React.MouseEvent<HTMLButtonElement>,
-                                                    dialogId: string
-                                                ) => {
-                                                    closeDialog(dialogId);
-                                                },
-                                            },
-                                        ],
-                                        maskClosable: false,
-                                    });
-                                },
-                            },
-                        ],
-                        maskClosable: true,
-                    });
+                    if (order.showType === PayUrlShowType.SHOW_QR_CODE) {
+                        // 如果订单支付地址的显示类型为二维码,则打开二维码支付对话框
+                        openQrPayDialog(order);
+                        return;
+                    }
+                    // 如果订单支付地址的显示类型为跳转,则跳转到支付地址并打开等待支付对话框
+                    openJumpToPayDialog(order);
                 })
                 .catch(() => {});
         },
-        [t, openDialog, closeDialog, deeplinkUrl, downloadUrlByPlatform]
+        [openLoginDialog, openQrPayDialog, openJumpToPayDialog],
     );
 
     return {

+ 2 - 0
src/services/config/typings.d.ts

@@ -1,5 +1,6 @@
 type PlanTagType = import('@/defines').PlanTagType;
 type PayOrderStatus = import('@/defines').PayOrderStatus;
+type PayUrlShowType = import('@/defines').PayUrlShowType;
 
 declare namespace API {
     type Plan = {
@@ -53,6 +54,7 @@ declare namespace API {
         platform: string;
         snNo: string;
         payUrl: string;
+        showType: PayUrlShowType;
         paidAddress: string;
         ip: string;
         remark: string;

+ 3 - 3
src/utils/stringUtils.ts

@@ -61,15 +61,15 @@ export function toUpperCaseFirstLetter(str: string) {
 }
 
 /**
- * 账号脱敏:前后四保留,中间用 '**' 代替;长度 ≤8 时仅保留后四位并前缀 '**'。空串返回 ''。
+ * 账号脱敏:前后四保留,中间用 '**' 代替;长度 ≤8 时仅保留后四位并前缀 '**'。空串返回 ''。
  * @param str 原始账号字符串
- * @returns 脱敏后的字符串,如 'user**com.'、'**5678'
+ * @returns 脱敏后的字符串,如 'us**com.'、'**5678'
  */
 export function maskAccount(str: string): string {
     if (!str) return '';
     const len = str.length;
     if (len <= 8) return '**' + str.slice(-4);
-    return str.slice(0, 4) + '**' + str.slice(-4);
+    return str.slice(0, 2) + '**' + str.slice(-4);
 }
 
 /**