|
|
@@ -7,55 +7,76 @@
|
|
|
</view>
|
|
|
|
|
|
<!-- 优惠券列表 -->
|
|
|
- <view v-if="!loading && templates.length > 0" class="coupon-list">
|
|
|
- <view
|
|
|
- v-for="item in templates"
|
|
|
- :key="item.id"
|
|
|
- class="ticket"
|
|
|
- >
|
|
|
- <!-- 左侧:金额 -->
|
|
|
- <view :class="['ticket-face', item.couponType ? 'ticket-face--type' + item.couponType : 'ticket-face--type1']">
|
|
|
- <view class="ticket-amount">
|
|
|
- <text class="ticket-currency">{{ item.couponType === 2 ? '' : '¥' }}</text>
|
|
|
- <text class="ticket-figure">{{ formatDiscountValue(item) }}</text>
|
|
|
+ <scroll-view
|
|
|
+ v-if="!loading && templates.length > 0"
|
|
|
+ class="coupon-scroll"
|
|
|
+ scroll-y
|
|
|
+ refresher-enabled
|
|
|
+ :refresher-triggered="refreshing"
|
|
|
+ @refresherrefresh="onRefresh"
|
|
|
+ >
|
|
|
+ <view class="coupon-list">
|
|
|
+ <view
|
|
|
+ v-for="item in templates"
|
|
|
+ :key="item.id"
|
|
|
+ class="ticket"
|
|
|
+ >
|
|
|
+ <!-- 左侧:金额 -->
|
|
|
+ <view :class="['ticket-face', item.couponType ? 'ticket-face--type' + item.couponType : 'ticket-face--type1']">
|
|
|
+ <view class="ticket-amount">
|
|
|
+ <text class="ticket-currency">{{ item.couponType === 2 ? '' : '¥' }}</text>
|
|
|
+ <text class="ticket-figure">{{ formatDiscountValue(item) }}</text>
|
|
|
+ </view>
|
|
|
+ <text class="ticket-threshold">
|
|
|
+ {{ item.minAmount && item.minAmount > 0 ? '满' + item.minAmount + '可用' : '无门槛' }}
|
|
|
+ </text>
|
|
|
</view>
|
|
|
- <text class="ticket-threshold">
|
|
|
- {{ item.minAmount && item.minAmount > 0 ? '满' + item.minAmount + '可用' : '无门槛' }}
|
|
|
- </text>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 锯齿分割 -->
|
|
|
- <view class="ticket-serration">
|
|
|
- <view class="serration-gap serration-gap--top"></view>
|
|
|
- <view class="serration-rail"></view>
|
|
|
- <view class="serration-gap serration-gap--bottom"></view>
|
|
|
- </view>
|
|
|
|
|
|
- <!-- 右侧:信息 -->
|
|
|
- <view class="ticket-body">
|
|
|
- <text class="ticket-title">{{ item.couponName || '优惠券' }}</text>
|
|
|
- <view class="ticket-meta">
|
|
|
- <view class="ticket-badge">{{ getCouponTypeLabel(item.couponType) }}</view>
|
|
|
- <text class="ticket-scope">{{ getScopeLabel(item.applyScope) }}</text>
|
|
|
+ <!-- 锯齿分割 -->
|
|
|
+ <view class="ticket-serration">
|
|
|
+ <view class="serration-gap serration-gap--top"></view>
|
|
|
+ <view class="serration-rail"></view>
|
|
|
+ <view class="serration-gap serration-gap--bottom"></view>
|
|
|
</view>
|
|
|
- <text v-if="item.couponDesc" class="ticket-desc">{{ item.couponDesc }}</text>
|
|
|
- <text class="ticket-period">
|
|
|
- <text v-if="item.validType === 1">{{ formatTime(item.validStartTime) }} ~ {{ formatTime(item.validEndTime) }}</text>
|
|
|
- <text v-else>领取后{{ item.validDays }}天内有效</text>
|
|
|
- </text>
|
|
|
-
|
|
|
- <!-- 领取按钮 -->
|
|
|
- <view
|
|
|
- :class="['ticket-grab', canReceive(item) ? '' : 'ticket-grab--out']"
|
|
|
- @click="handleReceive(item)"
|
|
|
- >
|
|
|
- <text class="ticket-grab-text">
|
|
|
- {{ receiving[item.id] ? '...' : (canReceive(item) ? '领取' : '已抢光') }}
|
|
|
+
|
|
|
+ <!-- 右侧:信息 -->
|
|
|
+ <view class="ticket-body">
|
|
|
+ <text class="ticket-title">{{ item.couponName || '优惠券' }}</text>
|
|
|
+ <view class="ticket-meta">
|
|
|
+ <view class="ticket-badge">{{ getCouponTypeLabel(item.couponType) }}</view>
|
|
|
+ <text class="ticket-scope">{{ getScopeLabel(item.applyScope) }}</text>
|
|
|
+ </view>
|
|
|
+ <text v-if="item.couponDesc" class="ticket-desc">{{ item.couponDesc }}</text>
|
|
|
+
|
|
|
+ <!-- 库存和领取信息 -->
|
|
|
+ <view class="ticket-stock-row">
|
|
|
+ <text class="ticket-stock">
|
|
|
+ 剩余 <text class="stock-num">{{ item.remainCount }}</text> / {{ item.totalCount }}
|
|
|
+ </text>
|
|
|
+ <text v-if="item.receiveLimit > 0" class="ticket-limit">
|
|
|
+ 每人限领{{ item.receiveLimit }}张
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 有效期 -->
|
|
|
+ <text class="ticket-period">
|
|
|
+ <text v-if="item.validType === 1">{{ formatTime(item.validStartTime) }} ~ {{ formatTime(item.validEndTime) }}</text>
|
|
|
+ <text v-else>领取后{{ item.validDays }}天内有效</text>
|
|
|
</text>
|
|
|
+
|
|
|
+ <!-- 领取按钮 -->
|
|
|
+ <view
|
|
|
+ :class="['ticket-grab', canReceive(item) ? '' : (item.alreadyReceived ? 'ticket-grab--received' : 'ticket-grab--out')]"
|
|
|
+ @click="handleReceive(item)"
|
|
|
+ >
|
|
|
+ <text class="ticket-grab-text">
|
|
|
+ {{ receiving[item.id] ? '领取中...' : (item.alreadyReceived ? '已领取' : (canReceive(item) ? '立即领取' : '已抢光')) }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
- </view>
|
|
|
+ </scroll-view>
|
|
|
|
|
|
<!-- 空状态 -->
|
|
|
<view v-if="!loading && templates.length === 0" class="empty">
|
|
|
@@ -74,6 +95,7 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
<text class="empty-title">暂无可领取的优惠券</text>
|
|
|
+ <text class="empty-desc">过段时间再来看看吧</text>
|
|
|
<view class="empty-cta" @click="goToMyCoupons">
|
|
|
<text class="empty-cta-text">查看我的优惠券</text>
|
|
|
</view>
|
|
|
@@ -93,16 +115,22 @@ import { getAvailableCoupons, receiveCoupon } from '../../api/coupon';
|
|
|
import type { CouponTemplateInfo } from '../../api/coupon';
|
|
|
import { checkAuth } from '../../utils/auth';
|
|
|
|
|
|
-const templates = ref<CouponTemplateInfo[]>([]);
|
|
|
+// 扩展接口,添加前端需要的字段
|
|
|
+interface CouponTemplateInfoExtended extends CouponTemplateInfo {
|
|
|
+ alreadyReceived?: boolean;
|
|
|
+}
|
|
|
+
|
|
|
+const templates = ref<CouponTemplateInfoExtended[]>([]);
|
|
|
const loading = ref(false);
|
|
|
+const refreshing = ref(false);
|
|
|
const receiving = reactive<Record<string, boolean>>({});
|
|
|
|
|
|
/**
|
|
|
* 格式化优惠值
|
|
|
*/
|
|
|
-const formatDiscountValue = (item: CouponTemplateInfo): string => {
|
|
|
+const formatDiscountValue = (item: CouponTemplateInfoExtended): string => {
|
|
|
if (item.couponType === 2) {
|
|
|
- return item.discountValue ? `${item.discountValue}折` : '';
|
|
|
+ return item.discountValue ? `${item.discountValue}折` : '0';
|
|
|
}
|
|
|
return item.discountValue ? `${item.discountValue}` : '0';
|
|
|
};
|
|
|
@@ -143,14 +171,14 @@ const formatTime = (timeStr: string): string => {
|
|
|
/**
|
|
|
* 判断是否可领取
|
|
|
*/
|
|
|
-const canReceive = (item: CouponTemplateInfo): boolean => {
|
|
|
- return item.remainCount > 0 && item.status === 1;
|
|
|
+const canReceive = (item: CouponTemplateInfoExtended): boolean => {
|
|
|
+ return item.remainCount > 0 && item.status === 1 && !item.alreadyReceived;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 领取优惠券
|
|
|
*/
|
|
|
-const handleReceive = async (item: CouponTemplateInfo) => {
|
|
|
+const handleReceive = async (item: CouponTemplateInfoExtended) => {
|
|
|
if (!canReceive(item) || receiving[item.id]) return;
|
|
|
|
|
|
receiving[item.id] = true;
|
|
|
@@ -162,6 +190,7 @@ const handleReceive = async (item: CouponTemplateInfo) => {
|
|
|
});
|
|
|
// 更新剩余数量
|
|
|
item.remainCount = Math.max(0, item.remainCount - 1);
|
|
|
+ item.alreadyReceived = true;
|
|
|
} catch (error: any) {
|
|
|
console.error('领取优惠券失败:', error);
|
|
|
// 错误信息已在 request 工具中展示
|
|
|
@@ -170,6 +199,18 @@ const handleReceive = async (item: CouponTemplateInfo) => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+/**
|
|
|
+ * 下拉刷新
|
|
|
+ */
|
|
|
+const onRefresh = async () => {
|
|
|
+ refreshing.value = true;
|
|
|
+ try {
|
|
|
+ await loadTemplates();
|
|
|
+ } finally {
|
|
|
+ refreshing.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
/**
|
|
|
* 跳转到我的优惠券
|
|
|
*/
|
|
|
@@ -183,16 +224,14 @@ const goToMyCoupons = () => {
|
|
|
* 加载可领取优惠券列表
|
|
|
*/
|
|
|
const loadTemplates = async () => {
|
|
|
- if (loading.value) return;
|
|
|
- loading.value = true;
|
|
|
-
|
|
|
try {
|
|
|
const result = await getAvailableCoupons();
|
|
|
- templates.value = result;
|
|
|
+ templates.value = result.map(item => ({
|
|
|
+ ...item,
|
|
|
+ alreadyReceived: false // 初始化为未领取
|
|
|
+ }));
|
|
|
} catch (error) {
|
|
|
console.error('加载可领取优惠券失败:', error);
|
|
|
- } finally {
|
|
|
- loading.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -210,6 +249,10 @@ onMounted(() => {
|
|
|
background: linear-gradient(180deg, #FFF1F0 0%, #F5F5F5 30%);
|
|
|
}
|
|
|
|
|
|
+.coupon-scroll {
|
|
|
+ height: calc(100vh - 140rpx);
|
|
|
+}
|
|
|
+
|
|
|
/* ========== Header ========== */
|
|
|
.header {
|
|
|
padding: 28rpx 36rpx 16rpx;
|
|
|
@@ -231,7 +274,7 @@ onMounted(() => {
|
|
|
|
|
|
/* ========== Coupon List ========== */
|
|
|
.coupon-list {
|
|
|
- padding: 8rpx 28rpx 0;
|
|
|
+ padding: 8rpx 28rpx 40rpx;
|
|
|
}
|
|
|
|
|
|
/* ========== Ticket Card ========== */
|
|
|
@@ -397,10 +440,27 @@ onMounted(() => {
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
+/* 库存行 */
|
|
|
+.ticket-stock-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 10rpx;
|
|
|
+}
|
|
|
+
|
|
|
.ticket-stock {
|
|
|
font-size: 20rpx;
|
|
|
+ color: #999999;
|
|
|
+}
|
|
|
+
|
|
|
+.stock-num {
|
|
|
color: #E8533E;
|
|
|
- font-weight: 500;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.ticket-limit {
|
|
|
+ font-size: 20rpx;
|
|
|
+ color: #BBBBBB;
|
|
|
}
|
|
|
|
|
|
.ticket-period {
|
|
|
@@ -432,6 +492,14 @@ onMounted(() => {
|
|
|
color: #CCCCCC;
|
|
|
}
|
|
|
|
|
|
+.ticket-grab--received {
|
|
|
+ background: #F0F0F0;
|
|
|
+}
|
|
|
+
|
|
|
+.ticket-grab--received .ticket-grab-text {
|
|
|
+ color: #999999;
|
|
|
+}
|
|
|
+
|
|
|
/* ========== Empty State ========== */
|
|
|
.empty {
|
|
|
display: flex;
|
|
|
@@ -526,6 +594,12 @@ onMounted(() => {
|
|
|
font-size: 30rpx;
|
|
|
color: #999999;
|
|
|
font-weight: 600;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-desc {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #BBBBBB;
|
|
|
margin-bottom: 36rpx;
|
|
|
}
|
|
|
|