Files
jurong_circle_frontdesk/src/views/Pay.vue
2025-08-26 11:36:01 +08:00

566 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="pay-page">
<!-- 导航栏 -->
<nav class="navbar">
<div class="nav-left">
<el-button
type="text"
@click="$router.go(-1)"
class="back-btn"
>
<el-icon><ArrowLeft /></el-icon>
</el-button>
</div>
<div class="nav-center">
<h1 class="nav-title">确认支付</h1>
</div>
<div class="nav-right">
</div>
</nav>
<div v-loading="loading" class="page-content">
<!-- 支付倒计时 -->
<div class="countdown-section">
<div class="countdown-header">
<el-icon class="clock-icon"><Clock /></el-icon>
<span class="countdown-label">支付剩余时间</span>
</div>
<div class="countdown-display">
<div class="time-block">
<span class="time-number">{{ formatTime(minutes) }}</span>
<span class="time-label"></span>
</div>
<div class="time-separator">:</div>
<div class="time-block">
<span class="time-number">{{ formatTime(seconds) }}</span>
<span class="time-label"></span>
</div>
</div>
<div class="countdown-tip">
<span v-if="timeLeft > 0">请在规定时间内完成支付</span>
<span v-else class="timeout-text">支付超时请重新下单</span>
</div>
</div>
<!-- 支付金额 -->
<div class="amount-section">
<h3 class="section-title">支付金额</h3>
<div class="amount-display">
<div class="total-amount-large">
<span class="currency-symbol">¥</span>
<span class="amount-number">{{ paymentData.totalAmount || 0 }}</span>
</div>
<div class="amount-breakdown" v-if="paymentData.pointsAmount > 0 || paymentData.beansAmount > 0">
<div v-if="paymentData.pointsAmount > 0" class="breakdown-item">
<el-icon><Coin /></el-icon>
<span>积分{{ paymentData.pointsAmount }}</span>
</div>
<div v-if="paymentData.beansAmount > 0" class="breakdown-item">
<el-icon><Orange /></el-icon>
<span>融豆{{ paymentData.beansAmount }}</span>
</div>
</div>
</div>
</div>
<!-- 支付方式选择 -->
<div class="payment-method-section">
<h3 class="section-title">支付方式</h3>
<div class="payment-options">
<div
class="payment-option"
:class="{ active: selectedPaymentMethod === 'beans' }"
@click="selectPaymentMethod('beans')"
>
<el-icon class="payment-icon"><Orange /></el-icon>
<div class="payment-info">
<div class="payment-name">融豆支付</div>
<div class="payment-desc">使用账户融豆进行支付</div>
</div>
<el-icon class="check-icon" v-if="selectedPaymentMethod === 'beans'"><Check /></el-icon>
</div>
<div
class="payment-option"
:class="{ active: selectedPaymentMethod === 'points' }"
@click="selectPaymentMethod('points')"
>
<el-icon class="payment-icon"><Coin /></el-icon>
<div class="payment-info">
<div class="payment-name">积分支付</div>
<div class="payment-desc">使用账户积分进行支付</div>
</div>
<el-icon class="check-icon" v-if="selectedPaymentMethod === 'points'"><Check /></el-icon>
</div>
<div
class="payment-option"
:class="{ active: selectedPaymentMethod === 'mixed' }"
@click="selectPaymentMethod('mixed')"
>
<div class="payment-icon-group">
<el-icon class="payment-icon"><Coin /></el-icon>
<span class="plus-sign">+</span>
<el-icon class="payment-icon"><Orange /></el-icon>
</div>
<div class="payment-info">
<div class="payment-name">积分+融豆</div>
<div class="payment-desc">使用积分和融豆组合支付</div>
</div>
<el-icon class="check-icon" v-if="selectedPaymentMethod === 'mixed'"><Check /></el-icon>
</div>
</div>
</div>
</div>
<!-- 底部支付按钮 -->
<div class="bottom-actions">
<div class="payment-summary">
<div class="total-amount">
<span>实付</span>
<span class="amount">¥{{ paymentData.totalAmount || 0 }}</span>
</div>
</div>
<el-button
type="primary"
size="large"
class="pay-button"
@click="confirmPayment"
:disabled="timeLeft <= 0 || paying || !selectedPaymentMethod"
:loading="paying"
>
{{ timeLeft <= 0 ? '支付超时' : paying ? '支付中...' : '确认支付' }}
</el-button>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
ArrowLeft,
Clock,
Coin,
Orange,
Check
} from '@element-plus/icons-vue'
import api from '@/utils/api'
const route = useRoute()
const router = useRouter()
// 响应式数据
const loading = ref(false)
const paying = ref(false)
const timeLeft = ref(0) // 从后端获取倒计时时间
const timer = ref(null)
const selectedPaymentMethod = ref('') // 当前选择的支付方式
const paymentData = ref({
totalAmount: 0,
pointsAmount: 0,
beansAmount: 0,
cartId: null
})
// 计算属性
const minutes = computed(() => Math.floor(timeLeft.value / 60))
const seconds = computed(() => timeLeft.value % 60)
// 方法
const formatTime = (time) => {
return time.toString().padStart(2, '0')
}
const selectPaymentMethod = (method) => {
selectedPaymentMethod.value = method
}
const startCountdown = () => {
timer.value = setInterval(() => {
if (timeLeft.value > 0) {
timeLeft.value--
} else {
clearInterval(timer.value)
ElMessage.error('支付超时,请重新下单')
}
}, 1000)
}
const fetchPaymentData = async () => {
try {
loading.value = true
const cartId = route.query.cartId
if (!cartId) {
ElMessage.error('缺少购物车信息')
router.go(-1)
return
}
// 获取支付信息
const response = await api.get(`/payment/info/${cartId}`)
if (response.data.success) {
const data = response.data.data
paymentData.value = {
totalAmount: data.totalAmount || 0,
pointsAmount: data.pointsAmount || 0,
beansAmount: data.beansAmount || 0,
cartId: cartId
}
// 设置倒计时时间(从后端获取,单位:秒)
timeLeft.value = data.remainingTime || 900 // 默认15分钟
// 开始倒计时
startCountdown()
} else {
throw new Error(response.data.message || '获取支付信息失败')
}
} catch (error) {
ElMessage.error(error.message || '获取支付信息失败')
router.go(-1)
} finally {
loading.value = false
}
}
const confirmPayment = async () => {
if (timeLeft.value <= 0) {
ElMessage.error('支付超时,请重新下单')
return
}
if (!selectedPaymentMethod.value) {
ElMessage.error('请选择支付方式')
return
}
try {
await ElMessageBox.confirm(
`确定要支付 ¥${paymentData.value.totalAmount} 吗?`,
'确认支付',
{
confirmButtonText: '确定支付',
cancelButtonText: '取消',
type: 'warning'
}
)
paying.value = true
// 构建支付数据
const submitData = {
cartId: paymentData.value.cartId,
paymentMethod: selectedPaymentMethod.value,
totalAmount: paymentData.value.totalAmount
}
// 发送支付请求到后端
const response = await api.post('/payment/confirm', submitData)
if (response.data.success) {
ElMessage.success('支付成功!')
// 清除定时器
if (timer.value) {
clearInterval(timer.value)
}
// 跳转到订单页面
router.push('/shop')
} else {
throw new Error(response.data.message || '支付失败')
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error(error.message || '支付失败,请重试')
}
} finally {
paying.value = false
}
}
// 生命周期
onMounted(() => {
fetchPaymentData()
})
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value)
}
})
</script>
<style scoped>
.pay-page {
min-height: 100vh;
background: #f5f5f5;
display: flex;
flex-direction: column;
}
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: white;
border-bottom: 1px solid #eee;
}
.nav-title {
font-size: 18px;
font-weight: 500;
margin: 0;
}
.back-btn {
color: #333;
padding: 0;
}
.page-content {
flex: 1;
padding: 0;
}
.countdown-section {
background: white;
padding: 24px 16px;
margin-bottom: 8px;
text-align: center;
}
.countdown-header {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-bottom: 16px;
}
.clock-icon {
color: #ff4757;
font-size: 18px;
}
.countdown-label {
font-size: 16px;
font-weight: 500;
color: #333;
}
.countdown-display {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-bottom: 12px;
}
.time-block {
display: flex;
flex-direction: column;
align-items: center;
background: #ff4757;
color: white;
padding: 12px 16px;
border-radius: 8px;
min-width: 60px;
}
.time-number {
font-size: 24px;
font-weight: bold;
line-height: 1;
}
.time-label {
font-size: 12px;
margin-top: 4px;
}
.time-separator {
font-size: 24px;
font-weight: bold;
color: #ff4757;
}
.countdown-tip {
font-size: 14px;
color: #666;
}
.timeout-text {
color: #ff4757;
font-weight: 500;
}
.amount-section,
.payment-method-section {
background: white;
padding: 16px;
margin-bottom: 8px;
}
.section-title {
font-size: 16px;
font-weight: 500;
margin: 0 0 12px 0;
color: #333;
}
.amount-display {
text-align: center;
padding: 20px 0;
}
.total-amount-large {
display: flex;
align-items: baseline;
justify-content: center;
gap: 4px;
margin-bottom: 16px;
}
.currency-symbol {
font-size: 20px;
color: #ff4757;
font-weight: 500;
}
.amount-number {
font-size: 36px;
font-weight: bold;
color: #ff4757;
}
.amount-breakdown {
display: flex;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
}
.breakdown-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
color: #666;
background: #f8f9fa;
padding: 6px 12px;
border-radius: 12px;
}
.payment-options {
display: flex;
flex-direction: column;
gap: 12px;
}
.payment-option {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border: 1px solid #eee;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.payment-option.active {
border-color: #ffae00;
background: #fff7e6;
}
.payment-icon {
color: #ffae00;
font-size: 20px;
}
.payment-icon-group {
display: flex;
align-items: center;
gap: 4px;
}
.plus-sign {
font-size: 14px;
color: #666;
font-weight: bold;
}
.payment-info {
flex: 1;
}
.payment-name {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.payment-desc {
font-size: 12px;
color: #666;
}
.check-icon {
color: #ffae00;
font-size: 18px;
}
.bottom-actions {
padding: 16px;
background: white;
border-top: 1px solid #eee;
display: flex;
align-items: center;
gap: 16px;
}
.payment-info {
flex: 1;
}
.payment-summary {
flex: 1;
}
.total-amount {
display: flex;
align-items: center;
gap: 4px;
font-size: 16px;
}
.amount {
color: #ff4757;
font-size: 18px;
font-weight: bold;
}
.pay-button {
min-width: 120px;
height: 48px;
background: #ffae00;
border: none;
border-radius: 24px;
font-size: 16px;
font-weight: 500;
}
.pay-button:hover {
background: #e69900;
}
.pay-button:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>