|
|
@@ -0,0 +1,311 @@
|
|
|
+import { useState } from 'react'
|
|
|
+import { View, Text, ScrollView, Input } from '@tarojs/components'
|
|
|
+import Taro from '@tarojs/taro'
|
|
|
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
|
+import { useForm } from 'react-hook-form'
|
|
|
+import { zodResolver } from '@hookform/resolvers/zod'
|
|
|
+import { z } from 'zod'
|
|
|
+import { redemptionCodeClient, wechatCouponClient } from '@/api'
|
|
|
+import { checkAuth, redirectToLogin, getCurrentUserId } from '@/utils/coupon-api'
|
|
|
+import { Button } from '@/components/ui/button'
|
|
|
+import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
|
|
+import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
|
|
|
+import { cn } from '@/utils/cn'
|
|
|
+
|
|
|
+// 兑换码验证schema
|
|
|
+const redeemSchema = z.object({
|
|
|
+ code: z.string()
|
|
|
+ .min(1, '请输入兑换码')
|
|
|
+ .max(20, '兑换码不能超过20位')
|
|
|
+ .regex(/^[a-zA-Z0-9]+$/, '兑换码只能包含字母和数字')
|
|
|
+})
|
|
|
+
|
|
|
+type RedeemFormData = z.infer<typeof redeemSchema>
|
|
|
+
|
|
|
+interface WechatCoupon {
|
|
|
+ id: number
|
|
|
+ couponCode: string
|
|
|
+ couponType: string
|
|
|
+ couponAmount: number
|
|
|
+ couponName: string
|
|
|
+ couponDescription: string
|
|
|
+ couponStatus: number
|
|
|
+ validBeginTime: string
|
|
|
+ validEndTime: string
|
|
|
+ receiveTime: string
|
|
|
+}
|
|
|
+
|
|
|
+export default function DuihuanPage() {
|
|
|
+ const queryClient = useQueryClient()
|
|
|
+ const [activeTab, setActiveTab] = useState<'redeem' | 'myCoupons'>('redeem')
|
|
|
+
|
|
|
+ // 兑换码表单
|
|
|
+ const form = useForm<RedeemFormData>({
|
|
|
+ resolver: zodResolver(redeemSchema),
|
|
|
+ defaultValues: {
|
|
|
+ code: ''
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 获取我的优惠券列表
|
|
|
+ const { data: coupons, isLoading: couponsLoading } = useQuery({
|
|
|
+ queryKey: ['my-coupons'],
|
|
|
+ queryFn: async () => {
|
|
|
+ if (!checkAuth()) {
|
|
|
+ throw new Error('请先登录')
|
|
|
+ }
|
|
|
+
|
|
|
+ const userId = getCurrentUserId()
|
|
|
+ if (!userId) throw new Error('请先登录')
|
|
|
+
|
|
|
+ const response = await wechatCouponClient.$get({
|
|
|
+ query: {
|
|
|
+ page: 1,
|
|
|
+ pageSize: 50,
|
|
|
+ filters: JSON.stringify({ userId })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ if (response.status !== 200) throw new Error('获取优惠券失败')
|
|
|
+ const result = await response.json()
|
|
|
+ return result.data as WechatCoupon[]
|
|
|
+ },
|
|
|
+ enabled: activeTab === 'myCoupons' && checkAuth()
|
|
|
+ })
|
|
|
+
|
|
|
+ // 兑换码兑换
|
|
|
+ const redeemMutation = useMutation({
|
|
|
+ mutationFn: async (code: string) => {
|
|
|
+ if (!checkAuth()) {
|
|
|
+ throw new Error('请先登录')
|
|
|
+ }
|
|
|
+
|
|
|
+ const userId = getCurrentUserId()
|
|
|
+ if (!userId) {
|
|
|
+ throw new Error('请先登录')
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await redemptionCodeClient.redeem.$post({
|
|
|
+ json: { code, userId }
|
|
|
+ })
|
|
|
+ if (response.status !== 200) {
|
|
|
+ const error = await response.json()
|
|
|
+ throw new Error(error.message || '兑换失败')
|
|
|
+ }
|
|
|
+ return response.json()
|
|
|
+ },
|
|
|
+ onSuccess: () => {
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['my-coupons'] })
|
|
|
+ form.reset()
|
|
|
+ Taro.showToast({
|
|
|
+ title: '兑换成功',
|
|
|
+ icon: 'success'
|
|
|
+ })
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ if (error instanceof Error && error.message === '请先登录') {
|
|
|
+ Taro.showModal({
|
|
|
+ title: '提示',
|
|
|
+ content: '请先登录后再兑换优惠券',
|
|
|
+ success: (res) => {
|
|
|
+ if (res.confirm) {
|
|
|
+ redirectToLogin()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ Taro.showToast({
|
|
|
+ title: error instanceof Error ? error.message : '兑换失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ const handleRedeem = (data: RedeemFormData) => {
|
|
|
+ if (!checkAuth()) {
|
|
|
+ Taro.showModal({
|
|
|
+ title: '提示',
|
|
|
+ content: '请先登录后再兑换优惠券',
|
|
|
+ success: (res) => {
|
|
|
+ if (res.confirm) {
|
|
|
+ redirectToLogin()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ redeemMutation.mutate(data.code)
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleCouponUse = (coupon: WechatCoupon) => {
|
|
|
+ // 跳转到使用页面或显示使用说明
|
|
|
+ Taro.showModal({
|
|
|
+ title: '使用说明',
|
|
|
+ content: coupon.couponDescription,
|
|
|
+ showCancel: false
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ const formatDate = (dateString: string) => {
|
|
|
+ return new Date(dateString).toLocaleDateString('zh-CN')
|
|
|
+ }
|
|
|
+
|
|
|
+ const getStatusText = (status: number) => {
|
|
|
+ switch (status) {
|
|
|
+ case 0: return '未使用'
|
|
|
+ case 1: return '已使用'
|
|
|
+ case 2: return '已过期'
|
|
|
+ default: return '未知'
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const getStatusColor = (status: number) => {
|
|
|
+ switch (status) {
|
|
|
+ case 0: return 'text-green-600'
|
|
|
+ case 1: return 'text-blue-600'
|
|
|
+ case 2: return 'text-gray-400'
|
|
|
+ default: return 'text-gray-600'
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <View className="min-h-screen bg-gray-50">
|
|
|
+ {/* 标签切换 */}
|
|
|
+ <View className="bg-white border-b">
|
|
|
+ <View className="flex">
|
|
|
+ <View
|
|
|
+ className={cn(
|
|
|
+ 'flex-1 py-3 text-center text-sm font-medium',
|
|
|
+ activeTab === 'redeem'
|
|
|
+ ? 'text-red-500 border-b-2 border-red-500'
|
|
|
+ : 'text-gray-600'
|
|
|
+ )}
|
|
|
+ onClick={() => setActiveTab('redeem')}
|
|
|
+ >
|
|
|
+ 兑换中心
|
|
|
+ </View>
|
|
|
+ <View
|
|
|
+ className={cn(
|
|
|
+ 'flex-1 py-3 text-center text-sm font-medium',
|
|
|
+ activeTab === 'myCoupons'
|
|
|
+ ? 'text-red-500 border-b-2 border-red-500'
|
|
|
+ : 'text-gray-600'
|
|
|
+ )}
|
|
|
+ onClick={() => setActiveTab('myCoupons')}
|
|
|
+ >
|
|
|
+ 我的券包
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ <ScrollView className="flex-1">
|
|
|
+ {activeTab === 'redeem' ? (
|
|
|
+ /* 兑换码输入 */
|
|
|
+ <View className="p-4">
|
|
|
+ <Card>
|
|
|
+ <CardHeader className="pb-4">
|
|
|
+ <Text className="text-lg font-bold text-gray-900">兑换码兑换</Text>
|
|
|
+ <Text className="text-sm text-gray-500">输入兑换码领取优惠券</Text>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <Form {...form}>
|
|
|
+ <FormField
|
|
|
+ control={form.control}
|
|
|
+ name="code"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>兑换码</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input
|
|
|
+ placeholder="请输入兑换码"
|
|
|
+ {...field}
|
|
|
+ className="h-10"
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+ <Button
|
|
|
+ type="submit"
|
|
|
+ className="w-full h-10 bg-red-500 text-white mt-4"
|
|
|
+ loading={redeemMutation.isPending}
|
|
|
+ onClick={form.handleSubmit(handleRedeem)}
|
|
|
+ >
|
|
|
+ 立即兑换
|
|
|
+ </Button>
|
|
|
+ </Form>
|
|
|
+
|
|
|
+ <View className="mt-6 p-4 bg-gray-50 rounded-lg">
|
|
|
+ <Text className="text-sm text-gray-600 mb-2">兑换说明:</Text>
|
|
|
+ <Text className="text-xs text-gray-500 leading-relaxed">
|
|
|
+ • 兑换码区分大小写,请准确输入{'\n'}
|
|
|
+ • 每个兑换码仅限使用一次{'\n'}
|
|
|
+ • 兑换成功后可在"我的券包"中查看{'\n'}
|
|
|
+ • 如有问题请联系客服
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ </View>
|
|
|
+ ) : (
|
|
|
+ /* 我的券包 */
|
|
|
+ <View className="p-4">
|
|
|
+ {couponsLoading ? (
|
|
|
+ <View className="flex items-center justify-center py-8">
|
|
|
+ <Text className="text-gray-400">加载中...</Text>
|
|
|
+ </View>
|
|
|
+ ) : coupons && coupons.length > 0 ? (
|
|
|
+ <View className="space-y-4">
|
|
|
+ {coupons.map((coupon) => (
|
|
|
+ <Card key={coupon.id}>
|
|
|
+ <CardContent className="p-4">
|
|
|
+ <View className="flex justify-between items-start mb-2">
|
|
|
+ <View>
|
|
|
+ <Text className="text-lg font-bold text-gray-900">
|
|
|
+ {coupon.couponName}
|
|
|
+ </Text>
|
|
|
+ <Text className="text-sm text-gray-600">
|
|
|
+ {coupon.couponDescription}
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ <View className="text-right">
|
|
|
+ <Text className="text-xl font-bold text-red-500">
|
|
|
+ ¥{coupon.couponAmount}
|
|
|
+ </Text>
|
|
|
+ <Text className={cn('text-sm', getStatusColor(coupon.couponStatus))}>
|
|
|
+ {getStatusText(coupon.couponStatus)}
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ <View className="flex justify-between items-center text-xs text-gray-500">
|
|
|
+ <Text>有效期:{formatDate(coupon.validBeginTime)} - {formatDate(coupon.validEndTime)}</Text>
|
|
|
+ <Text>领取时间:{formatDate(coupon.receiveTime)}</Text>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ {coupon.couponStatus === 0 && (
|
|
|
+ <Button
|
|
|
+ className="w-full h-8 bg-red-500 text-white text-sm mt-3"
|
|
|
+ onClick={() => handleCouponUse(coupon)}
|
|
|
+ >
|
|
|
+ 立即使用
|
|
|
+ </Button>
|
|
|
+ )}
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ ))}
|
|
|
+ </View>
|
|
|
+ ) : (
|
|
|
+ <View className="flex flex-col items-center justify-center py-12">
|
|
|
+ <View className="i-heroicons-ticket-20-solid w-16 h-16 text-gray-300 mb-4" />
|
|
|
+ <Text className="text-gray-400 mb-2">暂无优惠券</Text>
|
|
|
+ <Text className="text-sm text-gray-500">快去兑换中心看看吧</Text>
|
|
|
+ </View>
|
|
|
+ )}
|
|
|
+ </View>
|
|
|
+ )}
|
|
|
+ </ScrollView>
|
|
|
+ </View>
|
|
|
+ )
|
|
|
+}
|