Initial commit
This commit is contained in:
188
wechat-mini-program/pages/coupon/coupon.js
Normal file
188
wechat-mini-program/pages/coupon/coupon.js
Normal file
@@ -0,0 +1,188 @@
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
coupons: [],
|
||||
user: {}
|
||||
},
|
||||
onLoad() {
|
||||
this.fetchCoupons();
|
||||
},
|
||||
onShow() {
|
||||
this.getUserInfo();
|
||||
this.fetchCoupons();
|
||||
},
|
||||
getUserInfo() {
|
||||
const { request } = require('../../utils/request')
|
||||
const app = getApp()
|
||||
|
||||
// Fetch latest user info from backend to check if phone exists in DB
|
||||
request({ url: '/user/' }).then(user => {
|
||||
app.globalData.userInfo = user; // Sync global data
|
||||
this.setData({ user: user });
|
||||
}).catch(err => {
|
||||
console.error('Failed to fetch user info', err)
|
||||
// Fallback to global data if fetch fails
|
||||
if (app.globalData.userInfo) {
|
||||
this.setData({ user: app.globalData.userInfo });
|
||||
}
|
||||
})
|
||||
},
|
||||
goToProfile() {
|
||||
const app = getApp();
|
||||
app.globalData.profileAction = 'bind_phone';
|
||||
wx.switchTab({
|
||||
url: '/pages/profile/profile'
|
||||
})
|
||||
},
|
||||
fetchCoupons() {
|
||||
const { request } = require('../../utils/request')
|
||||
|
||||
Promise.all([
|
||||
request({ url: '/user-coupons/' }).catch(() => []),
|
||||
request({ url: '/available-coupons/' }).catch(() => [])
|
||||
]).then(([userCoupons, availableCoupons]) => {
|
||||
// 1. Process User Coupons
|
||||
const userList = Array.isArray(userCoupons) ? userCoupons : (userCoupons && userCoupons.results) || [];
|
||||
const formattedUserList = userList.map(item => {
|
||||
if (item.coupon_detail) {
|
||||
let scopeText = item.coupon_detail.scope_text || '全场通用';
|
||||
if (scopeText === '通用') scopeText = '全场通用';
|
||||
|
||||
return {
|
||||
...item.coupon_detail,
|
||||
id: item.id,
|
||||
coupon_id: item.coupon_detail.id,
|
||||
status: item.status,
|
||||
expiry: item.coupon_detail.expiry,
|
||||
displayDesc: scopeText,
|
||||
is_time_limited: item.coupon_detail.is_time_limited
|
||||
}
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
displayDesc: item.desc
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Process Available Coupons
|
||||
const userCouponIds = new Set(formattedUserList.map(item => item.coupon_id));
|
||||
|
||||
const availableList = Array.isArray(availableCoupons) ? availableCoupons : (availableCoupons && availableCoupons.results) || [];
|
||||
const formattedAvailableList = availableList
|
||||
.filter(c => !userCouponIds.has(c.id))
|
||||
.map(c => {
|
||||
let scopeText = c.scope_text || '全场通用';
|
||||
if (scopeText === '通用') scopeText = '全场通用';
|
||||
return {
|
||||
...c,
|
||||
id: 'avail_' + c.id, // Virtual ID
|
||||
coupon_id: c.id,
|
||||
status: 'can_claim', // Virtual Status
|
||||
displayDesc: scopeText
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Merge and Sort
|
||||
const allCoupons = [...formattedAvailableList, ...formattedUserList];
|
||||
|
||||
// Sort: can_claim -> assigned -> used -> expired
|
||||
const statusOrder = { 'can_claim': 0, 'assigned': 1, 'used': 2, 'expired': 3, 'revoked': 4 };
|
||||
|
||||
allCoupons.sort((a, b) => {
|
||||
const orderA = statusOrder[a.status] !== undefined ? statusOrder[a.status] : 99;
|
||||
const orderB = statusOrder[b.status] !== undefined ? statusOrder[b.status] : 99;
|
||||
return orderA - orderB;
|
||||
});
|
||||
|
||||
this.setData({ coupons: allCoupons });
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
// Mock data fallback
|
||||
this.setData({
|
||||
coupons: [
|
||||
{
|
||||
id: 1,
|
||||
amount: '50',
|
||||
unit: '元',
|
||||
title: '新人见面礼',
|
||||
desc: '无门槛使用,适用于所有课程',
|
||||
displayDesc: '全场通用',
|
||||
expiry: '2023-12-31',
|
||||
status: 'available',
|
||||
color: 'from-blue-500 to-cyan-400',
|
||||
bgStart: 'from-blue-50',
|
||||
bgEnd: 'to-cyan-50',
|
||||
shadow: 'shadow-blue-100',
|
||||
is_time_limited: true
|
||||
},
|
||||
// ... (existing mocks)
|
||||
]
|
||||
})
|
||||
})
|
||||
},
|
||||
handleCouponClick(e) {
|
||||
if (!this.data.user.phone) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '请先绑定手机号激活会员权益',
|
||||
confirmText: '去绑定',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.goToProfile();
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const status = e.currentTarget.dataset.status;
|
||||
|
||||
if (status === 'can_claim') {
|
||||
this.handleClaim(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// If coupon is available/assigned, redirect to home page to use it
|
||||
if (status !== 'used') {
|
||||
wx.switchTab({
|
||||
url: '/pages/index/index'
|
||||
});
|
||||
return;
|
||||
}
|
||||
},
|
||||
handleClaim(e) {
|
||||
const id = e.currentTarget.dataset.id; // This is 'avail_ID'
|
||||
const realId = id.toString().replace('avail_', '');
|
||||
|
||||
wx.showLoading({ title: '领取中...' });
|
||||
|
||||
const { request } = require('../../utils/request')
|
||||
|
||||
request({
|
||||
url: '/user-coupons/',
|
||||
method: 'POST',
|
||||
data: {
|
||||
coupon_id: realId
|
||||
}
|
||||
}).then(() => {
|
||||
wx.hideLoading();
|
||||
wx.showToast({
|
||||
title: '领取成功',
|
||||
icon: 'success'
|
||||
});
|
||||
// Refresh list
|
||||
this.fetchCoupons();
|
||||
}).catch(err => {
|
||||
wx.hideLoading();
|
||||
console.error(err);
|
||||
// Handle specific error message from backend
|
||||
const msg = (err && err.error) || (err && err.detail) || '领取失败';
|
||||
wx.showToast({
|
||||
title: msg,
|
||||
icon: 'none'
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
3
wechat-mini-program/pages/coupon/coupon.json
Normal file
3
wechat-mini-program/pages/coupon/coupon.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "优惠券"
|
||||
}
|
||||
43
wechat-mini-program/pages/coupon/coupon.wxml
Normal file
43
wechat-mini-program/pages/coupon/coupon.wxml
Normal file
@@ -0,0 +1,43 @@
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="title">优惠券中心</text>
|
||||
</view>
|
||||
|
||||
<!-- VIP Banner -->
|
||||
<view class="vip-banner" wx:if="{{!user.phone}}">
|
||||
<view class="vip-info">
|
||||
<view class="vip-title">VIP会员权益</view>
|
||||
<view class="vip-desc">开通会员享受更多优惠</view>
|
||||
</view>
|
||||
<button class="vip-btn" bindtap="goToProfile">立即开通</button>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- Coupon List -->
|
||||
<view class="section-title">{{user.phone ? '可使用的优惠券' : '可领取的优惠券'}}</view>
|
||||
<view class="coupon-list" style="{{!user.phone ? 'filter: grayscale(100%); opacity: 0.6;' : ''}}">
|
||||
<block wx:for="{{coupons}}" wx:key="id">
|
||||
<view class="coupon-card {{item.status === 'used' ? 'used' : ''}}" bindtap="handleCouponClick" data-id="{{item.id}}" data-status="{{item.status}}">
|
||||
<view class="limit-tag" wx:if="{{item.is_time_limited}}">限时</view>
|
||||
<view class="coupon-left">
|
||||
<view class="amount">
|
||||
<text class="num">{{item.amount}}</text>
|
||||
<text class="unit">{{item.unit}}</text>
|
||||
</view>
|
||||
<view class="info">
|
||||
<view class="coupon-name">{{item.title}}</view>
|
||||
<view class="coupon-desc">{{item.displayDesc}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coupon-right">
|
||||
<view class="btn-text" wx:if="{{item.status === 'can_claim'}}">立即领取</view>
|
||||
<view class="btn-text" wx:elif="{{item.status === 'assigned'}}">去使用</view>
|
||||
<view class="btn-text" wx:elif="{{item.status === 'used'}}">已使用</view>
|
||||
<view class="btn-text" wx:elif="{{item.status === 'expired'}}">已过期</view>
|
||||
<view class="btn-text" wx:else>去使用</view>
|
||||
<text class="expiry-date" wx:if="{{item.expiry}}">{{item.expiry}}到期</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
231
wechat-mini-program/pages/coupon/coupon.wxss
Normal file
231
wechat-mini-program/pages/coupon/coupon.wxss
Normal file
@@ -0,0 +1,231 @@
|
||||
.container {
|
||||
padding: 30rpx;
|
||||
background-color: var(--background-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
padding: 20rpx 0;
|
||||
text-align: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: var(--text-main);
|
||||
}
|
||||
.vip-banner {
|
||||
background: linear-gradient(to bottom right, #111827, #1f2937);
|
||||
border-radius: 30rpx;
|
||||
padding: 40rpx;
|
||||
color: var(--text-white);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 40rpx;
|
||||
box-shadow: 0 10rpx 20rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
.vip-title {
|
||||
color: #fcd34d;
|
||||
font-weight: bold;
|
||||
font-size: 32rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
.vip-desc {
|
||||
color: #d1d5db;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.vip-btn {
|
||||
background: linear-gradient(to right, #fcd34d, #fbbf24);
|
||||
color: #111827;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
padding: 10rpx 30rpx;
|
||||
border-radius: 40rpx;
|
||||
margin: 0;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20rpx;
|
||||
color: var(--text-main);
|
||||
}
|
||||
.coupon-list.disabled-list {
|
||||
filter: grayscale(100%);
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
.coupon-card {
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 24rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Colorful style for unused coupons */
|
||||
.coupon-card:not(.used) {
|
||||
background: linear-gradient(135deg, #fff1f2 0%, #ffe4e6 100%); /* More saturated gradient */
|
||||
border: 1px solid #fecdd3;
|
||||
}
|
||||
|
||||
.coupon-card.used {
|
||||
background: #f9fafb;
|
||||
box-shadow: none;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.coupon-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Dashed line separator */
|
||||
.coupon-left::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 20rpx;
|
||||
bottom: 20rpx;
|
||||
border-right: 2px dashed #e5e7eb;
|
||||
}
|
||||
|
||||
.coupon-card:not(.used) .coupon-left::after {
|
||||
border-right: 2px dashed #fca5a5; /* Reddish dashed line for unused */
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.amount {
|
||||
min-width: 120rpx;
|
||||
text-align: center;
|
||||
color: #ef4444; /* Red */
|
||||
margin-right: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.coupon-card.used .amount {
|
||||
color: #9ca3af; /* Gray */
|
||||
}
|
||||
|
||||
.num {
|
||||
font-size: 56rpx;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 24rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.coupon-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.coupon-card.used .coupon-name {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.coupon-desc {
|
||||
font-size: 22rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.coupon-card:not(.used) .coupon-desc {
|
||||
color: #7f1d1d; /* Darker red for description on colorful bg */
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.coupon-right {
|
||||
width: 160rpx; /* Increased width to fit date */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
padding: 10rpx 24rpx;
|
||||
border-radius: 30rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.expiry-date {
|
||||
font-size: 18rpx;
|
||||
color: #9ca3af;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.coupon-card:not(.used) .expiry-date {
|
||||
color: #ef4444;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Active button style */
|
||||
.coupon-card:not(.used) .btn-text {
|
||||
background: linear-gradient(to right, #ef4444, #f87171);
|
||||
color: white;
|
||||
box-shadow: 0 4rpx 10rpx rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
/* Used button style */
|
||||
.coupon-card.used .btn-text {
|
||||
background-color: #e5e7eb;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Circles for ticket effect */
|
||||
.coupon-card::before, .coupon-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
background-color: var(--background-color); /* Assuming this var is set on page/container */
|
||||
border-radius: 50%;
|
||||
right: 148rpx; /* Position at the separator (adjusted for new width) */
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.coupon-card::before {
|
||||
top: -12rpx;
|
||||
}
|
||||
|
||||
.coupon-card::after {
|
||||
bottom: -12rpx;
|
||||
}
|
||||
|
||||
.limit-tag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: linear-gradient(to bottom right, #f59e0b, #d97706);
|
||||
color: white;
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-bottom-right-radius: 16rpx;
|
||||
z-index: 20;
|
||||
}
|
||||
Reference in New Issue
Block a user