895 lines
24 KiB
Vue
895 lines
24 KiB
Vue
<script setup>
|
||
import { computed, ref } from "vue";
|
||
import { onShow } from "@dcloudio/uni-app";
|
||
import Container from "@/components/Container.vue";
|
||
import Signin from "@/components/Signin.vue";
|
||
import { virtualPayOrderAPI, getAppConfig, getHomeData, getOrderDetailAPI } from "@/apis";
|
||
import { capsuleHeight, wxLogin } 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 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" },
|
||
],
|
||
},
|
||
{
|
||
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" },
|
||
],
|
||
},
|
||
];
|
||
|
||
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;
|
||
};
|
||
|
||
// 未过期才展示“会员生效中”样式,已过期或无值继续展示未开通样式。
|
||
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 formatPrice = (value) => {
|
||
if (value === undefined || value === null || value === "") return "";
|
||
return String(value).replace("¥", "").replace("¥", "");
|
||
};
|
||
|
||
const getMenuPrice = (item) => {
|
||
return formatPrice(item && item.price);
|
||
};
|
||
|
||
const getMenuOriginalPrice = (item) => {
|
||
return formatPrice(item && item.originalPrice);
|
||
};
|
||
|
||
const getMenuSortValue = (item) => {
|
||
const value = item && item.sort;
|
||
if (value === undefined || value === null || value === "") return Number.MAX_SAFE_INTEGER;
|
||
const sort = Number(value);
|
||
return Number.isFinite(sort) ? sort : Number.MAX_SAFE_INTEGER;
|
||
};
|
||
|
||
const sortMenusBySort = (menus = []) => {
|
||
return menus
|
||
.map((item, index) => ({ item, index }))
|
||
.sort((a, b) => {
|
||
const sortDiff = getMenuSortValue(a.item) - getMenuSortValue(b.item);
|
||
return sortDiff || a.index - b.index;
|
||
})
|
||
.map(({ item }) => item);
|
||
};
|
||
|
||
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 getPackages = (type) => {
|
||
return sortMenusBySort(configMenus.value)
|
||
.filter((item) => getMenuType(item) === type.key)
|
||
.map((item) => {
|
||
return {
|
||
source: item,
|
||
id: item.id,
|
||
name: getMenuName(item),
|
||
price: getMenuPrice(item),
|
||
originalPrice: getMenuOriginalPrice(item),
|
||
icon: item.icon,
|
||
};
|
||
});
|
||
};
|
||
|
||
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 refreshUserAfterPay = async () => {
|
||
refreshing.value = true;
|
||
try {
|
||
const result = await getHomeData();
|
||
if (result.user) {
|
||
updateUser(result.user);
|
||
}
|
||
} catch (error) {
|
||
console.log("refresh user after pay error", error);
|
||
} finally {
|
||
refreshing.value = false;
|
||
}
|
||
};
|
||
|
||
const getVirtualPaySignData = (result) => {
|
||
// 后端可能返回字符串或对象,这里统一转成微信虚拟支付需要的 JSON 字符串。
|
||
const signDataObj = typeof result?.signData === "string" ? JSON.parse(result.signData) : result?.signData;
|
||
if (!signDataObj) return "";
|
||
|
||
// 微信虚拟支付短剧/虚拟商品模式需要购买数量;后端未返回时使用订单数量兜底。
|
||
if (!signDataObj.buyQuantity) {
|
||
signDataObj.buyQuantity = result.quantity || 1;
|
||
}
|
||
return JSON.stringify(signDataObj);
|
||
};
|
||
|
||
const isVirtualPayCancel = (res = {}) => {
|
||
const message = String(res.errMsg || res.message || "").toLowerCase();
|
||
const errCode = Number(res.errCode);
|
||
const errno = Number(res.errno);
|
||
|
||
return message.includes("cancel") || errCode === -23 || errno === -2;
|
||
};
|
||
|
||
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;
|
||
}
|
||
if (typeof wx === "undefined" || !wx.requestVirtualPayment) {
|
||
uni.showToast({
|
||
title: "当前环境不支持微信虚拟支付",
|
||
icon: "none",
|
||
});
|
||
return;
|
||
}
|
||
paying.value = true;
|
||
let waitingPayment = false;
|
||
let payToast = null;
|
||
try {
|
||
// 微信虚拟支付创建订单前需要登录 code,服务端用它换取本次支付签名参数。
|
||
const wxResult = await wxLogin();
|
||
const result = await virtualPayOrderAPI(vipId, wxResult.code);
|
||
const finalSignData = getVirtualPaySignData(result);
|
||
if (!finalSignData || !result?.paySig || !result?.signature) {
|
||
uni.showToast({
|
||
title: "支付参数生成失败",
|
||
icon: "none",
|
||
});
|
||
refreshing.value = false;
|
||
return;
|
||
}
|
||
wx.requestVirtualPayment({
|
||
signData: finalSignData,
|
||
paySig: result.paySig,
|
||
signature: result.signature,
|
||
mode: "short_series_goods",
|
||
async success() {
|
||
payToast = {
|
||
title: "支付成功",
|
||
icon: "success",
|
||
};
|
||
if (result?.outTradeNo) {
|
||
try {
|
||
const orderDetail = await getOrderDetailAPI(result.outTradeNo);
|
||
console.log("virtual pay order detail", orderDetail);
|
||
} catch (error) {
|
||
console.log("virtual pay order detail error", error);
|
||
}
|
||
}
|
||
// 客户端支付成功后,会员是否真正生效仍以服务端用户信息刷新结果为准。
|
||
refreshUserAfterPay();
|
||
},
|
||
fail(res) {
|
||
console.log("virtual pay error", res);
|
||
if (isVirtualPayCancel(res)) {
|
||
payToast = {
|
||
title: "支付已取消",
|
||
icon: "none",
|
||
};
|
||
return;
|
||
}
|
||
payToast = {
|
||
title: res.message || "支付失败,请稍后重试",
|
||
icon: "none",
|
||
};
|
||
},
|
||
complete() {
|
||
paying.value = false;
|
||
if (payToast) {
|
||
setTimeout(() => {
|
||
uni.showToast(payToast);
|
||
}, 200);
|
||
}
|
||
},
|
||
});
|
||
waitingPayment = true;
|
||
} catch (error) {
|
||
console.log("create virtual pay order error", error);
|
||
uni.showToast({
|
||
title: error.message || "下单失败",
|
||
icon: "none",
|
||
});
|
||
} finally {
|
||
if (!waitingPayment) paying.value = false;
|
||
}
|
||
};
|
||
|
||
onShow(loadVipConfig);
|
||
|
||
</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.id || pack.name || index}`"
|
||
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 v-if="pack.originalPrice" class="package-origin">
|
||
<text>¥{{ pack.originalPrice }}</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 || !selectedPackage"
|
||
@click="onPay"
|
||
>
|
||
<text v-if="loadingConfig">加载套餐中</text>
|
||
<text v-else-if="paying">创建订单中</text>
|
||
<text v-else-if="refreshing">刷新会员状态中</text>
|
||
<text v-else-if="selectedPackage">¥ {{ 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: inline-flex;
|
||
min-width: 100%;
|
||
padding: 6rpx 84rpx 6rpx 6rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.package-card {
|
||
position: relative;
|
||
width: 264rpx;
|
||
flex: 0 0 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>
|