Files
gym-manage/gym-manage-uniapp/pages/memberInfo/memberCard.vue
T

1866 lines
55 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="member-card-page">
<!-- 固定页面头部 -->
<view class="member-card-page-header">
<PageHeader title="我的会员卡" subtitle="" :show-back="true" />
</view>
<!-- 滚动内容区域 -->
<view class="member-card-scroll-wrap">
<scroll-view scroll-y class="member-card-scroll">
<view class="member-card-page__body">
<!-- 卡类型切换 -->
<view class="mc-type-tabs">
<view
class="mc-type-tab"
:class="{ 'mc-type-tab--active': currentCardType === 'stored' }"
@tap="switchCardType('stored')"
>
<text>储值卡</text>
</view>
<view
class="mc-type-tab"
:class="{ 'mc-type-tab--active': currentCardType === 'other' }"
@tap="switchCardType('other')"
>
<text>非储值卡</text>
<text class="mc-type-tab__count" v-if="otherCards.length > 0">{{ otherCards.length }}</text>
</view>
</view>
<!-- 储值卡区域 -->
<view v-if="currentCardType === 'stored'" class="mc-stored-section">
<!-- 储值卡卡片 -->
<view class="mc-stored-hero">
<view class="mc-stored-hero__inner">
<view class="mc-stored-hero__header">
<view class="mc-stored-hero__title-row">
<uni-icons type="star-filled" size="40rpx" color="#FFFFFF" />
<text class="mc-stored-hero__name">{{ displayStoredCard.name }}</text>
</view>
<view class="mc-stored-hero__badge">
<text>储值卡</text>
</view>
</view>
<view class="mc-stored-hero__balance">
<text class="mc-stored-hero__balance-label">账户余额</text>
<view class="mc-stored-hero__balance-amount">
<text class="mc-stored-hero__balance-symbol">¥</text>
<text class="mc-stored-hero__balance-num">{{ displayStoredCard.remainingAmount }}</text>
</view>
</view>
<view class="mc-stored-hero__footer">
<view class="mc-stored-hero__recharge" hover-class="mi-tap-btn--hover" :hover-stay-time="150" @tap="goRecharge">充值</view>
</view>
</view>
</view>
</view>
<!-- 非储值卡区域 -->
<view v-if="currentCardType === 'other'">
<!-- 筛选条件 -->
<view class="mc-filter-tabs">
<view
class="mc-filter-tab"
:class="{ 'mc-filter-tab--active': currentFilter === 'all' }"
@tap="changeFilter('all')"
>
<text>全部</text>
<text class="mc-filter-tab__count">{{ filterCounts.all }}</text>
</view>
<view
class="mc-filter-tab"
:class="{ 'mc-filter-tab--active': currentFilter === 'active' }"
@tap="changeFilter('active')"
>
<text>有效</text>
<text class="mc-filter-tab__count">{{ filterCounts.active }}</text>
</view>
<view
class="mc-filter-tab"
:class="{ 'mc-filter-tab--active': currentFilter === 'expired' }"
@tap="changeFilter('expired')"
>
<text>已失效</text>
<text class="mc-filter-tab__count">{{ filterCounts.expired }}</text>
</view>
</view>
<!-- 卡片轮播展示 -->
<view class="mc-swiper-cards" v-if="filteredOtherCards.length > 0">
<swiper
class="mc-swiper"
:current="currentOtherCardIndex"
:previous-margin="'60rpx'"
:next-margin="'60rpx'"
:circular="filteredOtherCards.length > 2"
:autoplay="false"
@change="onSwiperChange"
>
<swiper-item
v-for="(c, index) in filteredOtherCards"
:key="c.id || index"
class="mc-swiper-item"
>
<view
class="mc-swiper-card"
:class="{
'mc-swiper-card--active': index === currentOtherCardIndex,
'mc-swiper-card--blur': index !== currentOtherCardIndex
}"
@tap="selectOtherCard(index)"
>
<view class="mc-swiper-card__inner" :class="getCardStatusClass(c)">
<view class="mc-swiper-card__header">
<view class="mc-swiper-card__title-row">
<image
class="mc-swiper-card__crown"
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/crown.png"
mode="aspectFit"
/>
<text class="mc-swiper-card__name">{{ c.name }}</text>
</view>
<view class="mc-swiper-card__badge" :class="getBadgeClass(c)">
<text>{{ getStatusLabel(c) }}</text>
</view>
</view>
<text class="mc-swiper-card__validity" v-if="!isCountCard(c)">{{ c.validity }}</text>
<view class="mc-swiper-card__footer">
<view class="mc-swiper-card__days-info">
<text class="mc-swiper-card__days-label">{{ getCardDisplayLabel(c) }}</text>
<text class="mc-swiper-card__days-num" :class="{ 'mc-swiper-card__days-num--warning': isExpiringSoon(c), 'mc-swiper-card__days-num--expired': isExpired(c) }">
{{ getCardDisplayValue(c) }}
</text>
<text class="mc-swiper-card__days-unit">{{ getCardDisplayUnit(c) }}</text>
</view>
</view>
<view class="mc-swiper-card__warning" v-if="isExpiringSoon(c)">
<image class="mc-swiper-card__warning-icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/clock1.png" mode="aspectFit" />
<text class="mc-swiper-card__warning-text">即将到期请及时续费</text>
</view>
</view>
</view>
</swiper-item>
</swiper>
<view class="mc-swiper-dots" v-if="filteredOtherCards.length > 1">
<view
v-for="(c, index) in filteredOtherCards"
:key="index"
class="mc-swiper-dot"
:class="{ 'mc-swiper-dot--active': index === currentOtherCardIndex }"
></view>
</view>
</view>
<!-- 无卡片提示 -->
<view class="mc-empty" v-if="filteredOtherCards.length === 0">
<image class="mc-empty__icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/ticket.png" mode="aspectFit" />
<text class="mc-empty__text">{{ currentFilter === 'expired' ? '暂无失效会员卡' : '暂无会员卡' }}</text>
<view class="mc-empty__btn" @tap="goPurchaseCard" v-if="currentFilter !== 'expired'">
<text>购买会员卡</text>
</view>
</view>
</view>
<view class="mc-section-tabs">
<view
class="mc-section-tab"
:class="{ 'mc-section-tab--active': activeSection === 'records' }"
@tap="switchSection('records')"
>
<text>{{ currentCardType === 'stored' ? '充值记录' : '使用记录' }}</text>
</view>
<view
class="mc-section-tab"
:class="{ 'mc-section-tab--active': activeSection === 'transactions' }"
@tap="switchSection('transactions')"
>
<text>会员卡流水</text>
</view>
</view>
<view class="mc-records" v-if="activeSection === 'records'">
<view class="mc-records__header">
<text class="mc-records__title">{{ currentCardType === 'stored' ? '充值记录' : '使用记录' }}</text>
<view class="mc-records__tabs" v-if="currentCardType !== 'stored'">
<view
v-for="tab in recordTabs"
:key="tab.key"
class="mc-records__tab"
:class="{ 'mc-records__tab--active': activeFilter === tab.key }"
hover-class="mi-tap-tab--hover"
:hover-stay-time="150"
@tap="switchFilter(tab.key)"
>
<text
class="mc-records__tab-text"
:class="{ 'mc-records__tab-text--active': activeFilter === tab.key }"
>
{{ tab.label }}
</text>
</view>
</view>
</view>
<view class="mc-records__divider"></view>
<scroll-view
class="mc-records__scroll"
scroll-y="true"
:show-scrollbar="false"
>
<!-- 储值卡充值记录 -->
<template v-if="currentCardType === 'stored'">
<view
v-for="(item, index) in rechargeRecords"
:key="item.id || index"
class="mc-records__item"
>
<view v-if="index > 0" class="mc-records__divider"></view>
<view class="mc-records__item-inner">
<view class="mc-records__icon-wrap mc-records__icon-wrap--orange">
<image
class="mc-records__icon"
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/pluscircle.png"
mode="aspectFit"
/>
</view>
<view class="mc-records__info">
<text class="mc-records__item-title">储值卡充值</text>
<text class="mc-records__item-time">{{ formatRecordTime(item.payTime || item.createdAt) }}</text>
</view>
<text
class="mc-records__value"
:class="item.status === 'SUCCESS' ? 'mc-records__value--positive' : (item.status === 'FAILED' ? 'mc-records__value--negative' : 'mc-records__value--pending')"
>
+¥{{ item.totalAmount }}
</text>
</view>
</view>
<view v-if="!rechargeRecords.length && !loadingRechargeRecords" class="mc-records__empty">
<text class="mc-records__empty-text">暂无充值记录</text>
</view>
<view v-if="loadingRechargeRecords" class="mc-transactions__loading">
<text>加载中...</text>
</view>
</template>
<!-- 非储值卡使用记录 -->
<template v-else>
<view
v-for="(item, index) in filteredRecords"
:key="item.id"
class="mc-records__item"
>
<view v-if="index > 0" class="mc-records__divider"></view>
<view
class="mc-records__item-inner"
hover-class="mi-tap-row--hover"
:hover-stay-time="150"
@tap="showRecordDetail(item)"
>
<view
class="mc-records__icon-wrap"
:class="'mc-records__icon-wrap--' + getUsageRecordIconTheme(item)"
>
<text class="mc-records__icon-text">{{ getUsageRecordIcon(item) }}</text>
</view>
<view class="mc-records__info">
<text class="mc-records__item-title">{{ getUsageRecordTitle(item) }}</text>
<text class="mc-records__item-time">{{ formatTime(item.createdAt) }}</text>
</view>
<text
class="mc-records__value"
:class="'mc-records__value--negative'"
>
-{{ item.changeAmount || 0 }}
</text>
</view>
</view>
<view v-if="!filteredRecords.length" class="mc-records__empty">
<text class="mc-records__empty-text">暂无使用记录</text>
</view>
</template>
</scroll-view>
</view>
<view class="mc-transactions" v-if="activeSection === 'transactions'">
<view class="mc-transactions__header">
<text class="mc-transactions__title">会员卡流水</text>
<view class="mc-transactions__refresh" @tap="loadTransactions">
<text>刷新</text>
</view>
</view>
<scroll-view
class="mc-transactions__scroll"
scroll-y="true"
:show-scrollbar="false"
>
<view class="mc-transactions__list">
<view
v-for="(item, index) in transactions"
:key="item.id"
class="mc-transactions__item"
>
<view v-if="index > 0" class="mc-transactions__divider"></view>
<view
class="mc-transactions__item-inner"
hover-class="mi-tap-row--hover"
:hover-stay-time="150"
@tap="showTransactionDetail(item)"
>
<view
class="mc-transactions__icon-wrap"
:class="'mc-transactions__icon-wrap--' + getTransactionTypeClass(item.operationType)"
>
<text class="mc-transactions__icon-text">{{ getTransactionIcon(item.operationType) }}</text>
</view>
<view class="mc-transactions__info">
<text class="mc-transactions__item-title">{{ getTransactionTypeName(item.operationType) }}</text>
<text class="mc-transactions__item-time">{{ formatTime(item.createdAt) }}</text>
</view>
<text
class="mc-transactions__value"
:class="'mc-transactions__value--' + getTransactionValueClass(item.operationType)"
>
{{ formatTransactionValue(item) }}
</text>
</view>
</view>
<view v-if="!transactions.length && !loadingTransactions" class="mc-transactions__empty">
<text class="mc-transactions__empty-text">暂无流水记录</text>
</view>
<view v-if="loadingTransactions" class="mc-transactions__loading">
<text>加载中...</text>
</view>
</view>
</scroll-view>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { onShow, onLoad } from '@dcloudio/uni-app'
import PageHeader from '@/components/index/PageHeader.vue'
import {
loadMemberStore,
computeRemainingDays,
renewMemberCard,
saveMemberStore
} from '@/common/memberInfo/store.js'
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
import { getMyMemberCards, getMyMemberCardsWithStatus, getMemberCardTransactions, getMemberCardTransactionsByRecordId, checkPayPasswordSet, createPayment, getStoredCardInfo, getStoredCardRechargeRecords } from '@/api/main.js'
import { getMemberId } from '@/utils/request.js'
const currentFilter = ref('all')
const currentCardType = ref('stored') // 'stored' 或 'other'
const cards = ref([])
const allCards = ref([])
const storedCards = ref([]) // 储值卡
const otherCards = ref([]) // 非储值卡
const currentStoredCard = ref(null) // 当前选中的储值卡
const activeCards = ref([])
const expiredCards = ref([])
const currentOtherCardIndex = ref(0)
const recordTabs = ref([
{ key: 'all', label: '全部' },
{ key: 'GROUP_CLASS', label: '团课消费' },
{ key: 'CHECK_IN', label: '到店健身' }
])
const records = ref([])
const activeFilter = ref('all')
const activeSection = ref('records')
const transactions = ref([])
const loadingTransactions = ref(false)
const loading = ref(false)
const storedCardBalance = ref(0)
const rechargeRecords = ref([])
const loadingRechargeRecords = ref(false)
const filterCounts = computed(() => ({
all: otherCards.value.length,
active: otherCards.value.filter(c => isActiveCard(c)).length,
expired: otherCards.value.filter(c => isExpired(c)).length
}))
const otherStoredCards = computed(() => {
if (!currentStoredCard.value) {
return storedCards.value
}
return storedCards.value.filter(card => card.id !== currentStoredCard.value.id)
})
const filteredOtherCards = computed(() => {
if (currentFilter.value === 'all') return otherCards.value
if (currentFilter.value === 'active') return otherCards.value.filter(c => isActiveCard(c))
if (currentFilter.value === 'expired') return otherCards.value.filter(c => isExpired(c))
return otherCards.value
})
const filteredRecords = computed(() => {
const deductRecords = transactions.value.filter(item => item.operationType === 'DEDUCT')
if (activeFilter.value === 'all') {
return deductRecords
}
return deductRecords.filter(item => item.relatedBizType === activeFilter.value)
})
function getUsageRecordTitle(item) {
const bizMap = {
'GROUP_CLASS': '团课消费',
'CHECK_IN': '到店健身',
'PT_CLASS': '私教课消费'
}
return bizMap[item.relatedBizType] || '消费'
}
function getUsageRecordIcon(item) {
const iconMap = {
'GROUP_CLASS': '🏋️',
'CHECK_IN': '✅',
'PT_CLASS': '💪'
}
return iconMap[item.relatedBizType] || '📋'
}
function getUsageRecordIconTheme(item) {
const themeMap = {
'GROUP_CLASS': 'orange',
'CHECK_IN': 'green',
'PT_CLASS': 'purple'
}
return themeMap[item.relatedBizType] || 'default'
}
const displayStoredCard = computed(() => {
console.log("06281--"+storedCardBalance.value)
const balance = storedCardBalance.value != null ? storedCardBalance.value : 0
console.log("06281--"+balance)
return {
name: '储值卡',
remainingAmount: balance.toFixed(2)
}
})
// 加载储值卡余额
async function loadStoredCardBalance() {
const memberId = getMemberId()
if (!memberId) return
try {
const res = await getStoredCardInfo(memberId)
storedCardBalance.value = res.balance || 0
} catch (e) {
console.error('获取储值卡余额失败:', e)
storedCardBalance.value = 0
}
}
// 加载储值卡充值记录
async function loadRechargeRecords() {
const memberId = getMemberId()
if (!memberId) return
if (loadingRechargeRecords.value) return
loadingRechargeRecords.value = true
try {
const res = await getStoredCardRechargeRecords(memberId, 1, 50, { cache: false })
rechargeRecords.value = res.list || []
} catch (e) {
console.error('获取充值记录失败:', e)
rechargeRecords.value = []
} finally {
loadingRechargeRecords.value = false
}
}
function formatRecordTime(timeStr) {
if (!timeStr) return ''
const date = new Date(timeStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
}
function formatDate(dateStr) {
if (!dateStr) return '永久有效'
try {
const date = new Date(dateStr)
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}`
} catch (e) {
return dateStr
}
}
function formatChineseDate(dateStr) {
if (!dateStr) return ''
try {
const date = new Date(dateStr)
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}`
} catch (e) {
return dateStr
}
}
function switchCardType(type) {
currentCardType.value = type
currentOtherCardIndex.value = 0
activeSection.value = 'records'
if (type === 'stored') {
loadStoredCardBalance()
loadRechargeRecords()
} else {
if (filteredOtherCards.value.length > 0) {
loadTransactions()
}
}
}
function switchSection(section) {
activeSection.value = section
if (section === 'records') {
if (currentCardType.value === 'stored') {
loadRechargeRecords()
} else {
loadTransactions()
}
} else if (section === 'transactions') {
loadTransactions()
}
}
function selectStoredCard(card) {
currentStoredCard.value = card
loadTransactions(card.id)
}
function getCardRemainingDays(c) {
return computeRemainingDays(c.validityEnd || c.expireTime)
}
function getCardDaysDiff(c) {
const endDateStr = c.validityEnd || c.expireTime
if (!endDateStr) return 0
const end = new Date(endDateStr)
if (isNaN(end.getTime())) return 0
const now = new Date()
now.setHours(0, 0, 0, 0)
end.setHours(0, 0, 0, 0)
return Math.ceil((end - now) / 86400000)
}
function getCardDaysLabel(c) {
const diff = getCardDaysDiff(c)
return diff >= 0 ? '还剩' : '已过期'
}
function getCardDaysValue(c) {
const diff = getCardDaysDiff(c)
return Math.abs(diff)
}
function isStoredCard(c) {
if (c.memberCardType) {
return c.memberCardType === 'STORED_VALUE_CARD'
}
return c.remainingAmount !== null && c.remainingAmount !== undefined && c.remainingAmount > 0
}
function isTimeCard(c) {
if (c.memberCardType) {
return c.memberCardType === 'TIME_CARD'
}
return false
}
function isCountCard(c) {
if (c.memberCardType) {
return c.memberCardType === 'COUNT_CARD'
}
return false
}
function getCardDisplayLabel(c) {
if (isTimeCard(c)) {
return getCardDaysDiff(c) >= 0 ? '还剩' : '已过期'
} else if (isCountCard(c)) {
return '剩余'
}
return getCardDaysDiff(c) >= 0 ? '还剩' : '已过期'
}
function getCardDisplayValue(c) {
if (isTimeCard(c)) {
return Math.abs(getCardDaysDiff(c))
} else if (isCountCard(c)) {
return c.remainingTimes || 0
}
return Math.abs(getCardDaysDiff(c))
}
function getCardDisplayUnit(c) {
if (isTimeCard(c)) {
return '天'
} else if (isCountCard(c)) {
return '次'
}
return '天'
}
function isActiveCard(c) {
if (isCountCard(c)) {
// 次数卡:检查是否还有剩余次数
return (c.remainingTimes || 0) > 0 && (c.status === 'active' || c.status === 'ACTIVE')
}
// 时长卡:检查是否在有效期内
const diff = getCardDaysDiff(c)
return diff > 0 && (c.status === 'active' || c.status === 'ACTIVE')
}
function isExpiringSoon(c) {
if (isCountCard(c)) {
// 次数卡不显示临期提示
return false
}
const diff = getCardDaysDiff(c)
return diff > 0 && diff <= 3
}
function isExpired(c) {
if (isCountCard(c)) {
// 次数卡:没有剩余次数即为用完
return (c.remainingTimes || 0) <= 0 || c.status === 'USED_UP' || c.status === 'used_up'
}
// 时长卡:检查是否已过期
const diff = getCardDaysDiff(c)
return diff <= 0 || c.status === 'EXPIRED' || c.status === 'expired'
}
function getStatusLabel(c) {
if (isCountCard(c)) {
if ((c.remainingTimes || 0) <= 0) return '已用完'
return '有效'
}
if (isExpired(c)) return '已失效'
if (isExpiringSoon(c)) return '临期'
return '有效'
}
function getCardStatusClass(c) {
if (isCountCard(c)) {
if ((c.remainingTimes || 0) <= 0) return 'mc-swiper-card__inner--expired'
return 'mc-swiper-card__inner--active'
}
if (isExpired(c)) return 'mc-swiper-card__inner--expired'
if (isExpiringSoon(c)) return 'mc-swiper-card__inner--expiring'
return 'mc-swiper-card__inner--active'
}
function getBadgeClass(c) {
if (isCountCard(c)) {
if ((c.remainingTimes || 0) <= 0) return 'mc-swiper-card__badge--expired'
return 'mc-swiper-card__badge--active'
}
if (isExpired(c)) return 'mc-swiper-card__badge--expired'
if (isExpiringSoon(c)) return 'mc-swiper-card__badge--expiring'
return 'mc-swiper-card__badge--active'
}
function onSwiperChange(e) {
currentOtherCardIndex.value = e.detail.current
if (activeSection.value === 'records' || activeSection.value === 'transactions') {
loadTransactions()
}
}
function selectOtherCard(index) {
currentOtherCardIndex.value = index
if (activeSection.value === 'records' || activeSection.value === 'transactions') {
loadTransactions()
}
}
function changeFilter(filter) {
currentFilter.value = filter
currentOtherCardIndex.value = 0
}
function goPurchaseCard() {
navigateToPage(PAGE.PURCHASE_CARD)
}
function goRecharge() {
navigateToPage(`${PAGE.RECHARGE_STORED_CARD}`)
}
// 从后端刷新会员卡数据
async function refreshFromServer() {
const memberId = getMemberId()
if (!memberId) {
console.log('[MemberCard] 没有 memberId,无法刷新')
return
}
console.log('[MemberCard] 开始刷新会员卡数据, memberId:', memberId)
loading.value = true
uni.showLoading({ title: '加载中...' })
try {
const res = await getMyMemberCardsWithStatus(memberId, 'all')
console.log('[MemberCard] API 返回:', res)
uni.hideLoading()
loading.value = false
let rawCards = []
if (Array.isArray(res)) {
rawCards = res
} else if (res.code === 200 && res.data) {
rawCards = res.data
} else if (Array.isArray(res.data)) {
rawCards = res.data
}
console.log('[MemberCard] 解析后的会员卡列表:', rawCards)
const formattedCards = rawCards.map(myCard => {
const cardName = myCard.memberCardName || myCard.cardName || myCard.name || `会员卡#${myCard.memberCardId || myCard.id}`
const validityEnd = myCard.validityEnd || myCard.expireTime
const validityStart = myCard.validityStart || myCard.purchaseTime || myCard.createdAt
const cardType = myCard.memberCardType || myCard.cardType || ''
return {
id: myCard.id || myCard.memberCardRecordId,
name: cardName,
status: myCard.status || 'ACTIVE',
validity: `有效期至 ${formatChineseDate(validityEnd)}`,
validityEnd: validityEnd,
validityStart: validityStart,
remainingTimes: myCard.remainingTimes,
remainingAmount: myCard.remainingAmount,
memberCardType: cardType
}
})
// 分类存储
allCards.value = formattedCards
storedCards.value = formattedCards.filter(c => isStoredCard(c))
otherCards.value = formattedCards.filter(c => !isStoredCard(c))
activeCards.value = formattedCards.filter(c => isActiveCard(c))
expiredCards.value = formattedCards.filter(c => isExpired(c))
// 设置当前储值卡
if (storedCards.value.length > 0 && !currentStoredCard.value) {
currentStoredCard.value = storedCards.value[0]
} else if (storedCards.value.length === 0) {
currentStoredCard.value = null
}
// 更新store
store.cards = formattedCards
if (formattedCards.length > 0) {
const firstActiveCard = formattedCards.find(c => isActiveCard(c)) || formattedCards[0]
store.card = { ...firstActiveCard }
store.cardInfo = {
expireDate: firstActiveCard.validity,
remainingDays: getCardRemainingDays(firstActiveCard)
}
}
saveMemberStore(store)
console.log('[MemberCard] 数据刷新成功,总数:', formattedCards.length, '储值卡:', storedCards.value.length, '非储值卡:', otherCards.value.length)
} catch (e) {
uni.hideLoading()
loading.value = false
console.error('[MemberCard] 刷新会员卡数据失败:', e)
try {
const res = await getMyMemberCards(memberId)
console.log(111)
console.log(res)
let rawCards = []
if (Array.isArray(res)) {
rawCards = res
} else if (res.code === 200 && res.data) {
rawCards = res.data
} else if (Array.isArray(res.data)) {
rawCards = res.data
}
const formattedCards = rawCards.map(myCard => {
console.log(1212)
console.log(myCard)
const cardName = myCard.memberCardName || myCard.cardName || myCard.name || `会员卡#${myCard.memberCardId || myCard.id}`
const validityEnd = myCard.validityEnd || myCard.expireTime
const validityStart = myCard.validityStart || myCard.purchaseTime || myCard.createdAt
const cardType = myCard.memberCardType || myCard.cardType || ''
return {
id: myCard.id || myCard.memberCardRecordId,
name: cardName,
status: myCard.status || 'ACTIVE',
validity: `有效期至 ${formatChineseDate(validityEnd)}`,
validityEnd: validityEnd,
validityStart: validityStart,
remainingTimes: myCard.remainingTimes,
remainingAmount: myCard.remainingAmount,
memberCardType: cardType
}
})
allCards.value = formattedCards
storedCards.value = formattedCards.filter(c => isStoredCard(c))
otherCards.value = formattedCards.filter(c => !isStoredCard(c))
activeCards.value = formattedCards.filter(c => isActiveCard(c))
expiredCards.value = []
if (storedCards.value.length > 0 && !currentStoredCard.value) {
currentStoredCard.value = storedCards.value[0]
}
store.cards = formattedCards
saveMemberStore(store)
} catch (e2) {
console.error('[MemberCard] 旧API也失败:', e2)
}
}
if (currentCardType.value === 'other' && otherCards.value.length > 0) {
loadTransactions()
}
}
function switchFilter(filter) {
activeFilter.value = filter
}
function showRecordDetail(item) {
const title = getUsageRecordTitle(item)
const time = formatTime(item.createdAt)
const change = `-${item.changeAmount || 0}`
uni.showModal({
title: title,
content: `${time}\n变动:${change}`,
showCancel: false
})
}
async function loadTransactions(cardId) {
loadingTransactions.value = true
try {
let res
if (currentCardType.value === 'stored') {
const memberId = getMemberId()
if (!memberId) {
loadingTransactions.value = false
return
}
res = await getMemberCardTransactions(memberId)
} else {
const currentCard = getCurrentOtherCard()
if (currentCard && currentCard.id) {
res = await getMemberCardTransactionsByRecordId(currentCard.id)
} else {
transactions.value = []
loadingTransactions.value = false
return
}
}
if (Array.isArray(res)) {
transactions.value = res
} else if (res && Array.isArray(res.value)) {
transactions.value = res.value
} else if (res && Array.isArray(res.data)) {
transactions.value = res.data
} else {
transactions.value = []
}
} catch (e) {
console.error('加载会员卡流水失败:', e)
transactions.value = []
} finally {
loadingTransactions.value = false
}
}
function getCurrentOtherCard() {
if (filteredOtherCards.value.length > 0 && currentOtherCardIndex.value < filteredOtherCards.value.length) {
return filteredOtherCards.value[currentOtherCardIndex.value]
}
return null
}
function getTransactionTypeName(type) {
const map = {
PURCHASE: '购买',
DEDUCT: '扣减',
RENEW: '续费',
REFUND: '退款',
EXPIRE: '过期'
}
return map[type] || type
}
function getTransactionIcon(type) {
const map = {
PURCHASE: '💰',
DEDUCT: '💳',
RENEW: '🔄',
REFUND: '🔙',
EXPIRE: '⏰'
}
return map[type] || '📋'
}
function getTransactionTypeClass(type) {
const map = {
PURCHASE: 'purchase',
DEDUCT: 'deduct',
RENEW: 'renew',
REFUND: 'refund',
EXPIRE: 'expire'
}
return map[type] || 'default'
}
function getTransactionValueClass(type) {
if (type === 'PURCHASE' || type === 'RENEW') {
return 'positive'
}
return 'negative'
}
function formatTransactionValue(item) {
const parts = []
if (item.changeAmount !== null && item.changeAmount !== undefined) {
const prefix = (item.changeAmount > 0 ? '+' : '')
parts.push(`${prefix}${item.changeAmount}`)
}
if (item.changeBalance !== null && item.changeBalance !== undefined) {
const prefix = (item.changeBalance > 0 ? '+' : '')
parts.push(`${prefix}${item.changeBalance}`)
}
if (parts.length === 0) {
return '-'
}
return parts.join(' ')
}
function formatTime(timeStr) {
if (!timeStr) return '-'
try {
const date = new Date(timeStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
} catch (e) {
return timeStr
}
}
function showTransactionDetail(item) {
let content = `时间:${formatTime(item.createdAt)}\n`
content += `类型:${getTransactionTypeName(item.operationType)}\n`
content += `变动:${formatTransactionValue(item)}\n`
if (item.afterRemainingCount !== null && item.afterRemainingCount !== undefined) {
content += `剩余次数:${item.afterRemainingCount}\n`
}
if (item.afterRemainingBalance !== null && item.afterRemainingBalance !== undefined) {
content += `剩余金额:${item.afterRemainingBalance}\n`
}
if (item.remark) {
content += `备注:${item.remark}`
}
uni.showModal({
title: '流水详情',
content: content,
showCancel: false
})
}
watch(activeSection, (newSection) => {
if (newSection === 'transactions') {
loadTransactions()
}
})
onLoad((options) => {
if (options && options.tab) {
if (options.tab === 'stored') {
currentCardType.value = 'stored'
activeSection.value = 'transactions'
} else if (options.tab === 'other') {
currentCardType.value = 'other'
}
}
})
// 每次页面显示时从后端刷新会员卡数据
onShow(() => {
const memberId = getMemberId()
console.log('========== 当前登录用户信息 ==========')
console.log('会员ID (memberId):', memberId)
console.log('====================================')
refreshFromServer()
if (currentCardType.value === 'stored') {
loadStoredCardBalance()
loadRechargeRecords()
}
})
</script>
<style lang="scss">
@import '@/common/style/base.css';
@import '@/common/style/memberInfo/member-info-component-reset.css';
@import '@/common/style/memberInfo/member-info-tap.css';
@import '@/common/style/memberInfo/pages/page-reset.css';
@import '@/common/style/memberInfo/pages/sub-page-base.css';
// ================== 页面布局 ==================
.member-card-page {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
background: #F5F7FA;
}
.member-card-page-header {
flex-shrink: 0;
position: relative;
z-index: 10;
background: #F5F7FA;
}
.member-card-scroll-wrap {
flex: 1;
overflow: hidden;
}
.member-card-scroll {
height: 100%;
overflow-y: auto;
}
.member-card-page__body {
padding: 24rpx 32rpx;
}
// ================== 卡类型切换 ==================
.mc-type-tabs {
display: flex;
margin-bottom: 24rpx;
background: #FFFFFF;
border-radius: 16rpx;
padding: 8rpx;
gap: 8rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.mc-type-tab {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
padding: 20rpx 0;
border-radius: 12rpx;
font-size: 28rpx;
color: #718096;
background: #F7FAFC;
transition: all 0.2s;
}
.mc-type-tab--active {
background: linear-gradient(135deg, #1677FF, #4096FF);
color: #FFFFFF;
font-weight: 600;
}
.mc-type-tab__count {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 20rpx;
background: rgba(0, 0, 0, 0.08);
}
.mc-type-tab--active .mc-type-tab__count {
background: rgba(255, 255, 255, 0.3);
}
// ================== 储值卡 ==================
.mc-stored-section {
margin-bottom: 24rpx;
}
.mc-stored-hero__inner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20rpx;
padding: 32rpx;
color: #FFFFFF;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.25);
}
.mc-stored-hero__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.mc-stored-hero__title-row {
display: flex;
align-items: center;
gap: 8px;
}
.mc-stored-hero__name {
font-size: 18px;
font-weight: 600;
}
.mc-stored-hero__badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
background: rgba(255, 255, 255, 0.3);
}
.mc-stored-hero__balance {
margin-bottom: 20px;
}
.mc-stored-hero__balance-label {
font-size: 13px;
opacity: 0.8;
margin-bottom: 4px;
}
.mc-stored-hero__balance-amount {
display: flex;
align-items: baseline;
}
.mc-stored-hero__balance-symbol {
font-size: 18px;
font-weight: 600;
}
.mc-stored-hero__balance-num {
font-size: 36px;
font-weight: 700;
}
.mc-stored-hero__footer {
display: flex;
align-items: center;
justify-content: flex-end;
}
.mc-stored-hero__validity {
display: flex;
flex-direction: column;
gap: 2px;
}
.mc-stored-hero__validity-label {
font-size: 12px;
opacity: 0.8;
}
.mc-stored-hero__validity-date {
font-size: 14px;
}
.mc-stored-hero__recharge {
padding: 16rpx 36rpx;
border-radius: 32rpx;
background: #FFFFFF;
font-size: 28rpx;
font-weight: 600;
color: #667eea;
text-align: center;
line-height: 1;
}
// 无储值卡提示
.mc-stored-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 64rpx 32rpx;
background: #FFFFFF;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.mc-stored-empty__icon {
width: 64px;
height: 64px;
opacity: 0.4;
margin-bottom: 16px;
}
.mc-stored-empty__text {
font-size: 14px;
color: #8A99B4;
margin-bottom: 20px;
}
.mc-stored-empty__btn {
padding: 12px 32px;
border-radius: 24px;
background: #1677FF;
color: #FFFFFF;
font-size: 14px;
font-weight: 600;
}
// 储值卡列表
.mc-stored-list {
margin: 16px 16px 0;
background: #FFFFFF;
border-radius: 12px;
padding: 12px 16px;
}
.mc-stored-list__title {
font-size: 13px;
color: #8A99B4;
margin-bottom: 12px;
}
.mc-stored-list__item {
padding: 12px 0;
}
.mc-stored-list__item:not(:last-child) {
border-bottom: 1px solid #F0F0F0;
}
.mc-stored-list__item-inner {
display: flex;
align-items: center;
justify-content: space-between;
}
.mc-stored-list__item-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.mc-stored-list__item-name {
font-size: 14px;
color: #1A202C;
}
.mc-stored-list__item-balance {
font-size: 13px;
color: #667eea;
font-weight: 600;
}
.mc-stored-list__item-arrow {
width: 16px;
height: 16px;
opacity: 0.5;
}
// 筛选条件样式
.mc-filter-tabs {
display: flex;
margin-bottom: 24rpx;
background: #FFFFFF;
border-radius: 16rpx;
padding: 8rpx;
gap: 8rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.mc-filter-tab {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 10px 0;
border-radius: 8px;
font-size: 14px;
color: #718096;
background: #F7FAFC;
transition: all 0.2s;
&--active {
background: #1677FF;
color: #FFFFFF;
font-weight: 600;
}
}
.mc-filter-tab__count {
font-size: 12px;
padding: 2px 6px;
border-radius: 10px;
background: rgba(0, 0, 0, 0.1);
}
.mc-filter-tab--active .mc-filter-tab__count {
background: rgba(255, 255, 255, 0.3);
}
// 卡片轮播样式
.mc-swiper-cards {
margin-bottom: 24rpx;
padding-bottom: 16rpx;
}
.mc-swiper {
height: 240px;
}
.mc-swiper-item {
display: flex;
align-items: center;
justify-content: center;
}
.mc-swiper-card {
width: 100%;
height: 220px;
transition: all 0.3s ease;
transform-origin: center center;
overflow: hidden;
border-radius: 16px;
&--active {
transform: scale(1);
filter: blur(0);
opacity: 1;
}
&--blur {
transform: scale(0.85);
filter: blur(4px);
opacity: 0.6;
}
}
.mc-swiper-card__inner {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 20px;
color: #FFFFFF;
box-sizing: border-box;
display: flex;
flex-direction: column;
&--active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
&--expiring {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
&--expired {
background: linear-gradient(135deg, #c0c0c0 0%, #909090 100%);
}
}
.mc-swiper-card__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.mc-swiper-card__title-row {
display: flex;
align-items: center;
gap: 8px;
}
.mc-swiper-card__crown {
width: 24px;
height: 24px;
}
.mc-swiper-card__name {
font-size: 18px;
font-weight: 600;
}
.mc-swiper-card__badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
&--active {
background: rgba(255, 255, 255, 0.3);
}
&--expiring {
background: #FFE4E4;
color: #E74C3C;
}
&--expired {
background: rgba(255, 255, 255, 0.2);
}
}
.mc-swiper-card__validity {
font-size: 13px;
opacity: 0.8;
margin-bottom: 12px;
}
.mc-swiper-card__footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: auto;
}
.mc-swiper-card__days-info {
display: flex;
align-items: baseline;
gap: 4px;
}
.mc-swiper-card__days-label {
font-size: 14px;
opacity: 0.9;
}
.mc-swiper-card__days-num {
font-size: 32px;
font-weight: 700;
&--warning {
color: #FFE4E4;
}
&--expired {
color: #FF6B6B;
}
}
.mc-swiper-card__days-unit {
font-size: 14px;
opacity: 0.8;
}
.mc-swiper-card__renew {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.3);
}
.mc-swiper-card__renew-icon {
width: 16px;
height: 16px;
}
.mc-swiper-card__renew-text {
font-size: 13px;
}
.mc-swiper-card__warning {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
padding: 10px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.2);
}
.mc-swiper-card__warning-icon {
width: 16px;
height: 16px;
}
.mc-swiper-card__warning-text {
font-size: 13px;
}
.mc-swiper-dots {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin-top: 12px;
}
.mc-swiper-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
&--active {
width: 20px;
border-radius: 3px;
background: #667eea;
}
}
// 空状态样式
.mc-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 16px;
margin: 0 16px;
background: #FFFFFF;
border-radius: 16px;
}
.mc-empty__icon {
width: 64px;
height: 64px;
opacity: 0.4;
margin-bottom: 16px;
}
.mc-empty__text {
font-size: 14px;
color: #8A99B4;
margin-bottom: 20px;
}
.mc-empty__btn {
padding: 12px 32px;
border-radius: 24px;
background: #1677FF;
color: #FFFFFF;
font-size: 14px;
font-weight: 600;
}
.mc-section-tabs {
display: flex;
margin-bottom: 24rpx;
background: #FFFFFF;
border-radius: 16rpx;
padding: 8rpx;
gap: 8rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.mc-section-tab {
flex: 1;
padding: 20rpx 0;
text-align: center;
border-radius: 12rpx;
font-size: 28rpx;
color: #718096;
background: #F7FAFC;
transition: all 0.2s;
}
.mc-section-tab--active {
background: linear-gradient(135deg, #1677FF, #4096FF);
color: #FFFFFF;
font-weight: 600;
}
.mc-transactions {
background: #FFFFFF;
border-radius: 16rpx;
padding: 24rpx 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
margin-bottom: 24rpx;
}
.mc-transactions__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.mc-transactions__title {
font-size: 28rpx;
font-weight: 600;
color: #1E2A3A;
}
.mc-transactions__refresh {
font-size: 24rpx;
color: #1677FF;
padding: 8rpx 16rpx;
}
.mc-transactions__scroll {
height: 400rpx;
overflow: hidden;
}
.mc-transactions__list {
padding: 8px 0;
}
.mc-transactions__divider {
height: 1rpx;
background: #F0F0F0;
}
.mc-transactions__item-inner {
display: flex;
align-items: center;
padding: 24rpx 0;
gap: 20rpx;
}
.mc-transactions__icon-wrap {
width: 72rpx;
height: 72rpx;
border-radius: 36rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.mc-transactions__icon-wrap--purchase {
background: rgba(76, 175, 80, 0.1);
}
.mc-transactions__icon-wrap--deduct {
background: rgba(242, 153, 74, 0.1);
}
.mc-transactions__icon-wrap--renew {
background: rgba(33, 150, 243, 0.1);
}
.mc-transactions__icon-wrap--refund {
background: rgba(244, 67, 54, 0.1);
}
.mc-transactions__icon-wrap--expire {
background: rgba(158, 158, 158, 0.1);
}
.mc-transactions__icon-wrap--default {
background: rgba(22, 119, 255, 0.1);
}
.mc-transactions__icon-text {
font-size: 32rpx;
}
.mc-transactions__info {
flex: 1;
min-width: 0;
}
.mc-transactions__item-title {
font-size: 28rpx;
color: #1E2A3A;
display: block;
margin-bottom: 6rpx;
}
.mc-transactions__item-time {
font-size: 24rpx;
color: #999;
}
.mc-transactions__value {
font-size: 28rpx;
font-weight: 600;
flex-shrink: 0;
}
.mc-transactions__value--positive {
color: #4CAF50;
}
.mc-transactions__value--negative {
color: #333;
}
.mc-transactions__empty {
padding: 64rpx 32rpx;
display: flex;
align-items: center;
justify-content: center;
}
.mc-transactions__empty-text {
font-size: 26rpx;
color: #999;
}
.mc-transactions__loading {
display: flex;
align-items: center;
justify-content: center;
padding: 32px 16px;
}
.mc-records {
background: #FFFFFF;
border-radius: 16rpx;
padding: 24rpx 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
margin-bottom: 24rpx;
}
.mc-records__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.mc-records__title {
font-size: 28rpx;
font-weight: 600;
color: #1E2A3A;
}
.mc-records__tabs {
display: flex;
gap: 8rpx;
}
.mc-records__tab {
padding: 12rpx 20rpx;
border-radius: 12rpx;
font-size: 24rpx;
color: #718096;
background: #F7FAFC;
transition: all 0.2s;
}
.mc-records__tab--active {
background: #1677FF;
color: #FFFFFF;
}
.mc-records__divider {
height: 1rpx;
background: #F0F0F0;
}
.mc-records__scroll {
height: 400rpx;
overflow: hidden;
}
.mc-records__item-inner {
display: flex;
align-items: center;
gap: 20rpx;
padding: 24rpx 0;
}
.mc-records__icon-wrap {
width: 72rpx;
height: 72rpx;
border-radius: 36rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.mc-records__icon-wrap--orange {
background: rgba(242, 153, 74, 0.1);
}
.mc-records__icon-wrap--green {
background: rgba(76, 175, 80, 0.1);
}
.mc-records__icon-wrap--purple {
background: rgba(156, 39, 176, 0.1);
}
.mc-records__icon-wrap--default {
background: rgba(22, 119, 255, 0.1);
}
.mc-records__icon {
width: 36rpx;
height: 36rpx;
}
.mc-records__icon-text {
font-size: 32rpx;
}
.mc-records__info {
flex: 1;
min-width: 0;
}
.mc-records__item-title {
font-size: 28rpx;
color: #1E2A3A;
display: block;
margin-bottom: 6rpx;
}
.mc-records__item-time {
font-size: 24rpx;
color: #999;
}
.mc-records__value {
font-size: 28rpx;
font-weight: 600;
flex-shrink: 0;
}
.mc-records__value--positive {
color: #4CAF50;
}
.mc-records__value--negative {
color: #333;
}
.mc-records__value--pending {
color: #F2994A;
}
.mc-records__empty {
padding: 64rpx 32rpx;
display: flex;
align-items: center;
justify-content: center;
}
.mc-records__empty-text {
font-size: 26rpx;
color: #999;
}
</style>