@@ -21,7 +21,44 @@
< el-descriptions :column = "2" border >
< el-descriptions-item label = "订单号" > { { order . orderNo } } < / el-descriptions-item >
< el-descriptions-item label = "订单金额" >
< div class = "order-amount-section" >
< div class = "original-amount" >
< span class = "order-amount" > { { order . currency } } { { formatPrice ( order . totalAmount ) } } < / span >
< / div >
<!-- 货币转换信息 - 更醒目的提示 -- >
< div v-if = "order.paymentCurrency && order.paymentCurrency !== order.currency" class="currency-conversion-info" >
< el -alert
type = "warning"
:closable = "false"
show -icon
:effect = "'dark'"
class = "conversion-alert-box"
>
< template # title >
< div class = "conversion-alert-content" >
< div class = "conversion-title" >
< el-icon class = "warning-icon" > < Warning / > < / el-icon >
< strong class = "title-text" > 您将以 { { getCurrencyName ( order . paymentCurrency ) } } 支付 < / strong >
< / div >
< div class = "conversion-main-info" >
< div class = "payment-amount-highlight" >
< span class = "label" > 实际费用 : < / span >
< span class = "payment-amount-large" > { { order . paymentCurrency } } { { formatPrice ( order . paymentAmount ) } } < / span >
< / div >
< div v-if = "order.exchangeRate" class="exchange-rate-info" >
< span class = "equivalent-amount" > ( 约 { { order . currency } } { { formatPrice ( order . totalAmount ) } } ) < / span >
< span class = "rate-value" > 汇率 : { { formatRate ( order . exchangeRate ) } } < / span >
< / div >
< / div >
< div v-if = "order.rateLockedAt" class="rate-locked-info" >
< el -icon > < Clock / > < / el-icon >
< span > 汇率锁定时间 : { { formatDateTime ( order . rateLockedAt ) } } < / span >
< / div >
< / div >
< / template >
< / el-alert >
< / div >
< / div >
< / el-descriptions-item >
< el-descriptions-item label = "商品名称" :span = "2" > { { order . productName } } < / el-descriptions-item >
< el-descriptions-item label = "SKU名称" :span = "2" > { { order . skuName } } < / el-descriptions-item >
@@ -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 { createPayment Order } from '../api/payment '
import { Money , Clock , Warning } from '@element-plus/icons-vue'
import { getOrderByOrderNo , calculateCurrencyConversion } from '../api/order'
import { createPayPalOrder , getPayPalOrder , capturePayPal Order } 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 payment Data = {
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 : {
custo mer : {
firstName : order . value . customerName . split ( ' ' ) [ 0 ] || order . value . customerName ,
lastName : order . value . customerName . split ( ' ' ) . slice ( 1 ) . join ( ' ' ) || '' ,
email : order . value . customerEmail || '' ,
phon e : 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 paypalOrder Data = {
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 } ` ,
shippingNa me: order . value . shippingName ,
shippingAddressLine1 : order . value . shippingStreet ,
shippingCity : order . value . shippingCity ,
shippingState : order . value . shippingState || null ,
shippingPostalCod e: 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 ( response . message || '创建支付订单 失败')
ElMessage . error ( '获取PayPal支付链接 失败')
}
} else {
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 : 20 px ;
border - top : 1 px solid # e4e7ed ;
}
. order - amount - section {
display : flex ;
flex - direction : column ;
}
. currency - conversion - info {
margin - top : 15 px ;
}
. conversion - alert - box {
border : 2 px solid # e6a23c ;
border - radius : 8 px ;
background : linear - gradient ( 135 deg , # fff7e6 0 % , # fff3d9 100 % ) ;
box - shadow : 0 4 px 12 px rgba ( 230 , 162 , 60 , 0.2 ) ;
}
. conversion - alert - content {
line - height : 1.8 ;
padding : 5 px 0 ;
}
. conversion - title {
display : flex ;
align - items : center ;
gap : 8 px ;
margin - bottom : 12 px ;
font - size : 18 px ;
}
. warning - icon {
font - size : 24 px ;
color : # e6a23c ;
animation : pulse 2 s infinite ;
}
@ keyframes pulse {
0 % , 100 % {
opacity : 1 ;
}
50 % {
opacity : 0.7 ;
}
}
. title - text {
color : # e6a23c ;
font - size : 18 px ;
font - weight : 700 ;
}
. conversion - main - info {
background : # fff ;
padding : 15 px ;
border - radius : 6 px ;
margin : 10 px 0 ;
border - left : 4 px solid # e6a23c ;
}
. payment - amount - highlight {
display : flex ;
align - items : baseline ;
gap : 10 px ;
margin - bottom : 8 px ;
}
. payment - amount - highlight . label {
font - size : 16 px ;
color : # 606266 ;
font - weight : 600 ;
}
. payment - amount - large {
font - weight : 800 ;
color : # e6a23c ;
font - size : 28 px ;
text - shadow : 0 2 px 4 px rgba ( 230 , 162 , 60 , 0.3 ) ;
}
. exchange - rate - info {
display : flex ;
flex - direction : column ;
gap : 5 px ;
margin - top : 10 px ;
padding - top : 10 px ;
border - top : 1 px dashed # dcdfe6 ;
}
. equivalent - amount {
color : # 606266 ;
font - size : 15 px ;
font - weight : 500 ;
}
. rate - value {
color : # 909399 ;
font - size : 13 px ;
font - style : italic ;
}
. rate - locked - info {
margin - top : 10 px ;
font - size : 13 px ;
color : # 909399 ;
display : flex ;
align - items : center ;
gap : 6 px ;
padding - top : 8 px ;
border - top : 1 px solid # ebeef5 ;
}
< / style >