文件
fenleixinxi/uniapp/pages/member/index.vue

1385 行
33 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<template>
<view class="vip fc-page">
<!-- 自定义导航栏:深绿渐变沉浸式 -->
<fc-navbar title="会员中心" theme="gradient" :show-back="true" />
<!-- 顶部深绿沉浸底座 -->
<view class="vip__backdrop">
<view class="vip__backdrop-ambient"></view>
</view>
<!-- 浮空 VIP 尊享卡片 -->
<view class="vip__card-wrap">
<view class="vip__card">
<!-- 卡片装饰流光 -->
<view class="vip__card-sheen"></view>
<!-- 用户身份信息 -->
<view class="vip__card-user" @click="handleUserClick">
<view class="vip__avatar">
<text class="vip__avatar-txt">{{ userInitial }}</text>
</view>
<view class="vip__user-meta">
<view class="vip__user-title-row">
<text class="vip__user-name fc-ellipsis">{{ nickName }}</text>
<view class="vip__badge" :class="{ 'vip__badge--active': isMember }">
<image class="vip__badge-icon" :src="iconUrl('crown')" mode="aspectFit" />
<text class="vip__badge-txt">{{ isMember ? (userInfo.memberLevel || 'VIP 会员') : '未开通' }}</text>
</view>
</view>
<text class="vip__user-desc">
{{ isMember ? '有效期至 ' + expireText : '开通立享多项专属特权,尊享优质服务' }}
</text>
</view>
<image class="vip__card-crown" :src="iconUrl('home-crown')" mode="aspectFit" />
</view>
<!-- 资产数据三栏(通透呼吸感) -->
<view class="vip__assets">
<view class="vip__asset-col" @click="recharge">
<view class="vip__asset-val-box">
<text class="vip__asset-val">{{ balanceText }}</text>
<text class="vip__asset-unit">元</text>
</view>
<view class="vip__asset-label-row">
<text class="vip__asset-lbl">账户余额</text>
<text class="vip__asset-act">充值 ›</text>
</view>
</view>
<view class="vip__asset-sep"></view>
<view class="vip__asset-col" @click="goCoupons">
<view class="vip__asset-val-box">
<text class="vip__asset-val">{{ myCouponCount }}</text>
<text class="vip__asset-unit">张</text>
</view>
<view class="vip__asset-label-row">
<text class="vip__asset-lbl">优惠券</text>
<text class="vip__asset-act">查看 ›</text>
</view>
</view>
<view class="vip__asset-sep"></view>
<view class="vip__asset-col">
<view class="vip__asset-val-box">
<text class="vip__asset-val">{{ assets.points || 0 }}</text>
<text class="vip__asset-unit">分</text>
</view>
<view class="vip__asset-label-row">
<text class="vip__asset-lbl">可用积分</text>
</view>
</view>
</view>
</view>
</view>
<!-- VIP 尊享特权:2 列精致大卡片 -->
<view class="fc-card vip__section">
<view class="vip__sec-head">
<text class="vip__sec-title">VIP 尊享专属特权</text>
<text class="vip__sec-tip">开通即可享有以下权益</text>
</view>
<view class="vip__privileges-grid">
<!-- 特权 1:每日免费看(真实呈现已用与剩余) -->
<view class="vip__priv-item">
<view class="vip__priv-icon-wrap vip__priv-icon-wrap--green">
<image class="vip__priv-icon" :src="iconUrl('eye')" mode="aspectFit" />
</view>
<view class="vip__priv-content">
<text class="vip__priv-name">每日免费看</text>
<text class="vip__priv-desc">
{{ isMember ? `今日已看 ${benefits.freeViewUsed || 0} / 剩 ${benefits.freeViewLeft != null ? benefits.freeViewLeft : benefits.freeView} 次` : `每日享 ${benefits.freeView || 20} 条免费看` }}
</text>
</view>
</view>
<!-- 特权 2:发布折扣 -->
<view class="vip__priv-item">
<view class="vip__priv-icon-wrap vip__priv-icon-wrap--gold">
<image class="vip__priv-icon" :src="iconUrl('mine-post')" mode="aspectFit" />
</view>
<view class="vip__priv-content">
<text class="vip__priv-name">全场发布优惠</text>
<text class="vip__priv-desc">发布享 {{ benefits.postDiscount || '8折' }} 专属优惠</text>
</view>
</view>
<!-- 特权 3:赠送发布额度(真实呈现剩余额度) -->
<view class="vip__priv-item">
<view class="vip__priv-icon-wrap vip__priv-icon-wrap--green">
<image class="vip__priv-icon" :src="iconUrl('edit')" mode="aspectFit" />
</view>
<view class="vip__priv-content">
<text class="vip__priv-name">赠送发布额度</text>
<text class="vip__priv-desc">
{{ isMember ? `当前剩余 ${benefits.postQuotaLeft != null ? benefits.postQuotaLeft : 0} 条额度` : `开通套餐送 ${selectedPlanQuota} 条发布` }}
</text>
</view>
</view>
<!-- 特权 4:专属身份标识 -->
<view class="vip__priv-item">
<view class="vip__priv-icon-wrap vip__priv-icon-wrap--gold">
<image class="vip__priv-icon" :src="iconUrl('crown')" mode="aspectFit" />
</view>
<view class="vip__priv-content">
<text class="vip__priv-name">专属身份勋章</text>
<text class="vip__priv-desc">全站彰显 VIP 尊贵皇冠标</text>
</view>
</view>
</view>
</view>
<!-- 开通套餐选择 -->
<view class="fc-card vip__section">
<view class="vip__sec-head">
<text class="vip__sec-title">{{ isMember ? '续费套餐' : '选择开通套餐' }}</text>
<text class="vip__sec-tip">支持随时续期无缝顺延</text>
</view>
<!-- 套餐 3 栏卡片 -->
<view class="vip__plans-row">
<view
v-for="p in plans"
:key="p.planId"
class="vip__plan-card"
:class="{ 'vip__plan-card--active': planId === p.planId }"
@click="selectPlan(p)"
>
<view v-if="p.tag" class="vip__plan-badge">
<text>{{ p.tag }}</text>
</view>
<text class="vip__plan-name">{{ p.name }}</text>
<view class="vip__plan-price-row">
<text class="vip__plan-symbol">¥</text>
<text class="vip__plan-val">{{ p.price }}</text>
</view>
<text v-if="p.originPrice && p.originPrice > p.price" class="vip__plan-del">
¥{{ p.originPrice }}
</text>
<view v-if="p.postQuota && p.postQuota > 0" class="vip__plan-quota-chip">
<text>送{{ p.postQuota }}条发布</text>
</view>
</view>
</view>
<!-- 优惠券抵扣选择栏 -->
<view class="vip__coupon-row" @click="openCouponSheet">
<view class="vip__coupon-lbl-box">
<text class="vip__coupon-lbl">优惠券</text>
</view>
<view class="vip__coupon-act-box">
<text v-if="selectedCoupon" class="vip__coupon-tag vip__coupon-tag--active">
已选 1 张,立省 ¥{{ couponDiscountText }}
</text>
<text v-else-if="usableCoupons.length > 0" class="vip__coupon-tag vip__coupon-tag--has">
{{ usableCoupons.length }} 张可用
</text>
<text v-else class="vip__coupon-tag vip__coupon-tag--none">
暂无可用券
</text>
<text class="vip__coupon-arrow">›</text>
</view>
</view>
<!-- 支付方式切换 -->
<view class="vip__pay-toggle">
<text class="vip__pay-label">支付方式</text>
<view class="vip__pay-options">
<view
class="vip__pay-opt"
:class="{ 'vip__pay-opt--active': payType === 'wxpay' }"
@click="selectPayType('wxpay')"
>
<image class="vip__pay-opt-icon" :src="iconUrl('paywechat')" mode="aspectFit" />
<text class="vip__pay-opt-txt">微信支付</text>
<view class="vip__radio" :class="{ 'vip__radio--active': payType === 'wxpay' }">
<text v-if="payType === 'wxpay'" class="vip__radio-dot"></text>
</view>
</view>
<view
class="vip__pay-opt"
:class="{ 'vip__pay-opt--active': payType === 'balance' }"
@click="selectPayType('balance')"
>
<image class="vip__pay-opt-icon" :src="iconUrl('wallet')" mode="aspectFit" />
<text class="vip__pay-opt-txt">余额支付 (¥{{ balanceText }})</text>
<view class="vip__radio" :class="{ 'vip__radio--active': payType === 'balance' }">
<text v-if="payType === 'balance'" class="vip__radio-dot"></text>
</view>
</view>
</view>
</view>
</view>
<!-- 底部防遮挡垫高 -->
<view class="vip__dock-pad"></view>
<!-- 底部吸底结算栏 -->
<view class="vip__dock">
<view class="vip__dock-left">
<view class="vip__dock-cost-line">
<text class="vip__dock-tag">实付</text>
<text class="vip__dock-sym">¥</text>
<text class="vip__dock-amount">{{ finalPriceText }}</text>
<text v-if="savedMoneyText" class="vip__dock-saved-pill">{{ savedMoneyText }}</text>
</view>
<text class="vip__dock-meta">{{ quotaTip }}</text>
</view>
<button class="vip__dock-submit" :loading="submitting" :disabled="submitting" @click="openVip">
<text class="vip__dock-submit-txt">{{ isMember ? '立即续费' : '立即开通' }}</text>
</button>
</view>
<!-- 半屏优惠券选择弹窗 -->
<view v-if="showCouponSheet" class="vip__sheet-mask" @click="closeCouponSheet">
<view class="vip__sheet-panel" @click.stop>
<view class="vip__sheet-head">
<text class="vip__sheet-title">选择优惠券</text>
<view class="vip__sheet-close-btn" @click="closeCouponSheet">
<text class="vip__sheet-close-txt">✕</text>
</view>
</view>
<scroll-view class="vip__sheet-scroll" scroll-y>
<view class="vip__sheet-list">
<!-- 不使用优惠券选项 -->
<view
class="vip__sheet-item vip__sheet-item--opt"
:class="{ 'vip__sheet-item--active': !selectedCouponId }"
@click="chooseCoupon(null)"
>
<text class="vip__sheet-opt-txt">不使用优惠券</text>
<view class="vip__radio" :class="{ 'vip__radio--active': !selectedCouponId }">
<text v-if="!selectedCouponId" class="vip__radio-dot"></text>
</view>
</view>
<!-- 可用券列表 -->
<view
v-for="c in usableCoupons"
:key="c.id"
class="vip__sheet-item"
:class="{ 'vip__sheet-item--active': selectedCouponId === c.id }"
@click="chooseCoupon(c)"
>
<view class="vip__sheet-card-left">
<view class="vip__sheet-card-price">
<text class="vip__sheet-card-sym">¥</text>
<text class="vip__sheet-card-val">{{ formatAmount(c.amount) }}</text>
</view>
<text class="vip__sheet-card-cond">{{ c.minAmount > 0 ? `满${c.minAmount}可用` : '无门槛' }}</text>
</view>
<view class="vip__sheet-card-info">
<text class="vip__sheet-card-name fc-ellipsis">{{ c.name || '通用优惠券' }}</text>
<text class="vip__sheet-card-date">
{{ c.expireAt ? `有效期至 ${formatDate(c.expireAt)}` : '永久有效' }}
</text>
</view>
<view class="vip__radio" :class="{ 'vip__radio--active': selectedCouponId === c.id }">
<text v-if="selectedCouponId === c.id" class="vip__radio-dot"></text>
</view>
</view>
</view>
</scroll-view>
<view class="vip__sheet-bottom">
<view class="vip__sheet-btn" @click="closeCouponSheet">
<text class="vip__sheet-btn-txt">确定</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import FcNavbar from '@/components/fc-navbar/fc-navbar.vue'
import memberApi from '@/common/api/member.js'
import { useUserStore } from '@/common/store/index.js'
import { iconUrl, loadUiConfig } from '@/common/store/ui.js'
import { formatTime } from '@/common/utils/format.js'
import { pay } from '@/common/utils/pay.js'
import config from '@/common/config/index.js'
export default {
components: { FcNavbar },
data() {
return {
plans: [],
benefits: {},
assets: {},
userCoupons: [],
planId: 'M12',
payType: config.defaultPayType || 'wxpay',
submitting: false,
// 优惠券弹层与选券状态
showCouponSheet: false,
selectedCouponId: null,
}
},
computed: {
/**
* 当前登录用户信息
* @returns {Object}
*/
userInfo() {
try {
return useUserStore().state.userInfo || {}
} catch (e) {
return {}
}
},
/**
* 当前是否已登录
* @returns {boolean}
*/
isLogin() {
try {
return useUserStore().isLogin.value
} catch (e) {
return false
}
},
/**
* 昵称展示
* @returns {string}
*/
nickName() {
if (!this.isLogin) return '点击登录'
return this.userInfo.nickname || this.userInfo.phone || '同城居民'
},
/**
* 头像首字母
* @returns {string}
*/
userInitial() {
if (!this.isLogin) return '登'
const n = this.userInfo.nickname || this.userInfo.phone || 'V'
return n.slice(0, 1).toUpperCase()
},
/**
* 是否为会员状态
* @returns {boolean}
*/
isMember() {
try {
return useUserStore().isMember.value
} catch (e) {
return false
}
},
/**
* 到期时间文字
* @returns {string}
*/
expireText() {
if (!this.userInfo.memberExpireAt) return '--'
return formatTime(this.userInfo.memberExpireAt).slice(0, 10)
},
/**
* 余额格式化
* @returns {string}
*/
balanceText() {
const b = this.assets.balance
return b != null ? Number(b).toFixed(2) : '0.00'
},
/**
* 选中的套餐对象
* @returns {Object|null}
*/
selectedPlan() {
return this.plans.find((x) => x.planId === this.planId) || null
},
/**
* 当前套餐价格数字
* @returns {number}
*/
currentPlanPrice() {
return this.selectedPlan ? (Number(this.selectedPlan.price) || 0) : 0
},
/**
* 我的可用卡券总数
* @returns {number}
*/
myCouponCount() {
if (this.assets.couponCount != null) {
return Number(this.assets.couponCount)
}
return this.userCoupons.filter((c) => c.status === 'unused').length
},
/**
* 针对当前套餐金额适用的优惠券列表
* @returns {Array}
*/
usableCoupons() {
const price = this.currentPlanPrice
if (!Array.isArray(this.userCoupons)) return []
const now = Math.floor(Date.now() / 1000)
return this.userCoupons.filter((c) => {
// 状态必须为 unused
if (c.status !== 'unused') return false
// 检查过期时间
if (c.expiretime && Number(c.expiretime) < now) return false
// 检查门槛金额 min_amount
const min = Number(c.minAmount != null ? c.minAmount : (c.min_amount || 0))
return price >= min
})
},
/**
* 当前选中的优惠券
* @returns {Object|null}
*/
selectedCoupon() {
if (!this.selectedCouponId) return null
return this.usableCoupons.find((c) => c.id === this.selectedCouponId) || null
},
/**
* 优惠券抵扣金额(数字)
* @returns {number}
*/
couponDiscountAmount() {
if (!this.selectedCoupon) return 0
const c = this.selectedCoupon
const price = this.currentPlanPrice
let discount = 0
if (c.type === 'discount') {
const rate = Number(c.amount) / 10
discount = price * (1 - rate)
} else {
discount = Number(c.amount) || 0
}
// 最多减到实付 0.01 元
return Math.min(discount, Math.max(0, price - 0.01))
},
/**
* 优惠券抵扣金额文本
* @returns {string}
*/
couponDiscountText() {
return this.couponDiscountAmount.toFixed(2)
},
/**
* 最终实付金额
* @returns {string}
*/
finalPriceText() {
const price = this.currentPlanPrice
const discount = this.couponDiscountAmount
const finalVal = Math.max(0.01, price - discount)
return finalVal.toFixed(2)
},
/**
* 立省文案
* @returns {string}
*/
savedMoneyText() {
let totalSave = 0
if (this.selectedPlan && this.selectedPlan.originPrice && this.selectedPlan.originPrice > this.selectedPlan.price) {
totalSave += (Number(this.selectedPlan.originPrice) - Number(this.selectedPlan.price))
}
totalSave += this.couponDiscountAmount
return totalSave > 0 ? `省 ¥${totalSave.toFixed(0)}` : ''
},
/**
* 当前选中套餐的发布赠送条数
* @returns {number}
*/
selectedPlanQuota() {
return this.selectedPlan && this.selectedPlan.postQuota ? this.selectedPlan.postQuota : 30
},
/**
* 额度提示文案
* @returns {string}
*/
quotaTip() {
if (this.selectedPlan && this.selectedPlan.postQuota > 0) {
return `当前套餐送 ${this.selectedPlan.postQuota} 条发布`
}
return '开通即享专属会员全部特权'
},
},
/**
* 页面加载生命周期
*/
onLoad() {
loadUiConfig()
this.init()
},
/**
* 页面展示生命周期
*/
onShow() {
if (this.isLogin) {
this.loadAssets()
useUserStore().fetchProfile()
}
},
/**
* 下拉刷新生命周期
*/
async onPullDownRefresh() {
await this.init()
uni.stopPullDownRefresh()
},
methods: {
iconUrl,
/**
* 初始化数据
* @returns {Promise<void>}
*/
async init() {
try {
const [plans, benefits, assets] = await Promise.all([
memberApi.plans(),
memberApi.benefits(),
memberApi.assets(),
])
this.plans = Array.isArray(plans) ? plans : []
this.benefits = benefits || {}
this.assets = assets || {}
this.userCoupons = (assets && Array.isArray(assets.coupons)) ? assets.coupons : []
if (this.plans.length > 0) {
const rec = this.plans.find((p) => p.tag && p.tag.includes('推荐'))
this.planId = rec ? rec.planId : this.plans[0].planId
}
this.autoSelectBestCoupon()
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
}
},
/**
* 加载最新资产与优惠券
* @returns {Promise<void>}
*/
async loadAssets() {
try {
const res = await memberApi.assets()
this.assets = res || {}
this.userCoupons = (res && Array.isArray(res.coupons)) ? res.coupons : []
this.autoSelectBestCoupon()
} catch (e) {}
},
/**
* 选择套餐
* @param {Object} plan
*/
selectPlan(plan) {
if (!plan) return
this.planId = plan.planId
this.$nextTick(() => {
this.autoSelectBestCoupon()
})
},
/**
* 自动优选减免面额最大的一张可用券
*/
autoSelectBestCoupon() {
if (!this.usableCoupons || this.usableCoupons.length === 0) {
this.selectedCouponId = null
return
}
// 若当前已选券仍在可用范围内,则保留
if (this.selectedCouponId && this.usableCoupons.some((c) => c.id === this.selectedCouponId)) {
return
}
// 按面额从大到小排序,默认选中第一张
const sorted = [...this.usableCoupons].sort((a, b) => Number(b.amount || 0) - Number(a.amount || 0))
this.selectedCouponId = sorted[0].id
},
/**
* 打开优惠券选择弹窗
*/
openCouponSheet() {
if (this.usableCoupons.length === 0) {
uni.showToast({ title: '暂无当前套餐可适用的优惠券', icon: 'none' })
return
}
this.showCouponSheet = true
},
/**
* 关闭优惠券选择弹窗
*/
closeCouponSheet() {
this.showCouponSheet = false
},
/**
* 选中某张优惠券
* @param {Object|null} c
*/
chooseCoupon(c) {
this.selectedCouponId = c ? c.id : null
this.showCouponSheet = false
},
/**
* 切换支付类型
* @param {string} type
*/
selectPayType(type) {
this.payType = type
},
/**
* 检查登录状态
* @returns {boolean}
*/
checkLogin() {
if (!this.isLogin) {
uni.navigateTo({ url: '/pages/auth/login' })
return false
}
return true
},
/**
* 用户区域点击
*/
handleUserClick() {
if (!this.isLogin) {
uni.navigateTo({ url: '/pages/auth/login' })
}
},
/**
* 跳转到优惠券中心
*/
goCoupons() {
if (!this.checkLogin()) return
uni.navigateTo({ url: '/pages/member/coupons' })
},
/**
* 提交开通或续费
* @returns {Promise<void>}
*/
async openVip() {
if (!this.checkLogin()) return
if (!this.planId) {
uni.showToast({ title: '请选择套餐', icon: 'none' })
return
}
// 余额支付前进行实付金额判定
const finalCost = Number(this.finalPriceText)
if (this.payType === 'balance') {
const bal = Number(this.assets.balance || 0)
if (bal < finalCost) {
uni.showModal({
title: '余额不足',
content: `当前账户余额 (¥${this.balanceText}) 不足支付实付金额 (¥${this.finalPriceText}),是否前往充值?`,
confirmText: '去充值',
success: (r) => {
if (r.confirm) this.recharge()
},
})
return
}
}
this.submitting = true
uni.showLoading({ title: '正在处理…' })
try {
const res = await memberApi.open({
planId: this.planId,
payType: this.payType,
userCouponId: this.selectedCouponId || 0,
})
// 微信支付走拉起逻辑
if (this.payType === config.constants.PAY_TYPE.WXPAY && res && res.orderNo) {
await pay({ orderNo: res.orderNo, payType: this.payType })
}
uni.hideLoading()
this.submitting = false
// 同步刷新本地用户信息与资产
useUserStore().fetchProfile()
this.loadAssets()
// 支付成功直接平滑跳转至专业的支付结果页
uni.redirectTo({
url: `/pages/order/success?orderNo=${res.orderNo || ''}&type=member&title=${encodeURIComponent(this.selectedPlan ? this.selectedPlan.name : 'VIP会员')}&amount=${this.finalPriceText}&originAmount=${this.currentPlanPrice}&discount=${this.couponDiscountText}&payType=${this.payType}`,
})
} catch (e) {
uni.hideLoading()
this.submitting = false
uni.showToast({ title: e.message || '开通未完成', icon: 'none' })
}
},
/**
* 前往充值
*/
recharge() {
if (!this.checkLogin()) return
uni.navigateTo({ url: '/pages/member/recharge' })
},
/**
* 格式化金额数值
* @param {number|string} val
* @returns {string}
*/
formatAmount(val) {
const n = Number(val)
if (isNaN(n)) return '0'
return Number.isInteger(n) ? String(n) : n.toFixed(1)
},
/**
* 格式化时间字符串至年月日
* @param {string} dateStr
* @returns {string}
*/
formatDate(dateStr) {
if (!dateStr) return ''
return String(dateStr).slice(0, 10)
},
},
}
</script>
<style lang="scss" scoped>
.vip {
min-height: 100vh;
background: $fc-bg;
position: relative;
padding-bottom: 40rpx;
}
.vip__backdrop {
background: $fc-gradient-hero;
position: absolute;
top: 0;
left: 0;
right: 0;
height: 380rpx;
overflow: hidden;
z-index: 0;
}
.vip__backdrop-ambient {
position: absolute;
top: -60rpx;
right: -60rpx;
width: 320rpx;
height: 320rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(0, 224, 112, 0.28) 0%, rgba(0, 224, 112, 0) 70%);
filter: blur(20px);
}
.vip__card-wrap {
position: relative;
z-index: 2;
padding: 16rpx 28rpx 0;
}
.vip__card {
/* 尊享高定暖金黑金渐变:彻底告别老式墨绿发黑,与 mine 会员卡片视觉基因统一 */
background: linear-gradient(135deg, #ffe7a6 0%, #ffd06a 45%, #f5a623 100%);
border-radius: 28rpx;
padding: 36rpx 32rpx 28rpx;
position: relative;
overflow: hidden;
box-shadow: 0 16rpx 36rpx rgba(245, 166, 35, 0.22);
border: 1rpx solid rgba(255, 255, 255, 0.6);
}
.vip__card-sheen {
position: absolute;
top: -90rpx;
right: -90rpx;
width: 280rpx;
height: 280rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.55) 0%, rgba(255, 255, 255, 0) 70%);
pointer-events: none;
}
.vip__card-user {
display: flex;
align-items: center;
position: relative;
z-index: 2;
}
.vip__avatar {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
border: 3rpx solid rgba(255, 255, 255, 0.95);
box-shadow: 0 4rpx 14rpx rgba(74, 38, 0, 0.12);
flex-shrink: 0;
}
.vip__avatar-txt {
font-size: 38rpx;
font-weight: 800;
color: #4a2600;
}
.vip__user-meta {
margin-left: 20rpx;
flex: 1;
min-width: 0;
}
.vip__user-title-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.vip__user-name {
font-size: 32rpx;
font-weight: 800;
color: #3e2000;
}
.vip__badge {
display: inline-flex;
align-items: center;
padding: 4rpx 14rpx;
border-radius: 20rpx;
background: rgba(74, 38, 0, 0.08);
border: 1rpx solid rgba(74, 38, 0, 0.12);
}
.vip__badge--active {
background: #3e2000;
border-color: #3e2000;
}
.vip__badge-icon {
width: 22rpx;
height: 22rpx;
margin-right: 6rpx;
}
.vip__badge-txt {
font-size: 20rpx;
color: #4a2600;
font-weight: 700;
}
.vip__badge--active .vip__badge-txt {
color: #ffe7a6;
}
.vip__user-desc {
margin-top: 8rpx;
font-size: 22rpx;
color: #7a4600;
font-weight: 500;
}
.vip__card-crown {
width: 72rpx;
height: 72rpx;
opacity: 0.18;
}
.vip__assets {
margin-top: 32rpx;
padding-top: 24rpx;
border-top: 1rpx solid rgba(74, 38, 0, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
}
.vip__asset-col {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.vip__asset-val-box {
display: flex;
align-items: baseline;
}
.vip__asset-val {
font-size: 40rpx;
font-weight: 800;
color: #3e2000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.vip__asset-unit {
font-size: 20rpx;
color: #7a4600;
margin-left: 4rpx;
font-weight: 600;
}
.vip__asset-label-row {
margin-top: 6rpx;
display: flex;
align-items: center;
gap: 6rpx;
}
.vip__asset-lbl {
font-size: 22rpx;
color: #7a4600;
font-weight: 500;
}
.vip__asset-act {
font-size: 20rpx;
color: #4a2600;
font-weight: 700;
background: rgba(255, 255, 255, 0.45);
padding: 2rpx 10rpx;
border-radius: 10rpx;
}
.vip__asset-sep {
width: 1rpx;
height: 36rpx;
background: rgba(74, 38, 0, 0.12);
}
.vip__section {
margin: 24rpx 28rpx 0;
background: #ffffff;
border-radius: 28rpx;
padding: 32rpx 28rpx;
box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.04);
}
.vip__sec-head {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 24rpx;
}
.vip__sec-title {
font-size: 32rpx;
font-weight: 700;
color: #1e293b;
}
.vip__sec-tip {
font-size: 22rpx;
color: #64748b;
}
.vip__privileges-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.vip__priv-item {
display: flex;
align-items: center;
background: #f8fafc;
padding: 20rpx 18rpx;
border-radius: 20rpx;
}
.vip__priv-icon-wrap {
width: 64rpx;
height: 64rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&--green {
background: #eaf8f0;
}
&--gold {
background: #fff4e0;
}
}
.vip__priv-icon {
width: 36rpx;
height: 36rpx;
}
.vip__priv-content {
margin-left: 16rpx;
min-width: 0;
flex: 1;
}
.vip__priv-name {
font-size: 26rpx;
font-weight: 700;
color: #1e293b;
display: block;
}
.vip__priv-desc {
margin-top: 4rpx;
font-size: 20rpx;
color: #64748b;
display: block;
}
.vip__plans-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
margin-bottom: 10rpx;
}
.vip__plan-card {
background: #ffffff;
border: 2rpx solid #e2e8f0;
border-radius: 20rpx;
padding: 28rpx 10rpx 24rpx;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
transition: all 0.2s ease;
&--active {
border-color: #00e070;
background: linear-gradient(180deg, #f2fef7 0%, #ffffff 100%);
box-shadow: 0 6rpx 20rpx rgba(0, 224, 112, 0.15);
}
}
.vip__plan-badge {
position: absolute;
top: -12rpx;
right: -6rpx;
background: #00e070;
color: #0f2f1d;
font-size: 18rpx;
font-weight: 700;
padding: 2rpx 12rpx;
border-radius: 12rpx 0 12rpx 0;
}
.vip__plan-name {
font-size: 28rpx;
font-weight: 700;
color: #1e293b;
}
.vip__plan-price-row {
display: flex;
align-items: baseline;
color: #f5a623;
margin-top: 10rpx;
}
.vip__plan-symbol {
font-size: 24rpx;
font-weight: 700;
}
.vip__plan-val {
font-size: 44rpx;
font-weight: 800;
line-height: 1;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.vip__plan-del {
margin-top: 4rpx;
font-size: 20rpx;
color: #94a3b8;
text-decoration: line-through;
}
.vip__plan-quota-chip {
margin-top: 12rpx;
font-size: 18rpx;
color: #00a854;
background: #eaf8f0;
padding: 2rpx 10rpx;
border-radius: 8rpx;
font-weight: 600;
}
.vip__coupon-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
border-top: 1rpx solid #f1f5f9;
border-bottom: 1rpx solid #f1f5f9;
margin: 24rpx 0 16rpx;
}
.vip__coupon-lbl {
font-size: 26rpx;
color: #1e293b;
font-weight: 700;
}
.vip__coupon-act-box {
display: flex;
align-items: center;
gap: 10rpx;
}
.vip__coupon-tag {
font-size: 24rpx;
font-weight: 600;
padding: 6rpx 18rpx;
border-radius: 20rpx;
&--active {
color: #e65100;
background: #fff4e0;
}
&--has {
color: #00a854;
background: #eaf8f0;
}
&--none {
color: #64748b;
font-weight: normal;
}
}
.vip__coupon-arrow {
font-size: 28rpx;
color: #64748b;
}
.vip__pay-toggle {
margin-top: 16rpx;
}
.vip__pay-label {
font-size: 26rpx;
font-weight: 700;
color: #1e293b;
margin-bottom: 16rpx;
display: block;
}
.vip__pay-options {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
}
.vip__pay-opt {
display: flex;
align-items: center;
padding: 20rpx 18rpx;
border-radius: 18rpx;
background: #f8fafc;
border: 2rpx solid transparent;
transition: all 0.2s ease;
&--active {
background: #f0fdf6;
border-color: #00e070;
}
}
.vip__pay-opt-icon {
width: 36rpx;
height: 36rpx;
margin-right: 12rpx;
flex-shrink: 0;
}
.vip__pay-opt-txt {
flex: 1;
font-size: 24rpx;
color: #1e293b;
font-weight: 600;
}
.vip__radio {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid #cbd5e1;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&--active {
border-color: #00e070;
background: #00e070;
}
}
.vip__radio-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #0f2f1d;
}
.vip__dock-pad {
height: 180rpx;
}
.vip__dock {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #ffffff;
box-shadow: 0 -8rpx 28rpx rgba(15, 23, 42, 0.06);
padding: 20rpx 32rpx;
padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
justify-content: space-between;
z-index: 100;
}
.vip__dock-left {
display: flex;
flex-direction: column;
}
.vip__dock-cost-line {
display: flex;
align-items: baseline;
}
.vip__dock-tag {
font-size: 24rpx;
color: #64748b;
margin-right: 8rpx;
}
.vip__dock-sym {
font-size: 28rpx;
color: #f5a623;
font-weight: 700;
}
.vip__dock-amount {
font-size: 48rpx;
color: #f5a623;
font-weight: 800;
line-height: 1;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.vip__dock-saved-pill {
margin-left: 12rpx;
padding: 2rpx 12rpx;
border-radius: 12rpx;
background: #fff4e0;
color: #e65100;
font-size: 20rpx;
font-weight: 700;
}
.vip__dock-meta {
margin-top: 6rpx;
font-size: 20rpx;
color: #64748b;
}
.vip__dock-submit {
height: 84rpx;
padding: 0 48rpx;
border-radius: 42rpx;
background: $fc-gradient-primary;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(0, 224, 112, 0.28);
margin: 0;
border: none;
}
.vip__dock-submit-txt {
font-size: 30rpx;
font-weight: 700;
color: #0f2f1d;
}
.vip__sheet-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.55);
z-index: 999;
display: flex;
align-items: flex-end;
}
.vip__sheet-panel {
width: 100%;
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
max-height: 75vh;
display: flex;
flex-direction: column;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
.vip__sheet-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 36rpx 20rpx;
border-bottom: 1rpx solid #f1f5f9;
}
.vip__sheet-title {
font-size: 32rpx;
font-weight: 700;
color: #1e293b;
}
.vip__sheet-close-btn {
padding: 8rpx 16rpx;
}
.vip__sheet-close-txt {
font-size: 32rpx;
color: #64748b;
}
.vip__sheet-scroll {
max-height: 55vh;
padding: 20rpx 32rpx;
box-sizing: border-box;
}
.vip__sheet-list {
display: flex;
flex-direction: column;
gap: 16rpx;
padding-bottom: 20rpx;
}
.vip__sheet-item {
display: flex;
align-items: center;
padding: 20rpx 24rpx;
border-radius: 18rpx;
background: #f8fafc;
border: 2rpx solid transparent;
transition: all 0.2s ease;
&--active {
background: #f0fdf6;
border-color: #00e070;
}
&--opt {
justify-content: space-between;
padding: 24rpx;
}
}
.vip__sheet-opt-txt {
font-size: 28rpx;
color: #334155;
font-weight: 500;
}
.vip__sheet-card-left {
width: 140rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-shrink: 0;
border-right: 2rpx dashed #cbd5e1;
padding-right: 16rpx;
margin-right: 18rpx;
}
.vip__sheet-card-price {
display: flex;
align-items: baseline;
color: #f5a623;
}
.vip__sheet-card-sym {
font-size: 22rpx;
font-weight: 700;
}
.vip__sheet-card-val {
font-size: 38rpx;
font-weight: 800;
line-height: 1;
}
.vip__sheet-card-cond {
font-size: 18rpx;
color: #b45309;
margin-top: 4rpx;
}
.vip__sheet-card-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.vip__sheet-card-name {
font-size: 26rpx;
font-weight: 700;
color: #1e293b;
}
.vip__sheet-card-date {
font-size: 20rpx;
color: #64748b;
}
.vip__sheet-bottom {
padding: 20rpx 36rpx 24rpx;
border-top: 1rpx solid #f1f5f9;
}
.vip__sheet-btn {
height: 84rpx;
border-radius: 42rpx;
background: $fc-gradient-primary;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6rpx 20rpx rgba(0, 224, 112, 0.25);
}
.vip__sheet-btn-txt {
font-size: 28rpx;
font-weight: 700;
color: #0f2f1d;
}
</style>