feat: 支持同卡种续卡叠加并修复过期登录态
支付成功后将次数或有效期叠加到已有会员卡,首页与详情页引导续卡;接口 401 时同步清掉本地登录态。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -47,7 +47,10 @@
|
||||
<!-- Card info — aligns with card-cover height -->
|
||||
<view class="card-info">
|
||||
<view class="info-top">
|
||||
<text class="card-name">{{ card.name }}</text>
|
||||
<view class="card-name-row">
|
||||
<text class="card-name">{{ card.name }}</text>
|
||||
<text v-if="isRenewable(card)" class="renew-tag">续</text>
|
||||
</view>
|
||||
<text class="card-validity">有效期 {{ card.durationDays }} 天</text>
|
||||
</view>
|
||||
<view class="info-bottom">
|
||||
@@ -80,15 +83,32 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import type { CardType } from '@mp-pilates/shared'
|
||||
import { CardTypeCategory } from '@mp-pilates/shared'
|
||||
import { get } from '../utils/request'
|
||||
import { formatPrice, getCardCoverClass } from '../utils/format'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const cardTypes = ref<CardType[]>([])
|
||||
const loading = ref(false)
|
||||
const hasLoaded = ref(false)
|
||||
|
||||
const renewableCardTypeIds = computed(() => {
|
||||
const ids = new Set<string>()
|
||||
for (const membership of userStore.memberships) {
|
||||
if (membership.cardType.type !== CardTypeCategory.TRIAL) {
|
||||
ids.add(membership.cardTypeId)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
})
|
||||
|
||||
function isRenewable(card: CardType): boolean {
|
||||
return card.type !== CardTypeCategory.TRIAL && renewableCardTypeIds.value.has(card.id)
|
||||
}
|
||||
|
||||
async function fetchCardTypes() {
|
||||
// Stale-While-Revalidate: only show skeleton on first load
|
||||
// Subsequent refreshes silently update data in background
|
||||
@@ -250,7 +270,15 @@ function goToAllCards() {
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.card-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
min-width: 0;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: $text-primary;
|
||||
@@ -261,6 +289,18 @@ function goToAllCards() {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.renew-tag {
|
||||
flex-shrink: 0;
|
||||
font-size: 18rpx;
|
||||
font-weight: 700;
|
||||
color: #5f7a6e;
|
||||
background: rgba(143, 168, 154, 0.2);
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 8rpx;
|
||||
line-height: 1.4;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.card-validity {
|
||||
font-size: 23rpx;
|
||||
color: $text-secondary;
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
</view>
|
||||
|
||||
<!-- Running low: thin accent strip -->
|
||||
<view v-if="isRunningLow" class="renew-strip" @tap="scrollToCardShop">
|
||||
<text class="renew-strip-text">仅剩 {{ lowestRemainingTimes }} 次 · 续卡保持节奏</text>
|
||||
<view v-if="renewStripText" class="renew-strip" @tap="handleRenew">
|
||||
<text class="renew-strip-text">{{ renewStripText }}</text>
|
||||
<text class="renew-strip-arrow">›</text>
|
||||
</view>
|
||||
</template>
|
||||
@@ -43,7 +43,7 @@
|
||||
<view
|
||||
v-else
|
||||
class="entry-pill pill-expired"
|
||||
@tap="scrollToCardShop"
|
||||
@tap="handleRenew"
|
||||
>
|
||||
<view class="pill-dot dot-expired" />
|
||||
<text class="pill-label">会员卡已到期</text>
|
||||
@@ -91,6 +91,15 @@ function scrollToCardShop() {
|
||||
emit('scroll-to-card-shop')
|
||||
}
|
||||
|
||||
function handleRenew() {
|
||||
const cardTypeId = userStore.renewalHint?.cardTypeId
|
||||
if (cardTypeId) {
|
||||
uni.navigateTo({ url: `/pages/card/detail?id=${cardTypeId}` })
|
||||
return
|
||||
}
|
||||
scrollToCardShop()
|
||||
}
|
||||
|
||||
const activeMembershipLabel = computed(() => {
|
||||
const active = userStore.activeMemberships
|
||||
if (!active.length) return ''
|
||||
@@ -105,24 +114,19 @@ const activeMembershipLabel = computed(() => {
|
||||
return `${cardName} · 剩余 ${daysLeft} 天`
|
||||
})
|
||||
|
||||
const isRunningLow = computed(() => {
|
||||
return userStore.activeMemberships.some(
|
||||
(m) =>
|
||||
m.cardType.type === CardTypeCategory.TIMES &&
|
||||
m.remainingTimes !== null &&
|
||||
m.remainingTimes <= 2,
|
||||
)
|
||||
})
|
||||
|
||||
const lowestRemainingTimes = computed(() => {
|
||||
const timesCards = userStore.activeMemberships.filter(
|
||||
(m) =>
|
||||
m.cardType.type === CardTypeCategory.TIMES &&
|
||||
m.remainingTimes !== null &&
|
||||
m.remainingTimes <= 2,
|
||||
)
|
||||
if (!timesCards.length) return 0
|
||||
return Math.min(...timesCards.map((m) => m.remainingTimes as number))
|
||||
const renewStripText = computed(() => {
|
||||
const hint = userStore.renewalHint
|
||||
if (!hint || !userStore.hasValidMembership) return ''
|
||||
if (hint.kind === 'times_low') {
|
||||
return `仅剩 ${hint.remainingTimes ?? 0} 次 · 续卡保持节奏`
|
||||
}
|
||||
if (hint.kind === 'days_low') {
|
||||
return `还剩 ${hint.daysLeft ?? 0} 天到期 · 续卡不中断`
|
||||
}
|
||||
if (hint.kind === 'trial_low') {
|
||||
return '体验课将尽 · 选购会员卡'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -254,8 +258,8 @@ const lowestRemainingTimes = computed(() => {
|
||||
}
|
||||
|
||||
.action-renew {
|
||||
background: #e0e0e0;
|
||||
.pill-action-text { color: #555; }
|
||||
background: #e8efe9;
|
||||
.pill-action-text { color: #5f7a6e; }
|
||||
}
|
||||
|
||||
/* ── Renew strip (running low) ── */
|
||||
@@ -266,21 +270,21 @@ const lowestRemainingTimes = computed(() => {
|
||||
gap: 8rpx;
|
||||
margin-top: 12rpx;
|
||||
padding: 14rpx 24rpx;
|
||||
background: linear-gradient(135deg, #FF6B35, #FF8E53);
|
||||
background: linear-gradient(135deg, #8fa89a, #a8c0b4);
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.renew-strip-text {
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
color: #fffcfa;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
.renew-strip-arrow {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
color: rgba(255, 252, 250, 0.82);
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user