feat(content): enhance detail view with dynamic content and comments feat(order): add polling for payment status in the payment view feat(user): update dashboard to display wallet and recent orders feat(user): improve orders view with dynamic order fetching and status mapping feat(api): create API modules for auth, content, order, user, and common functionalities refactor(request): implement a centralized request utility for API calls
168 lines
7.5 KiB
Vue
168 lines
7.5 KiB
Vue
<template>
|
||
<div class="min-h-screen bg-slate-50 py-8">
|
||
<div class="mx-auto max-w-3xl">
|
||
|
||
<!-- Header -->
|
||
<div class="flex items-center justify-between px-4 mb-6">
|
||
<div class="flex items-center gap-2">
|
||
<div class="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center text-white font-bold">Q</div>
|
||
<span class="text-xl font-bold text-slate-900">收银台</span>
|
||
</div>
|
||
<div class="text-sm text-slate-500">订单号: {{ orderId }}</div>
|
||
</div>
|
||
|
||
<!-- Main Card -->
|
||
<div class="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
|
||
<!-- Countdown Timer -->
|
||
<div class="bg-orange-50 text-orange-700 text-center py-3 text-sm font-bold border-b border-orange-100">
|
||
<i class="pi pi-clock mr-1"></i> 支付剩余时间:{{ formatTime(timeLeft) }}
|
||
</div>
|
||
|
||
<div class="p-8 md:p-12">
|
||
<!-- Amount -->
|
||
<div class="text-center mb-10">
|
||
<p class="text-slate-500 mb-2">订单提交成功,请尽快支付</p>
|
||
<div class="text-4xl font-bold text-slate-900">¥ {{ amount }}</div>
|
||
<div class="text-sm text-slate-500 mt-2">商品:{{ productName }}</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-12">
|
||
<!-- Payment Methods -->
|
||
<div class="space-y-4">
|
||
<h3 class="font-bold text-slate-900 mb-4">选择支付方式</h3>
|
||
|
||
<label class="flex items-center gap-4 p-4 border rounded-xl cursor-pointer transition-all relative overflow-hidden"
|
||
:class="paymentMethod === 'wechat' ? 'border-green-500 bg-green-50/30 ring-1 ring-green-500' : 'border-slate-200 hover:border-slate-300'">
|
||
<input type="radio" v-model="paymentMethod" value="wechat" class="hidden">
|
||
<i class="pi pi-wechat text-3xl text-green-600"></i>
|
||
<div class="flex-1">
|
||
<div class="font-bold text-slate-900">微信支付</div>
|
||
<div class="text-xs text-slate-500">推荐使用</div>
|
||
</div>
|
||
<i v-if="paymentMethod === 'wechat'" class="pi pi-check-circle text-green-600 text-xl"></i>
|
||
</label>
|
||
|
||
<label class="flex items-center gap-4 p-4 border rounded-xl cursor-pointer transition-all"
|
||
:class="paymentMethod === 'alipay' ? 'border-blue-500 bg-blue-50/30 ring-1 ring-blue-500' : 'border-slate-200 hover:border-slate-300'">
|
||
<input type="radio" v-model="paymentMethod" value="alipay" class="hidden">
|
||
<i class="pi pi-alipay text-3xl text-blue-500"></i>
|
||
<div class="flex-1">
|
||
<div class="font-bold text-slate-900">支付宝</div>
|
||
</div>
|
||
<i v-if="paymentMethod === 'alipay'" class="pi pi-check-circle text-blue-500 text-xl"></i>
|
||
</label>
|
||
|
||
<label class="flex items-center gap-4 p-4 border rounded-xl cursor-pointer transition-all opacity-50 bg-slate-50">
|
||
<input type="radio" value="balance" disabled class="hidden">
|
||
<i class="pi pi-wallet text-3xl text-slate-400"></i>
|
||
<div class="flex-1">
|
||
<div class="font-bold text-slate-400">余额支付</div>
|
||
<div class="text-xs text-slate-400">余额不足 (¥ 0.00)</div>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- QR Code Area -->
|
||
<div class="flex flex-col items-center justify-center border-t md:border-t-0 md:border-l border-slate-100 pt-8 md:pt-0 md:pl-8">
|
||
<div class="bg-white p-4 border border-slate-200 rounded-xl shadow-sm mb-4">
|
||
<!-- Mock QR -->
|
||
<div class="w-48 h-48 bg-slate-100 flex items-center justify-center relative overflow-hidden">
|
||
<i class="pi pi-qrcode text-6xl text-slate-300"></i>
|
||
<!-- Loading Overlay -->
|
||
<div v-if="isScanning" class="absolute inset-0 bg-white/90 flex flex-col items-center justify-center">
|
||
<i class="pi pi-spin pi-spinner text-3xl text-primary-600 mb-2"></i>
|
||
<span class="text-xs text-slate-500">正在获取支付结果...</span>
|
||
</div>
|
||
<!-- Success Overlay -->
|
||
<div v-if="isSuccess" class="absolute inset-0 bg-green-500 flex flex-col items-center justify-center text-white animate-in zoom-in">
|
||
<i class="pi pi-check-circle text-5xl mb-2"></i>
|
||
<span class="font-bold">支付成功</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="text-center">
|
||
<p class="text-sm font-bold text-slate-700 mb-1">
|
||
使用 {{ paymentMethodName }} 扫一扫
|
||
</p>
|
||
<p class="text-xs text-slate-400">二维码有效时间 2小时</p>
|
||
</div>
|
||
|
||
<!-- Dev Tool -->
|
||
<button @click="simulateSuccess" class="mt-8 text-xs text-slate-300 hover:text-slate-500 underline">
|
||
[开发调试] 模拟支付成功
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||
import { useRoute, useRouter } from 'vue-router';
|
||
import { orderApi } from '../../api/order';
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
|
||
const orderId = route.params.id || '82934712';
|
||
const amount = '9.90'; // Should fetch order details first
|
||
const productName = '《霸王别姬》全本实录珍藏版';
|
||
|
||
const paymentMethod = ref('wechat');
|
||
const timeLeft = ref(900); // 15 minutes
|
||
const isScanning = ref(false);
|
||
const isSuccess = ref(false);
|
||
let timer = null;
|
||
let pollTimer = null;
|
||
|
||
const paymentMethodName = computed(() => {
|
||
return paymentMethod.value === 'wechat' ? '微信' : '支付宝';
|
||
});
|
||
|
||
const formatTime = (seconds) => {
|
||
const m = Math.floor(seconds / 60);
|
||
const s = seconds % 60;
|
||
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
||
};
|
||
|
||
const simulateSuccess = () => {
|
||
isScanning.value = true;
|
||
setTimeout(() => {
|
||
isScanning.value = false;
|
||
isSuccess.value = true;
|
||
setTimeout(() => {
|
||
router.replace(`/me/orders/${orderId}`);
|
||
}, 1500);
|
||
}, 1000);
|
||
};
|
||
|
||
onMounted(() => {
|
||
timer = setInterval(() => {
|
||
if (timeLeft.value > 0) timeLeft.value--;
|
||
}, 1000);
|
||
|
||
// Poll Status
|
||
pollTimer = setInterval(async () => {
|
||
try {
|
||
const res = await orderApi.status(orderId);
|
||
if (res.status === 'paid' || res.status === 'completed') {
|
||
isScanning.value = false;
|
||
isSuccess.value = true;
|
||
clearInterval(pollTimer);
|
||
setTimeout(() => router.replace(`/me/orders/${orderId}`), 1500);
|
||
}
|
||
} catch (e) {
|
||
console.error('Poll status failed', e);
|
||
}
|
||
}, 3000);
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
if (timer) clearInterval(timer);
|
||
if (pollTimer) clearInterval(pollTimer);
|
||
});
|
||
</script> |