You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

303 lines
8.4 KiB

<template>
<view class="member-card-scroll">
<scroll-view
class="card-scroll"
scroll-x="true"
scroll-with-animation
:show-scrollbar="false"
@scroll="onScroll"
>
<view class="card-scroll-inner" :style="innerStyle">
<view
v-for="(card, index) in cards"
:key="card.id || index"
class="card-item"
@click="onCardClick(card, index)"
>
<member-card
:card="card"
:size="size"
:selectable="selectable"
:selected="selectable && highlightIndex === index"
/>
<!-- v-if="selectable && !isCardUsable(card)"-->
<view class="unusable-tip">
<text v-if=" card.status == 'ACTIVE'" style="color:#00a89b;" >正常</text>
<text v-else style="color: #ff6b6b;">{{ getUnusableReason(card) }}</text>
</view>
</view>
</view>
</scroll-view>
<!-- 指示器 -->
<view v-if="cards && cards.length > 1" class="indicator">
<view
v-for="(card, index) in cards"
:key="index"
class="dot"
:class="{ active: index === currentIndex }"
></view>
</view>
</view>
</template>
<script>
import MemberCard from './member-card.vue';
export default {
name: 'MemberCardScroll',
components: { MemberCard },
props: {
cards: { type: Array, default: () => [] },
size: { type: String, default: 'mini' },
center: { type: Boolean, default: false },
selectable: { type: Boolean, default: false },
deductType: { type: String, default: 'COUNT' },
deductCount: { type: Number, default: 1 },
deductAmount: { type: Number, default: 0 },
// 默认卡片ID(后端可标记某张卡为默认卡)
defaultCardId: { type: [String, Number], default: null },
// 是否自动选中第一张可用卡(弹窗场景设为 false)
autoSelect: { type: Boolean, default: true }
},
data() {
return {
selectedIndex: -1,
currentIndex: 0,
screenWidthRpx: 750
};
},
computed: {
highlightIndex() {
if (this.selectedIndex >= 0) {
return this.selectedIndex;
}
if (this.selectable && this.defaultCard) {
return this.cards.findIndex(c => c.id === this.defaultCard.id);
}
return -1;
},
selectedCard() {
return this.selectedIndex >= 0 ? this.cards[this.selectedIndex] : null;
},
// 默认卡:后端指定的 defaultCardId,或第一张可用卡
defaultCard() {
if (this.defaultCardId != null) {
const card = this.cards.find(c => c.id === this.defaultCardId);
if (card && this.isCardUsable(card)) return card;
}
return this.cards.find(card => this.isCardUsable(card)) || null;
},
// 有效卡:用户选中的卡,没选就用默认卡
effectiveCard() {
return this.selectedCard || this.defaultCard;
},
// 卡片宽度映射(与 member-card.vue 保持一致)
cardWidthRpx() {
const map = { large: 690, small: 440, mini: 380 };
return map[this.size] || 380;
},
// 动态计算首尾 padding
innerStyle() {
if (!this.center) {
return { paddingLeft: '20rpx', paddingRight: '20rpx' };
}
const padding = Math.max(0, (this.screenWidthRpx - this.cardWidthRpx) / 2);
return {
paddingLeft: padding + 'rpx',
paddingRight: padding + 'rpx'
};
}
},
watch: {
cards: {
immediate: true,
handler() {
this.initSelected();
}
},
defaultCardId() {
this.initSelected();
}
},
created() {
this.screenWidthRpx = 750;
},
methods: {
onScroll(e) {
const scrollLeft = e.detail.scrollLeft;
const sysInfo = uni.getSystemInfoSync();
const screenWidth = sysInfo.windowWidth;
const rpxRatio = screenWidth / 750;
const cardWidthPx = this.cardWidthRpx * rpxRatio;
const gapPx = 20 * rpxRatio;
const paddingRpx = this.center
? Math.max(0, (750 - this.cardWidthRpx) / 2)
: 20;
const paddingPx = paddingRpx * rpxRatio;
const centerX = scrollLeft + screenWidth / 2;
let index = Math.round(
(centerX - paddingPx - cardWidthPx / 2) / (cardWidthPx + gapPx)
);
index = Math.max(0, Math.min(index, this.cards.length - 1));
// ✅ 只更新 currentIndex,用于指示器显示当前位置
// ✅ 不再修改 selectedIndex,选中卡只由点击/默认卡决定
this.currentIndex = index;
},
initSelected() {
if (!this.selectable) return;
if (!this.autoSelect) {
this.selectedIndex = -1;
return;
}
if (this.defaultCardId != null) {
const idx = this.cards.findIndex(c => c.id === this.defaultCardId);
if (idx >= 0 && this.isCardUsable(this.cards[idx])) {
this.selectedIndex = idx;
return;
}
}
const idx = this.cards.findIndex(card => this.isCardUsable(card));
this.selectedIndex = idx;
},
onCardClick(card, index) {
this.currentIndex = index;
if (this.selectable) {
if (!this.isCardUsable(card)) {
uni.showToast({ title: this.getUnusableReason(card), icon: 'none' });
return;
}
this.selectedIndex = index;
this.$emit('select', { card, index });
} else {
this.$emit('card-click', { card, index });
}
},
isCardUsable(card) {
if (!card) return false;
// ✅ 只有 ACTIVE 才可用
if (card.status && card.status !== 'ACTIVE') return false;
if (card.expireDate) {
const now = new Date();
const expire = new Date(card.expireDate.replace(/-/g, '/'));
if (now > expire) return false;
}
if (card.type === 'count') {
const available = (card.remainingCount || 0) - (card.frozenCount || 0);
if (available < this.deductCount) return false;
} else if (card.type === 'total_fee') {
const available = (card.remainingTotalFee || 0) - (card.frozenAmount || 0);
if (available < this.deductAmount) return false;
}
return true;
},
getUnusableReason(card) {
if (card.status && card.status == 'ACTIVE') {
return '卡片已激活';
}
if (card.status && card.status == 'INACTIVE') {
return '卡片未激活';
}
if (card.status && card.status == 'EXPIRED') {
return '卡片已过期';
}
if (card.status && card.status == 'SUSPENDED') {
return '卡片已暂停';
}
if (card.status && card.status == 'DEPLETED') {
return '卡片额度已用完';
}
if (card.expireDate) {
const now = new Date();
const expire = new Date(card.expireDate.replace(/-/g, '/'));
if (now > expire) return '卡片已过期';
}
if (card.type === 'count') {
const available = (card.remainingCount || 0) - (card.frozenCount || 0);
if (available < this.deductCount) return '剩余次数不足';
}
else if (card.type === 'total_fee') {
const available = (card.remainingTotalFee || 0) - (card.frozenAmount || 0);
if (available < this.deductAmount) return '余额不足';
}
return '不可用';
},
// 对外方法:获取有效卡(用于提交预约)
getEffectiveCard() {
return this.effectiveCard;
},
isManuallySelected() {
return this.selectedIndex >= 0;
},
getSelected() {
return { card: this.selectedCard, index: this.selectedIndex };
},
isSelectedValid() {
return this.selectedIndex >= 0 && this.isCardUsable(this.selectedCard);
}
}
};
</script>
<style lang="scss" scoped>
.member-card-scroll {
width: 100%;
}
.card-scroll {
width: 100%;
white-space: nowrap;
}
.card-scroll-inner {
display: inline-block;
white-space: nowrap;
font-size: 0;
}
.card-item {
display: inline-block;
vertical-align: top;
margin-right: 20rpx;
position: relative;
font-size: 28rpx;
}
.card-item:last-child {
margin-right: 0;
}
.unusable-tip {
margin-top: 8rpx;
font-size: 22rpx;
text-align: center;
}
.indicator {
display: flex;
justify-content: center;
align-items: center;
padding: 20rpx 0;
}
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #ddd;
margin: 0 6rpx;
transition: all 0.3s;
&.active {
width: 24rpx;
border-radius: 6rpx;
background: #00a89b;
}
}
</style>