项目背景
新入职的公司是做海外平台的类似于商城的一个app,里面涉及到支付,我写的是web端支付,现有的支付用的都是stripe调用,所以写下这篇笔记,未来如果再用到也算是有些思路
使用支付方式
我使用的支付方式是自己创建支付样式,而不是直接调用stripe平台的内置样式。Ps:我也试过stripe平台的内置支付组件可惜没搞懂,相关文档太少了,官方文档也大部分是英文的💔
支付逻辑
graph TD
用户选择服务完毕后点击前端按钮调起支付 --> 前端调用后端获取支付信息接口 --> 后端返回支付相关信息例如publishableKey --> 前端获取到支付信息调用stripeAPI --> 根据stripe返回的信息判断用户是否支付成功做后续处理
支付组件
这个就是我的stripe支付组件
js
<template>
<div class="payment-container">
<h2>确认支付</h2>
<!-- 订单信息(由父组件传入) -->
<div class="order-info" v-if="orderNo || amount">
<p v-if="orderNo">{{ t('payment.orderNo') }} {{ orderNo }}</p>
<p v-if="amount">{{ t('payment.amount') }}{{ amount }} {{ currencySymbol }}</p>
</div>
<form @submit.prevent="handlePay">
<div class="form-row">
<label>{{ t('payment.card') }}</label>
<div id="card-element" ref="cardElementRef"></div>
<div class="card-errors" v-if="cardError">{{ cardError }}</div>
</div>
<button type="submit" :disabled="isProcessing" class="pay-button">
{{ isProcessing ? t('payment.processing') : t('payment.pay') + amount + currencySymbol }}
</button>
</form>
<div v-if="resultMessage" class="result" :class="resultType">
{{ resultMessage }}
</div>
<!-- 自定义 Loading 遮罩 -->
<div v-if="showLoading" class="loading-overlay">
<div class="loading-content">
<div class="loading-spinner"></div>
<p class="loading-text">{{ loadingText }}</p>
</div>
</div>
<!-- 支付成功提示弹窗 -->
<div v-if="showSuccessModal" class="success-overlay">
<div class="success-modal">
<div class="success-icon">✓</div>
<h3>{{ t('payment.success') }}</h3>
<p class="success-message">{{ t('payment.successMessage') }}</p>
<p class="success-tip">{{ countDown }}{{ t('payment.autoRedirect') }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { loadStripe } from '@stripe/stripe-js'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { paySuccess } from '~/api/goods'
const { t } = useI18n()
// ---------- Props ----------
const props = defineProps({
// 必需:Stripe 公钥 (pk_live_xxx 或 pk_test_xxx)
publishableKey: {
type: String,
required: true
},
// 必需:PaymentIntent 的 client_secret
clientSecret: {
type: String,
required: true
},
// 可选:用于显示的订单号
orderNo: String,
// 可选:金额(显示用)
amount: [String, Number],
// 可选:货币
currency: {
type: String,
default: 'SGD'
},
currencySymbol: {
type: String,
default: '$'
},
orderId: {
type: String,
default: null
}
})
// ---------- Events ----------
const emit = defineEmits(['close'])
// ---------- 响应式状态 ----------
const cardElementRef = ref(null)
const cardError = ref('')
const isProcessing = ref(false)
const resultMessage = ref('')
const resultType = ref('') // 'success' 或 'error'
// 自定义 loading 状态
const showLoading = ref(false)
const loadingText = ref('')
// 支付成功弹窗状态
const showSuccessModal = ref(false)
const countDown = ref(3)
let stripe = null
let cardElement = null
// ---------- 路由实例 ----------
const router = useRouter()
// ---------- 支付逻辑 ----------
const handlePay = async () => {
if (!stripe || !cardElement) {
resultMessage.value = t('payment.notReady')
resultType.value = 'error'
return
}
isProcessing.value = true
resultMessage.value = ''
resultType.value = ''
// 显示自定义 loading
showLoading.value = true
loadingText.value = t('payment.processingPay')
try {
// 确认支付(自动处理 3D Secure)
// payment_method 应该是一个对象,包含 card 属性
const { error, paymentIntent } = await stripe.confirmCardPayment(prpos.clientSecret, {
payment_method: {
card: cardElement,
billing_details: {
// name: props.orderNo || 'Customer'
name: 'cus_UPZ6xMq5ziKgm9'
}
}
})
if (error) {
throw new Error(error.message)
}
if (paymentIntent.status === 'succeeded') {
// 隐藏 loading,显示支付成功弹窗
showLoading.value = false
try {
// 调用支付成功接口
await paySuccess({ orderId: props.orderId })
showSuccessModal.value = true;
countDown.value = 3;
const timer = setInterval(() => {
countDown.value--;
if(countDown.value <= 0) {
clearInterval(timer)
// 判断当前页面是否是订单列表页
if (router.currentRoute.value.path === '/order/list') {
// 如果当前就在订单列表页,强制刷新页面
window.location.reload()
} else {
// 否则跳转到订单列表页
router.push('/order/list')
}
}
}, 1000)
} catch (error) {
resultMessage.value = '❌' + t('payment.fail') + error.message
resultType.value = 'error'
}
} else if (paymentIntent.status === 'requires_action') {
// 3D Secure 验证中,Stripe 会自动处理,此处一般不会执行
loadingText.value = t('payment.verifying')
}
} catch (err) {
showLoading.value = false
resultMessage.value = '❌' + t('payment.fail') + err.message
resultType.value = 'error'
} finally {
isProcessing.value = false
}
}
// ---------- 生命周期 ----------
onMounted(async () => {
// 验证参数
if (!props.publishableKey || props.publishableKey.trim() === '') {
resultMessage.value = t('payment.noKey')
resultType.value = 'error'
return
}
// 加载 Stripe - 使用 props 传入的公钥,生产环境应从环境变量获取
stripe = await loadStripe(props.publishableKey)
if (!stripe) {
resultMessage.value = t('payment.initFail')
resultType.value = 'error'
return
}
// 创建卡片元素
const elements = stripe.elements()
cardElement = elements.create('card', {
style: {
base: {
fontSize: '16px',
color: '#32325d',
'::placeholder': { color: '#aab7c4' }
},
invalid: { color: '#dc3545' }
}
})
// 挂载到 DOM
if (cardElementRef.value) {
cardElement.mount(cardElementRef.value)
}
// 监听输入错误
cardElement.on('change', (event) => {
cardError.value = event.error ? event.error.message : ''
})
})
onUnmounted(() => {
if (cardElement) {
cardElement.destroy()
}
})
</script>
<style scoped>
.payment-container {
max-width: 500px;
margin: 20px auto;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
font-family: system-ui, -apple-system, sans-serif;
}
.order-info {
background: #f8f9fa;
padding: 12px;
border-radius: 6px;
margin-bottom: 20px;
font-size: 14px;
}
.form-row {
margin-bottom: 20px;
}
.form-row label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
#card-element {
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
transition: border 0.2s;
}
.card-errors {
color: #dc3545;
font-size: 13px;
margin-top: 8px;
}
.pay-button {
width: 100%;
padding: 12px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.pay-button:hover:not(:disabled) {
background: #0056b3;
}
.pay-button:disabled {
background: #ccc;
cursor: not-allowed;
}
/* 移动端适配 */
@media (max-width: 768px) {
.payment-container {
max-width: 100%;
margin: 10px;
padding: 16px;
border-radius: 12px;
}
.order-info {
padding: 10px;
font-size: 13px;
}
.form-row {
margin-bottom: 16px;
}
.form-row label {
font-size: 14px;
}
#card-element {
padding: 14px 12px;
font-size: 15px;
}
.pay-button {
padding: 14px;
font-size: 15px;
border-radius: 8px;
}
.success-modal {
padding: 32px 24px !important;
border-radius: 16px;
}
.success-icon {
width: 60px !important;
height: 60px !important;
font-size: 30px !important;
line-height: 60px !important;
margin-bottom: 16px !important;
}
.loading-content {
padding: 30px 24px !important;
border-radius: 12px;
}
.loading-spinner {
width: 40px !important;
height: 40px !important;
}
.loading-text {
font-size: 14px !important;
}
}
.result {
margin-top: 20px;
padding: 12px;
border-radius: 4px;
text-align: center;
}
.result.success {
background: #d4edda;
color: #155724;
}
.result.error {
background: #f8d7da;
color: #721c24;
}
/* 自定义 Loading 遮罩 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-content {
background: white;
border-radius: 12px;
padding: 40px;
text-align: center;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
.loading-spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 16px;
color: #333;
font-weight: 500;
}
/* 支付成功弹窗 */
.success-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.success-modal {
background: white;
border-radius: 16px;
padding: 48px 40px;
text-align: center;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
max-width: 400px;
width: 90%;
}
.success-icon {
width: 80px;
height: 80px;
background: linear-gradient(135deg, #28a745, #20c997);
border-radius: 50%;
color: white;
font-size: 40px;
line-height: 80px;
margin: 0 auto 24px;
animation: bounce 0.5s ease-out;
}
@keyframes bounce {
0% { transform: scale(0); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.success-modal h3 {
font-size: 24px;
color: #333;
margin-bottom: 12px;
}
.success-message {
font-size: 16px;
color: #666;
margin-bottom: 8px;
}
.success-tip {
font-size: 14px;
color: #999;
}
</style>