hookehuyr

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

- 移除静态数据,改为从API获取积分范围和优惠券列表
- 实现搜索、筛选和排序功能
- 添加加载状态和空状态显示
- 更新API端点路径,移除冗余参数
- 修复快速兑换选项的点击逻辑
......@@ -8,14 +8,14 @@
import { fn, fetch } from './fn';
const Api = {
COUPON_HOME: '/srv/?f=walk&a=coupon&t=home',
POINT_RANGES: '/srv/?f=walk&a=coupon&t=point_ranges',
COUPON_LIST: '/srv/?f=walk&a=coupon&t=list',
COUPON_DETAIL: '/srv/?f=walk&a=coupon&t=detail',
REDEEM_COUPON: '/srv/?f=walk&a=coupon&t=redeem',
MY_COUPON_LIST: '/srv/?f=walk&a=redemption&t=list',
MY_COUPON_DETAIL: '/srv/?f=walk&a=redemption&t=detail',
USE_COUPON: '/srv/?f=walk&a=coupon&t=use',
COUPON_HOME: '/srv/?a=coupon&t=home',
POINT_RANGES: '/srv/?a=coupon&t=point_ranges',
COUPON_LIST: '/srv/?a=coupon&t=list',
COUPON_DETAIL: '/srv/?a=coupon&t=detail',
REDEEM_COUPON: '/srv/?a=coupon&t=redeem',
MY_COUPON_LIST: '/srv/?a=redemption&t=list',
MY_COUPON_DETAIL: '/srv/?a=redemption&t=detail',
USE_COUPON: '/srv/?a=coupon&t=use',
}
/**
......
/*
* @Date: 2023-12-22 10:29:37
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-04 10:09:32
* @LastEditTime: 2025-09-08 12:24:35
* @FilePath: /lls_program/src/api/points.js
* @Description: 文件描述
*/
......@@ -10,7 +10,7 @@ import { fn, fetch } from './fn';
const Api = {
SYNC_WX_STEP: '/srv/?a=point&t=sync_wx_step',
COLLECT_POINT: '/srv/?a=point&t=collect',
POINT_LIST: '/srv/?f=walk&a=point&t=list',
POINT_LIST: '/srv/?a=point&t=list',
}
/**
......
<!--
* @Date: 2025-08-27 17:47:03
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-05 17:23:10
* @LastEditTime: 2025-09-08 12:13:02
* @FilePath: /lls_program/src/pages/RewardCategories/index.vue
* @Description: 积分兑换分类
-->
......@@ -33,29 +33,7 @@ import BottomNav from '../../components/BottomNav.vue';
// 导入接口
import { getCouponHomeAPI } from '@/api/coupon';
const categories = ref([
{
id: 'health',
title: '银龄健康特色兑换区',
note: '南京商圈线下实体店消费积分兑换',
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",
tips: ''
},
{
id: 'online',
title: '民政领域网上商城"银龄购"',
note: '线上康复健康购物积分兑换',
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',
tips: '请在"银龄购"线上商城进行积分兑换'
},
{
id: 'merchants',
title: '人驻商户多元场景广覆盖',
note: '丰富商户积分兑换',
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',
tips: ''
}
]);
const categories = ref([]);
const goToRewards = (category) => {
if (!category.tips) {
......@@ -77,8 +55,7 @@ const goToRewards = (category) => {
onMounted(async () => {
const { code, data } = await getCouponHomeAPI();
if (code) {
// categories.value = data;
console.warn(data);
categories.value = data;
}
})
</script>
......
<!--
* @Date: 2025-08-27 17:47:26
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-05 10:39:16
* @LastEditTime: 2025-09-08 12:25:56
* @FilePath: /lls_program/src/pages/Rewards/index.vue
* @Description: 文件描述
-->
......@@ -83,14 +83,14 @@
</view>
<view class="flex gap-3 mb-6">
<view v-for="option in quickExchangeOptions" :key="option.points" @click="selectedPoints = option.points"
<view v-for="option in quickExchangeOptions" :key="option.index" @click="selectedPoints = selectedPoints === option.range ? null : option.range"
:class="[
'flex-1 py-3 rounded-lg border text-center',
selectedPoints === option.points
selectedPoints === option.range
? 'border-blue-500 bg-blue-50 text-blue-500'
: 'border-gray-200 text-gray-700'
]">
<view class="text-center">
<view class="text-center text-xs">
<view>{{ option.label }}</view>
<view>可兑</view>
</view>
......@@ -105,18 +105,44 @@
</view>
<!-- Rewards list -->
<view class="space-y-4">
<!-- 加载状态 -->
<view v-if="loading && couponList.length === 0" class="text-center py-8 text-gray-500">
加载中...
</view>
<!-- 空状态 -->
<view v-else-if="!loading && sortedRewardItems.length === 0" class="text-center py-8 text-gray-500">
暂无可兑换的商品
</view>
<!-- 商品列表 -->
<template v-else>
<view v-for="reward in sortedRewardItems" :key="reward.id"
class="bg-white rounded-xl p-4 flex items-center shadow-[0_2px_8px_rgba(0,0,0,0.08)]">
<image :src="reward.logo" class="w-16 h-16 rounded-lg mr-4 flex-shrink-0" mode="aspectFill" />
<image :src="reward.thumbnail" class="w-16 h-16 rounded-lg mr-4 flex-shrink-0" mode="aspectFill" />
<view class="flex-1 min-w-0">
<view class="font-medium text-base">{{ reward.title }}</view>
<view class="text-gray-500 text-sm mt-1">{{ reward.merchant }}</view>
<view class="text-gray-500 text-sm mt-1">{{ reward.merchant_name || '商户' }}</view>
</view>
<view class="ml-4 px-4 py-2 bg-blue-500 text-white rounded-lg text-sm flex-shrink-0"
@click="goToRewardDetail(reward)">
{{ isCreator ? reward.points + '分兑换' : '查看' }}
{{ isCreator ? reward.points_cost + '分兑换' : '查看' }}
</view>
</view>
<!-- 加载更多 -->
<view v-if="hasMore && !loading && sortedRewardItems.length > 0"
class="text-center py-4 text-blue-500"
@click="fetchCouponList()">
加载更多
</view>
<!-- 没有更多数据 -->
<view v-if="!hasMore && sortedRewardItems.length > 0"
class="text-center py-4 text-gray-500">
没有更多数据了
</view>
</template>
</view>
</view>
</view>
......@@ -125,11 +151,13 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { ref, computed, onMounted, watch } from 'vue';
import Taro, { useDidShow } from '@tarojs/taro';
import { ScreenLittle, Search2, My, ArrowUp, ArrowDown } from '@nutui/icons-vue-taro';
// 导入接口
import { getUserProfileAPI } from '@/api/user';
import { getFamilyDashboardAPI } from '@/api/family';
import { getPointRangesAPI, getCouponListAPI } from '@/api/coupon';
const searchQuery = ref('');
const selectedPoints = ref(null);
......@@ -139,73 +167,59 @@ const totalPoints = ref(0);
const isCreator = ref(false);
const isStrategyExpanded = ref(false); // 积分攻略展开状态,默认收起
// API数据状态
const pointRanges = ref([]);
const couponList = ref([]);
const loading = ref(false);
const currentPage = ref(0);
const hasMore = ref(true);
const sortedRewardItems = computed(() => {
let items = [...rewardItems.value];
// 确保couponList.value是数组类型
const couponArray = Array.isArray(couponList.value) ? couponList.value : [];
let items = [...couponArray];
// Filter by search query
if (searchQuery.value) {
items = items.filter(item =>
item.merchant.toLowerCase().includes(searchQuery.value.toLowerCase())
item.title && item.title.toLowerCase().includes(searchQuery.value.toLowerCase())
);
}
// Sort items
return items.sort((a, b) => {
if (sortOrder.value === 'asc') {
return a.points - b.points;
} else {
return b.points - a.points;
}
});
// 积分范围筛选现在由API处理,这里不需要前端筛选
// 排序也由API处理,这里保持原始顺序
return items;
});
const toggleSortOrder = () => {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
};
const rewardItems = ref([
{
id: 1,
logo: 'https://placehold.co/400x400/e2f3ff/0369a1?text=LFX&font=roboto',
title: '杏花楼集团85折券',
merchant: '杏花楼集团',
points: 10
},
{
id: 2,
logo: 'https://placehold.co/400x400/e2f3ff/0369a1?text=LFX&font=roboto',
title: '吴良材眼镜店85折券',
merchant: '吴良材眼镜店',
points: 10
},
{
id: 3,
logo: 'https://placehold.co/400x400/e2f3ff/0369a1?text=LFX&font=roboto',
title: '老凤祥银楼20元抵用券',
merchant: '老凤祥银楼',
points: 1000
},
{
id: 4,
logo: 'https://placehold.co/400x400/e2f3ff/0369a1?text=LFX&font=roboto',
title: '沈大成双酿团2元抵用券',
merchant: '沈大成双酿团',
points: 100
},
{
id: 5,
logo: 'https://placehold.co/400x400/e2f3ff/0369a1?text=LFX&font=roboto',
title: '这是一个非常非常非常非常非常长的标题用于测试换行效果',
merchant: '一个名字很长的商家',
points: 500
// 快速兑换选项基于API返回的积分范围
const quickExchangeOptions = computed(() => {
// 确保pointRanges.value是数组类型
const rangesArray = Array.isArray(pointRanges.value) ? pointRanges.value : [];
return rangesArray.map((range, index) => {
// 分拆显示逻辑:将"1-100分"分为"1-100"和"分"两部分
let displayLine1 = range;
let displayLine2 = '';
if (range && range.includes('分')) {
const parts = range.split('分');
displayLine1 = parts[0]; // "1-100"
displayLine2 = '分'; // "分"
}
]);
const quickExchangeOptions = ref([
{ points: 3000, label: '1000-3000分' },
{ points: 1000, label: '100-1000分' },
{ points: 100, label: '1-100分' }
]);
return {
points: range,
label: range,
range: range, // 保持原始数据用于传值
displayLine1: displayLine1, // 第一行显示内容
displayLine2: displayLine2, // 第二行显示内容
index: index
};
});
});
const goToRewardDetail = (reward) => {
Taro.navigateTo({
......@@ -229,6 +243,58 @@ const toggleStrategyExpand = () => {
isStrategyExpanded.value = !isStrategyExpanded.value;
};
/**
* 获取积分范围
*/
const fetchPointRanges = async () => {
try {
const { code, data } = await getPointRangesAPI();
if (code) {
pointRanges.value = data || [];
}
} catch (error) {
console.error('获取积分范围失败:', error);
}
};
/**
* 获取优惠券列表
*/
const fetchCouponList = async (reset = false) => {
if (loading.value || (!hasMore.value && !reset)) return;
loading.value = true;
try {
const params = {
keyword: searchQuery.value || undefined,
point_range: selectedPoints.value || undefined,
sort: sortOrder.value.toUpperCase(),
page: reset ? 0 : currentPage.value,
limit: 10
};
const { code, data } = await getCouponListAPI(params);
if (code) {
const coupons = data?.coupons || [];
if (reset) {
couponList.value = coupons;
currentPage.value = 0;
} else {
couponList.value = [...couponList.value, ...coupons];
}
// 如果返回的数据少于limit,说明没有更多数据了
hasMore.value = coupons.length >= 10;
currentPage.value += 1;
}
} catch (error) {
console.error('获取优惠券列表失败:', error);
} finally {
loading.value = false;
}
};
const initData = async () => {
// 获取用户信息,判断是否为创建者
try {
......@@ -250,9 +316,20 @@ const initData = async () => {
console.error('获取用户信息失败:', error);
isCreator.value = false;
}
// 获取积分兑换相关数据
await Promise.all([
fetchPointRanges(),
fetchCouponList(true)
]);
console.warn('初始化数据')
}
// 监听搜索和筛选条件变化
watch([searchQuery, selectedPoints, sortOrder], () => {
fetchCouponList(true);
}, { deep: true });
useDidShow(() => {
initData();
})
......