feat: 支持秒杀活动
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<view class="membership-page" :style="{ paddingTop: navBarHeight }">
|
||||
<CustomNavBar title="我的会员卡" show-back />
|
||||
<!-- Pull-to-refresh scroll view -->
|
||||
<scroll-view
|
||||
class="scroll"
|
||||
scroll-y
|
||||
@@ -16,11 +15,14 @@
|
||||
|
||||
<!-- Empty state -->
|
||||
<view v-else-if="allMemberships.length === 0" class="empty-wrap">
|
||||
<text class="empty-icon">💳</text>
|
||||
<text class="empty-title">暂无会员卡</text>
|
||||
<text class="empty-sub">购买会员卡后即可预约课程</text>
|
||||
<view class="empty-btn" @tap="goStore">
|
||||
<text class="empty-btn-text">去购买</text>
|
||||
<view class="empty-card">
|
||||
<view class="empty-deco empty-deco--1" />
|
||||
<view class="empty-deco empty-deco--2" />
|
||||
<text class="empty-title">还没有会员卡</text>
|
||||
<text class="empty-sub">购买会员卡后即可预约课程</text>
|
||||
<view class="empty-btn" @tap="goStore">
|
||||
<text class="empty-btn-text">去选购</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -29,7 +31,6 @@
|
||||
<!-- Active cards -->
|
||||
<view v-if="activeMemberships.length > 0" class="group-section">
|
||||
<view class="group-header">
|
||||
<view class="group-dot group-dot--active" />
|
||||
<text class="group-title">有效会员卡</text>
|
||||
<text class="group-count">{{ activeMemberships.length }} 张</text>
|
||||
</view>
|
||||
@@ -37,56 +38,60 @@
|
||||
<view
|
||||
v-for="m in activeMemberships"
|
||||
:key="m.id"
|
||||
class="card-item"
|
||||
class="mc"
|
||||
:class="cardBgClass(m.cardType.type)"
|
||||
>
|
||||
<!-- Colored left border strip -->
|
||||
<view class="card-strip" :class="stripClass(m.cardType.type)" />
|
||||
<!-- Decorative circles -->
|
||||
<view class="mc-deco mc-deco--1" />
|
||||
<view class="mc-deco mc-deco--2" />
|
||||
|
||||
<!-- Card header (colored gradient) -->
|
||||
<view class="card-header" :class="headerClass(m.cardType.type)">
|
||||
<view class="card-header-left">
|
||||
<text class="card-name">{{ m.cardType.name }}</text>
|
||||
<view class="card-type-badge">
|
||||
<text class="card-type-badge-text">{{ getCardTypeLabel(m.cardType.type) }}</text>
|
||||
<!-- Top row: name + status -->
|
||||
<view class="mc-top">
|
||||
<view class="mc-name-area">
|
||||
<text class="mc-name">{{ m.cardType.name }}</text>
|
||||
<view class="mc-type-tag">
|
||||
<text class="mc-type-text">{{ getCardTypeLabel(m.cardType.type) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="status-badge status-badge--active">
|
||||
<text class="status-badge-text">有效</text>
|
||||
<view class="mc-status mc-status--active">
|
||||
<view class="mc-status-dot" />
|
||||
<text class="mc-status-text">有效</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Card body -->
|
||||
<view class="card-body">
|
||||
<!-- Times card: remaining times + progress -->
|
||||
<template v-if="m.remainingTimes !== null">
|
||||
<view class="highlight-row">
|
||||
<text class="highlight-label">剩余课时</text>
|
||||
<text class="highlight-value">
|
||||
<text class="highlight-number">{{ m.remainingTimes }}</text>
|
||||
<text class="highlight-unit"> 次</text>
|
||||
</text>
|
||||
<!-- Center: highlight number (times card) -->
|
||||
<view v-if="m.remainingTimes !== null" class="mc-center">
|
||||
<text class="mc-big-num">{{ m.remainingTimes }}</text>
|
||||
<text class="mc-big-unit">次剩余</text>
|
||||
<view v-if="m.cardType.totalTimes" class="mc-progress">
|
||||
<view class="mc-progress-track">
|
||||
<view
|
||||
class="mc-progress-fill"
|
||||
:style="{ width: getMembershipProgressWidth(m) }"
|
||||
/>
|
||||
</view>
|
||||
<view v-if="m.cardType.totalTimes" class="progress-wrap">
|
||||
<view class="progress-bar">
|
||||
<view
|
||||
class="progress-fill"
|
||||
:style="{ width: getMembershipProgressWidth(m) }"
|
||||
/>
|
||||
</view>
|
||||
<text class="progress-label">
|
||||
已使用 {{ getMembershipUsedTimes(m) }} / {{ m.cardType.totalTimes }} 次
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- Duration card: expiry -->
|
||||
<view class="info-row">
|
||||
<text class="info-label">有效期至</text>
|
||||
<text class="info-value">{{ m.expireDate.slice(0, 10) }}</text>
|
||||
<text class="mc-progress-label">
|
||||
已用 {{ getMembershipUsedTimes(m) }},共 {{ m.cardType.totalTimes }} 次
|
||||
</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">开始日期</text>
|
||||
<text class="info-value">{{ m.startDate.slice(0, 10) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- Center: duration card (no times) -->
|
||||
<view v-else class="mc-center">
|
||||
<text class="mc-big-num">{{ daysRemaining(m) }}</text>
|
||||
<text class="mc-big-unit">天剩余</text>
|
||||
</view>
|
||||
|
||||
<!-- Bottom: dates -->
|
||||
<view class="mc-bottom">
|
||||
<view class="mc-date-item">
|
||||
<text class="mc-date-label">开始</text>
|
||||
<text class="mc-date-value">{{ m.startDate.slice(0, 10) }}</text>
|
||||
</view>
|
||||
<view class="mc-date-sep" />
|
||||
<view class="mc-date-item">
|
||||
<text class="mc-date-label">到期</text>
|
||||
<text class="mc-date-value">{{ m.expireDate.slice(0, 10) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -95,7 +100,6 @@
|
||||
<!-- Expired / used up cards -->
|
||||
<view v-if="inactiveMemberships.length > 0" class="group-section">
|
||||
<view class="group-header">
|
||||
<view class="group-dot group-dot--inactive" />
|
||||
<text class="group-title">历史记录</text>
|
||||
<text class="group-count">{{ inactiveMemberships.length }} 张</text>
|
||||
</view>
|
||||
@@ -103,28 +107,30 @@
|
||||
<view
|
||||
v-for="m in inactiveMemberships"
|
||||
:key="m.id"
|
||||
class="card-item card-item--inactive"
|
||||
class="mc mc--inactive"
|
||||
>
|
||||
<view class="card-strip card-strip--inactive" />
|
||||
<view class="card-header card-header--inactive">
|
||||
<view class="card-header-left">
|
||||
<text class="card-name card-name--dim">{{ m.cardType.name }}</text>
|
||||
<view class="card-type-badge card-type-badge--dim">
|
||||
<text class="card-type-badge-text">{{ getCardTypeLabel(m.cardType.type) }}</text>
|
||||
<view class="mc-deco mc-deco--1" />
|
||||
|
||||
<view class="mc-top">
|
||||
<view class="mc-name-area">
|
||||
<text class="mc-name">{{ m.cardType.name }}</text>
|
||||
<view class="mc-type-tag">
|
||||
<text class="mc-type-text">{{ getCardTypeLabel(m.cardType.type) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="status-badge" :class="statusBadgeClass(m.status)">
|
||||
<text class="status-badge-text">{{ statusLabel(m.status) }}</text>
|
||||
<view class="mc-status" :class="inactiveStatusClass(m.status)">
|
||||
<text class="mc-status-text">{{ statusLabel(m.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view v-if="m.remainingTimes !== null" class="info-row">
|
||||
<text class="info-label">剩余课时</text>
|
||||
<text class="info-value">{{ m.remainingTimes }} 次</text>
|
||||
|
||||
<view class="mc-inactive-info">
|
||||
<view v-if="m.remainingTimes !== null" class="mc-date-item">
|
||||
<text class="mc-date-label">剩余</text>
|
||||
<text class="mc-date-value">{{ m.remainingTimes }} 次</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">有效期至</text>
|
||||
<text class="info-value">{{ m.expireDate.slice(0, 10) }}</text>
|
||||
<view class="mc-date-item">
|
||||
<text class="mc-date-label">有效期至</text>
|
||||
<text class="mc-date-value">{{ m.expireDate.slice(0, 10) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -136,8 +142,7 @@
|
||||
|
||||
<!-- Buy more FAB -->
|
||||
<view class="fab" @tap="goStore">
|
||||
<text class="fab-icon">+</text>
|
||||
<text class="fab-text">购买会员卡</text>
|
||||
<text class="fab-text">+ 购买会员卡</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -153,13 +158,10 @@ import CustomNavBar from '../../components/CustomNavBar.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// ─── Nav bar height ──────────────────────────────────────
|
||||
const navBarHeight = ref('64px')
|
||||
// ─── State ────────────────────────────────────────────────
|
||||
const loading = ref(false)
|
||||
const refreshing = ref(false)
|
||||
|
||||
// ─── Computed from store ───────────────────────────────────
|
||||
const allMemberships = computed(() => userStore.memberships as MembershipWithCardType[])
|
||||
|
||||
const activeMemberships = computed(() =>
|
||||
@@ -170,8 +172,6 @@ const inactiveMemberships = computed(() =>
|
||||
allMemberships.value.filter((m) => m.status !== MembershipStatus.ACTIVE),
|
||||
)
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────
|
||||
|
||||
function statusLabel(status: MembershipStatus): string {
|
||||
const map: Record<MembershipStatus, string> = {
|
||||
[MembershipStatus.ACTIVE]: '有效',
|
||||
@@ -181,25 +181,22 @@ function statusLabel(status: MembershipStatus): string {
|
||||
return map[status] ?? status
|
||||
}
|
||||
|
||||
function statusBadgeClass(status: MembershipStatus): string {
|
||||
if (status === MembershipStatus.EXPIRED) return 'status-badge--expired'
|
||||
if (status === MembershipStatus.USED_UP) return 'status-badge--used'
|
||||
return 'status-badge--expired'
|
||||
function inactiveStatusClass(status: MembershipStatus): string {
|
||||
if (status === MembershipStatus.USED_UP) return 'mc-status--used'
|
||||
return 'mc-status--expired'
|
||||
}
|
||||
|
||||
function stripClass(type: CardTypeCategory): string {
|
||||
if (type === CardTypeCategory.TRIAL) return 'card-strip--trial'
|
||||
if (type === CardTypeCategory.DURATION) return 'card-strip--duration'
|
||||
return 'card-strip--times'
|
||||
function cardBgClass(type: CardTypeCategory): string {
|
||||
if (type === CardTypeCategory.TRIAL) return 'mc--trial'
|
||||
if (type === CardTypeCategory.DURATION) return 'mc--duration'
|
||||
return 'mc--times'
|
||||
}
|
||||
|
||||
function headerClass(type: CardTypeCategory): string {
|
||||
if (type === CardTypeCategory.TRIAL) return 'card-header--trial'
|
||||
if (type === CardTypeCategory.DURATION) return 'card-header--duration'
|
||||
return 'card-header--times'
|
||||
function daysRemaining(m: MembershipWithCardType): number {
|
||||
const diff = new Date(m.expireDate).getTime() - Date.now()
|
||||
return Math.max(0, Math.ceil(diff / 86_400_000))
|
||||
}
|
||||
|
||||
// ─── Data loading ─────────────────────────────────────────
|
||||
async function loadMemberships() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -221,7 +218,6 @@ function goStore() {
|
||||
uni.switchTab({ url: '/pages/home/index' })
|
||||
}
|
||||
|
||||
// ─── Lifecycle ────────────────────────────────────────────
|
||||
onMounted(() => {
|
||||
navBarHeight.value = `${getSystemLayout().navBarHeight}px`
|
||||
loadMemberships()
|
||||
@@ -231,313 +227,373 @@ onMounted(() => {
|
||||
<style lang="scss" scoped>
|
||||
.membership-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f3f0;
|
||||
background: $bg-page;
|
||||
}
|
||||
|
||||
.scroll {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* ── Loading ─────────────────────────────────────────── */
|
||||
/* ── Loading ─────────────────────────────── */
|
||||
.loading-wrap {
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
height: 220rpx;
|
||||
border-radius: 20rpx;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
|
||||
height: 320rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(90deg, #f0ece8 25%, #e8e4df 50%, #f0ece8 75%);
|
||||
background-size: 400% 100%;
|
||||
animation: shimmer 1.4s infinite;
|
||||
}
|
||||
|
||||
/* ── Empty ───────────────────────────────────────────── */
|
||||
/* ── Empty ────────────────────────────────── */
|
||||
.empty-wrap {
|
||||
padding: 80rpx 24rpx;
|
||||
}
|
||||
|
||||
.empty-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #E8D5C4, #D8C8DC);
|
||||
border-radius: 24rpx;
|
||||
padding: 64rpx 40rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 40rpx;
|
||||
gap: 20rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
.empty-deco {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
|
||||
&--1 {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
top: -60rpx;
|
||||
right: -40rpx;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
&--2 {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
bottom: -40rpx;
|
||||
left: -20rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: $brand-color;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.empty-sub {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
color: $text-secondary;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.empty-btn {
|
||||
margin-top: 12rpx;
|
||||
padding: 22rpx 60rpx;
|
||||
border-radius: 44rpx;
|
||||
background: $primary-dark;
|
||||
box-shadow: 0 4rpx 16rpx rgba(201, 168, 124, 0.35);
|
||||
margin-top: 16rpx;
|
||||
padding: 20rpx 56rpx;
|
||||
border-radius: 40rpx;
|
||||
background: rgba(74, 64, 53, 0.12);
|
||||
z-index: 1;
|
||||
|
||||
&:active { background: rgba(74, 64, 53, 0.18); }
|
||||
}
|
||||
|
||||
.empty-btn-text {
|
||||
font-size: 30rpx;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
color: $brand-color;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── List ────────────────────────────────────────────── */
|
||||
/* ── List ─────────────────────────────────── */
|
||||
.list {
|
||||
padding: 24rpx 24rpx 0;
|
||||
padding: 16rpx 24rpx 0;
|
||||
}
|
||||
|
||||
/* ── Group section ───────────────────────────────────── */
|
||||
/* ── Group ────────────────────────────────── */
|
||||
.group-section {
|
||||
margin-bottom: 8rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
padding: 8rpx 4rpx 14rpx;
|
||||
}
|
||||
|
||||
.group-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--active { background: #4caf50; }
|
||||
&--inactive { background: #bbb; }
|
||||
justify-content: space-between;
|
||||
padding: 12rpx 8rpx 16rpx;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: 26rpx;
|
||||
color: #555;
|
||||
font-size: 28rpx;
|
||||
color: $text-primary;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.group-count {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
color: $text-hint;
|
||||
}
|
||||
|
||||
/* ── Card item ───────────────────────────────────────── */
|
||||
.card-item {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
/* ══════════════════════════════════════════════
|
||||
MEMBERSHIP CARD (mc)
|
||||
══════════════════════════════════════════════ */
|
||||
.mc {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.07);
|
||||
border-radius: 24rpx;
|
||||
padding: 28rpx 32rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
&--inactive {
|
||||
opacity: 0.72;
|
||||
/* Card type backgrounds */
|
||||
.mc--times {
|
||||
background: linear-gradient(135deg, #EDE0D4 0%, #E2D2C2 100%);
|
||||
box-shadow: 0 4rpx 20rpx rgba(212, 191, 168, 0.3);
|
||||
}
|
||||
|
||||
.mc--duration {
|
||||
background: linear-gradient(135deg, #E0D4E4 0%, #D4C6DA 100%);
|
||||
box-shadow: 0 4rpx 20rpx rgba(196, 174, 203, 0.3);
|
||||
}
|
||||
|
||||
.mc--trial {
|
||||
background: linear-gradient(135deg, #D4E2DC 0%, #C6D8D0 100%);
|
||||
box-shadow: 0 4rpx 20rpx rgba(169, 196, 188, 0.3);
|
||||
}
|
||||
|
||||
.mc--inactive {
|
||||
background: linear-gradient(135deg, #E8E4E0, #DDD9D5);
|
||||
box-shadow: 0 2rpx 12rpx rgba(180, 160, 130, 0.12);
|
||||
opacity: 0.75;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
/* Decorative circles */
|
||||
.mc-deco {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
|
||||
&--1 {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
top: -50rpx;
|
||||
right: -30rpx;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
&--2 {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
bottom: -30rpx;
|
||||
left: 40rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* Colored left border strip */
|
||||
.card-strip {
|
||||
height: 6rpx;
|
||||
|
||||
&--times { background: linear-gradient(90deg, #1a1a2e, #2d2d5e); }
|
||||
&--duration { background: linear-gradient(90deg, #6c3483, #9b59b6); }
|
||||
&--trial { background: linear-gradient(90deg, #5a7a8a, $primary-dark); }
|
||||
&--inactive { background: #ccc; }
|
||||
}
|
||||
|
||||
/* Card header gradient area */
|
||||
.card-header {
|
||||
padding: 22rpx 28rpx;
|
||||
/* ── Top row ──────────────────────────────── */
|
||||
.mc-top {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
|
||||
&--times { background: linear-gradient(90deg, #1a1a2e, #2d2d5e); }
|
||||
&--duration { background: linear-gradient(90deg, #6c3483, #9b59b6); }
|
||||
&--trial { background: linear-gradient(90deg, #5a7a8a, $primary-dark); }
|
||||
&--inactive { background: #888; }
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card-header-left {
|
||||
.mc-name-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
font-size: 32rpx;
|
||||
.mc-name {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
|
||||
&--dim { color: #ddd; }
|
||||
color: #2C2420;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.card-type-badge {
|
||||
.mc-type-tag {
|
||||
align-self: flex-start;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 12rpx;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.25);
|
||||
|
||||
&--dim {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
border-radius: 10rpx;
|
||||
background: rgba(44, 36, 32, 0.1);
|
||||
}
|
||||
|
||||
.card-type-badge-text {
|
||||
.mc-type-text {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
color: rgba(44, 36, 32, 0.6);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Status badge */
|
||||
.status-badge {
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.35);
|
||||
/* Status */
|
||||
.mc-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--active { background: rgba(76, 175, 80, 0.3); }
|
||||
&--expired { background: rgba(0, 0, 0, 0.2); }
|
||||
&--used { background: rgba(0, 0, 0, 0.2); }
|
||||
}
|
||||
|
||||
.status-badge-text {
|
||||
.mc-status--active {
|
||||
background: rgba(122, 158, 126, 0.18);
|
||||
}
|
||||
|
||||
.mc-status--expired,
|
||||
.mc-status--used {
|
||||
background: rgba(74, 64, 53, 0.08);
|
||||
}
|
||||
|
||||
.mc-status-dot {
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
border-radius: 50%;
|
||||
background: $success-color;
|
||||
}
|
||||
|
||||
.mc-status-text {
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
color: #2C2420;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Card body */
|
||||
.card-body {
|
||||
padding: 20rpx 28rpx 24rpx;
|
||||
/* ── Center: big number ───────────────────── */
|
||||
.mc-center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.highlight-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.highlight-label {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.highlight-number {
|
||||
font-size: 44rpx;
|
||||
font-weight: 800;
|
||||
color: $primary-dark;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.highlight-unit {
|
||||
font-size: 22rpx;
|
||||
color: $primary-dark;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8rpx 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
.mc-big-num {
|
||||
font-size: 80rpx;
|
||||
font-weight: 800;
|
||||
color: #2C2420;
|
||||
line-height: 1;
|
||||
font-family: 'DIN Alternate', 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
.mc-big-unit {
|
||||
font-size: 24rpx;
|
||||
color: rgba(44, 36, 32, 0.55);
|
||||
font-weight: 500;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
/* ── Progress bar ────────────────────────────────────── */
|
||||
.progress-wrap {
|
||||
/* Progress */
|
||||
.mc-progress {
|
||||
width: 100%;
|
||||
max-width: 400rpx;
|
||||
margin-top: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 8rpx;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4rpx;
|
||||
.mc-progress-track {
|
||||
height: 10rpx;
|
||||
background: rgba(44, 36, 32, 0.1);
|
||||
border-radius: 5rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
.mc-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, $primary-dark, $primary-color);
|
||||
border-radius: 4rpx;
|
||||
background: rgba(44, 36, 32, 0.35);
|
||||
border-radius: 5rpx;
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
text-align: right;
|
||||
.mc-progress-label {
|
||||
font-size: 20rpx;
|
||||
color: rgba(44, 36, 32, 0.45);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── FAB ─────────────────────────────────────────────── */
|
||||
/* ── Bottom: dates ────────────────────────── */
|
||||
.mc-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
z-index: 1;
|
||||
padding-top: 4rpx;
|
||||
border-top: 1rpx solid rgba(44, 36, 32, 0.1);
|
||||
}
|
||||
|
||||
.mc-date-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.mc-date-sep {
|
||||
width: 1rpx;
|
||||
height: 40rpx;
|
||||
background: rgba(44, 36, 32, 0.12);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mc-date-label {
|
||||
font-size: 20rpx;
|
||||
color: rgba(44, 36, 32, 0.4);
|
||||
}
|
||||
|
||||
.mc-date-value {
|
||||
font-size: 24rpx;
|
||||
color: #2C2420;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Inactive info ────────────────────────── */
|
||||
.mc-inactive-info {
|
||||
display: flex;
|
||||
gap: 40rpx;
|
||||
padding-left: 4rpx;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ── FAB ──────────────────────────────────── */
|
||||
.fab {
|
||||
position: fixed;
|
||||
bottom: calc(32rpx + env(safe-area-inset-bottom));
|
||||
right: 32rpx;
|
||||
background: #1a1a2e;
|
||||
background: $brand-color;
|
||||
border-radius: 44rpx;
|
||||
padding: 22rpx 36rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 6rpx 24rpx rgba(74, 64, 53, 0.25);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
|
||||
&:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
font-size: 36rpx;
|
||||
color: $primary-dark;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
&:active { opacity: 0.85; }
|
||||
}
|
||||
|
||||
.fab-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: $primary-dark;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
/* ── Spacer ──────────────────────────────────────────── */
|
||||
/* ── Spacer ───────────────────────────────── */
|
||||
.scroll-bottom-spacer {
|
||||
height: 120rpx;
|
||||
height: 140rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user