530 lines
16 KiB
Vue
530 lines
16 KiB
Vue
<template>
|
||
<div class="payment-result-container flex flex-col items-center p-6">
|
||
<!-- 加载动画,验证支付结果时显示 -->
|
||
<div v-if="isLoading" class="w-full">
|
||
<div class="flex flex-col items-center justify-center py-10">
|
||
<van-loading size="48px" color="#1989fa" />
|
||
<p class="mt-4 text-gray-600 text-lg">正在处理支付结果...</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 支付结果展示 -->
|
||
<div v-else class="w-full">
|
||
<!-- 支付成功 -->
|
||
<div v-if="paymentStatus === 'paid'" class="success-result">
|
||
<div class="success-animation mb-6">
|
||
<van-icon name="checked" size="64" color="#07c160" />
|
||
<div class="success-ring"></div>
|
||
</div>
|
||
|
||
<h1 class="text-2xl font-bold text-center text-gray-800 mb-4">
|
||
支付成功
|
||
</h1>
|
||
|
||
<div class="payment-info bg-white rounded-lg shadow-md p-6 mb-6 w-full">
|
||
<div class="flex justify-between mb-4">
|
||
<span class="text-gray-600">订单编号</span>
|
||
<span class="text-gray-800">{{ orderNo }}</span>
|
||
</div>
|
||
<div class="flex justify-between">
|
||
<span class="text-gray-600">支付类型</span>
|
||
<span class="text-gray-800">{{
|
||
paymentType === "agent_vip"
|
||
? "代理会员"
|
||
: "查询服务"
|
||
}}</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="paymentType === 'agent_vip'" class="text-center text-gray-600 mb-4">恭喜你成为高级代理会员,享受更多权益</div>
|
||
|
||
<div class="action-buttons grid grid-cols-1 gap-4">
|
||
<van-button block type="primary" class="rounded-lg" @click="handleNavigation">
|
||
{{
|
||
paymentType === "agent_vip"
|
||
? "查看会员权益"
|
||
: "查看查询结果"
|
||
}}
|
||
</van-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 退款状态 -->
|
||
<div v-else-if="paymentStatus === 'refunded'" class="refund-result">
|
||
<div v-if="paymentType === 'query'" class="success-animation mb-6">
|
||
<van-icon name="checked" size="64" color="#07c160" />
|
||
<div class="success-ring"></div>
|
||
</div>
|
||
<div v-else class="info-animation mb-6">
|
||
<van-icon name="info-o" size="64" color="#1989fa" />
|
||
<div class="info-ring"></div>
|
||
</div>
|
||
|
||
<h1 class="text-2xl font-bold text-center text-gray-800 mb-4">
|
||
{{ paymentType === "query" ? "已处理" : "订单已退款" }}
|
||
</h1>
|
||
|
||
<div class="payment-info bg-white rounded-lg shadow-md p-6 mb-6 w-full">
|
||
<div class="flex justify-between mb-4">
|
||
<span class="text-gray-600">订单编号</span>
|
||
<span class="text-gray-800">{{ orderNo }}</span>
|
||
</div>
|
||
<div class="flex justify-between mb-4">
|
||
<span class="text-gray-600">支付类型</span>
|
||
<span class="text-gray-800">{{
|
||
paymentType === "agent_vip"
|
||
? "代理会员"
|
||
: "查询服务"
|
||
}}</span>
|
||
</div>
|
||
<div class="flex justify-between">
|
||
<span class="text-gray-600">订单状态</span>
|
||
<span class="text-blue-600">已退款</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="paymentType === 'query'" class="action-buttons grid grid-cols-1 gap-4">
|
||
<van-button block type="primary" class="rounded-lg" @click="handleNavigation">
|
||
查看查询结果
|
||
</van-button>
|
||
</div>
|
||
|
||
<div v-else class="message-box p-4 bg-blue-50 rounded-lg mb-6">
|
||
<p class="text-center text-blue-800">
|
||
您的代理会员费用已退款,如有疑问请联系客服。
|
||
</p>
|
||
</div>
|
||
|
||
<div v-if="paymentType === 'agent_vip'" class="action-buttons grid grid-cols-1 gap-4">
|
||
<van-button block type="primary" class="rounded-lg" @click="contactService">
|
||
联系客服
|
||
</van-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 其他状态(待支付、失败、关闭) -->
|
||
<div v-else class="other-result">
|
||
<div class="info-animation mb-6">
|
||
<van-icon :name="getStatusIcon" size="64" :color="getStatusColor" />
|
||
<div class="info-ring" :class="getRingClass"></div>
|
||
</div>
|
||
|
||
<h1 class="text-2xl font-bold text-center text-gray-800 mb-4">
|
||
{{ statusText }}
|
||
</h1>
|
||
|
||
<!-- 添加轮询状态提示 -->
|
||
<div v-if="paymentStatus === 'pending'" class="text-center text-gray-500 mb-4">
|
||
<p>正在等待支付结果,请稍候...</p>
|
||
<p class="text-sm mt-1">
|
||
已等待
|
||
{{
|
||
Math.floor(
|
||
(pollingCount * getPollingInterval) / 1000
|
||
)
|
||
}}
|
||
秒
|
||
</p>
|
||
</div>
|
||
|
||
<div class="payment-info bg-white rounded-lg shadow-md p-6 mb-6 w-full">
|
||
<div class="flex justify-between mb-4">
|
||
<span class="text-gray-600">订单编号</span>
|
||
<span class="text-gray-800">{{ orderNo }}</span>
|
||
</div>
|
||
<div v-if="!isApiError" class="flex justify-between mb-4">
|
||
<span class="text-gray-600">支付类型</span>
|
||
<span class="text-gray-800">{{
|
||
paymentType === "agent_vip"
|
||
? "代理会员"
|
||
: "查询服务"
|
||
}}</span>
|
||
</div>
|
||
<div class="flex justify-between">
|
||
<span class="text-gray-600">订单状态</span>
|
||
<span :class="getStatusTextClass">{{
|
||
statusText
|
||
}}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="message-box p-4 bg-blue-50 rounded-lg mb-6">
|
||
<p class="text-center" :class="getMessageClass">
|
||
{{ statusMessage }}
|
||
</p>
|
||
</div>
|
||
|
||
<div class="action-buttons grid grid-cols-2 gap-4">
|
||
<van-button block type="default" class="rounded-lg" @click="goHome">
|
||
返回首页
|
||
</van-button>
|
||
<van-button block type="primary" class="rounded-lg" @click="contactService">
|
||
联系客服
|
||
</van-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import {
|
||
ref,
|
||
computed,
|
||
onBeforeMount,
|
||
defineExpose,
|
||
onBeforeUnmount,
|
||
} from "vue";
|
||
import { useRoute, useRouter } from "vue-router";
|
||
import { useAgentStore } from "@/stores/agentStore";
|
||
import { useUserStore } from "@/stores/userStore";
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
const orderNo = ref("");
|
||
const agentStore = useAgentStore();
|
||
const userStore = useUserStore();
|
||
|
||
// 状态变量
|
||
const isLoading = ref(true);
|
||
const paymentResult = ref(null);
|
||
const paymentType = ref("");
|
||
const paymentStatus = ref("");
|
||
const isApiError = ref(false);
|
||
const pollingInterval = ref(null);
|
||
const pollingCount = ref(0);
|
||
const maxPollingCount = 30; // 最大轮询次数
|
||
const baseInterval = 2000; // 基础轮询间隔(2秒)
|
||
|
||
// 计算属性
|
||
const statusText = computed(() => {
|
||
if (isApiError.value) {
|
||
return "系统繁忙";
|
||
}
|
||
|
||
switch (paymentStatus.value) {
|
||
case "pending":
|
||
return "正在支付";
|
||
case "failed":
|
||
return "支付失败";
|
||
case "closed":
|
||
return "订单已关闭";
|
||
default:
|
||
return "处理中";
|
||
}
|
||
});
|
||
|
||
const statusMessage = computed(() => {
|
||
if (isApiError.value) {
|
||
return "系统正在维护或网络繁忙,请稍后再试,或联系客服确认订单状态。";
|
||
}
|
||
|
||
switch (paymentStatus.value) {
|
||
case "pending":
|
||
return "您的订单正在支付,请稍后";
|
||
case "failed":
|
||
return "支付未成功,您可以返回重新支付,或联系客服确认详情。";
|
||
case "closed":
|
||
return "订单已关闭,如有疑问请联系客服。";
|
||
default:
|
||
return "系统正在处理您的订单,如有疑问请联系客服。";
|
||
}
|
||
});
|
||
|
||
// 状态图标
|
||
const getStatusIcon = computed(() => {
|
||
if (isApiError.value) {
|
||
return "warning-o";
|
||
}
|
||
return paymentStatus.value === "pending" ? "clock-o" : "close";
|
||
});
|
||
|
||
// 状态颜色
|
||
const getStatusColor = computed(() => {
|
||
if (isApiError.value) {
|
||
return "#ff9800"; // 橙色警告
|
||
}
|
||
return paymentStatus.value === "pending" ? "#ff976a" : "#ee0a24";
|
||
});
|
||
|
||
// 环形样式类
|
||
const getRingClass = computed(() => {
|
||
if (isApiError.value) {
|
||
return "api-error-ring";
|
||
}
|
||
return {
|
||
"pending-ring": paymentStatus.value === "pending",
|
||
"failed-ring": paymentStatus.value === "failed",
|
||
"closed-ring": paymentStatus.value === "closed",
|
||
};
|
||
});
|
||
|
||
// 状态文本样式
|
||
const getStatusTextClass = computed(() => {
|
||
if (isApiError.value) {
|
||
return "text-amber-600";
|
||
}
|
||
return {
|
||
"text-orange-500": paymentStatus.value === "pending",
|
||
"text-red-500":
|
||
paymentStatus.value === "failed" ||
|
||
paymentStatus.value === "closed",
|
||
};
|
||
});
|
||
|
||
// 消息文本样式
|
||
const getMessageClass = computed(() => {
|
||
if (isApiError.value) {
|
||
return "text-amber-800";
|
||
}
|
||
return {
|
||
"text-orange-700": paymentStatus.value === "pending",
|
||
"text-red-700":
|
||
paymentStatus.value === "failed" ||
|
||
paymentStatus.value === "closed",
|
||
};
|
||
});
|
||
|
||
// 计算轮询间隔时间(渐进式增加)
|
||
const getPollingInterval = computed(() => {
|
||
// 每5次轮询增加1秒,最大间隔10秒
|
||
const increment = Math.floor(pollingCount.value / 5);
|
||
return Math.min(baseInterval + increment * 1000, 10000);
|
||
});
|
||
|
||
// 检查支付状态
|
||
const checkPaymentStatus = async () => {
|
||
if (pollingCount.value >= maxPollingCount) {
|
||
// 超过最大轮询次数,停止轮询
|
||
stopPolling();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const { data, error } = await useApiFetch(`/pay/check`)
|
||
.post({
|
||
order_no: orderNo.value,
|
||
})
|
||
.json();
|
||
|
||
if (data.value && !error.value) {
|
||
paymentResult.value = data.value.data;
|
||
paymentType.value = data.value.data.type || "";
|
||
const newStatus = data.value.data.status || "";
|
||
|
||
// 状态发生变化时更新
|
||
if (paymentStatus.value !== newStatus) {
|
||
paymentStatus.value = newStatus;
|
||
|
||
// 对于查询类型,如果状态是已支付或已退款,直接跳转
|
||
if (
|
||
paymentType.value === "query" &&
|
||
(newStatus === "paid" || newStatus === "refunded")
|
||
) {
|
||
stopPolling();
|
||
router.replace({
|
||
path: "/report",
|
||
query: { orderNo: orderNo.value },
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 如果状态不是 pending,停止轮询
|
||
if (newStatus !== "pending") {
|
||
stopPolling();
|
||
}
|
||
}
|
||
} else {
|
||
console.error("API调用失败:", error.value);
|
||
// 不要立即停止轮询,继续尝试
|
||
}
|
||
} catch (err) {
|
||
console.error("验证支付状态失败:", err);
|
||
// 不要立即停止轮询,继续尝试
|
||
} finally {
|
||
pollingCount.value++;
|
||
isLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// 开始轮询
|
||
const startPolling = () => {
|
||
if (pollingInterval.value) return;
|
||
|
||
pollingCount.value = 0;
|
||
const poll = () => {
|
||
checkPaymentStatus();
|
||
if (
|
||
paymentStatus.value === "pending" &&
|
||
pollingCount.value < maxPollingCount
|
||
) {
|
||
pollingInterval.value = setTimeout(poll, getPollingInterval.value);
|
||
}
|
||
};
|
||
poll();
|
||
};
|
||
|
||
// 停止轮询
|
||
const stopPolling = () => {
|
||
if (pollingInterval.value) {
|
||
clearTimeout(pollingInterval.value);
|
||
pollingInterval.value = null;
|
||
}
|
||
};
|
||
|
||
// 在组件挂载前验证支付结果
|
||
onBeforeMount(async () => {
|
||
const query = new URLSearchParams(window.location.search);
|
||
orderNo.value = query.get("out_trade_no");
|
||
if (!orderNo.value) {
|
||
orderNo.value = route.query.orderNo;
|
||
}
|
||
if (!orderNo.value) {
|
||
router.push("/");
|
||
return;
|
||
}
|
||
|
||
// 检测是否为 iOS 浏览器且没有上级页面
|
||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||
const hasHistory = window.history.length > 1;
|
||
|
||
if (isIOS && !hasHistory) {
|
||
// iOS 浏览器且没有上级页面,关闭当前标签页
|
||
window.close();
|
||
return;
|
||
}
|
||
// 首次检查支付状态
|
||
await checkPaymentStatus();
|
||
|
||
// 如果状态是 pending,开始轮询
|
||
if (paymentStatus.value === "pending") {
|
||
startPolling();
|
||
}
|
||
});
|
||
|
||
// 组件卸载前清理轮询
|
||
onBeforeUnmount(() => {
|
||
stopPolling();
|
||
});
|
||
|
||
// 处理导航逻辑
|
||
function handleNavigation() {
|
||
if (paymentType.value === "agent_vip") {
|
||
// 跳转到代理会员页面
|
||
router.replace("/agent");
|
||
agentStore.fetchAgentStatus();
|
||
userStore.fetchUserInfo();
|
||
} else {
|
||
// 跳转到查询结果页面
|
||
router.replace({
|
||
path: "/report",
|
||
query: { orderNo: orderNo.value },
|
||
});
|
||
}
|
||
}
|
||
|
||
// 返回首页
|
||
function goHome() {
|
||
router.replace("/");
|
||
}
|
||
|
||
// 联系客服
|
||
function contactService() {
|
||
// 可以替换为实际的客服联系逻辑,如打开聊天窗口或跳转到客服页面
|
||
window.location.href =
|
||
"https://work.weixin.qq.com/kfid/kfc5c19b2b93a5e73b9"; // 跳转到客服页面
|
||
}
|
||
|
||
// 暴露方法和数据供父组件或路由调用
|
||
defineExpose({
|
||
paymentResult,
|
||
paymentType,
|
||
paymentStatus,
|
||
handleNavigation,
|
||
stopPolling, // 暴露停止轮询方法
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.payment-result-container {
|
||
min-height: 80vh;
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.success-animation,
|
||
.info-animation {
|
||
position: relative;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin: 2rem auto;
|
||
}
|
||
|
||
.success-ring,
|
||
.info-ring,
|
||
.pending-ring,
|
||
.failed-ring,
|
||
.closed-ring,
|
||
.api-error-ring {
|
||
position: absolute;
|
||
width: 80px;
|
||
height: 80px;
|
||
border-radius: 50%;
|
||
animation: pulse 1.5s infinite;
|
||
}
|
||
|
||
.success-ring {
|
||
border: 2px solid #07c160;
|
||
}
|
||
|
||
.info-ring {
|
||
border: 2px solid #1989fa;
|
||
}
|
||
|
||
.pending-ring {
|
||
border: 2px solid #ff976a;
|
||
}
|
||
|
||
.failed-ring,
|
||
.closed-ring {
|
||
border: 2px solid #ee0a24;
|
||
}
|
||
|
||
.api-error-ring {
|
||
border: 2px solid #ff9800;
|
||
/* 橙色警告 */
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% {
|
||
transform: scale(0.95);
|
||
opacity: 0.8;
|
||
}
|
||
|
||
70% {
|
||
transform: scale(1.1);
|
||
opacity: 0.3;
|
||
}
|
||
|
||
100% {
|
||
transform: scale(0.95);
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
.success-result,
|
||
.refund-result,
|
||
.other-result {
|
||
animation: fadeIn 0.5s ease-in-out;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
</style>
|