From 45f6a5020d88adddee11d4a65eeadb29efa585dd Mon Sep 17 00:00:00 2001 From: qiube <18969599531@163.com> Date: Tue, 23 Dec 2025 17:49:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(payment):=20=E9=9B=86=E6=88=90=20PayPal=20?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E5=B9=B6=E6=B7=BB=E5=8A=A0=E8=B4=A7=E5=B8=81?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 集成 PayPal 支付流程,包括订单创建、支付确认和取消页面 - 实现货币转换功能,支持不支持的货币自动转换为 PayPal 支持的货币 - 在订单确认页面显示货币转换信息,包括实际支付金额和汇率 - 添加 PayPal 支付成功和取消的路由处理 - 优化商品详情页图片预览,支持鼠标悬停显示主图 - 添加货币转换相关的 API 接口和工具函数 --- src/api/order.js | 11 ++ src/router/index.js | 12 ++ src/views/OrderConfirm.vue | 346 ++++++++++++++++++++++++++++-------- src/views/ProductDetail.vue | 15 ++ 4 files changed, 313 insertions(+), 71 deletions(-) diff --git a/src/api/order.js b/src/api/order.js index 6fccdee..3b02bdb 100644 --- a/src/api/order.js +++ b/src/api/order.js @@ -31,3 +31,14 @@ export function getOrderById(id) { }) } +/** + * 计算并更新订单的货币转换信息 + */ +export function calculateCurrencyConversion(data) { + return request({ + url: '/order/calculate-currency-conversion', + method: 'post', + data + }) +} + diff --git a/src/router/index.js b/src/router/index.js index 0f52e0d..f87e09f 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -36,6 +36,18 @@ const routes = [ name: 'OrderConfirm', component: () => import('../views/OrderConfirm.vue') }, + { + path: '/paypal/success', + name: 'PayPalSuccess', + component: () => import('../views/PayPalSuccess.vue'), + meta: { isCustomerPage: true } + }, + { + path: '/paypal/cancel', + name: 'PayPalCancel', + component: () => import('../views/PayPalCancel.vue'), + meta: { isCustomerPage: true } + }, { path: '/checkout', name: 'Checkout', diff --git a/src/views/OrderConfirm.vue b/src/views/OrderConfirm.vue index 932cbc4..c987aeb 100644 --- a/src/views/OrderConfirm.vue +++ b/src/views/OrderConfirm.vue @@ -21,7 +21,44 @@ {{ order.orderNo }} - {{ order.currency }} {{ formatPrice(order.totalAmount) }} +
+
+ {{ order.currency }} {{ formatPrice(order.totalAmount) }} +
+ +
+ + + +
+
{{ order.productName }} {{ order.skuName }} @@ -91,11 +128,10 @@ import { ref, onMounted } from 'vue' import { useRouter, useRoute } from 'vue-router' import { ElMessage } from 'element-plus' -import { Money } from '@element-plus/icons-vue' -import { getOrderByOrderNo } from '../api/order' -import { createPaymentOrder } from '../api/payment' +import { Money, Clock, Warning } from '@element-plus/icons-vue' +import { getOrderByOrderNo, calculateCurrencyConversion } from '../api/order' +import { createPayPalOrder, getPayPalOrder, capturePayPalOrder } from '../api/paypal' import { formatAmount } from '../utils/helpers' -import { generateOrderId } from '../utils/helpers' const router = useRouter() const route = useRoute() @@ -132,6 +168,30 @@ const formatPrice = (price) => { return formatAmount(price) } +// 获取货币名称 +const getCurrencyName = (currencyCode) => { + const currencyNames = { + 'USD': '美元', + 'EUR': '欧元', + 'GBP': '英镑', + 'CNY': '人民币', + 'MYR': '马来西亚林吉特', + 'VND': '越南盾', + 'JPY': '日元', + 'KRW': '韩元', + 'THB': '泰铢', + 'SGD': '新加坡元', + 'HKD': '港币' + } + return currencyNames[currencyCode] || currencyCode +} + +// 格式化汇率 +const formatRate = (rate) => { + if (!rate) return '' + return rate.toFixed(6) +} + // 格式化日期时间 const formatDateTime = (dateTime) => { if (!dateTime) return '-' @@ -169,6 +229,44 @@ const loadOrder = async () => { const response = await getOrderByOrderNo(orderNo) if (response.code === '0000' && response.data) { order.value = response.data + + // 如果订单未支付且需要货币转换,提前计算货币转换信息 + if (order.value.paymentStatus === 'UNPAID' && order.value.currency) { + // 检查是否需要货币转换(PayPal支持的货币列表) + const supportedCurrencies = ['USD', 'EUR', 'GBP', 'AUD', 'CAD', 'JPY', 'CNY', 'HKD', 'SGD', 'NZD', + 'CHF', 'SEK', 'NOK', 'DKK', 'PLN', 'MXN', 'BRL', 'INR', 'KRW', 'THB'] + + const needsConversion = !supportedCurrencies.includes(order.value.currency) + + // 如果订单还没有货币转换信息,或者需要更新,则计算 + if (needsConversion && (!order.value.paymentCurrency || order.value.paymentCurrency === order.value.currency)) { + try { + const conversionResponse = await calculateCurrencyConversion({ + orderNo: order.value.orderNo, + originalCurrency: order.value.currency, + originalAmount: order.value.totalAmount + }) + + if (conversionResponse.code === '0000' && conversionResponse.data) { + const conversion = conversionResponse.data + // 更新订单显示信息 + order.value = { + ...order.value, + originalCurrency: conversion.originalCurrency, + originalAmount: conversion.originalAmount, + paymentCurrency: conversion.paymentCurrency, + paymentAmount: conversion.paymentAmount, + exchangeRate: conversion.exchangeRate, + rateLockedAt: conversion.rateLockedAt + } + console.log('货币转换信息已计算并更新') + } + } catch (error) { + console.warn('计算货币转换信息失败:', error) + // 不显示错误,因为不影响订单显示 + } + } + } } else { ElMessage.error(response.message || '获取订单信息失败') order.value = null @@ -182,7 +280,7 @@ const loadOrder = async () => { } } -// 处理支付 +// 处理PayPal支付(步骤1-4) const handlePay = async () => { if (!order.value) { ElMessage.error('订单信息不存在') @@ -197,77 +295,73 @@ const handlePay = async () => { payLoading.value = true try { - // 构建支付请求数据 - const paymentData = { - merchantTransactionId: generateOrderId(), - amount: order.value.totalAmount.toFixed(2), - currency: order.value.currency, - paymentType: 'SALE', - merchantUserId: '', - shopperResultUrl: `${window.location.origin}/result`, - shopperCancelUrl: `${window.location.origin}/result`, - signType: 'MD5', - language: 'zh', - threeDSecure: 'N', - riskInfo: { - customer: { - firstName: order.value.customerName.split(' ')[0] || order.value.customerName, - lastName: order.value.customerName.split(' ').slice(1).join(' ') || '', - email: order.value.customerEmail || '', - phone: order.value.customerPhone, - registerTime: new Date().toISOString().replace(/[-:]/g, '').split('.')[0], - registerIp: '', - registerTerminal: 'PC', - registerRange: '1', - orderTime: new Date(order.value.createTime).toISOString().replace(/[-:]/g, '').split('.')[0], - orderIp: '', - orderCountry: order.value.shippingCountry || 'US' - }, - goods: [{ - name: order.value.productName, - description: order.value.skuName, - sku: order.value.skuName, - averageUnitPrice: order.value.unitPrice.toFixed(2), - number: order.value.quantity.toString(), - virtualProduct: 'N' - }], - shipping: { - firstName: order.value.shippingName.split(' ')[0] || order.value.shippingName, - lastName: order.value.shippingName.split(' ').slice(1).join(' ') || '', - street: order.value.shippingStreet, - city: order.value.shippingCity, - state: order.value.shippingState || '', - postcode: order.value.shippingPostcode || '', - country: order.value.shippingCountry - }, - billing: { - firstName: order.value.shippingName.split(' ')[0] || order.value.shippingName, - lastName: order.value.shippingName.split(' ').slice(1).join(' ') || '', - street: order.value.shippingStreet, - city: order.value.shippingCity, - state: order.value.shippingState || '', - postcode: order.value.shippingPostcode || '', - country: order.value.shippingCountry - } - }, - notificationUrl: `${window.location.origin}/api/callback/pingpong` + // 步骤2:构建PayPal订单创建请求 + const paypalOrderData = { + intent: 'CAPTURE', // 立即捕获 + referenceId: order.value.orderNo, // ERP订单号 + amount: order.value.totalAmount, + currencyCode: order.value.currency, + itemName: order.value.productName, + itemDescription: order.value.skuName, + itemSku: order.value.skuName, + itemQuantity: order.value.quantity, + itemUnitAmount: order.value.unitPrice, + returnUrl: `${window.location.origin}/paypal/success?orderNo=${order.value.orderNo}`, + cancelUrl: `${window.location.origin}/paypal/cancel?orderNo=${order.value.orderNo}`, + shippingName: order.value.shippingName, + shippingAddressLine1: order.value.shippingStreet, + shippingCity: order.value.shippingCity, + shippingState: order.value.shippingState || null, + shippingPostalCode: order.value.shippingPostcode || null, + shippingCountryCode: order.value.shippingCountry, + emailAddress: order.value.customerEmail || null } - const response = await createPaymentOrder(paymentData) + // 调用后端创建PayPal订单 + const response = await createPayPalOrder(paypalOrderData) - if (response.code === '0000' && response.data && response.data.token) { - ElMessage.success('正在跳转到支付页面...') - // 跳转到收银台页面 - router.push({ - path: '/checkout', - query: { token: response.data.token } - }) + if (response.code === '0000' && response.data) { + const responseData = response.data + const paypalOrder = responseData.paypalOrder || responseData // 兼容新旧格式 + const currencyConversion = responseData.currencyConversion + + // 如果有货币转换信息,更新订单显示 + if (currencyConversion) { + order.value = { + ...order.value, + originalCurrency: currencyConversion.originalCurrency, + originalAmount: currencyConversion.originalAmount, + paymentCurrency: currencyConversion.paymentCurrency, + paymentAmount: currencyConversion.paymentAmount, + exchangeRate: currencyConversion.exchangeRate, + rateLockedAt: currencyConversion.rateLockedAt + } + + // 显示货币转换提示 + if (currencyConversion.conversionRequired) { + ElMessage.info({ + message: `您将以${getCurrencyName(currencyConversion.paymentCurrency)}支付,实际费用为:${currencyConversion.paymentCurrency} ${formatPrice(currencyConversion.paymentAmount)}`, + duration: 5000 + }) + } + } + + // 步骤3-4:获取approval_url并跳转到PayPal登录页 + const approvalLink = paypalOrder.links?.find(link => link.rel === 'payer-action') + + if (approvalLink && approvalLink.href) { + ElMessage.success('正在跳转到PayPal支付页面...') + // 步骤4:跳转到PayPal登录页 + window.location.href = approvalLink.href + } else { + ElMessage.error('获取PayPal支付链接失败') + } } else { - ElMessage.error(response.message || '创建支付订单失败') + ElMessage.error(response.message || '创建PayPal订单失败') } } catch (error) { - console.error('创建支付订单失败:', error) - ElMessage.error(error.response?.data?.message || '创建支付订单失败,请稍后重试') + console.error('创建PayPal订单失败:', error) + ElMessage.error(error.response?.data?.message || '创建PayPal订单失败,请稍后重试') } finally { payLoading.value = false } @@ -326,5 +420,115 @@ onMounted(() => { padding-top: 20px; border-top: 1px solid #e4e7ed; } + +.order-amount-section { + display: flex; + flex-direction: column; +} + +.currency-conversion-info { + margin-top: 15px; +} + +.conversion-alert-box { + border: 2px solid #e6a23c; + border-radius: 8px; + background: linear-gradient(135deg, #fff7e6 0%, #fff3d9 100%); + box-shadow: 0 4px 12px rgba(230, 162, 60, 0.2); +} + +.conversion-alert-content { + line-height: 1.8; + padding: 5px 0; +} + +.conversion-title { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + font-size: 18px; +} + +.warning-icon { + font-size: 24px; + color: #e6a23c; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.7; + } +} + +.title-text { + color: #e6a23c; + font-size: 18px; + font-weight: 700; +} + +.conversion-main-info { + background: #fff; + padding: 15px; + border-radius: 6px; + margin: 10px 0; + border-left: 4px solid #e6a23c; +} + +.payment-amount-highlight { + display: flex; + align-items: baseline; + gap: 10px; + margin-bottom: 8px; +} + +.payment-amount-highlight .label { + font-size: 16px; + color: #606266; + font-weight: 600; +} + +.payment-amount-large { + font-weight: 800; + color: #e6a23c; + font-size: 28px; + text-shadow: 0 2px 4px rgba(230, 162, 60, 0.3); +} + +.exchange-rate-info { + display: flex; + flex-direction: column; + gap: 5px; + margin-top: 10px; + padding-top: 10px; + border-top: 1px dashed #dcdfe6; +} + +.equivalent-amount { + color: #606266; + font-size: 15px; + font-weight: 500; +} + +.rate-value { + color: #909399; + font-size: 13px; + font-style: italic; +} + +.rate-locked-info { + margin-top: 10px; + font-size: 13px; + color: #909399; + display: flex; + align-items: center; + gap: 6px; + padding-top: 8px; + border-top: 1px solid #ebeef5; +} diff --git a/src/views/ProductDetail.vue b/src/views/ProductDetail.vue index 0e1aa99..32d9aac 100644 --- a/src/views/ProductDetail.vue +++ b/src/views/ProductDetail.vue @@ -41,6 +41,8 @@ class="thumbnail-item" :class="{ active: currentImageIndex === index }" @click="selectImage(index)" + @mouseenter="hoverImage(img)" + @mouseleave="hoverImage(null)" > @@ -263,6 +265,8 @@ const router = useRouter() const loading = ref(false) const productLoading = ref(true) const currentImageIndex = ref(0) +// 鼠标悬停的主图(优先展示) +const hoveredImage = ref(null) const quantity = ref(1) const activeTab = ref('detail') const selectedSku = ref(null) @@ -324,12 +328,19 @@ const currentCurrencySkus = computed(() => { // 当前显示的图片 const currentImage = computed(() => { + // 1) 若有悬停主图,优先展示 + if (hoveredImage.value) { + return hoveredImage.value + } + // 2) 已选SKU且有SKU图 if (selectedSku.value && selectedSku.value.skuImage) { return selectedSku.value.skuImage } + // 3) 主图列表 if (product.value.mainImages && product.value.mainImages.length > 0) { return product.value.mainImages[currentImageIndex.value] || product.value.mainImages[0] } + // 4) 兜底单张主图 return product.value.mainImage || '' }) @@ -378,6 +389,10 @@ const formatSize = (sizeJson) => { const selectImage = (index) => { currentImageIndex.value = index } +// 悬停主图时优先展示该主图 +const hoverImage = (img) => { + hoveredImage.value = img +} // 立即购买 const handleBuyNow = () => {