补充个人中心页面api & 首页弹唱查看活动 & 联系客服

This commit is contained in:
Eamon
2026-09-08 16:19:40 +08:00
parent f59e929843
commit 7f03dbfb56
45 changed files with 2146 additions and 1359 deletions

View File

@@ -1,169 +1,106 @@
import { CheckCircle, Clock, Gift, Tag, Ticket, XCircle } from "lucide-react"
import { useMemo } from "react"
import { fetchMyCoupons } from "@/api/coupon"
import Page from "@/components/page"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { useList } from "@/lib/hooks/useList"
import type { MyCouponItem } from "@/lib/models/coupon"
import { formatDate } from "@/lib/utils"
// TODO: 优惠券接口就绪后接入(见 src/api/coupon.ts fetchCouponPage 等)
const COUPON_TYPE_DISCOUNT = 1
interface Coupon {
couponType: "Discount" | "Minus"
couponValue: number
name: string
allowMinAmount: number
}
function CouponCard({ item }: { item: MyCouponItem }) {
const isDiscount = item.Coupon.CouponType === COUPON_TYPE_DISCOUNT
const isUsed = item.IsUsed
interface UserCouponModel {
coupon: Coupon
orgin: {
startTime: string
endTime: string
remark: string
}
isUsed: boolean
}
return (
<Card
className={`relative overflow-hidden transition-shadow hover:shadow-md ${isUsed ? "opacity-60" : ""}`}
>
<CardContent className="p-4">
<div className="flex items-start gap-4">
<div className="flex min-w-20 flex-col items-center justify-center rounded-lg bg-primary/10 p-3">
{isDiscount ? (
<>
<span className="text-2xl font-bold text-primary">
{item.Coupon.CouponValue}
</span>
<span className="text-xs text-muted-foreground">折</span>
</>
) : (
<>
<span className="text-xs text-muted-foreground">¥</span>
<span className="text-2xl font-bold text-primary">
{item.Coupon.CouponValue}
</span>
</>
)}
</div>
// Mock 数据
const mockCoupons: UserCouponModel[] = [
{
coupon: {
couponType: "Discount",
couponValue: 8,
name: "八折优惠券",
allowMinAmount: 100,
},
orgin: {
startTime: "2026-01-01",
endTime: "2026-12-31",
remark: "新用户注册赠送",
},
isUsed: false,
},
{
coupon: {
couponType: "Minus",
couponValue: 50,
name: "满减优惠券",
allowMinAmount: 200,
},
orgin: {
startTime: "2026-06-01",
endTime: "2026-09-30",
remark: "活动期间领取",
},
isUsed: false,
},
{
coupon: {
couponType: "Discount",
couponValue: 9,
name: "九折优惠券",
allowMinAmount: 0,
},
orgin: {
startTime: "2026-08-01",
endTime: "2026-10-01",
remark: "VIP专享",
},
isUsed: true,
},
{
coupon: {
couponType: "Minus",
couponValue: 20,
name: "通用优惠券",
allowMinAmount: 50,
},
orgin: {
startTime: "2026-07-01",
endTime: "2026-08-31",
remark: "签到领取",
},
isUsed: false,
},
]
export default function CouponPage() {
const availableCoupons = mockCoupons.filter(item => !item.isUsed)
const usedCoupons = mockCoupons.filter(item => item.isUsed)
const renderCoupon = (item: UserCouponModel) => {
const isDiscount = item.coupon.couponType === "Discount"
const isUsed = item.isUsed
return (
<Card
key={`${item.coupon.name}-${item.orgin.startTime}`}
className={`relative overflow-hidden transition-shadow hover:shadow-md ${isUsed ? "opacity-60" : ""}`}
>
<CardContent className="p-4">
{/* 优惠券左侧 - 面额 */}
<div className="flex items-start gap-4">
<div className="flex min-w-20 flex-col items-center justify-center rounded-lg bg-primary/10 p-3">
{isDiscount ? (
<>
<span className="text-2xl font-bold text-primary">
{item.coupon.couponValue}
</span>
<span className="text-xs text-muted-foreground">折</span>
</>
<div className="flex-1 space-y-1">
<div className="flex items-center gap-2">
<h4 className="font-medium">{item.Coupon.Name}</h4>
{isUsed ? (
<Badge variant="secondary" className="text-xs">
<CheckCircle className="mr-1 h-3 w-3" />
已使用
</Badge>
) : item.IsExpired ? (
<Badge variant="secondary" className="text-xs">
<XCircle className="mr-1 h-3 w-3" />
已过期
</Badge>
) : (
<>
<span className="text-xs text-muted-foreground">¥</span>
<span className="text-2xl font-bold text-primary">
{item.coupon.couponValue}
</span>
</>
<Badge variant="default" className="text-xs">
<Ticket className="mr-1 h-3 w-3" />
可使用
</Badge>
)}
</div>
{/* 优惠券右侧 - 详情 */}
<div className="flex-1 space-y-1">
<div className="flex items-center gap-2">
<h4 className="font-medium">{item.coupon.name}</h4>
{isUsed ? (
<Badge variant="secondary" className="text-xs">
<CheckCircle className="mr-1 h-3 w-3" />
已使用
</Badge>
) : (
<Badge variant="default" className="text-xs">
<Ticket className="mr-1 h-3 w-3" />
可使用
</Badge>
)}
</div>
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<Tag className="h-3 w-3" />
{item.coupon.allowMinAmount > 0
? `满${item.coupon.allowMinAmount}元可用`
: "无限制"}
</span>
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{new Date(item.orgin.startTime).toLocaleDateString()} -{" "}
{new Date(item.orgin.endTime).toLocaleDateString()}
</span>
<span className="flex items-center gap-1">
<Gift className="h-3 w-3" />
{item.orgin.remark}
</span>
</div>
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<Tag className="h-3 w-3" />
{item.Coupon.AllowMinAmount > 0
? `满${item.Coupon.AllowMinAmount}元可用`
: "无限制"}
</span>
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{formatDate(item.Orgin.StartTime)} -{" "}
{formatDate(item.Orgin.EndTime)}
</span>
<span className="flex items-center gap-1">
<Gift className="h-3 w-3" />
{item.Orgin.Remark}
</span>
</div>
</div>
</div>
{/* 已使用水印效果 */}
{isUsed && (
<div className="absolute right-4 top-4">
<XCircle className="h-6 w-6 text-muted-foreground/50" />
</div>
)}
</CardContent>
</Card>
)
}
{isUsed && (
<div className="absolute right-4 top-4">
<XCircle className="h-6 w-6 text-muted-foreground/50" />
</div>
)}
</CardContent>
</Card>
)
}
export default function CouponPage() {
const { data: coupons, status } = useList(fetchMyCoupons)
const availableCoupons = useMemo(
() => coupons.filter(item => !item.IsUsed && !item.IsExpired),
[coupons],
)
const usedCoupons = useMemo(
() => coupons.filter(item => item.IsUsed),
[coupons],
)
return (
<Page>
@@ -172,7 +109,7 @@ export default function CouponPage() {
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">我的优惠券</h3>
<Badge variant="secondary" className="text-sm">
共 {mockCoupons.length} 张
共 {coupons.length} 张
</Badge>
</div>
@@ -187,7 +124,16 @@ export default function CouponPage() {
</TabsList>
<TabsContent value="available" className="mt-4">
{availableCoupons.length === 0 ? (
{status === "load" ? (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<p>加载中...</p>
</div>
) : status === "fail" ? (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<XCircle className="mb-4 h-12 w-12 opacity-50" />
<p>加载失败,请刷新重试</p>
</div>
) : availableCoupons.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<Ticket className="mb-4 h-12 w-12 opacity-50" />
<p>暂无可用优惠券</p>
@@ -195,7 +141,9 @@ export default function CouponPage() {
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{availableCoupons.map(renderCoupon)}
{availableCoupons.map(item => (
<CouponCard key={item.Orgin.Id} item={item} />
))}
</div>
)}
</TabsContent>
@@ -208,7 +156,9 @@ export default function CouponPage() {
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{usedCoupons.map(renderCoupon)}
{usedCoupons.map(item => (
<CouponCard key={item.Orgin.Id} item={item} />
))}
</div>
)}
</TabsContent>