hookehuyr

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

重构优惠券列表页面,添加分页加载和状态筛选功能
优化优惠券详情页面,完善状态显示和核销逻辑
新增蓝色主题相关样式类
更新API接口路径和返回字段
......@@ -19,6 +19,7 @@ declare module 'vue' {
NutDialog: typeof import('@nutui/nutui-taro')['Dialog']
NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview']
NutInput: typeof import('@nutui/nutui-taro')['Input']
NutLoading: typeof import('@nutui/nutui-taro')['Loading']
NutPicker: typeof import('@nutui/nutui-taro')['Picker']
NutPopup: typeof import('@nutui/nutui-taro')['Popup']
NutRow: typeof import('@nutui/nutui-taro')['Row']
......
/*
* @Date: 2024-01-01 00:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-08 13:15:39
* @LastEditTime: 2025-09-08 16:23:23
* @FilePath: /lls_program/src/api/coupon.js
* @Description: 优惠券相关接口
*/
......@@ -15,7 +15,7 @@ const Api = {
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',
USE_COUPON: '/srv/?a=redemption&t=use',
}
/**
......@@ -111,6 +111,7 @@ export const redeemCouponAPI = (params) => fn(fetch.post(Api.REDEEM_COUPON, para
* @returns {string} response.data.thumbnail - 优惠券缩略图
* @returns {string} response.data.status - 优惠券状态, UNUSED=未使用, USED=已使用, EXPIRED=已过期
* @returns {string} response.data.expire_time - 有效期截止时间
* @returns {string} response.data.used_time - 核销时间
*/
export const getMyCouponListAPI = (params = {}) => fn(fetch.get(Api.MY_COUPON_LIST, params));
......@@ -127,6 +128,7 @@ export const getMyCouponListAPI = (params = {}) => fn(fetch.get(Api.MY_COUPON_LI
* @returns {string} response.data.banner - 优惠券banner图
* @returns {string} response.data.status - 优惠券状态, UNUSED=未使用, USED=已使用, EXPIRED=已过期
* @returns {string} response.data.expire_time - 有效期截止时间
* @returns {string} response.data.expiration_rules[string] - 有效期
* @returns {Array} response.data.applicable_stores[string] - 可用门店
* @returns {Array} response.data.usage_rules[string] - 使用规则
*/
......
......@@ -5,3 +5,11 @@
.bg-blue-500 {
background-color: #4A90E2 !important;
}
.text-blue-500 {
color: #4A90E2 !important;
}
.border-blue-500 {
border-color: #4A90E2 !important;
}
......
This diff is collapsed. Click to expand it.
<template>
<view class="min-h-screen bg-gray-100">
<!-- <AppHeader title="我的券" :showBack="true" /> -->
<view class="min-h-screen bg-gray-50">
<!-- Tabs -->
<view class="bg-white flex justify-around items-center border-b border-gray-200">
<view class="bg-white px-4 py-2 border-b border-gray-200">
<view class="flex space-x-6">
<view
v-for="tab in tabs"
:key="tab.name"
@click="activeTab = tab.name"
class="py-3 text-center w-1/4 text-base font-medium cursor-pointer"
:class="[
activeTab === tab.name
? 'text-blue-500 border-b-2 border-blue-500'
: 'text-gray-500'
]"
@click="handleTabChange(tab.name)"
:class="{
'text-blue-500 border-b-2 border-blue-500': activeTab === tab.name,
'text-gray-500': activeTab !== tab.name
}"
class="pb-2 px-1 text-sm font-medium transition-colors"
>
{{ tab.label }}
</view>
</view>
</view>
<!-- Loading State -->
<view v-if="loading" class="flex justify-center items-center py-20">
<view class="text-gray-500">加载中...</view>
</view>
<!-- Rewards List -->
<view class="p-4">
<!-- Content -->
<view v-else class="p-4">
<view v-if="filteredRewards.length > 0" class="space-y-4">
<view
v-for="reward in filteredRewards"
:key="reward.id"
class="bg-white rounded-lg shadow-sm p-4 flex justify-between items-center"
class="bg-white rounded-xl p-4 flex items-center shadow-[0_2px_8px_rgba(0,0,0,0.08)]"
>
<view class="flex-1">
<h3 class="text-lg font-semibold text-gray-800">{{ reward.title }}</h3>
<p class="text-sm text-gray-500 mt-1">有效期至 {{ reward.expiryDate }}</p>
<p v-if="reward.status === 'used'" class="text-sm text-gray-400 mt-1">使用于 {{ reward.usedDate }}</p>
<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" />
<view class="flex-1 min-w-0">
<view class="font-medium text-base mb-1">{{ reward.title }}</view>
<view v-if="reward.status === 'UNUSED'" class="text-xs text-gray-400">
有效期至:{{ formatDate(reward.expire_time) }}
</view>
<view v-if="reward.status === 'USED'" class="text-xs text-red-500">
使用日期:{{ formatDate(reward.used_time) }}
</view>
</view>
<view class="ml-4 flex flex-col items-end">
<button
@click="handleUseReward(reward)"
:disabled="reward.status !== 'unused'"
class="px-6 py-2 rounded-full text-white font-medium text-sm transition-colors"
:disabled="reward.status !== 'UNUSED'"
:class="{
'bg-blue-500 hover:bg-blue-600': reward.status === 'unused',
'bg-gray-300 cursor-not-allowed': reward.status !== 'unused'
'bg-blue-500 text-white': reward.status === 'UNUSED',
'bg-gray-300 text-gray-500': reward.status !== 'UNUSED'
}"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors flex-shrink-0"
>
{{ getButtonText(reward.status) }}
</button>
</view>
</view>
</view>
<!-- Empty State -->
<view v-else class="text-center py-20">
<p class="text-gray-500">暂无相关兑换券</p>
<view class="text-gray-400 text-lg mb-2">暂无券</view>
<view class="text-gray-500 text-sm">您还没有任何券</view>
</view>
<!-- Load More -->
<view v-if="hasMore && filteredRewards.length > 0" class="text-center mt-6">
<view
@click="loadMore"
class="text-blue-500 py-4"
>
{{ loading ? '加载中...' : '加载更多' }}
</view>
</view>
<!-- No More Data -->
<view v-if="!hasMore && filteredRewards.length > 0" class="text-center py-4 text-gray-500">
没有更多数据了
</view>
</view>
</view>
......@@ -56,104 +85,150 @@
import { ref, computed } from 'vue';
import Taro from '@tarojs/taro';
import { useDidShow } from '@tarojs/taro';
// 导入接口
import { getMyCouponListAPI } from '@/api/coupon';
const tabs = ref([
{ name: 'all', label: '全部' },
{ name: 'unused', label: '未使用' },
{ name: 'used', label: '已使用' },
{ name: 'expired', label: '已过期' },
{ name: 'all', label: '全部', status: '' },
{ name: 'unused', label: '未使用', status: 'UNUSED' },
{ name: 'used', label: '已使用', status: 'USED' },
{ name: 'expired', label: '已过期', status: 'EXPIRED' },
]);
const activeTab = ref('all');
const loading = ref(false);
const rewards = ref([]);
const currentPage = ref(0);
const hasMore = ref(true);
const filteredRewards = computed(() => {
if (activeTab.value === 'all') {
// 由于API已经根据status参数进行了筛选,这里直接返回rewards
return rewards.value;
}
return rewards.value.filter(reward => reward.status === activeTab.value);
});
/**
* 获取按钮文本
*/
const getButtonText = (status) => {
switch (status) {
case 'unused':
case 'UNUSED':
return '使用';
case 'used':
case 'USED':
return '已使用';
case 'expired':
case 'EXPIRED':
return '已过期';
default:
return '';
}
};
/**
* 获取状态文本
*/
// const getStatusText = (status) => {
// switch (status) {
// case 'UNUSED':
// return '未使用';
// case 'USED':
// return '已使用';
// case 'EXPIRED':
// return '已过期';
// default:
// return '';
// }
// };
/**
* 格式化日期
*/
const formatDate = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('zh-CN');
};
/**
* 处理优惠券使用
*/
const handleUseReward = (reward) => {
if (reward.status === 'unused') {
// Here you would typically navigate to a usage/QR code page
// For now, we can just log it or update the status for demo purposes
console.log(`Using reward: ${reward.title}`);
// Example of updating status:
// const item = rewards.value.find(r => r.id === reward.id);
// if (item) {
// item.status = 'used';
// item.usedDate = new Date().toISOString().split('T')[0];
// }
if (reward.status === 'UNUSED') {
// 跳转到卡券详情页
Taro.navigateTo({
url: '/pages/CouponDetail/index?id=' + reward.id
})
});
}
};
/**
* 获取我的优惠券列表
*/
const fetchMyCouponList = async (reset = false) => {
if (loading.value || (!hasMore.value && !reset)) return;
loading.value = true;
try {
const currentTab = tabs.value.find(tab => tab.name === activeTab.value);
const params = {
status: currentTab?.status || '',
page: reset ? 0 : currentPage.value,
limit: 10
};
const response = await getMyCouponListAPI(params);
if (response && response.data) {
const coupons = Array.isArray(response.data) ? response.data : [];
if (reset) {
rewards.value = coupons;
currentPage.value = 0;
} else {
rewards.value = [...rewards.value, ...coupons];
}
// 如果返回的数据少于limit,说明没有更多数据了
hasMore.value = coupons.length >= 10;
currentPage.value += 1;
}
} catch (error) {
console.error('获取我的优惠券列表失败:', error);
Taro.showToast({
title: '获取列表失败',
icon: 'error'
});
} finally {
loading.value = false;
}
};
/**
* 处理标签页切换
*/
const handleTabChange = (tabName) => {
if (activeTab.value === tabName) return;
activeTab.value = tabName;
currentPage.value = 0;
hasMore.value = true;
fetchMyCouponList(true); // 重置并重新加载数据
};
/**
* 加载更多
*/
const loadMore = () => {
fetchMyCouponList(false);
};
/**
* 初始化页面数据
*/
const initPageData = () => {
fetchMyCouponList(true);
};
// 页面显示时刷新数据
useDidShow(() => {
initPageData();
});
const initPageData = () => {
rewards.value = [
{
id: 1,
title: '杏花楼集团 85折券',
expiryDate: '2025-08-28',
status: 'unused',
usedDate: null,
},
{
id: 2,
title: '老凤祥银楼 20元抵用券',
expiryDate: '2025-08-28',
status: 'unused',
usedDate: null,
},
{
id: 3,
title: '吴良材眼镜 5折券',
expiryDate: '2024-05-20',
status: 'used',
usedDate: '2024-05-01',
},
{
id: 4,
title: '沈大成双酿团 免费券',
expiryDate: '2024-03-15',
status: 'expired',
usedDate: null,
},
{
id: 5,
title: '沈大成双酿团 免费券',
expiryDate: '2024-03-15',
status: 'expired',
usedDate: null,
},
{
id: 6,
title: '沈大成双酿团 免费券',
expiryDate: '2024-03-15',
status: 'expired',
usedDate: null,
},
];
}
</script>
......