886 lines
23 KiB
Vue
886 lines
23 KiB
Vue
<script setup>
|
||
import { computed, ref, onBeforeUnmount } from "vue";
|
||
import { onShow } from "@dcloudio/uni-app";
|
||
import Container from "@/components/Container.vue";
|
||
import Signin from "@/components/Signin.vue";
|
||
import { createOrderAPI, getAppConfig, getHomeData } from "@/apis";
|
||
import { capsuleHeight } from "@/util";
|
||
import useStore from "@/store";
|
||
import { storeToRefs } from "pinia";
|
||
|
||
const store = useStore();
|
||
const { user, config } = storeToRefs(store);
|
||
const { updateConfig, updateUser } = store;
|
||
|
||
const currentTypeIndex = ref(1);
|
||
const selectedPackageIndex = ref(0);
|
||
const showModal = ref(false);
|
||
const loadingConfig = ref(false);
|
||
const paying = ref(false);
|
||
const refreshing = ref(false);
|
||
const timer = ref(null);
|
||
const lastDate = ref(user.value.expiredAt || 0);
|
||
const maxRefreshTimes = 12;
|
||
|
||
// 会员页核心展示数据:视觉、权益、套餐均按蓝湖当前两张设计稿拆分。
|
||
const memberTypes = [
|
||
{
|
||
key: "normal",
|
||
tab: "VIP",
|
||
title: "普通会员",
|
||
prefix: "成为射灵星球",
|
||
desc: "特享约战竞技次数包、专属会员标识",
|
||
benefitTitle: "普通会员专属权益",
|
||
themeClass: "vip-page--normal",
|
||
heroCard: "https://static.shelingxingqiu.com/shootmini/static/vip/vip-title.png",
|
||
activeHeroCard: "https://static.shelingxingqiu.com/shootmini/static/vip/vip-title2.png",
|
||
orderIcon: "../../static/vip/vip-order.png",
|
||
heroBadge: "../../static/vip/normal-hero-badge.png",
|
||
buttonClass: "activate-btn--normal",
|
||
benefits: [
|
||
{ label: "专属会员标识", icon: "../../static/vip/vip-badge.png" },
|
||
{ label: "教练点评", icon: "../../static/vip/vip-comment.png" },
|
||
{ label: "专享VIP客服", icon: "../../static/vip/vip-service.png" },
|
||
{ label: "排位赛\n每日+20次", icon: "../../static/vip/vip-rank.png" },
|
||
{ label: "约战\n每日+20次", icon: "../../static/vip/vip-battle.png" },
|
||
],
|
||
packages: [
|
||
{ name: "连续包月", price: "20", original: "35" },
|
||
{ name: "12个月", price: "300", original: "420" },
|
||
{ name: "3个月", price: "84", original: "105" },
|
||
],
|
||
},
|
||
{
|
||
key: "super",
|
||
tab: "SVIP",
|
||
title: "超级会员",
|
||
prefix: "成为射灵星球",
|
||
desc: "尊享专属特效、无限制约战竞技、专属会员标识",
|
||
benefitTitle: "超级会员专属权益",
|
||
themeClass: "vip-page--super",
|
||
heroCard: "https://static.shelingxingqiu.com/shootmini/static/vip/svip-title.png",
|
||
activeHeroCard: "https://static.shelingxingqiu.com/shootmini/static/vip/svip-title2.png",
|
||
orderIcon: "../../static/vip/svip-order.png",
|
||
heroBadge: "../../static/vip/super-hero-badge.png",
|
||
buttonClass: "activate-btn--super",
|
||
benefits: [
|
||
{ label: "专属落点标识", icon: "../../static/vip/svip-point.png" },
|
||
{ label: "专属命中效果", icon: "../../static/vip/svip-hit.png" },
|
||
{ label: "专属射箭效果", icon: "../../static/vip/svip-arrow.png" },
|
||
{ label: "专属会员标识", icon: "../../static/vip/svip-badge.png" },
|
||
{ label: "教练点评", icon: "../../static/vip/svip-comment.png" },
|
||
{ label: "约战无限制", icon: "../../static/vip/svip-battle.png" },
|
||
{ label: "排位赛无限制", icon: "../../static/vip/svip-rank.png" },
|
||
{ label: "专享SVIP客服", icon: "../../static/vip/svip-service.png" },
|
||
],
|
||
packages: [
|
||
{ name: "连续包月", price: "20", original: "35" },
|
||
{ name: "12个月", price: "300", original: "420" },
|
||
{ name: "3个月", price: "84", original: "105" },
|
||
],
|
||
},
|
||
];
|
||
|
||
const currentType = computed(() => memberTypes[currentTypeIndex.value]);
|
||
|
||
// 后端到期时间可能是秒级时间戳、毫秒级时间戳或日期字符串,这里统一转成毫秒。
|
||
const toTimestamp = (value) => {
|
||
if (!value) return 0;
|
||
const numberValue = Number(value);
|
||
if (!Number.isNaN(numberValue)) {
|
||
return numberValue < 1000000000000 ? numberValue * 1000 : numberValue;
|
||
}
|
||
const time = new Date(value).getTime();
|
||
return Number.isNaN(time) ? 0 : time;
|
||
};
|
||
|
||
// 当前卡片只关心本 tab 对应的会员到期时间,普通会员和超级会员互不兜底。
|
||
const getVipExpiredValue = (type, source = user.value) => {
|
||
if (!source) return 0;
|
||
return type.key === "super" ? source.superVipExpiredAt : source.normalVipExpiredAt;
|
||
};
|
||
|
||
// 支付后轮询需要比较最新会员到期时间,兼容旧字段 expiredAt。
|
||
const getLatestVipExpiredTime = (source = user.value) => {
|
||
if (!source) return 0;
|
||
return Math.max(
|
||
toTimestamp(source.normalVipExpiredAt),
|
||
toTimestamp(source.superVipExpiredAt),
|
||
toTimestamp(source.expiredAt)
|
||
);
|
||
};
|
||
|
||
// 未过期才展示“会员生效中”样式,已过期或无值继续展示未开通样式。
|
||
const isVipActive = (type) => {
|
||
return toTimestamp(getVipExpiredValue(type)) > Date.now();
|
||
};
|
||
|
||
// 会员卡片展示完整到期时间。
|
||
const formatVipDate = (value) => {
|
||
const timestamp = toTimestamp(value);
|
||
if (!timestamp) return "";
|
||
const date = new Date(timestamp);
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||
const day = String(date.getDate()).padStart(2, "0");
|
||
const hour = String(date.getHours()).padStart(2, "0");
|
||
const minute = String(date.getMinutes()).padStart(2, "0");
|
||
const second = String(date.getSeconds()).padStart(2, "0");
|
||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
||
};
|
||
|
||
// 会员生效时使用 title2 切图,未生效时沿用原来的开通引导图。
|
||
const getHeroCard = (type) => {
|
||
return isVipActive(type) ? type.activeHeroCard : type.heroCard;
|
||
};
|
||
|
||
const getActiveVipExpiredDate = (type) => {
|
||
return formatVipDate(getVipExpiredValue(type));
|
||
};
|
||
|
||
// 设计稿里的“下期自动续费”按到期日前一天展示。
|
||
const getActiveVipRenewDate = (type) => {
|
||
const timestamp = toTimestamp(getVipExpiredValue(type));
|
||
if (!timestamp) return "";
|
||
return formatVipDate(timestamp - 24 * 60 * 60 * 1000);
|
||
};
|
||
|
||
const configMenus = computed(() => {
|
||
return config.value.vipMenus || [];
|
||
});
|
||
|
||
const getMenuName = (item) => {
|
||
return item.name || item.vipName || item.title || "";
|
||
};
|
||
|
||
const getMenuPrice = (item) => {
|
||
const value = item.price || item.total || item.amount || item.money;
|
||
return value ? String(value).replace("¥", "").replace("¥", "") : "";
|
||
};
|
||
|
||
const getMenuType = (item) => {
|
||
const vipType = Number(item.vipType);
|
||
if (vipType === 1) return "normal";
|
||
if (vipType === 2) return "super";
|
||
|
||
const type = String(item.type || "").toLowerCase();
|
||
if (["super", "svip"].includes(type)) return "super";
|
||
if (["normal", "vip"].includes(type)) return "normal";
|
||
|
||
const name = getMenuName(item);
|
||
if (/超级|SVIP/i.test(name)) return "super";
|
||
if (/普通|VIP|会员/i.test(name)) return "normal";
|
||
return "";
|
||
};
|
||
|
||
const getPackageMonths = (name) => {
|
||
if (/连续包月/.test(name)) return 1;
|
||
const match = String(name || "").match(/(\d+)\s*个?月/);
|
||
return match ? Number(match[1]) : 0;
|
||
};
|
||
|
||
const matchPackageSource = (type, pack, index) => {
|
||
const menus = configMenus.value;
|
||
const typedMenus = menus.filter((item) => {
|
||
const menuType = getMenuType(item);
|
||
if (type.key === "super") return menuType === "super";
|
||
return menuType === "normal";
|
||
});
|
||
const pool = typedMenus.length ? typedMenus : menus;
|
||
const packMonths = getPackageMonths(pack.name);
|
||
return (
|
||
pool.find((item) => getMenuName(item).indexOf(pack.name) !== -1) ||
|
||
pool.find((item) => packMonths && getPackageMonths(getMenuName(item)) === packMonths) ||
|
||
pool.find((item) => getMenuPrice(item) === pack.price) ||
|
||
pool[index]
|
||
);
|
||
};
|
||
|
||
const getPackages = (type) => {
|
||
return type.packages.map((item, index) => {
|
||
const source = matchPackageSource(type, item, index);
|
||
return {
|
||
...item,
|
||
source,
|
||
name: source ? getMenuName(source) || item.name : item.name,
|
||
price: source ? getMenuPrice(source) || item.price : item.price,
|
||
icon: source && source.icon,
|
||
id: source && source.id,
|
||
};
|
||
});
|
||
};
|
||
|
||
const currentPackages = computed(() => {
|
||
return getPackages(currentType.value);
|
||
});
|
||
|
||
const selectedPackage = computed(() => {
|
||
return currentPackages.value[selectedPackageIndex.value] || currentPackages.value[0];
|
||
});
|
||
|
||
const switchType = (index) => {
|
||
currentTypeIndex.value = index;
|
||
selectedPackageIndex.value = 0;
|
||
};
|
||
|
||
const onSwiperChange = (event) => {
|
||
currentTypeIndex.value = event.detail.current;
|
||
selectedPackageIndex.value = 0;
|
||
};
|
||
|
||
const selectPackage = (index) => {
|
||
selectedPackageIndex.value = index;
|
||
};
|
||
|
||
const toPackageDesc = () => {
|
||
uni.navigateTo({
|
||
url: "/pages/member/vip-intro",
|
||
});
|
||
};
|
||
|
||
const toOrderPage = () => {
|
||
uni.navigateTo({
|
||
url: "/pages/member/orders",
|
||
});
|
||
};
|
||
|
||
const toAgreement = (type) => {
|
||
uni.navigateTo({
|
||
url: `/pages/member/agreement?type=${type}`,
|
||
});
|
||
};
|
||
|
||
const loadVipConfig = async () => {
|
||
if (loadingConfig.value || configMenus.value.length) return;
|
||
loadingConfig.value = true;
|
||
try {
|
||
const result = await getAppConfig();
|
||
if (result) updateConfig(result);
|
||
} catch (error) {
|
||
console.log("load vip config error", error);
|
||
} finally {
|
||
loadingConfig.value = false;
|
||
}
|
||
};
|
||
|
||
const clearRefreshTimer = () => {
|
||
if (timer.value) {
|
||
clearInterval(timer.value);
|
||
timer.value = null;
|
||
}
|
||
};
|
||
|
||
const refreshUserAfterPay = () => {
|
||
clearRefreshTimer();
|
||
let refreshTimes = 0;
|
||
// 先记录支付前的最大到期时间,轮询到更大的值就认为会员状态已刷新。
|
||
lastDate.value = getLatestVipExpiredTime();
|
||
refreshing.value = true;
|
||
timer.value = setInterval(async () => {
|
||
refreshTimes += 1;
|
||
try {
|
||
const result = await getHomeData();
|
||
const latestExpiredAt = getLatestVipExpiredTime(result.user);
|
||
if (result.user && latestExpiredAt > lastDate.value) {
|
||
lastDate.value = latestExpiredAt;
|
||
updateUser(result.user);
|
||
clearRefreshTimer();
|
||
refreshing.value = false;
|
||
} else if (refreshTimes >= maxRefreshTimes) {
|
||
clearRefreshTimer();
|
||
refreshing.value = false;
|
||
}
|
||
} catch (error) {
|
||
console.log("refresh user after pay error", error);
|
||
if (refreshTimes >= maxRefreshTimes) {
|
||
clearRefreshTimer();
|
||
refreshing.value = false;
|
||
}
|
||
}
|
||
}, 1000);
|
||
};
|
||
|
||
const onPay = async () => {
|
||
if (paying.value || refreshing.value) return;
|
||
if (!user.value.id) {
|
||
showModal.value = true;
|
||
return;
|
||
}
|
||
if (!configMenus.value.length) {
|
||
await loadVipConfig();
|
||
}
|
||
const vipId = selectedPackage.value && selectedPackage.value.id;
|
||
if (!vipId) {
|
||
uni.showToast({
|
||
title: loadingConfig.value ? "套餐配置加载中" : "套餐暂不可购买",
|
||
icon: "none",
|
||
});
|
||
return;
|
||
}
|
||
paying.value = true;
|
||
let waitingPayment = false;
|
||
try {
|
||
const result = await createOrderAPI(vipId);
|
||
const params = result?.pay?.order?.jsApi?.params;
|
||
if (!params?.timeStamp || !params?.nonceStr || !params?.package || !params?.paySign) {
|
||
uni.showToast({
|
||
title: "支付参数生成失败",
|
||
icon: "none",
|
||
});
|
||
refreshing.value = false;
|
||
return;
|
||
}
|
||
waitingPayment = true;
|
||
wx.requestPayment({
|
||
timeStamp: params.timeStamp, // 微信支付时间戳
|
||
nonceStr: params.nonceStr, // 微信支付随机串
|
||
package: params.package, // 预支付交易会话标识
|
||
paySign: params.paySign, // 微信支付签名
|
||
signType: params.signType || "RSA",
|
||
success() {
|
||
uni.showToast({
|
||
title: "支付成功",
|
||
icon: "none",
|
||
});
|
||
refreshUserAfterPay();
|
||
},
|
||
fail(res) {
|
||
console.log("pay error", res);
|
||
if (res.errMsg && res.errMsg.indexOf("cancel") !== -1) return;
|
||
uni.showToast({
|
||
title: "支付失败,请稍后重试",
|
||
icon: "none",
|
||
});
|
||
},
|
||
complete() {
|
||
paying.value = false;
|
||
},
|
||
});
|
||
} catch (error) {
|
||
console.log("create vip order error", error);
|
||
} finally {
|
||
if (!waitingPayment) paying.value = false;
|
||
}
|
||
};
|
||
|
||
onShow(loadVipConfig);
|
||
|
||
onBeforeUnmount(() => {
|
||
clearRefreshTimer();
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<Container title="" :bgType="10" :scroll="false" :showBottom="false">
|
||
<view class="vip-page" :class="currentType.themeClass">
|
||
<view class="type-tabs" :style="{ top: capsuleHeight + 'px' }">
|
||
<view
|
||
v-for="(item, index) in memberTypes"
|
||
:key="item.key"
|
||
class="type-tab"
|
||
:class="{ 'type-tab--active': currentTypeIndex === index }"
|
||
@click="switchType(index)"
|
||
>
|
||
<text>{{ item.tab }}</text>
|
||
<view class="type-tab__indicator" />
|
||
</view>
|
||
</view>
|
||
<swiper
|
||
class="type-swiper"
|
||
:current="currentTypeIndex"
|
||
:duration="260"
|
||
@change="onSwiperChange"
|
||
>
|
||
<swiper-item v-for="type in memberTypes" :key="type.key">
|
||
<scroll-view scroll-y class="type-scroll" :show-scrollbar="false">
|
||
<view class="vip-content">
|
||
<view
|
||
class="hero-card"
|
||
:class="{ 'hero-card--active': isVipActive(type) }"
|
||
>
|
||
<image class="hero-card__bg" :src="getHeroCard(type)" mode="scaleToFill" />
|
||
<!-- 生效态卡片使用 title2 底图,补充有效期和订单入口叠层。 -->
|
||
<view v-if="isVipActive(type)" class="hero-card__content">
|
||
<text class="hero-card__date">
|
||
有效期至:{{ getActiveVipExpiredDate(type) }}
|
||
</text>
|
||
<!-- <text v-if="type.key === 'super'" class="hero-card__renew">
|
||
下期会员将于:{{ getActiveVipRenewDate(type) }}自动续费
|
||
</text> -->
|
||
<view class="hero-card__order" @click.stop="toOrderPage">
|
||
<image class="hero-card__order-icon" :src="type.orderIcon" mode="aspectFit" />
|
||
<text>订单管理</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="benefit-title">
|
||
<view class="benefit-title__line" />
|
||
<text>{{ type.benefitTitle }}</text>
|
||
<view class="benefit-title__line" />
|
||
</view>
|
||
|
||
<view class="benefit-grid" :class="{ 'benefit-grid--normal': type.key === 'normal' }">
|
||
<view
|
||
v-for="benefit in type.benefits"
|
||
:key="benefit.label"
|
||
class="benefit-item"
|
||
>
|
||
<view class="benefit-icon">
|
||
<image class="benefit-icon__img" :src="benefit.icon" mode="aspectFit" />
|
||
</view>
|
||
<text class="benefit-item__label">{{ benefit.label }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="package-header">
|
||
<text class="package-header__title">选择套餐</text>
|
||
<view class="package-header__link" @click="toPackageDesc">
|
||
<text>套餐说明</text>
|
||
<image
|
||
class="package-header__icon"
|
||
src="../../static/enter.png"
|
||
mode="aspectFit"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<scroll-view scroll-x class="package-scroll" :show-scrollbar="false">
|
||
<view class="package-list">
|
||
<view
|
||
v-for="(pack, index) in getPackages(type)"
|
||
:key="`${type.key}-${pack.name}`"
|
||
class="package-card"
|
||
:class="{ 'package-card--active': selectedPackageIndex === index }"
|
||
@click="selectPackage(index)"
|
||
>
|
||
<view class="package-card__inner">
|
||
<text class="package-name">{{ pack.name }}</text>
|
||
<view class="package-price">
|
||
<text class="package-price__symbol">¥</text>
|
||
<text class="package-price__value">{{ pack.price }}</text>
|
||
</view>
|
||
<view class="package-origin">
|
||
<text>¥{{ pack.original }}</text>
|
||
<view class="package-origin__line" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<button
|
||
hover-class="none"
|
||
class="activate-btn"
|
||
:class="type.buttonClass"
|
||
:disabled="loadingConfig || paying || refreshing"
|
||
@click="onPay"
|
||
>
|
||
<text v-if="loadingConfig">加载套餐中</text>
|
||
<text v-else-if="paying">创建订单中</text>
|
||
<text v-else-if="!refreshing">¥ {{ selectedPackage.price }} 一键激活</text>
|
||
<text v-else>刷新会员状态中</text>
|
||
</button>
|
||
|
||
<view class="agreement">
|
||
<text>支付即同意</text>
|
||
《 <text class="agreement__link" @click.stop="toAgreement('renew')">会员自动续费服务协议</text>》
|
||
《 <text class="agreement__link" @click.stop="toAgreement('deduct')">扣款授权服务协议</text>》
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</swiper-item>
|
||
</swiper>
|
||
<Signin :show="showModal" :onClose="() => (showModal = false)" />
|
||
</view>
|
||
</Container>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.vip-page {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
.type-tabs {
|
||
position: fixed;
|
||
left: 0;
|
||
z-index: 998;
|
||
width: 50%;
|
||
height: 50px;
|
||
margin-left: 50%;
|
||
transform:translateX(-60%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.type-tab {
|
||
width: 132rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: rgba(255, 255, 255, 0.45);
|
||
font-size: 34rpx;
|
||
font-weight: 700;
|
||
font-style: italic;
|
||
}
|
||
|
||
.type-tab__indicator {
|
||
width: 40rpx;
|
||
height: 4rpx;
|
||
border-radius: 4rpx;
|
||
margin-top: 8rpx;
|
||
background-color: transparent;
|
||
}
|
||
|
||
.type-tab--active {
|
||
color: #ffffff;
|
||
}
|
||
|
||
.vip-page--normal .type-tab--active .type-tab__indicator {
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.vip-page--super .type-tab--active {
|
||
color: #fedab5;
|
||
}
|
||
|
||
.vip-page--super .type-tab--active .type-tab__indicator {
|
||
background-color: #fedab5;
|
||
}
|
||
|
||
.type-swiper {
|
||
position: relative;
|
||
z-index: 1;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.type-scroll {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.vip-content {
|
||
min-height: 100%;
|
||
padding: 32rpx 24rpx 52rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.hero-card {
|
||
position: relative;
|
||
width: 702rpx;
|
||
height: 260rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.hero-card__bg {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
width: 702rpx;
|
||
height: 260rpx;
|
||
}
|
||
|
||
.hero-card__content {
|
||
position: relative;
|
||
z-index: 1;
|
||
/* 对齐 title2 切图左侧预留文案区域。 */
|
||
padding: 132rpx 0 0 44rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.hero-card__date,
|
||
.hero-card__renew {
|
||
color: #8d6d55;
|
||
font-size: 24rpx;
|
||
line-height: 34rpx;
|
||
}
|
||
|
||
.vip-page--normal .hero-card__date,
|
||
.vip-page--normal .hero-card__renew {
|
||
color: #555555;
|
||
}
|
||
|
||
.hero-card__order {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-top: 10rpx;
|
||
color: #6d5644;
|
||
font-size: 24rpx;
|
||
line-height: 34rpx;
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.vip-page--normal .hero-card__order {
|
||
color: #555555;
|
||
}
|
||
|
||
.hero-card__order-icon {
|
||
width: 28rpx;
|
||
height: 32rpx;
|
||
margin-right: 8rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.benefit-title {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-top: 38rpx;
|
||
color: #ffffff;
|
||
font-size: 24rpx;
|
||
line-height: 34rpx;
|
||
}
|
||
|
||
.benefit-title__line {
|
||
width: 14rpx;
|
||
height: 2rpx;
|
||
background-color: #ffffff;
|
||
margin: 0 10rpx;
|
||
}
|
||
|
||
.benefit-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
width: 622rpx;
|
||
margin: 38rpx auto 0;
|
||
}
|
||
|
||
.benefit-grid--normal {
|
||
width: 672rpx;
|
||
}
|
||
|
||
.benefit-item {
|
||
width: 144rpx;
|
||
height: 122rpx;
|
||
margin-right: 94rpx;
|
||
margin-bottom: 30rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.benefit-item:nth-child(3n) {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.benefit-grid--normal .benefit-item {
|
||
margin-right: 120rpx;
|
||
}
|
||
|
||
.benefit-grid--normal .benefit-item:nth-child(3n) {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.benefit-icon {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.benefit-icon__img {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
display: block;
|
||
}
|
||
|
||
.benefit-item__label {
|
||
margin-top: 8rpx;
|
||
color: #ffffff;
|
||
opacity: 0.7;
|
||
font-size: 24rpx;
|
||
line-height: 34rpx;
|
||
text-align: center;
|
||
white-space: pre-line;
|
||
}
|
||
|
||
.package-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-top: 56rpx;
|
||
padding-right: 8rpx;
|
||
}
|
||
|
||
.benefit-grid--normal + .package-header {
|
||
margin-top: 146rpx;
|
||
}
|
||
|
||
.package-header__title {
|
||
color: #ffffff;
|
||
font-size: 28rpx;
|
||
line-height: 40rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.vip-page--super .package-header__title {
|
||
color: #e7ba80;
|
||
}
|
||
|
||
.package-header__link {
|
||
display: flex;
|
||
align-items: center;
|
||
color: #999999;
|
||
font-size: 22rpx;
|
||
line-height: 32rpx;
|
||
}
|
||
|
||
.package-header__icon {
|
||
width: 24rpx;
|
||
height: 28rpx;
|
||
}
|
||
|
||
.package-scroll {
|
||
width: 100%;
|
||
margin-top: 40rpx;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.package-list {
|
||
display: flex;
|
||
width: 984rpx;
|
||
padding: 6rpx 84rpx 6rpx 6rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.package-card {
|
||
position: relative;
|
||
width: 264rpx;
|
||
height: 224rpx;
|
||
border-radius: 16rpx;
|
||
border: 2rpx solid #999999;
|
||
margin-right: 32rpx;
|
||
padding: 0;
|
||
overflow: hidden;
|
||
box-sizing: border-box;
|
||
color: #999999;
|
||
}
|
||
|
||
.package-card__inner {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-sizing: border-box;
|
||
border-radius: inherit;
|
||
}
|
||
|
||
.package-card--active {
|
||
border-width: 6rpx;
|
||
border-color: #fed847;
|
||
}
|
||
|
||
.vip-page--super .package-card--active {
|
||
border: none;
|
||
padding: 6rpx;
|
||
background: linear-gradient(180deg, #fef3e6 0%, #f0c191 100%);
|
||
}
|
||
|
||
.vip-page--super .package-card--active .package-card__inner {
|
||
border-radius: 10rpx;
|
||
background-color: #050b19;
|
||
}
|
||
|
||
.package-name {
|
||
font-size: 24rpx;
|
||
line-height: 34rpx;
|
||
}
|
||
|
||
.package-price {
|
||
display: flex;
|
||
align-items: baseline;
|
||
margin-top: 12rpx;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.package-card--active .package-price {
|
||
color: #fed847;
|
||
}
|
||
|
||
.vip-page--super .package-card--active .package-price {
|
||
color: #ffe8cd;
|
||
}
|
||
|
||
.package-price__symbol {
|
||
font-size: 30rpx;
|
||
line-height: 36rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.package-price__value {
|
||
font-size: 56rpx;
|
||
line-height: 66rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.package-origin {
|
||
position: relative;
|
||
margin-top: 2rpx;
|
||
color: #999999;
|
||
font-size: 24rpx;
|
||
line-height: 34rpx;
|
||
}
|
||
|
||
.package-origin__line {
|
||
position: absolute;
|
||
left: -4rpx;
|
||
right: -4rpx;
|
||
top: 16rpx;
|
||
height: 2rpx;
|
||
background-color: #999999;
|
||
}
|
||
|
||
.activate-btn {
|
||
width: 686rpx;
|
||
height: 88rpx;
|
||
border-radius: 44rpx;
|
||
margin: 34rpx auto 0;
|
||
padding: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #000000;
|
||
font-size: 30rpx;
|
||
line-height: 42rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.activate-btn::after {
|
||
border: none;
|
||
}
|
||
|
||
.activate-btn--normal {
|
||
background-color: #fed847;
|
||
}
|
||
|
||
.activate-btn--super {
|
||
background: linear-gradient( 181deg, #FDFDFC 0%, #FDF8F2 0%, #FFC992 100%, #FFB96C 100%);
|
||
}
|
||
|
||
.agreement {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin-top: 24rpx;
|
||
color: #999999;
|
||
font-size: 20rpx;
|
||
line-height: 28rpx;
|
||
}
|
||
|
||
.agreement__link {
|
||
margin-left: -6rpx;
|
||
text-decoration: underline;
|
||
}
|
||
</style>
|