Files
glt-taro/src/user/coupon/index.tsx
赵忠林 5d4dc4518f refactor(user): 优化用户优惠券相关功能
- 移除新增收货地址页面
- 删除 ShopArticle 相关代码
- 优化优惠券详情页面布局- 更新优惠券筛选和使用逻辑
- 调整 app配置,重新启用优惠券功能
- 优化 Tailwind CSS配置,禁用不必要功能
2025-08-13 10:44:27 +08:00

472 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
import {Button, Empty, ConfigProvider, SearchBar, InfiniteLoading, Loading, PullToRefresh, Tabs, TabPane} from '@nutui/nutui-react-taro'
import {Gift, Search, Plus, Filter} from '@nutui/icons-react-taro'
import {View} from '@tarojs/components'
import {ShopCoupon} from "@/api/shop/shopCoupon/model";
import {pageShopCoupon} from "@/api/shop/shopCoupon";
import CouponList from "@/components/CouponList";
import CouponStats from "@/components/CouponStats";
import CouponGuide from "@/components/CouponGuide";
import CouponFilter from "@/components/CouponFilter";
import CouponExpireNotice, {ExpiringSoon} from "@/components/CouponExpireNotice";
import {CouponCardProps} from "@/components/CouponCard";
import dayjs from "dayjs";
const CouponManage = () => {
const [list, setList] = useState<ShopCoupon[]>([])
const [loading, setLoading] = useState(false)
const [hasMore, setHasMore] = useState(true)
const [searchValue, setSearchValue] = useState('')
const [page, setPage] = useState(1)
const [total, setTotal] = useState(0)
const [activeTab, setActiveTab] = useState('0') // 0-可用 1-已使用 2-已过期
const [stats, setStats] = useState({
available: 0,
used: 0,
expired: 0
})
const [showGuide, setShowGuide] = useState(false)
const [showFilter, setShowFilter] = useState(false)
const [showExpireNotice, setShowExpireNotice] = useState(false)
const [expiringSoonCoupons, setExpiringSoonCoupons] = useState<ExpiringSoon[]>([])
const [filters, setFilters] = useState({
type: [] as number[],
minAmount: undefined as number | undefined,
sortBy: 'createTime' as 'createTime' | 'amount' | 'expireTime',
sortOrder: 'desc' as 'asc' | 'desc'
})
// 获取优惠券状态过滤条件
const getStatusFilter = () => {
switch (activeTab) {
case '0': // 可用
return { status: 0, isExpire: 0 }
case '1': // 已使用
return { status: 1 }
case '2': // 已过期
return { isExpire: 1 }
default:
return {}
}
}
const reload = async (isRefresh = false) => {
if (isRefresh) {
setPage(1)
setList([])
setHasMore(true)
}
setLoading(true)
try {
const currentPage = isRefresh ? 1 : page
const statusFilter = getStatusFilter()
const res = await pageShopCoupon({
page: currentPage,
limit: 10,
keywords: searchValue,
...statusFilter,
// 应用筛选条件
...(filters.type.length > 0 && { type: filters.type[0] }),
...(filters.minAmount && { minAmount: filters.minAmount }),
sortBy: filters.sortBy,
sortOrder: filters.sortOrder
})
if (res && res.list) {
const newList = isRefresh ? res.list : [...list, ...res.list]
setList(newList)
setTotal(res.count || 0)
// 判断是否还有更多数据
setHasMore(res.list.length === 10) // 如果返回的数据等于limit说明可能还有更多
if (!isRefresh) {
setPage(currentPage + 1)
} else {
setPage(2) // 刷新后下一页是第2页
}
} else {
setHasMore(false)
setTotal(0)
}
} catch (error) {
console.error('获取优惠券失败:', error)
Taro.showToast({
title: '获取优惠券失败',
icon: 'error'
});
} finally {
setLoading(false)
}
}
// 搜索功能
const handleSearch = (value: string) => {
setSearchValue(value)
reload(true)
}
// 下拉刷新
const handleRefresh = async () => {
await reload(true)
}
// Tab切换
const handleTabChange = (value: string) => {
setActiveTab(value)
setPage(1)
setList([])
setHasMore(true)
// 延迟执行reload确保状态更新完成
setTimeout(() => {
reload(true)
}, 100)
}
// 转换优惠券数据为CouponCard组件所需格式
const transformCouponData = (coupon: ShopCoupon): CouponCardProps => {
// 判断优惠券状态
let status: 0 | 1 | 2 = 0 // 默认未使用
if (coupon.isExpire === 1) {
status = 2 // 已过期
} else if (coupon.status === 1) {
status = 1 // 已使用
}
// 根据优惠券类型计算金额显示
let amount = 0
let type: 1 | 2 | 3 = 1
if (coupon.type === 10) { // 满减券
type = 1
amount = parseFloat(coupon.reducePrice || '0')
} else if (coupon.type === 20) { // 折扣券
type = 2
amount = coupon.discount || 0
} else if (coupon.type === 30) { // 免费券
type = 3
amount = 0
}
return {
amount,
type,
status,
minAmount: parseFloat(coupon.minPrice || '0'),
title: coupon.name || '优惠券',
startTime: coupon.startTime,
endTime: coupon.endTime,
showUseBtn: status === 0, // 只有未使用的券显示使用按钮
onUse: () => handleUseCoupon(coupon),
theme: getThemeByType(coupon.type)
}
}
// 根据优惠券类型获取主题色
const getThemeByType = (type?: number): 'red' | 'orange' | 'blue' | 'purple' | 'green' => {
switch (type) {
case 10: return 'red' // 满减券
case 20: return 'orange' // 折扣券
case 30: return 'green' // 免费券
default: return 'blue'
}
}
// 使用优惠券
const handleUseCoupon = (coupon: ShopCoupon) => {
Taro.showModal({
title: '使用优惠券',
content: `确定要使用"${coupon.name}"吗?`,
success: (res) => {
if (res.confirm) {
// 这里可以跳转到商品页面或购物车页面
Taro.navigateTo({
url: '/pages/index/index'
})
}
}
})
}
// 优惠券点击事件
const handleCouponClick = (coupon: CouponCardProps, index: number) => {
const originalCoupon = list[index]
if (originalCoupon) {
// 显示优惠券详情
showCouponDetail(originalCoupon)
}
}
// 显示优惠券详情
const showCouponDetail = (coupon: ShopCoupon) => {
// 跳转到优惠券详情页
Taro.navigateTo({
url: `/user/coupon/detail?id=${coupon.id}`
})
}
// 加载优惠券统计数据
const loadCouponStats = async () => {
try {
// 并行获取各状态的优惠券数量
const [availableRes, usedRes, expiredRes] = await Promise.all([
pageShopCoupon({ page: 1, limit: 1, status: 0, isExpire: 0 }),
pageShopCoupon({ page: 1, limit: 1, status: 1 }),
pageShopCoupon({ page: 1, limit: 1, isExpire: 1 })
])
setStats({
available: availableRes?.count || 0,
used: usedRes?.count || 0,
expired: expiredRes?.count || 0
})
} catch (error) {
console.error('获取优惠券统计失败:', error)
}
}
// 统计卡片点击事件
const handleStatsClick = (type: 'available' | 'used' | 'expired') => {
const tabMap = {
available: '0',
used: '1',
expired: '2'
}
handleTabChange(tabMap[type])
}
// 筛选条件变更
const handleFiltersChange = (newFilters: any) => {
setFilters(newFilters)
reload(true).then()
}
// 检查即将过期的优惠券
const checkExpiringSoonCoupons = async () => {
try {
// 获取即将过期的优惠券3天内过期
const res = await pageShopCoupon({
page: 1,
limit: 50,
status: 0, // 未使用
isExpire: 0 // 未过期
})
if (res && res.list) {
const now = dayjs()
const expiringSoon = res.list
.map(coupon => {
const endTime = dayjs(coupon.endTime)
const daysLeft = endTime.diff(now, 'day')
return {
id: coupon.id || 0,
name: coupon.name || '',
type: coupon.type || 10,
amount: coupon.type === 10 ? coupon.reducePrice || '0' :
coupon.type === 20 ? coupon.discount?.toString() || '0' : '0',
minAmount: coupon.minPrice,
endTime: coupon.endTime || '',
daysLeft
}
})
.filter(coupon => coupon.daysLeft >= 0 && coupon.daysLeft <= 3)
.sort((a, b) => a.daysLeft - b.daysLeft)
if (expiringSoon.length > 0) {
setExpiringSoonCoupons(expiringSoon)
// 延迟显示提醒,避免与页面加载冲突
setTimeout(() => {
setShowExpireNotice(true)
}, 1000)
}
}
} catch (error) {
console.error('检查即将过期优惠券失败:', error)
}
}
// 使用即将过期的优惠券
const handleUseExpiringSoonCoupon = (coupon: ExpiringSoon) => {
console.log(coupon, '使用即将过期优惠券')
setShowExpireNotice(false)
// 跳转到商品页面
Taro.navigateTo({
url: '/pages/index/index'
})
}
// 加载更多
const loadMore = async () => {
if (!loading && hasMore) {
await reload(false) // 不刷新,追加数据
}
}
useDidShow(() => {
reload(true).then()
loadCouponStats().then()
// 只在可用优惠券tab时检查即将过期的优惠券
if (activeTab === '0') {
checkExpiringSoonCoupons().then()
}
});
return (
<ConfigProvider>
{/* 搜索栏和领取入口 */}
<View className="bg-white px-4 py-3">
<View className="flex items-center gap-3">
<View className="flex-1">
<SearchBar
placeholder="搜索优惠券名称"
value={searchValue}
onChange={setSearchValue}
onSearch={handleSearch}
leftIcon={<Search />}
/>
</View>
<Button
size="small"
type="primary"
icon={<Plus />}
onClick={() => Taro.navigateTo({url: '/user/coupon/receive'})}
>
</Button>
<Button
size="small"
fill="outline"
icon={<Filter />}
onClick={() => setShowFilter(true)}
>
</Button>
<Button
size="small"
fill="outline"
onClick={() => setShowGuide(true)}
>
</Button>
</View>
</View>
{/* 优惠券统计 */}
<CouponStats
availableCount={stats.available}
usedCount={stats.used}
expiredCount={stats.expired}
onStatsClick={handleStatsClick}
/>
{/* Tab切换 */}
<View className="bg-white">
<Tabs value={activeTab} onChange={handleTabChange}>
<TabPane title="可用" value="0">
</TabPane>
<TabPane title="已使用" value="1">
</TabPane>
<TabPane title="已过期" value="2">
</TabPane>
</Tabs>
</View>
{/* 统计信息 */}
{total > 0 && (
<View className="px-4 py-2 text-sm text-gray-500 bg-gray-50">
{total}
</View>
)}
{/* 优惠券列表 */}
<PullToRefresh
onRefresh={handleRefresh}
headHeight={60}
>
<View style={{ height: 'calc(100vh - 200px)', overflowY: 'auto' }} id="coupon-scroll">
{list.length === 0 && !loading ? (
<View className="flex flex-col justify-center items-center" style={{height: 'calc(100vh - 300px)'}}>
<Empty
description={
activeTab === '0' ? "暂无可用优惠券" :
activeTab === '1' ? "暂无已使用优惠券" :
"暂无已过期优惠券"
}
style={{backgroundColor: 'transparent'}}
/>
</View>
) : (
<InfiniteLoading
target="coupon-scroll"
hasMore={hasMore}
onLoadMore={loadMore}
loadingText={
<View className="flex justify-center items-center py-4">
<Loading />
<View className="ml-2">...</View>
</View>
}
loadMoreText={
<View className="text-center py-4 text-gray-500">
{list.length === 0 ? "暂无数据" : "没有更多了"}
</View>
}
>
<CouponList
coupons={list.map(transformCouponData)}
onCouponClick={handleCouponClick}
showEmpty={false}
/>
</InfiniteLoading>
)}
</View>
</PullToRefresh>
{/* 底部提示 */}
{activeTab === '0' && list.length === 0 && !loading && (
<View className="text-center py-8">
<View className="text-gray-400 mb-4">
<Gift size="48" />
</View>
<View className="text-gray-500 mb-2"></View>
<Button
size="small"
type="primary"
onClick={() => Taro.navigateTo({url: '/pages/index/index'})}
>
</Button>
</View>
)}
{/* 使用指南弹窗 */}
<CouponGuide
visible={showGuide}
onClose={() => setShowGuide(false)}
/>
{/*/!* 筛选弹窗 *!/*/}
<CouponFilter
visible={showFilter}
filters={filters}
onFiltersChange={handleFiltersChange}
onClose={() => setShowFilter(false)}
/>
{/*/!* 到期提醒弹窗 *!/*/}
<CouponExpireNotice
visible={showExpireNotice}
expiringSoonCoupons={expiringSoonCoupons}
onClose={() => setShowExpireNotice(false)}
onUseCoupon={handleUseExpiringSoonCoupon}
/>
</ConfigProvider>
);
};
export default CouponManage;