|
@@ -0,0 +1,281 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="page" :style="{ '--font-scale': fontScale }">
|
|
|
|
|
+ <view v-if="loading" class="loading-wrap">
|
|
|
|
|
+ <text class="loading-text">加载中...</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-else-if="couponList.length === 0" class="empty-state">
|
|
|
|
|
+ <text class="empty-icon">🎫</text>
|
|
|
|
|
+ <text class="empty-text">暂无可领取的优惠券</text>
|
|
|
|
|
+ <text class="empty-hint">平台发放的优惠券将在这里展示,敬请期待</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <scroll-view v-else scroll-y class="coupon-scroll">
|
|
|
|
|
+ <view class="coupon-list">
|
|
|
|
|
+ <view v-for="item in couponList" :key="item.id" class="coupon-card">
|
|
|
|
|
+ <view class="card-left">
|
|
|
|
|
+ <text class="card-value">{{ formatPriceWithSymbol(item.value) }}</text>
|
|
|
|
|
+ <text class="card-type-label">{{ getTypeLabel(item.type) }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="card-right">
|
|
|
|
|
+ <view class="card-header">
|
|
|
|
|
+ <text class="card-name">{{ item.name }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="card-condition">{{ getConditionText(item) }}</text>
|
|
|
|
|
+ <text class="card-scope">{{ getScopeText(item.applicableTo) }}</text>
|
|
|
|
|
+ <text class="card-expiry">有效期至 {{ formatDate(item.validUntil) }}</text>
|
|
|
|
|
+ <button class="claim-btn" @click="handleClaim(item)">立即领取</button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </scroll-view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import { getAvailableCouponTemplates, claimCoupon } from '../../utils/api.js'
|
|
|
|
|
+import { parseDate } from '../../utils/format.js'
|
|
|
|
|
+import fontScaleMixin from '@/mixins/font-scale.js'
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ mixins: [fontScaleMixin],
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ couponList: [],
|
|
|
|
|
+ loading: false,
|
|
|
|
|
+ claiming: false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ onLoad() {
|
|
|
|
|
+ this.loadCoupons()
|
|
|
|
|
+ this._onLoadFired = true
|
|
|
|
|
+ },
|
|
|
|
|
+ onShow() {
|
|
|
|
|
+ // 首次进入由 onLoad 加载,避免 onLoad/onShow 双发;返回本页时刷新
|
|
|
|
|
+ if (this._onLoadFired) {
|
|
|
|
|
+ this._onLoadFired = false
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.loadCoupons()
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ loadCoupons() {
|
|
|
|
|
+ var self = this
|
|
|
|
|
+ this.loading = true
|
|
|
|
|
+ getAvailableCouponTemplates().then(function(res) {
|
|
|
|
|
+ if (res && res.code === 200) {
|
|
|
|
|
+ self.couponList = res.data || []
|
|
|
|
|
+ } else {
|
|
|
|
|
+ self.couponList = []
|
|
|
|
|
+ }
|
|
|
|
|
+ }).catch(function(e) {
|
|
|
|
|
+ console.error('加载领券中心失败', e)
|
|
|
|
|
+ self.couponList = []
|
|
|
|
|
+ }).then(function() {
|
|
|
|
|
+ self.loading = false
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ handleClaim(item) {
|
|
|
|
|
+ if (this.claiming) return
|
|
|
|
|
+ var self = this
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '确认领取',
|
|
|
|
|
+ content: '确认领取「' + (item.name || '') + '」优惠券?',
|
|
|
|
|
+ success: function(r) {
|
|
|
|
|
+ if (!r.confirm) return
|
|
|
|
|
+ self.claiming = true
|
|
|
|
|
+ // /api/coupon/claim 契约:couponId 为券模板 id(available-templates 返回 List<Coupon>,字段为 id)
|
|
|
|
|
+ claimCoupon(item.id).then(function(res) {
|
|
|
|
|
+ self.claiming = false
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ uni.showToast({ title: res.message || '领取成功', icon: 'success' })
|
|
|
|
|
+ self.loadCoupons()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ uni.showToast({ title: res.message || '领取失败', icon: 'none' })
|
|
|
|
|
+ }
|
|
|
|
|
+ }).catch(function(e) {
|
|
|
|
|
+ self.claiming = false
|
|
|
|
|
+ uni.showToast({ title: (e && e.message) || '领取失败', icon: 'none' })
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ getTypeLabel(type) {
|
|
|
|
|
+ if (type === 'FIXED') return '满减券'
|
|
|
|
|
+ if (type === 'CASH') return '无门槛券'
|
|
|
|
|
+ if (type === 'DISCOUNT') return '折扣券'
|
|
|
|
|
+ return type || ''
|
|
|
|
|
+ },
|
|
|
|
|
+ getConditionText(item) {
|
|
|
|
|
+ if (item.type === 'DISCOUNT') {
|
|
|
|
|
+ return '折扣券 ' + ((item.discountRate || 0) / 100).toFixed(1) + '折'
|
|
|
|
|
+ }
|
|
|
|
|
+ if (item.minSpend && item.minSpend > 0) {
|
|
|
|
|
+ return '满' + (item.minSpend / 100).toFixed(2) + '元可用'
|
|
|
|
|
+ }
|
|
|
|
|
+ return '无门槛'
|
|
|
|
|
+ },
|
|
|
|
|
+ getScopeText(applicableTo) {
|
|
|
|
|
+ if (applicableTo === 'ALL') return '全品类适用'
|
|
|
|
|
+ if (applicableTo === 'MEMBERSHIP') return '会员专享'
|
|
|
|
|
+ return applicableTo || '全品类适用'
|
|
|
|
|
+ },
|
|
|
|
|
+ formatDate(date) {
|
|
|
|
|
+ if (!date) return ''
|
|
|
|
|
+ var d = parseDate(date)
|
|
|
|
|
+ if (!d) return date
|
|
|
|
|
+ var y = d.getFullYear()
|
|
|
|
|
+ var m = String(d.getMonth() + 1).padStart(2, '0')
|
|
|
|
|
+ var day = String(d.getDate()).padStart(2, '0')
|
|
|
|
|
+ return y + '.' + m + '.' + day
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+.page {
|
|
|
|
|
+ min-height: 100vh;
|
|
|
|
|
+ background: $facade-fill-2;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.coupon-scroll {
|
|
|
|
|
+ height: 100vh;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.loading-wrap {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding-top: 200rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.loading-text {
|
|
|
|
|
+ font-size: calc(28rpx * var(--font-scale, 1));
|
|
|
|
|
+ color: $facade-text-muted;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.empty-state {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding-top: 200rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.empty-icon {
|
|
|
|
|
+ font-size: calc(80rpx * var(--font-scale, 1));
|
|
|
|
|
+ margin-bottom: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.empty-text {
|
|
|
|
|
+ font-size: calc(28rpx * var(--font-scale, 1));
|
|
|
|
|
+ color: $facade-text-muted;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.empty-hint {
|
|
|
|
|
+ font-size: calc(24rpx * var(--font-scale, 1));
|
|
|
|
|
+ color: $facade-text-ghost;
|
|
|
|
|
+ margin-top: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.coupon-list {
|
|
|
|
|
+ padding: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.coupon-card {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ background: $app-surface;
|
|
|
|
|
+ border-radius: 16rpx;
|
|
|
|
|
+ margin-bottom: 20rpx;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.card-left {
|
|
|
|
|
+ width: 200rpx;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ background: linear-gradient(135deg, $brand-primary, $brand-primary-light);
|
|
|
|
|
+ padding: 30rpx 12rpx;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.card-value {
|
|
|
|
|
+ font-size: calc(40rpx * var(--font-scale, 1));
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ color: $app-surface;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.card-type-label {
|
|
|
|
|
+ font-size: calc(20rpx * var(--font-scale, 1));
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.8);
|
|
|
|
|
+ margin-top: 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.card-right {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ padding: 24rpx 24rpx 20rpx;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.card-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ margin-bottom: 10rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.card-name {
|
|
|
|
|
+ font-size: calc(28rpx * var(--font-scale, 1));
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: $facade-text-strong;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.card-condition {
|
|
|
|
|
+ font-size: calc(24rpx * var(--font-scale, 1));
|
|
|
|
|
+ color: $brand-primary;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ margin-bottom: 6rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.card-scope {
|
|
|
|
|
+ font-size: calc(22rpx * var(--font-scale, 1));
|
|
|
|
|
+ color: $uni-info;
|
|
|
|
|
+ background: $tint-sky-ghost;
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ padding: 2rpx 12rpx;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+ margin-bottom: 6rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.card-expiry {
|
|
|
|
|
+ font-size: calc(22rpx * var(--font-scale, 1));
|
|
|
|
|
+ color: $facade-text-ghost-light;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.claim-btn {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: 24rpx;
|
|
|
|
|
+ bottom: 20rpx;
|
|
|
|
|
+ background: linear-gradient(135deg, $brand-primary, $brand-primary-light);
|
|
|
|
|
+ color: $app-surface;
|
|
|
|
|
+ font-size: calc(24rpx * var(--font-scale, 1));
|
|
|
|
|
+ padding: 10rpx 28rpx;
|
|
|
|
|
+ border-radius: 28rpx;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+ height: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.claim-btn::after {
|
|
|
|
|
+ border: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.claim-btn:active {
|
|
|
|
|
+ opacity: 0.85;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|