Files
qitongxue-wx/src/components/vip/VipCard.vue
2026-09-17 17:39:06 +08:00

281 lines
7.0 KiB
Vue

<script setup lang="ts">
/**
* 首页会员卡片。
*
* 与工具页的悬浮广告是同一套视觉语言(深炭底 + 金边 + 呼吸光),
* 区别是它承担「整体介绍」职责:会员状态、额度三池概览、两个转化入口。
*
* 会员且额度充足时不再「催买」,改为展示额度状态 + 续费入口,避免打扰付费用户。
*/
import { computed, onMounted, ref } from 'vue'
import { useQuotaStore } from '@/stores/quota'
const store = useQuotaStore()
const ready = ref(false)
onMounted(async () => {
try {
await store.fetchMine()
ready.value = true
}
catch {
// 未登录 / 网络异常:退化为通用文案,不阻塞首页
}
})
/** 三池合计(跨工具求和,首页只看总量) */
const agg = computed(() => {
const list = store.mine?.tools || []
return {
free: list.reduce((sum, t) => sum + t.free_left, 0),
member: list.reduce((sum, t) => sum + t.member_left, 0),
paid: list.reduce((sum, t) => sum + t.paid_left, 0),
}
})
const isMember = computed(() => !!store.mine?.is_member)
const levelName = computed(() => store.mine?.level_name || '')
const totalLeft = computed(() => store.mine?.total_left ?? 0)
const periodDays = computed(() => store.periodDays)
const title = computed(() => (isMember.value ? `${levelName.value}会员` : '开通会员 · 解锁全部工具'))
const subline = computed(() => {
if (isMember.value) {
const expire = store.mine?.level_expire_at
return expire ? `${expire} 到期 · 额度每 ${periodDays.value} 天重置` : `额度每 ${periodDays.value} 天重置`
}
return `免费额度每 ${periodDays.value} 天重置,会员再送 10 倍额度`
})
const pools = computed(() => [
{ key: 'free', label: '免费', value: ready.value ? String(agg.value.free) : '—' },
{ key: 'member', label: '会员', value: isMember.value ? String(agg.value.member) : '未开通' },
{ key: 'paid', label: '次数包', value: ready.value ? String(agg.value.paid) : '—' },
])
function goMember() {
uni.navigateTo({ url: '/pkg-member/member/index' })
}
function goPacks() {
uni.navigateTo({ url: '/pkg-member/quota/index' })
}
function goMyQuota() {
uni.navigateTo({ url: '/pkg-member/quota/my' })
}
</script>
<template>
<view class="vcard">
<view class="vcard__aura" />
<view class="vcard__sweep" />
<view class="vcard__top">
<view class="vcard__crown">
<text class="vcard__crown-text">
VIP
</text>
</view>
<view class="vcard__meta">
<text class="vcard__title">
{{ title }}
</text>
<text class="vcard__sub">
{{ subline }}
</text>
</view>
<view class="vcard__mine press" hover-class="press-active" @tap.stop="goMyQuota">
<text>我的额度</text>
<wd-icon name="arrow-right" size="22rpx" color="rgba(255,233,184,0.75)" />
</view>
</view>
<view class="vcard__pools">
<view v-for="p in pools" :key="p.key" class="vpool">
<text class="vpool__val">
{{ p.value }}
</text>
<text class="vpool__label">
{{ p.label }}
</text>
</view>
<view class="vpool vpool--total">
<text class="vpool__val vpool__val--gold">
{{ ready ? totalLeft : '—' }}
</text>
<text class="vpool__label">
本周期可用
</text>
</view>
</view>
<view class="vcard__acts">
<view class="vbtn vbtn--gold press" hover-class="press-active" @tap.stop="goMember">
<text>{{ isMember ? '续费会员' : '立即开通会员' }}</text>
</view>
<view class="vbtn vbtn--ghost press" hover-class="press-active" @tap.stop="goPacks">
<text>买次数包</text>
</view>
</view>
</view>
</template>
<style scoped>
.vcard {
position: relative;
overflow: hidden;
padding: 28rpx 26rpx 26rpx;
border-radius: 34rpx;
background: linear-gradient(160deg, #2b1a10 0%, #3d2413 52%, #22130a 100%);
border: 2rpx solid rgba(245, 197, 107, 0.55);
box-shadow: 0 18rpx 42rpx rgba(43, 26, 16, 0.34), 0 0 0 6rpx rgba(245, 197, 107, 0.08);
}
.vcard__aura {
position: absolute;
top: -120rpx;
right: -80rpx;
width: 340rpx;
height: 340rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(245, 197, 107, 0.3) 0%, rgba(245, 197, 107, 0) 70%);
}
.vcard__sweep {
position: absolute;
top: 0;
left: -50%;
width: 36%;
height: 100%;
background: linear-gradient(100deg, rgba(255, 255, 255, 0) 0%, rgba(255, 226, 168, 0.16) 50%, rgba(255, 255, 255, 0) 100%);
animation: vcardSweep 4.6s ease-in-out infinite;
}
.vcard__top {
position: relative;
display: flex;
align-items: center;
gap: 20rpx;
}
.vcard__crown {
flex-shrink: 0;
width: 80rpx;
height: 80rpx;
border-radius: 24rpx;
background: linear-gradient(140deg, #ffe9b8 0%, #f5c56b 50%, #d89a35 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 10rpx 24rpx rgba(216, 154, 53, 0.42);
}
.vcard__crown-text {
font-size: 26rpx;
font-weight: 900;
letter-spacing: 0.06em;
color: #3d2413;
}
.vcard__meta {
flex: 1;
min-width: 0;
}
.vcard__title {
display: block;
font-size: 32rpx;
font-weight: 800;
color: #ffe9b8;
letter-spacing: 0.01em;
}
.vcard__sub {
display: block;
margin-top: 8rpx;
font-size: 21rpx;
color: rgba(255, 233, 184, 0.62);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.vcard__mine {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 2rpx;
padding: 8rpx 16rpx;
border-radius: 999rpx;
border: 2rpx solid rgba(255, 233, 184, 0.24);
font-size: 21rpx;
color: rgba(255, 233, 184, 0.78);
}
.vcard__pools {
position: relative;
display: flex;
gap: 12rpx;
margin-top: 24rpx;
}
.vpool {
flex: 1;
min-width: 0;
padding: 16rpx 8rpx;
border-radius: 22rpx;
background: rgba(245, 197, 107, 0.09);
border: 2rpx solid rgba(245, 197, 107, 0.16);
display: flex;
flex-direction: column;
align-items: center;
gap: 4rpx;
}
.vpool--total {
background: rgba(245, 197, 107, 0.16);
border-color: rgba(255, 233, 184, 0.3);
}
.vpool__val {
font-size: 30rpx;
font-weight: 800;
color: #ffe9b8;
line-height: 1.1;
}
.vpool__val--gold {
background: linear-gradient(180deg, #fff6e0 0%, #f5c56b 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.vpool__label {
font-size: 19rpx;
color: rgba(255, 233, 184, 0.55);
}
.vcard__acts {
position: relative;
display: flex;
gap: 14rpx;
margin-top: 24rpx;
}
.vbtn {
flex: 1;
text-align: center;
padding: 20rpx 0;
border-radius: 999rpx;
font-size: 26rpx;
font-weight: 700;
}
.vbtn--gold {
color: #3d2413;
background: linear-gradient(135deg, #ffe9b8 0%, #f5c56b 55%, #e0a63f 100%);
box-shadow: 0 12rpx 26rpx rgba(224, 166, 63, 0.34);
}
.vbtn--ghost {
color: #ffe9b8;
border: 2rpx solid rgba(245, 197, 107, 0.5);
background: rgba(255, 255, 255, 0.04);
}
@keyframes vcardSweep {
0% {
left: -50%;
}
55%, 100% {
left: 140%;
}
}
</style>