2
0

feat: paypal支付

This commit is contained in:
caiyuchao
2025-04-23 16:00:21 +08:00
parent 092864e661
commit 3cbaebb5d3
9 changed files with 167 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ import { defineProps, defineEmits, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { AlipayOutlined, WechatOutlined, WalletOutlined } from '@ant-design/icons-vue';
import { Modal } from 'ant-design-vue';
import PaypalButton from '@/components/payment/paypal-button.vue';
const { t } = useI18n();
@@ -128,6 +129,9 @@ const handleCancel = () => {
<WechatOutlined class="payment-icon wxpay-icon" />
<span>{{ t('page.order.wxpay') }}</span>
</div>
<PaypalButton
:order-info="props.orderInfo"
/>
</div>
</div>
</a-spin>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { constructPaypalBtn } from '@/utils/paypal';
interface Props {
orderInfo: {
orderId: string;
orderType: number; // 0: 购买套餐, 1: 余额充值
orderAmount: number;
};
}
const props = defineProps<Props>();
onMounted(async()=>{
await constructPaypalBtn(props.orderInfo.orderId, "USD");
})
</script>
<template>
<div id="paypal-buttons"></div>
</template>
<style scoped>
</style>

View File

@@ -37,3 +37,20 @@ export function payBalance(params: { orderId: string }) {
}
});
}
/** Paypal createOrder */
export function payPalOrders(params: {orderId: number}) {
return request({
url: '/payment/paypal/orders',
method: 'post',
params
});
}
/** Paypal captureOrder */
export function payPalCapture(paypalOrderId: string, orderId: number) {
return request({
url: `/payment/paypal/orders/${paypalOrderId}/capture/${orderId}`,
method: 'post'
});
}

View File

@@ -30,6 +30,7 @@ declare global {
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const constructPaypalBtn: typeof import('../utils/paypal')['constructPaypalBtn']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
@@ -180,6 +181,8 @@ declare global {
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const payBalance: typeof import('../service/api/payment')['payBalance']
const payPalCapture: typeof import('../service/api/payment')['payPalCapture']
const payPalOrders: typeof import('../service/api/payment')['payPalOrders']
const pick: typeof import('lodash-es')['pick']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']

View File

@@ -80,6 +80,7 @@ declare module 'vue' {
LookForward: typeof import('./../components/custom/look-forward.vue')['default']
MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default']
OrderConfirmModal: typeof import('./../components/order-confirm/orderConfirmModal.vue')['default']
PaypalButton: typeof import('./../components/payment/paypal-button.vue')['default']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']

99
src/utils/paypal.ts Normal file
View File

@@ -0,0 +1,99 @@
import { loadScript } from "@paypal/paypal-js";
import { payPalOrders, payPalCapture } from '@/service/api/payment';
export const constructPaypalBtn = async(orderId: number, currency: string) =>{
loadScript({ clientId: import.meta.env.VITE_PAYPAL_CLIENT_ID, disableFunding: ["paylater"], currency })
.then((paypal: any) => {
paypal
.Buttons({
style: {
shape: "rect",
layout: "vertical",
color: "white",
label: "paypal",
},
message: {
},
async createOrder() {
try {
const response = await payPalOrders({orderId: orderId});
const orderData = response.data;
if (orderData.id) {
return orderData.id;
}
const errorDetail = orderData?.details?.[0];
const errorMessage = errorDetail
? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})`
: JSON.stringify(orderData);
throw new Error(errorMessage);
} catch (error) {
console.error(error);
// resultMessage(`Could not initiate PayPal Checkout...<br><br>${error}`);
}
},
async onApprove(data, actions) {
try {
const response = await payPalCapture(data.orderID, orderId)
const orderData = response.data
// Three cases to handle:
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
// (2) Other non-recoverable errors -> Show a failure message
// (3) Successful transaction -> Show confirmation or thank you message
const errorDetail = orderData?.details?.[0];
if (errorDetail?.issue === "INSTRUMENT_DECLINED") {
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
// recoverable state, per
// https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/
return actions.restart();
} else if (errorDetail) {
// (2) Other non-recoverable errors -> Show a failure message
throw new Error(`${errorDetail.description} (${orderData.debug_id})`);
} else if (!orderData.purchase_units) {
throw new Error(JSON.stringify(orderData));
} else {
// (3) Successful transaction -> Show confirmation or thank you message
// Or go to another URL: actions.redirect('thank_you.html');
const transaction =
orderData?.purchase_units?.[0]?.payments?.captures?.[0] ||
orderData?.purchase_units?.[0]?.payments?.authorizations?.[0];
// resultMessage(
// `Transaction ${transaction.status}: ${transaction.id}<br>
//<br>See console for all available details`
//);
console.log(
"Capture result",
orderData,
JSON.stringify(orderData, null, 2)
);
}
} catch (error) {
console.error(error);
// resultMessage(
// `Sorry, your transaction could not be processed...<br><br>${error}`
//);
}
},
})
.render("#paypal-buttons")
.catch((error) => {
console.error("failed to render the PayPal Buttons", error);
});
})
.catch((error) => {
console.error("failed to load the PayPal JS SDK script", error);
});
}
// Example function to show a result to the user. Your site's UI library can be used instead.
function resultMessage(message: any) {
const container: any = document.querySelector("#result-message");
container.innerHTML = message;
}