hookehuyr

feat(优惠券): 重构优惠券列表和详情页面,添加加载状态和API集成

重构优惠券列表页面,添加分页加载和状态筛选功能
优化优惠券详情页面,完善状态显示和核销逻辑
新增蓝色主题相关样式类
更新API接口路径和返回字段
...@@ -19,6 +19,7 @@ declare module 'vue' { ...@@ -19,6 +19,7 @@ declare module 'vue' {
19 NutDialog: typeof import('@nutui/nutui-taro')['Dialog'] 19 NutDialog: typeof import('@nutui/nutui-taro')['Dialog']
20 NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview'] 20 NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview']
21 NutInput: typeof import('@nutui/nutui-taro')['Input'] 21 NutInput: typeof import('@nutui/nutui-taro')['Input']
22 + NutLoading: typeof import('@nutui/nutui-taro')['Loading']
22 NutPicker: typeof import('@nutui/nutui-taro')['Picker'] 23 NutPicker: typeof import('@nutui/nutui-taro')['Picker']
23 NutPopup: typeof import('@nutui/nutui-taro')['Popup'] 24 NutPopup: typeof import('@nutui/nutui-taro')['Popup']
24 NutRow: typeof import('@nutui/nutui-taro')['Row'] 25 NutRow: typeof import('@nutui/nutui-taro')['Row']
......
1 /* 1 /*
2 * @Date: 2024-01-01 00:00:00 2 * @Date: 2024-01-01 00:00:00
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-09-08 13:15:39 4 + * @LastEditTime: 2025-09-08 16:23:23
5 * @FilePath: /lls_program/src/api/coupon.js 5 * @FilePath: /lls_program/src/api/coupon.js
6 * @Description: 优惠券相关接口 6 * @Description: 优惠券相关接口
7 */ 7 */
...@@ -15,7 +15,7 @@ const Api = { ...@@ -15,7 +15,7 @@ const Api = {
15 REDEEM_COUPON: '/srv/?a=coupon&t=redeem', 15 REDEEM_COUPON: '/srv/?a=coupon&t=redeem',
16 MY_COUPON_LIST: '/srv/?a=redemption&t=list', 16 MY_COUPON_LIST: '/srv/?a=redemption&t=list',
17 MY_COUPON_DETAIL: '/srv/?a=redemption&t=detail', 17 MY_COUPON_DETAIL: '/srv/?a=redemption&t=detail',
18 - USE_COUPON: '/srv/?a=coupon&t=use', 18 + USE_COUPON: '/srv/?a=redemption&t=use',
19 } 19 }
20 20
21 /** 21 /**
...@@ -111,6 +111,7 @@ export const redeemCouponAPI = (params) => fn(fetch.post(Api.REDEEM_COUPON, para ...@@ -111,6 +111,7 @@ export const redeemCouponAPI = (params) => fn(fetch.post(Api.REDEEM_COUPON, para
111 * @returns {string} response.data.thumbnail - 优惠券缩略图 111 * @returns {string} response.data.thumbnail - 优惠券缩略图
112 * @returns {string} response.data.status - 优惠券状态, UNUSED=未使用, USED=已使用, EXPIRED=已过期 112 * @returns {string} response.data.status - 优惠券状态, UNUSED=未使用, USED=已使用, EXPIRED=已过期
113 * @returns {string} response.data.expire_time - 有效期截止时间 113 * @returns {string} response.data.expire_time - 有效期截止时间
114 + * @returns {string} response.data.used_time - 核销时间
114 */ 115 */
115 export const getMyCouponListAPI = (params = {}) => fn(fetch.get(Api.MY_COUPON_LIST, params)); 116 export const getMyCouponListAPI = (params = {}) => fn(fetch.get(Api.MY_COUPON_LIST, params));
116 117
...@@ -127,6 +128,7 @@ export const getMyCouponListAPI = (params = {}) => fn(fetch.get(Api.MY_COUPON_LI ...@@ -127,6 +128,7 @@ export const getMyCouponListAPI = (params = {}) => fn(fetch.get(Api.MY_COUPON_LI
127 * @returns {string} response.data.banner - 优惠券banner图 128 * @returns {string} response.data.banner - 优惠券banner图
128 * @returns {string} response.data.status - 优惠券状态, UNUSED=未使用, USED=已使用, EXPIRED=已过期 129 * @returns {string} response.data.status - 优惠券状态, UNUSED=未使用, USED=已使用, EXPIRED=已过期
129 * @returns {string} response.data.expire_time - 有效期截止时间 130 * @returns {string} response.data.expire_time - 有效期截止时间
131 + * @returns {string} response.data.expiration_rules[string] - 有效期
130 * @returns {Array} response.data.applicable_stores[string] - 可用门店 132 * @returns {Array} response.data.applicable_stores[string] - 可用门店
131 * @returns {Array} response.data.usage_rules[string] - 使用规则 133 * @returns {Array} response.data.usage_rules[string] - 使用规则
132 */ 134 */
......
...@@ -5,3 +5,11 @@ ...@@ -5,3 +5,11 @@
5 .bg-blue-500 { 5 .bg-blue-500 {
6 background-color: #4A90E2 !important; 6 background-color: #4A90E2 !important;
7 } 7 }
8 +
9 +.text-blue-500 {
10 + color: #4A90E2 !important;
11 +}
12 +
13 +.border-blue-500 {
14 + border-color: #4A90E2 !important;
15 +}
......
This diff is collapsed. Click to expand it.
1 <template> 1 <template>
2 - <view class="min-h-screen bg-gray-100"> 2 + <view class="min-h-screen bg-gray-50">
3 - <!-- <AppHeader title="我的券" :showBack="true" /> -->
4 -
5 <!-- Tabs --> 3 <!-- Tabs -->
6 - <view class="bg-white flex justify-around items-center border-b border-gray-200"> 4 + <view class="bg-white px-4 py-2 border-b border-gray-200">
7 - <view 5 + <view class="flex space-x-6">
8 - v-for="tab in tabs" 6 + <view
9 - :key="tab.name" 7 + v-for="tab in tabs"
10 - @click="activeTab = tab.name" 8 + :key="tab.name"
11 - class="py-3 text-center w-1/4 text-base font-medium cursor-pointer" 9 + @click="handleTabChange(tab.name)"
12 - :class="[ 10 + :class="{
13 - activeTab === tab.name 11 + 'text-blue-500 border-b-2 border-blue-500': activeTab === tab.name,
14 - ? 'text-blue-500 border-b-2 border-blue-500' 12 + 'text-gray-500': activeTab !== tab.name
15 - : 'text-gray-500' 13 + }"
16 - ]" 14 + class="pb-2 px-1 text-sm font-medium transition-colors"
17 - > 15 + >
18 - {{ tab.label }} 16 + {{ tab.label }}
17 + </view>
19 </view> 18 </view>
20 </view> 19 </view>
21 20
22 - <!-- Rewards List --> 21 + <!-- Loading State -->
23 - <view class="p-4"> 22 + <view v-if="loading" class="flex justify-center items-center py-20">
23 + <view class="text-gray-500">加载中...</view>
24 + </view>
25 +
26 + <!-- Content -->
27 + <view v-else class="p-4">
24 <view v-if="filteredRewards.length > 0" class="space-y-4"> 28 <view v-if="filteredRewards.length > 0" class="space-y-4">
25 <view 29 <view
26 v-for="reward in filteredRewards" 30 v-for="reward in filteredRewards"
27 :key="reward.id" 31 :key="reward.id"
28 - class="bg-white rounded-lg shadow-sm p-4 flex justify-between items-center" 32 + class="bg-white rounded-xl p-4 flex items-center shadow-[0_2px_8px_rgba(0,0,0,0.08)]"
29 > 33 >
30 - <view class="flex-1"> 34 + <image :src="reward.thumbnail || 'https://placehold.co/120x120/e2f3ff/0369a1?text=券&font=roboto'" class="w-16 h-16 rounded-lg mr-4 flex-shrink-0" mode="aspectFill" />
31 - <h3 class="text-lg font-semibold text-gray-800">{{ reward.title }}</h3> 35 + <view class="flex-1 min-w-0">
32 - <p class="text-sm text-gray-500 mt-1">有效期至 {{ reward.expiryDate }}</p> 36 + <view class="font-medium text-base mb-1">{{ reward.title }}</view>
33 - <p v-if="reward.status === 'used'" class="text-sm text-gray-400 mt-1">使用于 {{ reward.usedDate }}</p> 37 + <view v-if="reward.status === 'UNUSED'" class="text-xs text-gray-400">
38 + 有效期至:{{ formatDate(reward.expire_time) }}
39 + </view>
40 + <view v-if="reward.status === 'USED'" class="text-xs text-red-500">
41 + 使用日期:{{ formatDate(reward.used_time) }}
42 + </view>
43 + </view>
44 + <view class="ml-4 flex flex-col items-end">
45 + <button
46 + @click="handleUseReward(reward)"
47 + :disabled="reward.status !== 'UNUSED'"
48 + :class="{
49 + 'bg-blue-500 text-white': reward.status === 'UNUSED',
50 + 'bg-gray-300 text-gray-500': reward.status !== 'UNUSED'
51 + }"
52 + class="px-4 py-2 rounded-lg text-sm font-medium transition-colors flex-shrink-0"
53 + >
54 + {{ getButtonText(reward.status) }}
55 + </button>
34 </view> 56 </view>
35 - <button
36 - @click="handleUseReward(reward)"
37 - :disabled="reward.status !== 'unused'"
38 - class="px-6 py-2 rounded-full text-white font-medium text-sm transition-colors"
39 - :class="{
40 - 'bg-blue-500 hover:bg-blue-600': reward.status === 'unused',
41 - 'bg-gray-300 cursor-not-allowed': reward.status !== 'unused'
42 - }"
43 - >
44 - {{ getButtonText(reward.status) }}
45 - </button>
46 </view> 57 </view>
47 </view> 58 </view>
59 +
60 + <!-- Empty State -->
48 <view v-else class="text-center py-20"> 61 <view v-else class="text-center py-20">
49 - <p class="text-gray-500">暂无相关兑换券</p> 62 + <view class="text-gray-400 text-lg mb-2">暂无券</view>
63 + <view class="text-gray-500 text-sm">您还没有任何券</view>
64 + </view>
65 +
66 + <!-- Load More -->
67 + <view v-if="hasMore && filteredRewards.length > 0" class="text-center mt-6">
68 + <view
69 + @click="loadMore"
70 + class="text-blue-500 py-4"
71 + >
72 + {{ loading ? '加载中...' : '加载更多' }}
73 + </view>
74 + </view>
75 +
76 + <!-- No More Data -->
77 + <view v-if="!hasMore && filteredRewards.length > 0" class="text-center py-4 text-gray-500">
78 + 没有更多数据了
50 </view> 79 </view>
51 </view> 80 </view>
52 </view> 81 </view>
...@@ -56,104 +85,150 @@ ...@@ -56,104 +85,150 @@
56 import { ref, computed } from 'vue'; 85 import { ref, computed } from 'vue';
57 import Taro from '@tarojs/taro'; 86 import Taro from '@tarojs/taro';
58 import { useDidShow } from '@tarojs/taro'; 87 import { useDidShow } from '@tarojs/taro';
88 +// 导入接口
89 +import { getMyCouponListAPI } from '@/api/coupon';
59 90
60 const tabs = ref([ 91 const tabs = ref([
61 - { name: 'all', label: '全部' }, 92 + { name: 'all', label: '全部', status: '' },
62 - { name: 'unused', label: '未使用' }, 93 + { name: 'unused', label: '未使用', status: 'UNUSED' },
63 - { name: 'used', label: '已使用' }, 94 + { name: 'used', label: '已使用', status: 'USED' },
64 - { name: 'expired', label: '已过期' }, 95 + { name: 'expired', label: '已过期', status: 'EXPIRED' },
65 ]); 96 ]);
66 97
67 const activeTab = ref('all'); 98 const activeTab = ref('all');
68 - 99 +const loading = ref(false);
69 const rewards = ref([]); 100 const rewards = ref([]);
101 +const currentPage = ref(0);
102 +const hasMore = ref(true);
70 103
71 const filteredRewards = computed(() => { 104 const filteredRewards = computed(() => {
72 - if (activeTab.value === 'all') { 105 + // 由于API已经根据status参数进行了筛选,这里直接返回rewards
73 - return rewards.value; 106 + return rewards.value;
74 - }
75 - return rewards.value.filter(reward => reward.status === activeTab.value);
76 }); 107 });
77 108
109 +/**
110 + * 获取按钮文本
111 + */
78 const getButtonText = (status) => { 112 const getButtonText = (status) => {
79 switch (status) { 113 switch (status) {
80 - case 'unused': 114 + case 'UNUSED':
81 return '使用'; 115 return '使用';
82 - case 'used': 116 + case 'USED':
83 return '已使用'; 117 return '已使用';
84 - case 'expired': 118 + case 'EXPIRED':
85 return '已过期'; 119 return '已过期';
86 default: 120 default:
87 return ''; 121 return '';
88 } 122 }
89 }; 123 };
90 124
125 +/**
126 + * 获取状态文本
127 + */
128 +// const getStatusText = (status) => {
129 +// switch (status) {
130 +// case 'UNUSED':
131 +// return '未使用';
132 +// case 'USED':
133 +// return '已使用';
134 +// case 'EXPIRED':
135 +// return '已过期';
136 +// default:
137 +// return '';
138 +// }
139 +// };
140 +
141 +/**
142 + * 格式化日期
143 + */
144 +const formatDate = (dateString) => {
145 + if (!dateString) return '';
146 + const date = new Date(dateString);
147 + return date.toLocaleDateString('zh-CN');
148 +};
149 +
150 +/**
151 + * 处理优惠券使用
152 + */
91 const handleUseReward = (reward) => { 153 const handleUseReward = (reward) => {
92 - if (reward.status === 'unused') { 154 + if (reward.status === 'UNUSED') {
93 - // Here you would typically navigate to a usage/QR code page
94 - // For now, we can just log it or update the status for demo purposes
95 - console.log(`Using reward: ${reward.title}`);
96 - // Example of updating status:
97 - // const item = rewards.value.find(r => r.id === reward.id);
98 - // if (item) {
99 - // item.status = 'used';
100 - // item.usedDate = new Date().toISOString().split('T')[0];
101 - // }
102 // 跳转到卡券详情页 155 // 跳转到卡券详情页
103 Taro.navigateTo({ 156 Taro.navigateTo({
104 url: '/pages/CouponDetail/index?id=' + reward.id 157 url: '/pages/CouponDetail/index?id=' + reward.id
105 - }) 158 + });
159 + }
160 +};
161 +
162 +/**
163 + * 获取我的优惠券列表
164 + */
165 +const fetchMyCouponList = async (reset = false) => {
166 + if (loading.value || (!hasMore.value && !reset)) return;
167 +
168 + loading.value = true;
169 + try {
170 + const currentTab = tabs.value.find(tab => tab.name === activeTab.value);
171 + const params = {
172 + status: currentTab?.status || '',
173 + page: reset ? 0 : currentPage.value,
174 + limit: 10
175 + };
176 +
177 + const response = await getMyCouponListAPI(params);
178 +
179 + if (response && response.data) {
180 + const coupons = Array.isArray(response.data) ? response.data : [];
181 +
182 + if (reset) {
183 + rewards.value = coupons;
184 + currentPage.value = 0;
185 + } else {
186 + rewards.value = [...rewards.value, ...coupons];
187 + }
188 +
189 + // 如果返回的数据少于limit,说明没有更多数据了
190 + hasMore.value = coupons.length >= 10;
191 + currentPage.value += 1;
192 + }
193 + } catch (error) {
194 + console.error('获取我的优惠券列表失败:', error);
195 + Taro.showToast({
196 + title: '获取列表失败',
197 + icon: 'error'
198 + });
199 + } finally {
200 + loading.value = false;
106 } 201 }
107 }; 202 };
108 203
204 +/**
205 + * 处理标签页切换
206 + */
207 +const handleTabChange = (tabName) => {
208 + if (activeTab.value === tabName) return;
209 +
210 + activeTab.value = tabName;
211 + currentPage.value = 0;
212 + hasMore.value = true;
213 + fetchMyCouponList(true); // 重置并重新加载数据
214 +};
215 +
216 +/**
217 + * 加载更多
218 + */
219 +const loadMore = () => {
220 + fetchMyCouponList(false);
221 +};
222 +
223 +/**
224 + * 初始化页面数据
225 + */
226 +const initPageData = () => {
227 + fetchMyCouponList(true);
228 +};
229 +
230 +// 页面显示时刷新数据
109 useDidShow(() => { 231 useDidShow(() => {
110 initPageData(); 232 initPageData();
111 }); 233 });
112 -
113 -const initPageData = () => {
114 - rewards.value = [
115 - {
116 - id: 1,
117 - title: '杏花楼集团 85折券',
118 - expiryDate: '2025-08-28',
119 - status: 'unused',
120 - usedDate: null,
121 - },
122 - {
123 - id: 2,
124 - title: '老凤祥银楼 20元抵用券',
125 - expiryDate: '2025-08-28',
126 - status: 'unused',
127 - usedDate: null,
128 - },
129 - {
130 - id: 3,
131 - title: '吴良材眼镜 5折券',
132 - expiryDate: '2024-05-20',
133 - status: 'used',
134 - usedDate: '2024-05-01',
135 - },
136 - {
137 - id: 4,
138 - title: '沈大成双酿团 免费券',
139 - expiryDate: '2024-03-15',
140 - status: 'expired',
141 - usedDate: null,
142 - },
143 - {
144 - id: 5,
145 - title: '沈大成双酿团 免费券',
146 - expiryDate: '2024-03-15',
147 - status: 'expired',
148 - usedDate: null,
149 - },
150 - {
151 - id: 6,
152 - title: '沈大成双酿团 免费券',
153 - expiryDate: '2024-03-15',
154 - status: 'expired',
155 - usedDate: null,
156 - },
157 - ];
158 -}
159 </script> 234 </script>
......