newfront/src/views/Subscribe.vue
2025-12-13 20:25:49 +08:00

603 lines
13 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="subscribe-page">
<Navbar />
<div class="container">
<div class="header-section">
<h1 class="page-title">支持我们</h1>
<div class="control-card">
<p class="description">所有赞助收入都将用于网站服务的维护</p>
</div>
</div>
<div class="content-section">
<div class="subscribe-card">
<div class="card-header">
<h2>赞助服务</h2>
<div class="price">
<span class="currency">¥</span>
<span class="integer">9</span>
<span class="decimal">.90</span>
</div>
</div>
<div class="card-body">
<ul class="privileges-list">
<li>
<span class="icon">🚀</span>
<span class="text">高速下载通道</span>
</li>
<li>
<span class="icon">💰</span>
<span class="text">5000东币</span>
</li>
<li>
<span class="icon">💬</span>
<span class="text">100次课程评分追问机会</span>
</li>
<li>
<span class="icon"></span>
<span class="text">炫彩昵称</span>
</li>
</ul>
</div>
<div class="card-footer">
<button
class="purchase-btn"
:disabled="!canPurchase"
@click="handlePurchase"
>
{{ buttonText }}
</button>
</div>
</div>
</div>
</div>
<!-- 支付模态框 -->
<div v-if="showPaymentModal" class="modal-overlay" @click.self="closeModal">
<div class="payment-modal">
<div class="modal-header">
<h3>扫码支付</h3>
<button class="close-btn" @click="closeModal">×</button>
</div>
<div class="modal-body">
<p class="instruction">请使用微信扫一扫支付</p>
<p class="sub-instruction">暂不支持保存图片到扫一扫打开</p>
<div class="qr-container">
<img v-if="qrUrl" :src="qrUrl" alt="支付二维码" class="qr-code">
<div v-else class="loading-qr">二维码加载中...</div>
</div>
<div class="notification-area">
<p v-if="notification" :class="['notification', notificationType]">
{{ notification }}
</p>
</div>
</div>
<div class="modal-footer">
<button
class="complete-btn"
:disabled="isCheckingStatus"
@click="checkPaymentStatus"
>
{{ isCheckingStatus ? '查询中...' : '支付完成' }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import Navbar from '@/components/NavBar.vue';
import Cookies from 'js-cookie';
import axios from 'axios';
export default {
name: 'Subscribe',
components: {
Navbar
},
data() {
return {
isLogin: false,
isVip: false,
loading: true,
showPaymentModal: false,
qrUrl: '',
orderUuid: '',
notification: '',
notificationType: 'info', // info, success, error
isCheckingStatus: false
};
},
computed: {
canPurchase() {
return this.isLogin && !this.isVip;
},
buttonText() {
if (!this.isLogin) return '请先登录';
if (this.isVip) return '您已拥有';
return '立即购买';
}
},
mounted() {
this.checkLoginStatus();
document.title = '支持我们 - NEU小站';
},
methods: {
async checkLoginStatus() {
const token = Cookies.get('token');
if (!token) {
this.isLogin = false;
this.loading = false;
return;
}
this.isLogin = true;
await this.checkVipStatus();
this.loading = false;
},
async checkVipStatus() {
try {
const token = Cookies.get('token');
const response = await axios.get('https://newfront.xn--xhq44jb2fzpc.com/subscribe/vip', {
headers: { 'Authorization': token }
});
this.isVip = response.data.is_vip;
} catch (error) {
console.error('获取VIP状态失败:', error);
// 如果是403说明可能是VIP不对403是后端返回的错误信息这里我们应该看接口文档
// 根据之前的实现,/subscribe/vip 返回 { is_vip: boolean }
}
},
async handlePurchase() {
if (!this.canPurchase) return;
try {
const token = Cookies.get('token');
const response = await axios.get('https://newfront.xn--xhq44jb2fzpc.com/subscribe/order', {
headers: { 'Authorization': token }
});
// 成功获取订单
this.qrUrl = response.data.qr_url;
this.orderUuid = response.data.uuid;
this.showPaymentModal = true;
this.notification = '';
} catch (error) {
if (error.response && error.response.status === 403) {
// 已经是VIP
this.isVip = true;
alert(error.response.data.error || '您已经是VIP会员');
} else {
console.error('创建订单失败:', error);
alert('创建订单失败,请稍后重试');
}
}
},
async checkPaymentStatus() {
if (!this.orderUuid) return;
this.isCheckingStatus = true;
this.notification = '';
try {
const token = Cookies.get('token');
const response = await axios.post('https://newfront.xn--xhq44jb2fzpc.com/subscribe/status',
{ uuid: this.orderUuid },
{ headers: { 'Authorization': token } }
);
if (response.data.is_paid) {
this.notification = '支付成功,请稍后...';
this.notificationType = 'success';
this.isVip = true;
// 3秒后刷新页面
setTimeout(() => {
window.location.reload();
}, 3000);
} else {
this.notification = '尚未检测到支付完成,请稍后重试';
this.notificationType = 'info';
}
} catch (error) {
console.error('查询支付状态失败:', error);
this.notification = '查询失败,请重试';
this.notificationType = 'error';
} finally {
this.isCheckingStatus = false;
}
},
closeModal() {
this.showPaymentModal = false;
this.qrUrl = '';
this.orderUuid = '';
this.notification = '';
}
}
};
</script>
<style scoped>
.subscribe-page {
min-height: 100vh;
background: linear-gradient(180deg, #e6f7ff 0%, #f5f7fa 100%);
padding-top: 60px; /* Navbar height reservation */
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 40px 20px;
}
.header-section {
text-align: center;
margin-bottom: 50px;
}
.page-title {
font-size: 2.8rem;
color: #1a2a3a;
margin-bottom: 16px;
font-weight: 800;
letter-spacing: -0.5px;
}
.control-card {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-radius: 50px;
padding: 12px 30px;
box-shadow: 0 4px 20px rgba(0, 100, 255, 0.08);
display: inline-block;
border: 1px solid rgba(255, 255, 255, 0.5);
}
.description {
font-size: 1.05rem;
color: #5b6b7f;
margin: 0;
font-weight: 500;
}
.content-section {
display: flex;
justify-content: center;
perspective: 1000px;
}
.subscribe-card {
background: #fff;
border-radius: 24px;
box-shadow: 0 20px 60px rgba(24, 144, 255, 0.15);
width: 100%;
max-width: 380px;
overflow: hidden;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid rgba(24, 144, 255, 0.1);
position: relative;
}
.subscribe-card:hover {
transform: translateY(-10px);
box-shadow: 0 30px 80px rgba(24, 144, 255, 0.25);
}
.card-header {
background: linear-gradient(135deg, #2979ff 0%, #00e5ff 100%);
color: white;
padding: 40px 30px;
text-align: center;
position: relative;
overflow: hidden;
}
/* Add a subtle shine effect to header */
.card-header::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.2) 0%, transparent 60%);
transform: rotate(30deg);
}
.card-header h2 {
margin: 0;
font-size: 1.4rem;
font-weight: 600;
margin-bottom: 12px;
opacity: 0.95;
position: relative;
}
.price {
font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, sans-serif;
color: #fff;
display: flex;
align-items: baseline;
justify-content: center;
text-shadow: 0 2px 10px rgba(0,0,0,0.1);
line-height: 1;
}
.currency {
font-size: 1.4rem;
margin-right: 2px;
font-weight: 500;
opacity: 0.9;
}
.integer {
font-family: 'DIN Alternate', 'Roboto', 'Arial', sans-serif;
font-size: 3.8rem;
font-weight: bold;
letter-spacing: -1px;
}
.decimal {
font-family: 'DIN Alternate', 'Roboto', 'Arial', sans-serif;
font-size: 2.4rem;
font-weight: 600;
opacity: 0.9;
}
.card-body {
padding: 40px 30px;
}
.privileges-list {
list-style: none;
padding: 0;
margin: 0;
}
.privileges-list li {
display: flex;
align-items: center;
margin-bottom: 20px;
font-size: 1.1rem;
color: #4a5568;
font-weight: 500;
}
.privileges-list li:last-child {
margin-bottom: 0;
}
.icon {
margin-right: 16px;
font-size: 1.2rem;
width: 42px;
height: 42px;
background: #f0f7ff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #2979ff;
flex-shrink: 0;
}
.card-footer {
padding: 0 30px 40px;
text-align: center;
}
.purchase-btn {
background: linear-gradient(90deg, #2979ff, #00b0ff);
color: white;
border: none;
border-radius: 50px;
padding: 16px 40px;
font-size: 1.2rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
box-shadow: 0 10px 25px rgba(41, 121, 255, 0.3);
letter-spacing: 1px;
}
.purchase-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 15px 35px rgba(41, 121, 255, 0.4);
filter: brightness(1.05);
}
.purchase-btn:disabled {
background: #e2e8f0;
color: #a0aec0;
box-shadow: none;
cursor: not-allowed;
transform: none;
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(4px);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.payment-modal {
background: white;
border-radius: 24px;
width: 90%;
max-width: 380px;
overflow: hidden;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25);
animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(40px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #f0f0f0;
}
.modal-header h3 {
margin: 0;
font-size: 1.2rem;
color: #1a2a3a;
font-weight: 700;
}
.close-btn {
background: #f7fafc;
border: none;
width: 32px;
height: 32px;
border-radius: 50%;
font-size: 1.2rem;
color: #64748b;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.close-btn:hover {
background: #e2e8f0;
color: #ef4444;
}
.modal-body {
padding: 30px 24px;
text-align: center;
}
.instruction {
font-size: 1.1rem;
color: #1a2a3a;
margin-bottom: 8px;
font-weight: 600;
}
.sub-instruction {
font-size: 0.9rem;
color: #64748b;
margin-bottom: 24px;
}
.qr-container {
margin: 0 auto 24px;
width: 220px;
height: 220px;
background: white;
display: flex;
justify-content: center;
align-items: center;
border-radius: 16px;
border: 2px dashed #e2e8f0;
padding: 10px;
}
.qr-code {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 8px;
}
.loading-qr {
color: #64748b;
font-size: 0.9rem;
}
.notification-area {
min-height: 24px;
}
.notification {
font-size: 0.9rem;
margin: 0;
padding: 8px 12px;
border-radius: 8px;
display: inline-block;
}
.notification.success { background: #dcfce7; color: #166534; }
.notification.error { background: #fee2e2; color: #991b1b; }
.notification.info { background: #e0f2fe; color: #075985; }
.modal-footer {
padding: 20px 24px;
border-top: 1px solid #f0f0f0;
text-align: center;
background: #f8fafc;
}
.complete-btn {
background: linear-gradient(90deg, #10b981, #059669);
color: white;
border: none;
border-radius: 12px;
padding: 12px 40px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2);
}
.complete-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(16, 185, 129, 0.3);
}
.complete-btn:disabled {
background: #cbd5e1;
cursor: wait;
box-shadow: none;
}
/* Responsive */
@media (max-width: 600px) {
.page-title {
font-size: 2.2rem;
}
.container {
padding: 16px;
}
.subscribe-card {
max-width: 100%;
}
.card-header {
padding: 30px 20px;
}
.price {
font-size: 2.8rem;
}
}
</style>