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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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>