feat: 支持同卡种续卡叠加并修复过期登录态
支付成功后将次数或有效期叠加到已有会员卡,首页与详情页引导续卡;接口 401 时同步清掉本地登录态。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<view class="card-detail-page" :style="{ paddingTop: navBarHeight }">
|
||||
<CustomNavBar title="购买会员卡" show-back />
|
||||
<view class="card-detail-page" :class="{ 'page--renew': isRenewal }" :style="{ paddingTop: navBarHeight }">
|
||||
<CustomNavBar :title="isRenewal ? '续卡' : '购买会员卡'" show-back />
|
||||
<!-- Loading state -->
|
||||
<view v-if="loading" class="loading-wrap">
|
||||
<view class="skeleton-header" />
|
||||
@@ -54,7 +54,10 @@
|
||||
<!-- Card info — aligns with card-cover height -->
|
||||
<view class="card-info">
|
||||
<view class="info-top">
|
||||
<text class="card-name">{{ c.name }}</text>
|
||||
<view class="card-name-row">
|
||||
<text class="card-name">{{ c.name }}</text>
|
||||
<text v-if="isOwnedForRenew(c)" class="renew-tag">续</text>
|
||||
</view>
|
||||
<text class="card-validity">有效期 {{ c.durationDays }} 天</text>
|
||||
</view>
|
||||
<view class="info-bottom">
|
||||
@@ -85,7 +88,7 @@
|
||||
<!-- Card content (single card mode) -->
|
||||
<template v-else>
|
||||
<!-- Hero section -->
|
||||
<view class="card-hero" :class="cardData.coverUrl ? 'hero--custom' : heroClass">
|
||||
<view class="card-hero" :class="[cardData.coverUrl ? 'hero--custom' : heroClass, isRenewal ? 'hero--renew' : '']">
|
||||
<!-- Cover image background -->
|
||||
<image
|
||||
v-if="cardData.coverUrl"
|
||||
@@ -99,9 +102,15 @@
|
||||
<view class="hero-deco hero-deco--2" />
|
||||
</template>
|
||||
|
||||
<view class="hero-badge">
|
||||
<text class="hero-badge-text">{{ typeLabel }}</text>
|
||||
<view class="hero-badge-row">
|
||||
<view class="hero-badge">
|
||||
<text class="hero-badge-text">{{ typeLabel }}</text>
|
||||
</view>
|
||||
<view v-if="isRenewal" class="hero-badge hero-badge--renew">
|
||||
<text class="hero-badge-text">续卡</text>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="isRenewal" class="hero-kicker">把练习节奏接下去</text>
|
||||
<text class="hero-name">{{ cardData.name }}</text>
|
||||
<view class="hero-price-row">
|
||||
<text class="hero-currency">¥</text>
|
||||
@@ -144,15 +153,49 @@
|
||||
<text class="desc-content">{{ cardData.description }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="renewalPreview" class="renew-compare">
|
||||
<view class="renew-compare-head">
|
||||
<text class="renew-compare-kicker">续卡对照</text>
|
||||
<text class="renew-compare-status">{{ renewalPreview.statusLabel }}</text>
|
||||
</view>
|
||||
|
||||
<view class="renew-compare-body">
|
||||
<view class="renew-col">
|
||||
<text class="renew-col-label">当前</text>
|
||||
<text class="renew-col-value">{{ renewalPreview.currentHighlight }}</text>
|
||||
<text class="renew-col-unit">{{ renewalPreview.highlightUnit }}</text>
|
||||
<text class="renew-col-date">{{ renewalPreview.currentExpire }}</text>
|
||||
</view>
|
||||
|
||||
<view class="renew-arrow">
|
||||
<view class="renew-arrow-line" />
|
||||
<view class="renew-arrow-diamond" />
|
||||
<view class="renew-arrow-line" />
|
||||
</view>
|
||||
|
||||
<view class="renew-col renew-col--after">
|
||||
<text class="renew-col-label">续后</text>
|
||||
<text class="renew-col-value">{{ renewalPreview.afterHighlight }}</text>
|
||||
<text class="renew-col-unit">{{ renewalPreview.highlightUnit }}</text>
|
||||
<text class="renew-col-date">{{ renewalPreview.afterExpire }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="renew-compare-foot">
|
||||
<text class="renew-compare-add">{{ renewalPreview.addedLabel }}</text>
|
||||
<text v-if="renewalPreview.forfeitNote" class="renew-compare-note">{{ renewalPreview.forfeitNote }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Features list -->
|
||||
<view class="features-card">
|
||||
<view class="section-header">
|
||||
<view class="section-dot" />
|
||||
<text class="section-title">购买须知</text>
|
||||
<text class="section-title">{{ isRenewal ? '续卡须知' : '购买须知' }}</text>
|
||||
</view>
|
||||
<view class="feature-item">
|
||||
<text class="feature-dot">•</text>
|
||||
<text class="feature-text">购买后立即生效,有效期 {{ cardData.durationDays }} 天</text>
|
||||
<text class="feature-text">{{ isRenewal ? `续卡后有效期接续,再延长 ${cardData.durationDays} 天` : `购买后立即生效,有效期 ${cardData.durationDays} 天` }}</text>
|
||||
</view>
|
||||
<view v-if="cardData.totalTimes" class="feature-item">
|
||||
<text class="feature-dot">•</text>
|
||||
@@ -186,17 +229,17 @@
|
||||
</view>
|
||||
|
||||
<!-- Bottom action bar -->
|
||||
<view class="bottom-bar">
|
||||
<view class="bottom-bar" :class="{ 'bottom-bar--renew': isRenewal }">
|
||||
<view class="price-summary">
|
||||
<text class="summary-label">实付金额</text>
|
||||
<text class="summary-label">{{ isRenewal ? '续卡金额' : '实付金额' }}</text>
|
||||
<text class="summary-price">¥{{ formatPrice(cardData.price) }}</text>
|
||||
</view>
|
||||
<view
|
||||
class="buy-btn"
|
||||
:class="{ 'buy-btn--loading': buying }"
|
||||
:class="{ 'buy-btn--loading': buying, 'buy-btn--renew': isRenewal }"
|
||||
@tap="handleBuy"
|
||||
>
|
||||
<text class="buy-btn-text">{{ buying ? '支付中...' : '立即购买' }}</text>
|
||||
<text class="buy-btn-text">{{ buying ? '支付中...' : (isRenewal ? '确认续卡' : '立即购买') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -206,10 +249,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import type { CardType, CreateOrderResponse } from '@mp-pilates/shared'
|
||||
import { CardTypeCategory } from '@mp-pilates/shared'
|
||||
import { CardTypeCategory, MembershipStatus, computeMembershipGrant, pickRenewalTarget, getMembershipDaysLeft } from '@mp-pilates/shared'
|
||||
import { getErrorMessage } from '../../utils/auth'
|
||||
import { get, post } from '../../utils/request'
|
||||
import { formatPrice, getCardTypeLabel, getCardCoverClass } from '../../utils/format'
|
||||
import { formatPrice, getCardCoverClass } from '../../utils/format'
|
||||
import { getSystemLayout } from '../../utils/system'
|
||||
import { useUserStore } from '../../stores/user'
|
||||
import { requestOrderPaidSubscriptionMessage } from '../../utils/wechat-subscription'
|
||||
@@ -261,6 +304,78 @@ const unitPrice = computed(() => {
|
||||
|
||||
const cardData = computed<CardType>(() => card.value as CardType)
|
||||
|
||||
const renewalTarget = computed(() => {
|
||||
if (!card.value || card.value.type === CardTypeCategory.TRIAL) return null
|
||||
return pickRenewalTarget(userStore.memberships, card.value.id)
|
||||
})
|
||||
|
||||
const isRenewal = computed(() => !!renewalTarget.value)
|
||||
|
||||
const grantPreview = computed(() => {
|
||||
if (!card.value || !renewalTarget.value) return null
|
||||
return computeMembershipGrant({
|
||||
existing: renewalTarget.value,
|
||||
cardType: card.value,
|
||||
now: new Date(),
|
||||
})
|
||||
})
|
||||
|
||||
function formatStudioDate(value: Date | string): string {
|
||||
const date = typeof value === 'string' ? new Date(value) : value
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}.${month}.${day}`
|
||||
}
|
||||
|
||||
const renewalPreview = computed(() => {
|
||||
const target = renewalTarget.value
|
||||
const grant = grantPreview.value
|
||||
const currentCard = card.value
|
||||
if (!target || !grant || !currentCard) return null
|
||||
|
||||
const isTimes = grant.remainingTimes !== null
|
||||
const isExpired =
|
||||
target.status === MembershipStatus.EXPIRED ||
|
||||
new Date(target.expireDate).getTime() <= Date.now()
|
||||
const statusLabel =
|
||||
target.status === MembershipStatus.USED_UP
|
||||
? '次数已用完'
|
||||
: isExpired
|
||||
? '已到期'
|
||||
: '使用中'
|
||||
|
||||
return {
|
||||
isTimes,
|
||||
isExpired,
|
||||
statusLabel,
|
||||
currentHighlight: isTimes ? String(target.remainingTimes ?? 0) : String(Math.max(0, getMembershipDaysLeft(target.expireDate))),
|
||||
afterHighlight: isTimes ? String(grant.remainingTimes ?? 0) : String(Math.max(0, getMembershipDaysLeft(grant.expireDate))),
|
||||
highlightUnit: isTimes ? '次剩余' : '天有效',
|
||||
currentExpire: formatStudioDate(target.expireDate),
|
||||
afterExpire: formatStudioDate(grant.expireDate),
|
||||
addedLabel: isTimes
|
||||
? `本次续入 +${currentCard.totalTimes ?? 0} 次 · +${currentCard.durationDays} 天`
|
||||
: `本次续入 +${currentCard.durationDays} 天`,
|
||||
forfeitNote: isExpired && isTimes && (target.remainingTimes ?? 0) > 0
|
||||
? '到期余次不结转,按新购次数起算'
|
||||
: '',
|
||||
}
|
||||
})
|
||||
|
||||
const renewalPreviewText = computed(() => {
|
||||
const preview = renewalPreview.value
|
||||
if (!preview) return ''
|
||||
if (preview.isTimes) {
|
||||
return `续卡后剩余 ${preview.afterHighlight} 次,有效期至 ${preview.afterExpire}`
|
||||
}
|
||||
return `续卡后有效期至 ${preview.afterExpire}`
|
||||
})
|
||||
|
||||
function isOwnedForRenew(target: CardType): boolean {
|
||||
return target.type !== CardTypeCategory.TRIAL && !!pickRenewalTarget(userStore.memberships, target.id)
|
||||
}
|
||||
|
||||
// ─── Data loading ─────────────────────────────────────────
|
||||
async function loadCard() {
|
||||
loading.value = true
|
||||
@@ -324,8 +439,10 @@ async function handleBuy() {
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: '确认购买',
|
||||
content: `确认购买「${card.value.name}」,实付 ¥${formatPrice(card.value.price)}?`,
|
||||
title: isRenewal.value ? '确认续卡' : '确认购买',
|
||||
content: isRenewal.value
|
||||
? `确认续卡「${card.value.name}」,${renewalPreviewText.value},实付 ¥${formatPrice(card.value.price)}?`
|
||||
: `确认购买「${card.value.name}」,实付 ¥${formatPrice(card.value.price)}?`,
|
||||
confirmText: '确认支付',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
@@ -364,7 +481,7 @@ async function doPurchase() {
|
||||
|
||||
// Payment succeeded — refresh memberships then navigate
|
||||
await requestOrderPaidSubscriptionMessage().catch(() => undefined)
|
||||
uni.showToast({ title: '购买成功!', icon: 'success' })
|
||||
uni.showToast({ title: isRenewal.value ? '续卡成功!' : '购买成功!', icon: 'success' })
|
||||
await userStore.fetchMemberships()
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({ url: '/pages/profile/membership' })
|
||||
@@ -391,6 +508,9 @@ onMounted(() => {
|
||||
isTrial.value = options.trial === '1'
|
||||
showAll.value = options.showAll === '1'
|
||||
loadCard()
|
||||
if (userStore.loggedIn) {
|
||||
userStore.fetchMemberships()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -401,6 +521,10 @@ onMounted(() => {
|
||||
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.page--renew {
|
||||
background: #f4f1eb;
|
||||
}
|
||||
|
||||
/* ── Loading ─────────────────────────────────────────── */
|
||||
.loading-wrap {
|
||||
padding: 0 0 32rpx;
|
||||
@@ -473,19 +597,23 @@ onMounted(() => {
|
||||
overflow: hidden;
|
||||
|
||||
&.hero--times {
|
||||
background: linear-gradient(135deg, #E8D5C4 0%, #D4BFA8 100%);
|
||||
background: linear-gradient(160deg, #f3e9de 0%, #e6d5c4 100%);
|
||||
}
|
||||
|
||||
&.hero--duration {
|
||||
background: linear-gradient(135deg, #D8C8DC 0%, #C4AECB 100%);
|
||||
background: linear-gradient(160deg, #eee6ea 0%, #dccfd6 100%);
|
||||
}
|
||||
|
||||
&.hero--trial {
|
||||
background: linear-gradient(135deg, #C8D8D2 0%, #A9C4BC 100%);
|
||||
background: linear-gradient(160deg, #e4eee9 0%, #c9dbd3 100%);
|
||||
}
|
||||
|
||||
&.hero--custom {
|
||||
background: #333;
|
||||
background: #5c534c;
|
||||
}
|
||||
|
||||
&.hero--renew {
|
||||
background: linear-gradient(165deg, #e4ede8 0%, #f3ebe3 52%, #e8dcd0 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,7 +629,7 @@ onMounted(() => {
|
||||
.hero-deco {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
background: rgba(255, 255, 255, 0.38);
|
||||
pointer-events: none;
|
||||
|
||||
&--1 {
|
||||
@@ -519,26 +647,57 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.hero--renew .hero-deco--1 {
|
||||
background: rgba(143, 168, 154, 0.22);
|
||||
}
|
||||
|
||||
.hero--renew .hero-deco--2 {
|
||||
background: rgba(255, 252, 248, 0.45);
|
||||
}
|
||||
|
||||
.hero-badge-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
align-self: flex-start;
|
||||
padding: 8rpx 22rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(74, 64, 53, 0.1);
|
||||
border: 1rpx solid rgba(74, 64, 53, 0.15);
|
||||
background: rgba(255, 252, 248, 0.45);
|
||||
border: 1rpx solid rgba(95, 122, 110, 0.18);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-badge--renew {
|
||||
background: rgba(143, 168, 154, 0.22);
|
||||
border-color: rgba(95, 122, 110, 0.22);
|
||||
|
||||
.hero-badge-text {
|
||||
color: #5f7a6e;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-badge-text {
|
||||
font-size: 22rpx;
|
||||
color: $brand-color;
|
||||
color: #5c534c;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1rpx;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.hero-kicker {
|
||||
font-size: 24rpx;
|
||||
color: #7a8f85;
|
||||
letter-spacing: 4rpx;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-name {
|
||||
font-size: 48rpx;
|
||||
font-weight: 800;
|
||||
color: $brand-color;
|
||||
color: #4f5d56;
|
||||
letter-spacing: 1rpx;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -553,20 +712,20 @@ onMounted(() => {
|
||||
.hero-currency {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(74, 64, 53, 0.7);
|
||||
color: #7a8f85;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.hero-price {
|
||||
font-size: 64rpx;
|
||||
font-weight: 800;
|
||||
color: $brand-color;
|
||||
color: #4f5d56;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.hero-original {
|
||||
font-size: 28rpx;
|
||||
color: rgba(74, 64, 53, 0.4);
|
||||
color: rgba(92, 83, 76, 0.4);
|
||||
text-decoration: line-through;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
@@ -592,7 +751,7 @@ onMounted(() => {
|
||||
width: 6rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 3rpx;
|
||||
background: $primary-dark;
|
||||
background: #8fa89a;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -633,13 +792,13 @@ onMounted(() => {
|
||||
.cell-value {
|
||||
font-size: 44rpx;
|
||||
font-weight: 800;
|
||||
color: #1a1a1a;
|
||||
color: #4f5d56;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.cell-label {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
color: #9a9088;
|
||||
}
|
||||
|
||||
/* ── Description card ────────────────────────────────── */
|
||||
@@ -656,6 +815,128 @@ onMounted(() => {
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.renew-compare {
|
||||
background: linear-gradient(180deg, #fffcfa 0%, #f3eee6 100%);
|
||||
border-radius: 28rpx;
|
||||
padding: 32rpx 28rpx 28rpx;
|
||||
box-shadow: 0 10rpx 32rpx rgba(95, 122, 110, 0.08);
|
||||
border: 1rpx solid rgba(143, 168, 154, 0.28);
|
||||
}
|
||||
|
||||
.renew-compare-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.renew-compare-kicker {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: 4rpx;
|
||||
color: #7a8f85;
|
||||
}
|
||||
|
||||
.renew-compare-status {
|
||||
font-size: 20rpx;
|
||||
color: #f7faf8;
|
||||
background: #8fa89a;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.renew-compare-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.renew-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.renew-col-label {
|
||||
font-size: 20rpx;
|
||||
color: #a3988e;
|
||||
letter-spacing: 2rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.renew-col-value {
|
||||
font-size: 72rpx;
|
||||
font-weight: 800;
|
||||
color: #8a7e74;
|
||||
line-height: 1;
|
||||
font-family: 'DIN Alternate', 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.renew-col--after .renew-col-value {
|
||||
color: #5f7a6e;
|
||||
}
|
||||
|
||||
.renew-col-unit {
|
||||
font-size: 22rpx;
|
||||
color: #8a7e74;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.renew-col-date {
|
||||
font-size: 22rpx;
|
||||
color: #a3988e;
|
||||
margin-top: 12rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.renew-arrow {
|
||||
width: auto;
|
||||
padding: 0 8rpx 28rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.renew-arrow-line {
|
||||
width: 28rpx;
|
||||
height: 2rpx;
|
||||
background: rgba(143, 168, 154, 0.45);
|
||||
}
|
||||
|
||||
.renew-arrow-diamond {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
background: #8fa89a;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.renew-compare-foot {
|
||||
margin-top: 28rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1rpx dashed rgba(143, 168, 154, 0.4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.renew-compare-add {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #5f7a6e;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.renew-compare-note {
|
||||
font-size: 20rpx;
|
||||
color: #a3988e;
|
||||
}
|
||||
|
||||
/* ── Features card ───────────────────────────────────── */
|
||||
.features-card {
|
||||
background: #fff;
|
||||
@@ -676,14 +957,14 @@ onMounted(() => {
|
||||
|
||||
.feature-dot {
|
||||
font-size: 26rpx;
|
||||
color: $primary-dark;
|
||||
color: #8fa89a;
|
||||
line-height: 1.65;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
font-size: 26rpx;
|
||||
color: #555;
|
||||
color: #6f675f;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
@@ -693,14 +974,19 @@ onMounted(() => {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #f0ece8;
|
||||
background: rgba(255, 252, 250, 0.96);
|
||||
border-top: 1rpx solid #efe8df;
|
||||
padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 -8rpx 28rpx rgba(95, 122, 110, 0.06);
|
||||
}
|
||||
|
||||
.bottom-bar--renew {
|
||||
background: #fffcfa;
|
||||
border-top-color: rgba(143, 168, 154, 0.22);
|
||||
}
|
||||
|
||||
.price-summary {
|
||||
@@ -711,24 +997,24 @@ onMounted(() => {
|
||||
|
||||
.summary-label {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
color: #9a9088;
|
||||
}
|
||||
|
||||
.summary-price {
|
||||
font-size: 40rpx;
|
||||
font-weight: 800;
|
||||
color: $primary-dark;
|
||||
color: #5f7a6e;
|
||||
}
|
||||
|
||||
.buy-btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
background: linear-gradient(90deg, #1a1a2e, #2d2d5e);
|
||||
background: #6e8b7e;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 16rpx rgba(26, 26, 46, 0.3);
|
||||
box-shadow: 0 8rpx 24rpx rgba(95, 122, 110, 0.22);
|
||||
|
||||
&:active {
|
||||
opacity: 0.85;
|
||||
@@ -737,13 +1023,17 @@ onMounted(() => {
|
||||
&--loading {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&--renew {
|
||||
background: linear-gradient(135deg, #6e8b7e 0%, #8fa89a 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.buy-btn-text {
|
||||
font-size: 32rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: $primary-dark;
|
||||
letter-spacing: 2rpx;
|
||||
color: #fffcfa;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
/* ── All cards list ────────────────────────────────────── */
|
||||
@@ -849,6 +1139,25 @@ onMounted(() => {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.card-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
Reference in New Issue
Block a user