- 移除新增收货地址页面 - 删除 ShopArticle 相关代码 - 优化优惠券详情页面布局- 更新优惠券筛选和使用逻辑 - 调整 app配置,重新启用优惠券功能 - 优化 Tailwind CSS配置,禁用不必要功能
472 lines
14 KiB
TypeScript
472 lines
14 KiB
TypeScript
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;
|