feat(Payment): 添加微信 JSAPI 支付页面
- 新增 WechatJsapiPayment.tsx 微信公众号支付页面 - 新增 wechat.d.ts 微信 JSSDK 类型定义 - 添加支付相关路由配置 - payment.js handler 添加 JSAPI 支付接口 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -186,5 +186,57 @@ export const paymentHandlers = [
|
|||||||
success: true,
|
success: true,
|
||||||
message: '订单已取消'
|
message: '订单已取消'
|
||||||
});
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
// ==================== 微信 JSAPI 支付 ====================
|
||||||
|
|
||||||
|
// 5. 创建 JSAPI 支付订单(小程序 H5 支付)
|
||||||
|
http.post('/api/payment/wechat/jsapi/create-order', async ({ request }) => {
|
||||||
|
await delay(NETWORK_DELAY);
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { plan_id, user_id, openid } = body;
|
||||||
|
|
||||||
|
console.log('[Mock] 创建 JSAPI 支付订单:', { plan_id, user_id, openid });
|
||||||
|
|
||||||
|
if (!plan_id || !user_id || !openid) {
|
||||||
|
return HttpResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '参数不完整:需要 plan_id, user_id, openid'
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟套餐价格
|
||||||
|
const planPrices = {
|
||||||
|
'pro_monthly': { name: 'Pro 月度会员', amount: 29.9 },
|
||||||
|
'pro_yearly': { name: 'Pro 年度会员', amount: 299 },
|
||||||
|
'max_monthly': { name: 'Max 月度会员', amount: 99 },
|
||||||
|
'max_yearly': { name: 'Max 年度会员', amount: 999 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const plan = planPrices[plan_id] || { name: '测试套餐', amount: 0.01 };
|
||||||
|
|
||||||
|
// 创建订单
|
||||||
|
const orderNo = `JSAPI_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
|
||||||
|
// 模拟微信支付参数(实际由后端生成)
|
||||||
|
const paymentParams = {
|
||||||
|
appId: 'wx0edeaab76d4fa414',
|
||||||
|
timeStamp: String(Math.floor(Date.now() / 1000)),
|
||||||
|
nonceStr: Math.random().toString(36).slice(2, 18),
|
||||||
|
package: `prepay_id=mock_prepay_${orderNo}`,
|
||||||
|
signType: 'MD5',
|
||||||
|
paySign: 'MOCK_SIGN_' + Math.random().toString(36).slice(2, 10).toUpperCase(),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[Mock] JSAPI 订单创建成功:', { orderNo, plan: plan.name, amount: plan.amount });
|
||||||
|
|
||||||
|
return HttpResponse.json({
|
||||||
|
success: true,
|
||||||
|
order_no: orderNo,
|
||||||
|
plan_name: plan.name,
|
||||||
|
amount: plan.amount,
|
||||||
|
payment_params: paymentParams,
|
||||||
|
});
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ export const lazyComponents = {
|
|||||||
UserAgreement: React.lazy(() => import('@views/Pages/UserAgreement')),
|
UserAgreement: React.lazy(() => import('@views/Pages/UserAgreement')),
|
||||||
WechatCallback: React.lazy(() => import('@views/Pages/WechatCallback')),
|
WechatCallback: React.lazy(() => import('@views/Pages/WechatCallback')),
|
||||||
|
|
||||||
|
// 支付模块
|
||||||
|
WechatJsapiPayment: React.lazy(() => import('@views/Payment/WechatJsapiPayment')),
|
||||||
|
|
||||||
// 社区/内容模块
|
// 社区/内容模块
|
||||||
Community: React.lazy(() => import('@views/Community')),
|
Community: React.lazy(() => import('@views/Community')),
|
||||||
ConceptCenter: React.lazy(() => import('@views/Concept')),
|
ConceptCenter: React.lazy(() => import('@views/Concept')),
|
||||||
@@ -69,6 +72,7 @@ export const {
|
|||||||
PrivacyPolicy,
|
PrivacyPolicy,
|
||||||
UserAgreement,
|
UserAgreement,
|
||||||
WechatCallback,
|
WechatCallback,
|
||||||
|
WechatJsapiPayment,
|
||||||
Community,
|
Community,
|
||||||
ConceptCenter,
|
ConceptCenter,
|
||||||
StockOverview,
|
StockOverview,
|
||||||
|
|||||||
@@ -214,6 +214,18 @@ export const routeConfig = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ==================== 支付模块 ====================
|
||||||
|
{
|
||||||
|
path: 'payment/wechat',
|
||||||
|
component: lazyComponents.WechatJsapiPayment,
|
||||||
|
protection: PROTECTION_MODES.PUBLIC,
|
||||||
|
layout: 'none', // 无布局,独立页面
|
||||||
|
meta: {
|
||||||
|
title: '微信支付',
|
||||||
|
description: '微信 JSAPI 支付页面'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// ==================== Agent模块 ====================
|
// ==================== Agent模块 ====================
|
||||||
{
|
{
|
||||||
path: 'agent-chat',
|
path: 'agent-chat',
|
||||||
|
|||||||
42
src/types/wechat.d.ts
vendored
Normal file
42
src/types/wechat.d.ts
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* 微信 JS Bridge 类型声明
|
||||||
|
* 用于微信 H5 JSAPI 支付
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 微信支付参数 */
|
||||||
|
interface WechatPayParams {
|
||||||
|
appId: string;
|
||||||
|
timeStamp: string;
|
||||||
|
nonceStr: string;
|
||||||
|
package: string;
|
||||||
|
signType: 'MD5' | 'HMAC-SHA256';
|
||||||
|
paySign: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 微信支付响应 */
|
||||||
|
interface WechatPayResponse {
|
||||||
|
err_msg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WeixinJSBridge invoke 回调 */
|
||||||
|
type WeixinJSBridgeCallback = (response: WechatPayResponse) => void;
|
||||||
|
|
||||||
|
/** WeixinJSBridge 接口 */
|
||||||
|
interface WeixinJSBridge {
|
||||||
|
invoke(
|
||||||
|
api: 'getBrandWCPayRequest',
|
||||||
|
params: WechatPayParams,
|
||||||
|
callback: WeixinJSBridgeCallback
|
||||||
|
): void;
|
||||||
|
on(event: string, callback: () => void): void;
|
||||||
|
call(api: string, params?: Record<string, unknown>): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 扩展 Window 接口 */
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
WeixinJSBridge?: WeixinJSBridge;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { WechatPayParams, WechatPayResponse, WeixinJSBridge };
|
||||||
158
src/views/Payment/WechatJsapiPayment.tsx
Normal file
158
src/views/Payment/WechatJsapiPayment.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
/**
|
||||||
|
* 微信 JSAPI 支付页面
|
||||||
|
*
|
||||||
|
* 用于小程序跳转 H5 完成支付的场景
|
||||||
|
* URL 参数:
|
||||||
|
* - plan_id: 套餐 ID
|
||||||
|
* - user_id: 用户 ID
|
||||||
|
* - openid: 微信用户 openid
|
||||||
|
* - return_scheme: 支付完成后跳转的小程序 scheme
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { getApiBase } from '@/utils/apiConfig';
|
||||||
|
import { useWechatPay } from './hooks/useWechatPay';
|
||||||
|
import { PaymentStatus } from './components/PaymentStatus';
|
||||||
|
import type { WechatPayParams } from '@/types/wechat';
|
||||||
|
|
||||||
|
/** 订单创建响应 */
|
||||||
|
interface CreateOrderResponse {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
payment_params?: WechatPayParams;
|
||||||
|
order_no?: string;
|
||||||
|
amount?: number;
|
||||||
|
plan_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WechatJsapiPayment: React.FC = () => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const { status, error, isWechatEnv, invokePayment, reset } = useWechatPay();
|
||||||
|
|
||||||
|
// URL 参数
|
||||||
|
const planId = searchParams.get('plan_id');
|
||||||
|
const userId = searchParams.get('user_id');
|
||||||
|
const openid = searchParams.get('openid');
|
||||||
|
const returnScheme = searchParams.get('return_scheme');
|
||||||
|
|
||||||
|
// 订单信息
|
||||||
|
const [orderInfo, setOrderInfo] = useState<{
|
||||||
|
planName?: string;
|
||||||
|
amount?: number;
|
||||||
|
orderNo?: string;
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
// 初始化错误
|
||||||
|
const [initError, setInitError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建订单并发起支付
|
||||||
|
*/
|
||||||
|
const createOrderAndPay = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
// 调用后端创建 JSAPI 订单
|
||||||
|
const response = await fetch(`${getApiBase()}/api/payment/wechat/jsapi/create-order`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
plan_id: planId,
|
||||||
|
user_id: userId,
|
||||||
|
openid,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data: CreateOrderResponse = await response.json();
|
||||||
|
|
||||||
|
if (!data.success || !data.payment_params) {
|
||||||
|
throw new Error(data.error || '创建订单失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存订单信息
|
||||||
|
setOrderInfo({
|
||||||
|
planName: data.plan_name,
|
||||||
|
amount: data.amount,
|
||||||
|
orderNo: data.order_no,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 调起微信支付
|
||||||
|
const result = await invokePayment(data.payment_params);
|
||||||
|
|
||||||
|
// 支付成功,跳转回小程序
|
||||||
|
if (result.success && returnScheme) {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = returnScheme;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setInitError(err instanceof Error ? err.message : '支付初始化失败');
|
||||||
|
}
|
||||||
|
}, [planId, userId, openid, returnScheme, invokePayment]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数验证和初始化
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
// 参数验证
|
||||||
|
if (!planId || !userId || !openid) {
|
||||||
|
setInitError('缺少必要参数');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 环境检测
|
||||||
|
if (!isWechatEnv) {
|
||||||
|
setInitError('请在微信浏览器中打开此页面');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发起支付
|
||||||
|
createOrderAndPay();
|
||||||
|
}, [planId, userId, openid, isWechatEnv, createOrderAndPay]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重试支付
|
||||||
|
*/
|
||||||
|
const handleRetry = useCallback(() => {
|
||||||
|
reset();
|
||||||
|
setInitError(null);
|
||||||
|
createOrderAndPay();
|
||||||
|
}, [reset, createOrderAndPay]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回小程序
|
||||||
|
*/
|
||||||
|
const handleBack = useCallback(() => {
|
||||||
|
if (returnScheme) {
|
||||||
|
window.location.href = returnScheme;
|
||||||
|
} else {
|
||||||
|
window.history.back();
|
||||||
|
}
|
||||||
|
}, [returnScheme]);
|
||||||
|
|
||||||
|
// 显示初始化错误
|
||||||
|
if (initError) {
|
||||||
|
return (
|
||||||
|
<PaymentStatus
|
||||||
|
status="failed"
|
||||||
|
error={initError}
|
||||||
|
onRetry={handleRetry}
|
||||||
|
onBack={handleBack}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PaymentStatus
|
||||||
|
status={status}
|
||||||
|
error={error}
|
||||||
|
planName={orderInfo.planName}
|
||||||
|
amount={orderInfo.amount}
|
||||||
|
onRetry={handleRetry}
|
||||||
|
onBack={handleBack}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WechatJsapiPayment;
|
||||||
171
src/views/Payment/components/PaymentStatus.tsx
Normal file
171
src/views/Payment/components/PaymentStatus.tsx
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
/**
|
||||||
|
* 支付状态展示组件
|
||||||
|
* 用于显示不同支付状态的 UI
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
VStack,
|
||||||
|
Text,
|
||||||
|
Spinner,
|
||||||
|
Icon,
|
||||||
|
Button,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { CheckCircle, XCircle, AlertCircle } from 'lucide-react';
|
||||||
|
import type { PaymentStatus as PaymentStatusType } from '../hooks/useWechatPay';
|
||||||
|
|
||||||
|
interface PaymentStatusProps {
|
||||||
|
status: PaymentStatusType;
|
||||||
|
error?: string | null;
|
||||||
|
planName?: string;
|
||||||
|
amount?: number;
|
||||||
|
onRetry?: () => void;
|
||||||
|
onBack?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 状态配置 */
|
||||||
|
const STATUS_CONFIG: Record<PaymentStatusType, {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon?: React.ElementType;
|
||||||
|
iconColor?: string;
|
||||||
|
showSpinner?: boolean;
|
||||||
|
}> = {
|
||||||
|
idle: {
|
||||||
|
title: '准备支付',
|
||||||
|
description: '正在初始化支付环境...',
|
||||||
|
showSpinner: true,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
title: '正在加载',
|
||||||
|
description: '正在创建支付订单...',
|
||||||
|
showSpinner: true,
|
||||||
|
},
|
||||||
|
paying: {
|
||||||
|
title: '等待支付',
|
||||||
|
description: '请在微信支付弹窗中完成支付',
|
||||||
|
showSpinner: true,
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
title: '支付成功',
|
||||||
|
description: '您的订阅已生效',
|
||||||
|
icon: CheckCircle,
|
||||||
|
iconColor: 'green.400',
|
||||||
|
},
|
||||||
|
failed: {
|
||||||
|
title: '支付失败',
|
||||||
|
description: '支付过程中发生错误',
|
||||||
|
icon: XCircle,
|
||||||
|
iconColor: 'red.400',
|
||||||
|
},
|
||||||
|
cancelled: {
|
||||||
|
title: '已取消',
|
||||||
|
description: '您已取消本次支付',
|
||||||
|
icon: AlertCircle,
|
||||||
|
iconColor: 'orange.400',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PaymentStatus: React.FC<PaymentStatusProps> = ({
|
||||||
|
status,
|
||||||
|
error,
|
||||||
|
planName,
|
||||||
|
amount,
|
||||||
|
onRetry,
|
||||||
|
onBack,
|
||||||
|
}) => {
|
||||||
|
const config = STATUS_CONFIG[status];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
minH="100vh"
|
||||||
|
bg="gray.900"
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
px={4}
|
||||||
|
>
|
||||||
|
<VStack
|
||||||
|
spacing={6}
|
||||||
|
p={8}
|
||||||
|
bg="gray.800"
|
||||||
|
borderRadius="xl"
|
||||||
|
maxW="400px"
|
||||||
|
w="full"
|
||||||
|
textAlign="center"
|
||||||
|
>
|
||||||
|
{/* 图标或加载动画 */}
|
||||||
|
{config.showSpinner ? (
|
||||||
|
<Spinner size="xl" color="gold.400" thickness="4px" />
|
||||||
|
) : config.icon ? (
|
||||||
|
<Icon as={config.icon} boxSize={16} color={config.iconColor} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* 标题 */}
|
||||||
|
<Text fontSize="2xl" fontWeight="bold" color="white">
|
||||||
|
{config.title}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* 描述 */}
|
||||||
|
<Text color="gray.400">
|
||||||
|
{error || config.description}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* 订单信息 */}
|
||||||
|
{planName && amount && status !== 'failed' && status !== 'cancelled' && (
|
||||||
|
<Box
|
||||||
|
w="full"
|
||||||
|
p={4}
|
||||||
|
bg="gray.700"
|
||||||
|
borderRadius="lg"
|
||||||
|
>
|
||||||
|
<VStack spacing={2}>
|
||||||
|
<Text color="gray.300" fontSize="sm">
|
||||||
|
订阅套餐
|
||||||
|
</Text>
|
||||||
|
<Text color="white" fontWeight="semibold">
|
||||||
|
{planName}
|
||||||
|
</Text>
|
||||||
|
<Text color="gold.400" fontSize="xl" fontWeight="bold">
|
||||||
|
¥{amount.toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<VStack spacing={3} w="full" pt={4}>
|
||||||
|
{(status === 'failed' || status === 'cancelled') && onRetry && (
|
||||||
|
<Button
|
||||||
|
w="full"
|
||||||
|
colorScheme="yellow"
|
||||||
|
onClick={onRetry}
|
||||||
|
>
|
||||||
|
重新支付
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onBack && (
|
||||||
|
<Button
|
||||||
|
w="full"
|
||||||
|
variant="ghost"
|
||||||
|
color="gray.400"
|
||||||
|
onClick={onBack}
|
||||||
|
>
|
||||||
|
返回
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* 支付中提示 */}
|
||||||
|
{status === 'paying' && (
|
||||||
|
<Text fontSize="xs" color="gray.500">
|
||||||
|
请勿关闭此页面,支付完成后将自动跳转
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PaymentStatus;
|
||||||
141
src/views/Payment/hooks/useWechatPay.ts
Normal file
141
src/views/Payment/hooks/useWechatPay.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* 微信 JSAPI 支付 Hook
|
||||||
|
* 封装微信支付逻辑,包括环境检测和支付调用
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import type { WechatPayParams } from '@/types/wechat';
|
||||||
|
|
||||||
|
/** 支付状态 */
|
||||||
|
export type PaymentStatus = 'idle' | 'loading' | 'paying' | 'success' | 'failed' | 'cancelled';
|
||||||
|
|
||||||
|
/** 支付结果 */
|
||||||
|
interface PaymentResult {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hook 返回值 */
|
||||||
|
interface UseWechatPayReturn {
|
||||||
|
status: PaymentStatus;
|
||||||
|
error: string | null;
|
||||||
|
isWechatEnv: boolean;
|
||||||
|
invokePayment: (params: WechatPayParams) => Promise<PaymentResult>;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测是否在微信浏览器环境
|
||||||
|
*/
|
||||||
|
const checkWechatEnv = (): boolean => {
|
||||||
|
if (typeof window === 'undefined') return false;
|
||||||
|
const ua = window.navigator.userAgent.toLowerCase();
|
||||||
|
return ua.includes('micromessenger');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待 WeixinJSBridge 就绪
|
||||||
|
*/
|
||||||
|
const waitForBridge = (): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof window.WeixinJSBridge !== 'undefined') {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeout: NodeJS.Timeout;
|
||||||
|
|
||||||
|
const onBridgeReady = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
document.removeEventListener('WeixinJSBridgeReady', onBridgeReady);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('WeixinJSBridgeReady', onBridgeReady);
|
||||||
|
|
||||||
|
// 10秒超时
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
document.removeEventListener('WeixinJSBridgeReady', onBridgeReady);
|
||||||
|
reject(new Error('WeixinJSBridge 加载超时'));
|
||||||
|
}, 10000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信 JSAPI 支付 Hook
|
||||||
|
*/
|
||||||
|
export const useWechatPay = (): UseWechatPayReturn => {
|
||||||
|
const [status, setStatus] = useState<PaymentStatus>('idle');
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const isWechatEnv = checkWechatEnv();
|
||||||
|
|
||||||
|
const invokePayment = useCallback(async (params: WechatPayParams): Promise<PaymentResult> => {
|
||||||
|
// 环境检查
|
||||||
|
if (!isWechatEnv) {
|
||||||
|
const errMsg = '请在微信浏览器中打开';
|
||||||
|
setError(errMsg);
|
||||||
|
setStatus('failed');
|
||||||
|
return { success: false, message: errMsg };
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus('loading');
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 等待 Bridge 就绪
|
||||||
|
await waitForBridge();
|
||||||
|
|
||||||
|
setStatus('paying');
|
||||||
|
|
||||||
|
// 调用微信支付
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
window.WeixinJSBridge!.invoke(
|
||||||
|
'getBrandWCPayRequest',
|
||||||
|
{
|
||||||
|
appId: params.appId,
|
||||||
|
timeStamp: params.timeStamp,
|
||||||
|
nonceStr: params.nonceStr,
|
||||||
|
package: params.package,
|
||||||
|
signType: params.signType,
|
||||||
|
paySign: params.paySign,
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
if (res.err_msg === 'get_brand_wcpay_request:ok') {
|
||||||
|
setStatus('success');
|
||||||
|
resolve({ success: true, message: '支付成功' });
|
||||||
|
} else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
|
||||||
|
setStatus('cancelled');
|
||||||
|
setError('用户取消支付');
|
||||||
|
resolve({ success: false, message: '用户取消支付' });
|
||||||
|
} else {
|
||||||
|
setStatus('failed');
|
||||||
|
const errMsg = res.err_msg || '支付失败';
|
||||||
|
setError(errMsg);
|
||||||
|
resolve({ success: false, message: errMsg });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const errMsg = err instanceof Error ? err.message : '支付过程发生错误';
|
||||||
|
setStatus('failed');
|
||||||
|
setError(errMsg);
|
||||||
|
return { success: false, message: errMsg };
|
||||||
|
}
|
||||||
|
}, [isWechatEnv]);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
setStatus('idle');
|
||||||
|
setError(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
error,
|
||||||
|
isWechatEnv,
|
||||||
|
invokePayment,
|
||||||
|
reset,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useWechatPay;
|
||||||
Reference in New Issue
Block a user