|
|
- <template>
- <div class="wechat-payment">
- <!-- 支付弹窗 -->
- <el-dialog
- v-model="visible"
- title="微信支付"
- width="400px"
- :close-on-click-modal="false"
- :close-on-press-escape="false"
- @close="handleCancel"
- >
- <div class="payment-content">
- <!-- 支付信息 -->
- <div class="payment-info">
- <div class="payment-title">{{ paymentData.title }}</div>
- <div class="payment-amount">
- ¥<span class="amount-value">{{ formatAmount(paymentData.amount) }}</span>
- </div>
- </div>
-
- <!-- 支付状态 -->
- <div class="payment-status">
- <template v-if="paymentStatus === 'pending'">
- <div class="qr-container">
- <div class="qr-code" ref="qrCodeRef">
- <!-- 二维码将在这里生成 -->
- </div>
- <div class="qr-tips">
- <el-icon class="scan-icon"><Iphone /></el-icon>
- <p>请使用微信扫码支付</p>
- <p class="tips-text">扫码后请在手机上完成支付</p>
- </div>
- </div>
- <!-- 倒计时 -->
- <div class="countdown">
- <span>支付剩余时间:</span>
- <span class="countdown-time">{{ formatTime(countdown) }}</span>
- </div>
- </template>
-
- <template v-else-if="paymentStatus === 'loading'">
- <div class="loading-status">
- <el-icon class="loading-icon"><Loading /></el-icon>
- <p>正在生成支付二维码...</p>
- </div>
- </template>
-
- <template v-else-if="paymentStatus === 'success'">
- <div class="success-status">
- <el-icon class="success-icon"><CircleCheck /></el-icon>
- <p>支付成功!</p>
- <p class="success-tips">感谢您的支付,页面即将跳转...</p>
- </div>
- </template>
-
- <template v-else-if="paymentStatus === 'failed'">
- <div class="failed-status">
- <el-icon class="failed-icon"><CircleClose /></el-icon>
- <p>支付失败</p>
- <p class="failed-tips">{{ errorMessage || '支付过程中出现异常,请重试' }}</p>
- </div>
- </template>
-
- <template v-else-if="paymentStatus === 'timeout'">
- <div class="timeout-status">
- <el-icon class="timeout-icon"><Clock /></el-icon>
- <p>支付超时</p>
- <p class="timeout-tips">二维码已过期,请重新发起支付</p>
- </div>
- </template>
- </div>
- </div>
-
- <!-- 操作按钮 -->
- <template #footer>
- <div class="dialog-footer">
- <el-button
- v-if="paymentStatus === 'pending'"
- @click="refreshQrCode"
- :loading="refreshing"
- >
- 刷新二维码
- </el-button>
- <el-button
- v-if="paymentStatus === 'failed' || paymentStatus === 'timeout'"
- type="primary"
- @click="retryPayment"
- >
- 重新支付
- </el-button>
- <el-button
- @click="handleCancel"
- :disabled="paymentStatus === 'loading'"
- >
- {{ paymentStatus === 'success' ? '关闭' : '取消支付' }}
- </el-button>
- </div>
- </template>
- </el-dialog>
- </div>
- </template>
-
- <script>
- import { ref, computed, nextTick, onUnmounted, watch } from 'vue';
- import { ElMessage, ElMessageBox } from 'element-plus';
- import { orderApi } from '@/api/user';
- import {
- Iphone,
- Loading,
- CircleCheck,
- CircleClose,
- Clock
- } from '@element-plus/icons-vue';
- import { wechatPayApi } from '@/api/user.js';
- import QRCode from 'qrcode';
-
- export default {
- name: 'WechatPayment',
- components: {
- Iphone,
- Loading,
- CircleCheck,
- CircleClose,
- Clock
- },
- props: {
- // 是否显示支付弹窗
- modelValue: {
- type: Boolean,
- default: false
- },
- // 支付数据
- paymentData: {
- type: Object,
- default: () => ({
- orderId: '', // 订单ID
- title: '商品支付', // 支付标题
- amount: 0, // 支付金额(分)
- body: '' // 商品描述
- })
- }
- },
- emits: ['update:modelValue', 'success', 'cancel', 'failed'],
- setup(props, { emit }) {
- const visible = computed({
- get: () => props.modelValue,
- set: (value) => emit('update:modelValue', value)
- });
-
- const qrCodeRef = ref(null);
- const paymentStatus = ref('loading');
- const countdown = ref(300);
- const refreshing = ref(false);
- const errorMessage = ref('');
-
- let pollingTimer = null;
- let countdownTimer = null;
- let outTradeNo = '';
-
- // 格式化金额
- const formatAmount = (amount) => {
- return (amount / 100).toFixed(2);
- };
-
- // 格式化时间
- const formatTime = (seconds) => {
- const minutes = Math.floor(seconds / 60);
- const remainingSeconds = seconds % 60;
- return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
- };
-
- // 生成二维码
- const generateQrCode = async (codeUrl) => {
- try {
- await nextTick();
- if (qrCodeRef.value) {
- qrCodeRef.value.innerHTML = '';
- await QRCode.toCanvas(qrCodeRef.value, codeUrl, {
- width: 200,
- height: 200,
- margin: 1,
- color: {
- dark: '#000000',
- light: '#FFFFFF'
- }
- });
- }
- } catch (error) {
- console.error('生成二维码失败:', error);
- ElMessage.error('生成二维码失败');
- }
- };
-
- // 创建支付订单
- const createPaymentOrder = async () => {
- try {
- paymentStatus.value = 'loading';
-
- const params = {
- orderId: props.paymentData.orderId,
- totalFee: props.paymentData.amount,
- body: props.paymentData.body || props.paymentData.title
- };
-
- // 模拟API响应
- await new Promise(resolve => setTimeout(resolve, 1000));
- const mockResponse = {
- code: 200,
- result: {
- codeUrl: `weixin://wxpay/bizpayurl?pr=${Math.random().toString(36).substr(2, 9)}`,
- outTradeNo: `ORDER_${Date.now()}`
- }
- };
-
- if (mockResponse.code === 200 && mockResponse.result) {
- const { codeUrl, outTradeNo: tradeNo } = mockResponse.result;
- outTradeNo = tradeNo;
-
- await generateQrCode(codeUrl);
-
- paymentStatus.value = 'pending';
- startPolling();
- startCountdown();
- } else {
- throw new Error('创建支付订单失败');
- }
- } catch (error) {
- console.error('创建支付订单失败:', error);
- paymentStatus.value = 'failed';
- errorMessage.value = error.message;
- ElMessage.error(error.message || '创建支付订单失败');
- }
- };
-
- // 开始轮询支付状态
- const startPolling = () => {
- if (pollingTimer) {
- clearInterval(pollingTimer);
- }
-
- pollingTimer = setInterval(async () => {
- await checkPaymentStatus();
- }, 2000);
- };
-
- // 停止轮询
- const stopPolling = () => {
- if (pollingTimer) {
- clearInterval(pollingTimer);
- pollingTimer = null;
- }
- };
-
- // 检查支付状态
- const checkPaymentStatus = async () => {
- try {
- // 模拟随机支付成功(演示用)
- if (Math.random() > 0.95) {
- handlePaymentSuccess();
- return;
- }
-
- // 实际项目中调用真实API
- // const response = await wechatPayApi.queryWechatPayStatus(outTradeNo);
- } catch (error) {
- console.error('查询支付状态失败:', error);
- }
- };
-
- // 处理支付成功
- const handlePaymentSuccess = () => {
- paymentStatus.value = 'success';
- stopPolling();
- stopCountdown();
-
- ElMessage.success('支付成功!');
- emit('success', outTradeNo);
-
- setTimeout(() => {
- visible.value = false;
- }, 3000);
- };
-
- // 处理支付失败
- const handlePaymentFailed = (message) => {
- paymentStatus.value = 'failed';
- errorMessage.value = message;
- stopPolling();
- stopCountdown();
-
- ElMessage.error(message || '支付失败');
- emit('failed', message);
- };
-
- // 开始倒计时
- const startCountdown = () => {
- countdown.value = 300;
-
- countdownTimer = setInterval(() => {
- countdown.value--;
-
- if (countdown.value <= 0) {
- handlePaymentTimeout();
- }
- }, 1000);
- };
-
- // 停止倒计时
- const stopCountdown = () => {
- if (countdownTimer) {
- clearInterval(countdownTimer);
- countdownTimer = null;
- }
- };
-
- // 处理支付超时
- const handlePaymentTimeout = () => {
- paymentStatus.value = 'timeout';
- stopPolling();
- stopCountdown();
-
- ElMessage.warning('支付超时,请重新发起支付');
- };
-
- // 刷新二维码
- const refreshQrCode = async () => {
- refreshing.value = true;
- try {
- await createPaymentOrder();
- } finally {
- refreshing.value = false;
- }
- };
-
- // 重新支付
- const retryPayment = () => {
- errorMessage.value = '';
- createPaymentOrder();
- };
-
- // 取消支付
- const handleCancel = async () => {
- if (paymentStatus.value === 'success') {
- visible.value = false;
- return;
- }
-
- try {
- await ElMessageBox.confirm(
- '确定要取消支付吗?',
- '取消支付',
- {
- confirmButtonText: '确定',
- cancelButtonText: '继续支付',
- type: 'warning',
- }
- );
-
- stopPolling();
- stopCountdown();
-
- visible.value = false;
- emit('cancel');
- } catch {
- // 用户选择继续支付
- }
- };
-
- // 监听弹窗显示状态
- watch(visible, (newVal) => {
- if (newVal && props.paymentData.orderId) {
- createPaymentOrder();
- } else {
- stopPolling();
- stopCountdown();
- paymentStatus.value = 'loading';
- errorMessage.value = '';
- outTradeNo = '';
- }
- });
-
- // 组件卸载时清理
- onUnmounted(() => {
- stopPolling();
- stopCountdown();
- });
-
- return {
- visible,
- qrCodeRef,
- paymentStatus,
- countdown,
- refreshing,
- errorMessage,
- formatAmount,
- formatTime,
- refreshQrCode,
- retryPayment,
- handleCancel
- };
- }
- };
- </script>
-
- <style lang="scss" scoped>
- .wechat-payment {
- .payment-content {
- text-align: center;
- padding: 20px 0;
-
- .payment-info {
- margin-bottom: 20px;
-
- .payment-title {
- font-size: 18px;
- font-weight: 600;
- color: #303133;
- margin-bottom: 8px;
- }
-
- .payment-amount {
- font-size: 24px;
- color: #0A2463;
- font-weight: 700;
-
- .amount-value {
- font-size: 32px;
- }
- }
- }
-
- .payment-status {
- min-height: 300px;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
-
- .qr-container {
- .qr-code {
- margin-bottom: 16px;
- display: flex;
- justify-content: center;
-
- :deep(canvas) {
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- }
- }
-
- .qr-tips {
- .scan-icon {
- font-size: 32px;
- color: #0A2463;
- margin-bottom: 8px;
- }
-
- p {
- margin: 4px 0;
- color: #606266;
-
- &:first-of-type {
- font-size: 16px;
- font-weight: 600;
- color: #303133;
- }
- }
-
- .tips-text {
- font-size: 14px;
- color: #909399;
- }
- }
- }
-
- .countdown {
- margin-top: 16px;
- font-size: 14px;
- color: #909399;
-
- .countdown-time {
- color: #F56C6C;
- font-weight: 600;
- }
- }
-
- .loading-status,
- .success-status,
- .failed-status,
- .timeout-status {
- display: flex;
- flex-direction: column;
- align-items: center;
-
- .loading-icon {
- font-size: 48px;
- color: #0A2463;
- animation: rotate 2s linear infinite;
- margin-bottom: 16px;
- }
-
- .success-icon {
- font-size: 48px;
- color: #67C23A;
- margin-bottom: 16px;
- }
-
- .failed-icon,
- .timeout-icon {
- font-size: 48px;
- color: #F56C6C;
- margin-bottom: 16px;
- }
-
- p {
- margin: 4px 0;
- font-size: 16px;
- color: #303133;
-
- &:first-of-type {
- font-weight: 600;
- font-size: 18px;
- }
- }
-
- .success-tips,
- .failed-tips,
- .timeout-tips {
- font-size: 14px;
- color: #909399;
- }
- }
- }
- }
-
- .dialog-footer {
- display: flex;
- justify-content: center;
- gap: 12px;
-
- .el-button {
- &.el-button--primary {
- background-color: #0A2463;
- border-color: #0A2463;
-
- &:hover {
- background-color: #083354;
- border-color: #083354;
- }
- }
- }
- }
- }
-
- @keyframes rotate {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
- }
-
- // 响应式设计
- @media (max-width: 768px) {
- .wechat-payment {
- :deep(.el-dialog) {
- width: 90% !important;
- margin: 5vh auto !important;
- }
-
- .payment-content {
- padding: 16px 0;
-
- .payment-info {
- .payment-title {
- font-size: 16px;
- }
-
- .payment-amount {
- font-size: 20px;
-
- .amount-value {
- font-size: 28px;
- }
- }
- }
-
- .payment-status {
- min-height: 250px;
-
- .qr-container {
- .qr-code {
- :deep(canvas) {
- width: 160px !important;
- height: 160px !important;
- }
- }
- }
- }
- }
- }
- }
- </style>
|