Files
spring-festival-greetings/pages/mine/vip.vue
2026-01-31 22:23:39 +08:00

564 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>
<view class="vip-page" :style="{ paddingTop: navBarHeight + 'px' }">
<!-- Custom Navbar -->
<view
class="nav-bar"
:style="{
height: navBarHeight + 'px',
paddingTop: statusBarHeight + 'px',
}"
>
<view class="back" @click="goBack"></view>
<text class="nav-title">会员中心</text>
</view>
<!-- Content -->
<view class="content">
<!-- User Info Card -->
<view class="user-card">
<view class="avatar-box">
<image
class="avatar"
:src="
userInfo.avatarUrl ||
'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
"
mode="aspectFill"
/>
<view class="fire-badge">
<uni-icons type="fire-filled" size="14" color="#fff" />
</view>
</view>
<view class="user-info">
<view class="name-row">
<text class="username">{{ userInfo.nickName || "微信用户" }}</text>
<view class="vip-tag" v-if="userInfo.isVip">VIP 祥瑞会员</view>
<view class="vip-tag gray" v-else>普通用户</view>
</view>
<text class="expiry-date" v-if="userInfo.isVip">
{{ userInfo.vipExpireAt }} 到期
</text>
<text class="expiry-date" v-else>开通会员解锁专属权益</text>
</view>
</view>
<!-- Plans -->
<view class="section-title">选择会员方案</view>
<view class="plans-grid">
<view
v-for="(plan, index) in plans"
:key="index"
class="plan-card"
:class="{ active: selectedPlanIndex === index }"
@click="selectPlan(index)"
>
<view v-if="plan.badge" class="plan-badge" :class="plan.badgeType">
{{ plan.badge }}
</view>
<text class="plan-duration">{{ plan.name }}</text>
<view class="plan-price">
<text class="currency">¥</text>
<text class="amount">{{ plan.price / 100 }}</text>
</view>
</view>
</view>
<!-- Benefits -->
<view class="benefits-card">
<view class="card-header">
<uni-icons type="vip-filled" size="20" color="#ff3b30" />
<text class="card-title">会员专属权益</text>
</view>
<view class="benefits-grid">
<view
v-for="(benefit, index) in benefits"
:key="index"
class="benefit-item"
>
<view class="benefit-icon-box" :style="{ background: benefit.bg }">
<uni-icons
:type="benefit.icon"
size="24"
:color="benefit.color"
/>
</view>
<text class="benefit-name">{{ benefit.name }}</text>
</view>
</view>
</view>
<!-- Purchase Notes -->
<view class="notes-section">
<text class="notes-title">购买说明</text>
<view class="notes-list">
<view class="note-item" v-for="(note, index) in notes" :key="index">
<text class="dot"></text>
<text class="note-text">{{ note }}</text>
</view>
</view>
</view>
</view>
<!-- Bottom Action Bar -->
<view class="bottom-bar safe-area-bottom">
<button class="buy-btn" @click="handlePurchase">
<text>立即开通</text>
<uni-icons
type="arrowright"
size="18"
color="#fff"
style="margin-left: 4rpx"
/>
</button>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { storeToRefs } from "pinia";
import {
getBavBarHeight,
getStatusBarHeight as getStatus,
} from "@/utils/system";
import { useUserStore } from "@/stores/user";
import { createOrder, getVipPlan } from "@/api/pay.js";
const navBarHeight = getBavBarHeight();
const statusBarHeight = getStatus();
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const selectedPlanIndex = ref(1);
const plans = ref([]);
const benefits = [
{ name: "高级模板", icon: "star-filled", color: "#ff3b30", bg: "#fff0f0" },
{ name: "无限制下载", icon: "image-filled", color: "#ff6b00", bg: "#fff7e6" },
{ name: "马年头像框", icon: "medal-filled", color: "#bfa46f", bg: "#fffbe6" },
{ name: "高速渲染", icon: "upload-filled", color: "#ff3b30", bg: "#fff0f0" },
{
name: "数据永久保存",
icon: "paperplane-filled",
color: "#409eff",
bg: "#ecf5ff",
},
{ name: "1对1服务", icon: "headphones", color: "#9053fa", bg: "#f5f0ff" },
];
const notes = [
"会员服务为虚拟产品,支付后立即生效,不支持退款。",
"会员权益在有效期内全平台通用。",
"如有疑问,请通过“我的-使用说明”联系客服处理。",
"最终解释权归 2026 新春助手团队所有。",
];
onMounted(() => {
getVipPlanList();
});
const getVipPlanList = async () => {
const planRes = await getVipPlan();
plans.value = planRes;
};
const goBack = () => {
uni.navigateBack();
};
const selectPlan = (index) => {
selectedPlanIndex.value = index;
};
const handlePurchase = async () => {
if (selectedPlanIndex.value < 0 || !plans.value[selectedPlanIndex.value]) {
uni.showToast({ title: "请选择会员方案", icon: "none" });
return;
}
const plan = plans.value[selectedPlanIndex.value];
uni.showLoading({ title: "正在发起支付...", mask: true });
try {
const orderRes = await createOrder({
planId: plan.id,
});
if (orderRes?.payParams) {
uni.requestPayment({
provider: "wxpay",
...orderRes.payParams,
success(res) {
uni.showToast({ title: "支付成功", icon: "success" });
// 支付成功后可以刷新用户信息
userStore.fetchUserInfo();
},
fail(err) {
console.log("payment fail", err);
if (err.errMsg.indexOf("cancel") > -1) {
uni.showToast({ title: "支付已取消", icon: "none" });
} else {
uni.showToast({ title: "支付失败", icon: "none" });
}
},
complete() {
uni.hideLoading();
},
});
} else {
uni.hideLoading();
uni.showToast({ title: "获取支付参数失败", icon: "none" });
}
} catch (e) {
uni.hideLoading();
uni.showToast({ title: "创建订单失败", icon: "none" });
console.error(e);
}
};
</script>
<style lang="scss" scoped>
.vip-page {
min-height: 100vh;
background-color: #f8f8f8;
box-sizing: border-box;
padding-bottom: 200rpx; // Space for bottom bar
}
.nav-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
background-color: #fff;
display: flex;
align-items: center;
padding: 0 24rpx;
z-index: 100;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
.back {
font-size: 50rpx;
margin-right: 24rpx;
line-height: 1;
color: #333;
padding: 20rpx;
margin-left: -20rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #000;
flex: 1;
text-align: center;
margin-right: 50rpx;
}
}
.content {
padding: 30rpx;
}
/* User Card */
.user-card {
background: #fff;
border-radius: 30rpx;
padding: 30rpx;
display: flex;
align-items: center;
box-shadow: 0 10rpx 40rpx rgba(255, 59, 48, 0.08);
margin-bottom: 40rpx;
position: relative;
overflow: hidden;
// Decorative gradient background hint
&::before {
content: "";
position: absolute;
top: -50%;
right: -20%;
width: 300rpx;
height: 300rpx;
background: radial-gradient(
circle,
rgba(255, 59, 48, 0.1) 0%,
transparent 70%
);
border-radius: 50%;
}
}
.avatar-box {
position: relative;
margin-right: 24rpx;
}
.avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
border: 4rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.fire-badge {
position: absolute;
top: -6rpx;
right: -6rpx;
background: #ff3b30;
width: 36rpx;
height: 36rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid #fff;
}
.user-info {
flex: 1;
display: flex;
flex-direction: column;
}
.name-row {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.username {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-right: 16rpx;
}
.vip-tag {
background: rgba(255, 59, 48, 0.1);
color: #ff3b30;
font-size: 20rpx;
padding: 2rpx 10rpx;
border-radius: 20rpx;
font-weight: 500;
&.gray {
background: rgba(0, 0, 0, 0.05);
color: #666;
}
}
.expiry-date {
font-size: 24rpx;
color: #999;
}
/* Plans Grid */
.section-title {
font-size: 28rpx;
color: #666;
font-weight: 500;
margin-bottom: 24rpx;
}
.plans-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-bottom: 40rpx;
}
.plan-card {
width: 330rpx; // (750 - 60 - 30 gap) / 2
height: 180rpx;
background: #fff;
border-radius: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 30rpx;
position: relative;
border: 2rpx solid transparent;
transition: all 0.2s;
&.active {
border-color: #ff3b30;
background: #fff5f5;
.plan-price {
color: #ff3b30;
}
}
}
.plan-badge {
position: absolute;
top: -16rpx;
right: -10rpx;
font-size: 20rpx;
color: #fff;
padding: 4rpx 12rpx;
border-radius: 12rpx 0 12rpx 0;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.1);
&.hot {
background: #ff3b30;
}
&.best {
background: #e6a23c;
}
}
.plan-duration {
font-size: 28rpx;
color: #666;
margin-bottom: 12rpx;
}
.plan-price {
color: #333;
font-weight: bold;
display: flex;
align-items: baseline;
}
.currency {
font-size: 28rpx;
margin-right: 4rpx;
}
.amount {
font-size: 48rpx;
}
/* Benefits Card */
.benefits-card {
background: #fff;
border-radius: 30rpx;
padding: 30rpx;
margin-bottom: 40rpx;
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 30rpx;
}
.card-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-left: 12rpx;
}
.benefits-grid {
display: flex;
flex-wrap: wrap;
}
.benefit-item {
width: 33.33%;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 30rpx;
&:nth-last-child(-n + 3) {
margin-bottom: 0;
}
}
.benefit-icon-box {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
}
.benefit-name {
font-size: 24rpx;
color: #666;
}
/* Notes */
.notes-section {
padding: 0 10rpx;
margin-bottom: 40rpx;
}
.notes-title {
font-size: 28rpx;
font-weight: bold;
color: #666;
margin-bottom: 16rpx;
display: block;
}
.notes-list {
display: flex;
flex-direction: column;
}
.note-item {
display: flex;
align-items: flex-start;
margin-bottom: 12rpx;
}
.dot {
color: #999;
margin-right: 10rpx;
font-size: 24rpx;
line-height: 1.4;
}
.note-text {
flex: 1;
font-size: 24rpx;
color: #999;
line-height: 1.4;
}
/* Bottom Bar */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background: #fff;
padding: 20rpx 30rpx;
box-sizing: border-box;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05);
padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
z-index: 99;
}
.buy-btn {
background: linear-gradient(90deg, #ff3b30 0%, #ff1a1a 100%);
color: #fff;
border-radius: 50rpx;
height: 90rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: bold;
border: none;
&:active {
opacity: 0.9;
}
}
</style>