hookehuyr

feat(兑换页面): 重构积分兑换页面并连接后端API

- 移除静态数据,改为从API获取积分范围和优惠券列表
- 实现搜索、筛选和排序功能
- 添加加载状态和空状态显示
- 更新API端点路径,移除冗余参数
- 修复快速兑换选项的点击逻辑
...@@ -8,14 +8,14 @@ ...@@ -8,14 +8,14 @@
8 import { fn, fetch } from './fn'; 8 import { fn, fetch } from './fn';
9 9
10 const Api = { 10 const Api = {
11 - COUPON_HOME: '/srv/?f=walk&a=coupon&t=home', 11 + COUPON_HOME: '/srv/?a=coupon&t=home',
12 - POINT_RANGES: '/srv/?f=walk&a=coupon&t=point_ranges', 12 + POINT_RANGES: '/srv/?a=coupon&t=point_ranges',
13 - COUPON_LIST: '/srv/?f=walk&a=coupon&t=list', 13 + COUPON_LIST: '/srv/?a=coupon&t=list',
14 - COUPON_DETAIL: '/srv/?f=walk&a=coupon&t=detail', 14 + COUPON_DETAIL: '/srv/?a=coupon&t=detail',
15 - REDEEM_COUPON: '/srv/?f=walk&a=coupon&t=redeem', 15 + REDEEM_COUPON: '/srv/?a=coupon&t=redeem',
16 - MY_COUPON_LIST: '/srv/?f=walk&a=redemption&t=list', 16 + MY_COUPON_LIST: '/srv/?a=redemption&t=list',
17 - MY_COUPON_DETAIL: '/srv/?f=walk&a=redemption&t=detail', 17 + MY_COUPON_DETAIL: '/srv/?a=redemption&t=detail',
18 - USE_COUPON: '/srv/?f=walk&a=coupon&t=use', 18 + USE_COUPON: '/srv/?a=coupon&t=use',
19 } 19 }
20 20
21 /** 21 /**
......
1 /* 1 /*
2 * @Date: 2023-12-22 10:29:37 2 * @Date: 2023-12-22 10:29:37
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-09-04 10:09:32 4 + * @LastEditTime: 2025-09-08 12:24:35
5 * @FilePath: /lls_program/src/api/points.js 5 * @FilePath: /lls_program/src/api/points.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
...@@ -10,7 +10,7 @@ import { fn, fetch } from './fn'; ...@@ -10,7 +10,7 @@ import { fn, fetch } from './fn';
10 const Api = { 10 const Api = {
11 SYNC_WX_STEP: '/srv/?a=point&t=sync_wx_step', 11 SYNC_WX_STEP: '/srv/?a=point&t=sync_wx_step',
12 COLLECT_POINT: '/srv/?a=point&t=collect', 12 COLLECT_POINT: '/srv/?a=point&t=collect',
13 - POINT_LIST: '/srv/?f=walk&a=point&t=list', 13 + POINT_LIST: '/srv/?a=point&t=list',
14 } 14 }
15 15
16 /** 16 /**
......
1 <!-- 1 <!--
2 * @Date: 2025-08-27 17:47:03 2 * @Date: 2025-08-27 17:47:03
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-09-05 17:23:10 4 + * @LastEditTime: 2025-09-08 12:13:02
5 * @FilePath: /lls_program/src/pages/RewardCategories/index.vue 5 * @FilePath: /lls_program/src/pages/RewardCategories/index.vue
6 * @Description: 积分兑换分类 6 * @Description: 积分兑换分类
7 --> 7 -->
...@@ -33,29 +33,7 @@ import BottomNav from '../../components/BottomNav.vue'; ...@@ -33,29 +33,7 @@ import BottomNav from '../../components/BottomNav.vue';
33 // 导入接口 33 // 导入接口
34 import { getCouponHomeAPI } from '@/api/coupon'; 34 import { getCouponHomeAPI } from '@/api/coupon';
35 35
36 -const categories = ref([ 36 +const categories = ref([]);
37 - {
38 - id: 'health',
39 - title: '银龄健康特色兑换区',
40 - note: '南京商圈线下实体店消费积分兑换',
41 - background_url: "https://cdn.ipadbiz.cn/lls_prog/images/%E5%8D%97%E4%BA%AC%E8%B7%AF%E5%95%86%E5%9C%88.jpeg",
42 - tips: ''
43 - },
44 - {
45 - id: 'online',
46 - title: '民政领域网上商城"银龄购"',
47 - note: '线上康复健康购物积分兑换',
48 - background_url: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%97%A0%E5%AD%97-%E9%93%B6%E9%BE%84%E8%B4%AD.jpeg',
49 - tips: '请在"银龄购"线上商城进行积分兑换'
50 - },
51 - {
52 - id: 'merchants',
53 - title: '人驻商户多元场景广覆盖',
54 - note: '丰富商户积分兑换',
55 - background_url: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%97%A0%E5%AD%97-%E7%A7%AF%E5%88%86%E5%95%86%E5%9F%8E.jpg',
56 - tips: ''
57 - }
58 -]);
59 37
60 const goToRewards = (category) => { 38 const goToRewards = (category) => {
61 if (!category.tips) { 39 if (!category.tips) {
...@@ -77,8 +55,7 @@ const goToRewards = (category) => { ...@@ -77,8 +55,7 @@ const goToRewards = (category) => {
77 onMounted(async () => { 55 onMounted(async () => {
78 const { code, data } = await getCouponHomeAPI(); 56 const { code, data } = await getCouponHomeAPI();
79 if (code) { 57 if (code) {
80 - // categories.value = data; 58 + categories.value = data;
81 - console.warn(data);
82 } 59 }
83 }) 60 })
84 </script> 61 </script>
......
1 <!-- 1 <!--
2 * @Date: 2025-08-27 17:47:26 2 * @Date: 2025-08-27 17:47:26
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-09-05 10:39:16 4 + * @LastEditTime: 2025-09-08 12:25:56
5 * @FilePath: /lls_program/src/pages/Rewards/index.vue 5 * @FilePath: /lls_program/src/pages/Rewards/index.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
...@@ -83,14 +83,14 @@ ...@@ -83,14 +83,14 @@
83 </view> 83 </view>
84 84
85 <view class="flex gap-3 mb-6"> 85 <view class="flex gap-3 mb-6">
86 - <view v-for="option in quickExchangeOptions" :key="option.points" @click="selectedPoints = option.points" 86 + <view v-for="option in quickExchangeOptions" :key="option.index" @click="selectedPoints = selectedPoints === option.range ? null : option.range"
87 :class="[ 87 :class="[
88 'flex-1 py-3 rounded-lg border text-center', 88 'flex-1 py-3 rounded-lg border text-center',
89 - selectedPoints === option.points 89 + selectedPoints === option.range
90 ? 'border-blue-500 bg-blue-50 text-blue-500' 90 ? 'border-blue-500 bg-blue-50 text-blue-500'
91 : 'border-gray-200 text-gray-700' 91 : 'border-gray-200 text-gray-700'
92 ]"> 92 ]">
93 - <view class="text-center"> 93 + <view class="text-center text-xs">
94 <view>{{ option.label }}</view> 94 <view>{{ option.label }}</view>
95 <view>可兑</view> 95 <view>可兑</view>
96 </view> 96 </view>
...@@ -105,18 +105,44 @@ ...@@ -105,18 +105,44 @@
105 </view> 105 </view>
106 <!-- Rewards list --> 106 <!-- Rewards list -->
107 <view class="space-y-4"> 107 <view class="space-y-4">
108 - <view v-for="reward in sortedRewardItems" :key="reward.id" 108 + <!-- 加载状态 -->
109 - class="bg-white rounded-xl p-4 flex items-center shadow-[0_2px_8px_rgba(0,0,0,0.08)]"> 109 + <view v-if="loading && couponList.length === 0" class="text-center py-8 text-gray-500">
110 - <image :src="reward.logo" class="w-16 h-16 rounded-lg mr-4 flex-shrink-0" mode="aspectFill" /> 110 + 加载中...
111 - <view class="flex-1 min-w-0"> 111 + </view>
112 - <view class="font-medium text-base">{{ reward.title }}</view> 112 +
113 - <view class="text-gray-500 text-sm mt-1">{{ reward.merchant }}</view> 113 + <!-- 空状态 -->
114 + <view v-else-if="!loading && sortedRewardItems.length === 0" class="text-center py-8 text-gray-500">
115 + 暂无可兑换的商品
116 + </view>
117 +
118 + <!-- 商品列表 -->
119 + <template v-else>
120 + <view v-for="reward in sortedRewardItems" :key="reward.id"
121 + class="bg-white rounded-xl p-4 flex items-center shadow-[0_2px_8px_rgba(0,0,0,0.08)]">
122 + <image :src="reward.thumbnail" class="w-16 h-16 rounded-lg mr-4 flex-shrink-0" mode="aspectFill" />
123 + <view class="flex-1 min-w-0">
124 + <view class="font-medium text-base">{{ reward.title }}</view>
125 + <view class="text-gray-500 text-sm mt-1">{{ reward.merchant_name || '商户' }}</view>
126 + </view>
127 + <view class="ml-4 px-4 py-2 bg-blue-500 text-white rounded-lg text-sm flex-shrink-0"
128 + @click="goToRewardDetail(reward)">
129 + {{ isCreator ? reward.points_cost + '分兑换' : '查看' }}
130 + </view>
114 </view> 131 </view>
115 - <view class="ml-4 px-4 py-2 bg-blue-500 text-white rounded-lg text-sm flex-shrink-0" 132 +
116 - @click="goToRewardDetail(reward)"> 133 + <!-- 加载更多 -->
117 - {{ isCreator ? reward.points + '分兑换' : '查看' }} 134 + <view v-if="hasMore && !loading && sortedRewardItems.length > 0"
135 + class="text-center py-4 text-blue-500"
136 + @click="fetchCouponList()">
137 + 加载更多
118 </view> 138 </view>
119 - </view> 139 +
140 + <!-- 没有更多数据 -->
141 + <view v-if="!hasMore && sortedRewardItems.length > 0"
142 + class="text-center py-4 text-gray-500">
143 + 没有更多数据了
144 + </view>
145 + </template>
120 </view> 146 </view>
121 </view> 147 </view>
122 </view> 148 </view>
...@@ -125,11 +151,13 @@ ...@@ -125,11 +151,13 @@
125 </template> 151 </template>
126 152
127 <script setup> 153 <script setup>
128 -import { ref, computed, onMounted } from 'vue'; 154 +import { ref, computed, onMounted, watch } from 'vue';
129 import Taro, { useDidShow } from '@tarojs/taro'; 155 import Taro, { useDidShow } from '@tarojs/taro';
130 import { ScreenLittle, Search2, My, ArrowUp, ArrowDown } from '@nutui/icons-vue-taro'; 156 import { ScreenLittle, Search2, My, ArrowUp, ArrowDown } from '@nutui/icons-vue-taro';
157 +// 导入接口
131 import { getUserProfileAPI } from '@/api/user'; 158 import { getUserProfileAPI } from '@/api/user';
132 import { getFamilyDashboardAPI } from '@/api/family'; 159 import { getFamilyDashboardAPI } from '@/api/family';
160 +import { getPointRangesAPI, getCouponListAPI } from '@/api/coupon';
133 161
134 const searchQuery = ref(''); 162 const searchQuery = ref('');
135 const selectedPoints = ref(null); 163 const selectedPoints = ref(null);
...@@ -139,73 +167,59 @@ const totalPoints = ref(0); ...@@ -139,73 +167,59 @@ const totalPoints = ref(0);
139 const isCreator = ref(false); 167 const isCreator = ref(false);
140 const isStrategyExpanded = ref(false); // 积分攻略展开状态,默认收起 168 const isStrategyExpanded = ref(false); // 积分攻略展开状态,默认收起
141 169
170 +// API数据状态
171 +const pointRanges = ref([]);
172 +const couponList = ref([]);
173 +const loading = ref(false);
174 +const currentPage = ref(0);
175 +const hasMore = ref(true);
176 +
142 const sortedRewardItems = computed(() => { 177 const sortedRewardItems = computed(() => {
143 - let items = [...rewardItems.value]; 178 + // 确保couponList.value是数组类型
179 + const couponArray = Array.isArray(couponList.value) ? couponList.value : [];
180 + let items = [...couponArray];
144 181
145 // Filter by search query 182 // Filter by search query
146 if (searchQuery.value) { 183 if (searchQuery.value) {
147 items = items.filter(item => 184 items = items.filter(item =>
148 - item.merchant.toLowerCase().includes(searchQuery.value.toLowerCase()) 185 + item.title && item.title.toLowerCase().includes(searchQuery.value.toLowerCase())
149 ); 186 );
150 } 187 }
151 188
152 - // Sort items 189 + // 积分范围筛选现在由API处理,这里不需要前端筛选
153 - return items.sort((a, b) => { 190 + // 排序也由API处理,这里保持原始顺序
154 - if (sortOrder.value === 'asc') { 191 + return items;
155 - return a.points - b.points;
156 - } else {
157 - return b.points - a.points;
158 - }
159 - });
160 }); 192 });
161 193
162 const toggleSortOrder = () => { 194 const toggleSortOrder = () => {
163 sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'; 195 sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
164 }; 196 };
165 197
166 -const rewardItems = ref([ 198 +// 快速兑换选项基于API返回的积分范围
167 - { 199 +const quickExchangeOptions = computed(() => {
168 - id: 1, 200 + // 确保pointRanges.value是数组类型
169 - logo: 'https://placehold.co/400x400/e2f3ff/0369a1?text=LFX&font=roboto', 201 + const rangesArray = Array.isArray(pointRanges.value) ? pointRanges.value : [];
170 - title: '杏花楼集团85折券', 202 + return rangesArray.map((range, index) => {
171 - merchant: '杏花楼集团', 203 + // 分拆显示逻辑:将"1-100分"分为"1-100"和"分"两部分
172 - points: 10 204 + let displayLine1 = range;
173 - }, 205 + let displayLine2 = '';
174 - {
175 - id: 2,
176 - logo: 'https://placehold.co/400x400/e2f3ff/0369a1?text=LFX&font=roboto',
177 - title: '吴良材眼镜店85折券',
178 - merchant: '吴良材眼镜店',
179 - points: 10
180 - },
181 - {
182 - id: 3,
183 - logo: 'https://placehold.co/400x400/e2f3ff/0369a1?text=LFX&font=roboto',
184 - title: '老凤祥银楼20元抵用券',
185 - merchant: '老凤祥银楼',
186 - points: 1000
187 - },
188 - {
189 - id: 4,
190 - logo: 'https://placehold.co/400x400/e2f3ff/0369a1?text=LFX&font=roboto',
191 - title: '沈大成双酿团2元抵用券',
192 - merchant: '沈大成双酿团',
193 - points: 100
194 - },
195 - {
196 - id: 5,
197 - logo: 'https://placehold.co/400x400/e2f3ff/0369a1?text=LFX&font=roboto',
198 - title: '这是一个非常非常非常非常非常长的标题用于测试换行效果',
199 - merchant: '一个名字很长的商家',
200 - points: 500
201 - }
202 -]);
203 206
204 -const quickExchangeOptions = ref([ 207 + if (range && range.includes('分')) {
205 - { points: 3000, label: '1000-3000分' }, 208 + const parts = range.split('分');
206 - { points: 1000, label: '100-1000分' }, 209 + displayLine1 = parts[0]; // "1-100"
207 - { points: 100, label: '1-100分' } 210 + displayLine2 = '分'; // "分"
208 -]); 211 + }
212 +
213 + return {
214 + points: range,
215 + label: range,
216 + range: range, // 保持原始数据用于传值
217 + displayLine1: displayLine1, // 第一行显示内容
218 + displayLine2: displayLine2, // 第二行显示内容
219 + index: index
220 + };
221 + });
222 +});
209 223
210 const goToRewardDetail = (reward) => { 224 const goToRewardDetail = (reward) => {
211 Taro.navigateTo({ 225 Taro.navigateTo({
...@@ -229,6 +243,58 @@ const toggleStrategyExpand = () => { ...@@ -229,6 +243,58 @@ const toggleStrategyExpand = () => {
229 isStrategyExpanded.value = !isStrategyExpanded.value; 243 isStrategyExpanded.value = !isStrategyExpanded.value;
230 }; 244 };
231 245
246 +/**
247 + * 获取积分范围
248 + */
249 +const fetchPointRanges = async () => {
250 + try {
251 + const { code, data } = await getPointRangesAPI();
252 + if (code) {
253 + pointRanges.value = data || [];
254 + }
255 + } catch (error) {
256 + console.error('获取积分范围失败:', error);
257 + }
258 +};
259 +
260 +/**
261 + * 获取优惠券列表
262 + */
263 +const fetchCouponList = async (reset = false) => {
264 + if (loading.value || (!hasMore.value && !reset)) return;
265 +
266 + loading.value = true;
267 +
268 + try {
269 + const params = {
270 + keyword: searchQuery.value || undefined,
271 + point_range: selectedPoints.value || undefined,
272 + sort: sortOrder.value.toUpperCase(),
273 + page: reset ? 0 : currentPage.value,
274 + limit: 10
275 + };
276 +
277 + const { code, data } = await getCouponListAPI(params);
278 + if (code) {
279 + const coupons = data?.coupons || [];
280 + if (reset) {
281 + couponList.value = coupons;
282 + currentPage.value = 0;
283 + } else {
284 + couponList.value = [...couponList.value, ...coupons];
285 + }
286 +
287 + // 如果返回的数据少于limit,说明没有更多数据了
288 + hasMore.value = coupons.length >= 10;
289 + currentPage.value += 1;
290 + }
291 + } catch (error) {
292 + console.error('获取优惠券列表失败:', error);
293 + } finally {
294 + loading.value = false;
295 + }
296 +};
297 +
232 const initData = async () => { 298 const initData = async () => {
233 // 获取用户信息,判断是否为创建者 299 // 获取用户信息,判断是否为创建者
234 try { 300 try {
...@@ -250,9 +316,20 @@ const initData = async () => { ...@@ -250,9 +316,20 @@ const initData = async () => {
250 console.error('获取用户信息失败:', error); 316 console.error('获取用户信息失败:', error);
251 isCreator.value = false; 317 isCreator.value = false;
252 } 318 }
319 +
320 + // 获取积分兑换相关数据
321 + await Promise.all([
322 + fetchPointRanges(),
323 + fetchCouponList(true)
324 + ]);
253 console.warn('初始化数据') 325 console.warn('初始化数据')
254 } 326 }
255 327
328 +// 监听搜索和筛选条件变化
329 +watch([searchQuery, selectedPoints, sortOrder], () => {
330 + fetchCouponList(true);
331 +}, { deep: true });
332 +
256 useDidShow(() => { 333 useDidShow(() => {
257 initData(); 334 initData();
258 }) 335 })
......