|
|
@@ -0,0 +1,714 @@
|
|
|
+import { useState } from 'react'
|
|
|
+import { useQuery } from '@tanstack/react-query'
|
|
|
+import { useForm } from 'react-hook-form'
|
|
|
+import { zodResolver } from '@hookform/resolvers/zod'
|
|
|
+import { format } from 'date-fns'
|
|
|
+import { zhCN } from 'date-fns/locale'
|
|
|
+import { Plus, Search, Edit, Trash2, Download, Upload } from 'lucide-react'
|
|
|
+import { toast } from 'sonner'
|
|
|
+import type { InferRequestType, InferResponseType } from 'hono/client'
|
|
|
+
|
|
|
+import { redemptionCodeClient } from '@/client/api'
|
|
|
+import { CreateRedemptionCodeDto, UpdateRedemptionCodeDto } from '@/server/modules/redemption-codes/redemption-code.schema'
|
|
|
+import { Button } from '@/client/components/ui/button'
|
|
|
+import { Input } from '@/client/components/ui/input'
|
|
|
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card'
|
|
|
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/client/components/ui/table'
|
|
|
+import { Badge } from '@/client/components/ui/badge'
|
|
|
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog'
|
|
|
+import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form'
|
|
|
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select'
|
|
|
+import { Textarea } from '@/client/components/ui/textarea'
|
|
|
+import { DataTablePagination } from '@/client/admin-shadcn/components/DataTablePagination'
|
|
|
+import { wechatCouponStockClient } from '@/client/api'
|
|
|
+
|
|
|
+type CreateRequest = InferRequestType<typeof redemptionCodeClient.$post>['json']
|
|
|
+type UpdateRequest = InferRequestType<typeof redemptionCodeClient[':id']['$put']>['json']
|
|
|
+type CodeResponse = InferResponseType<typeof redemptionCodeClient.$get, 200>['data'][0]
|
|
|
+
|
|
|
+export const RedemptionCodesPage = () => {
|
|
|
+ const [searchParams, setSearchParams] = useState({
|
|
|
+ page: 1,
|
|
|
+ limit: 10,
|
|
|
+ search: '',
|
|
|
+ status: '',
|
|
|
+ batchId: '',
|
|
|
+ })
|
|
|
+ const [isModalOpen, setIsModalOpen] = useState(false)
|
|
|
+ const [editingData, setEditingData] = useState<CodeResponse | null>(null)
|
|
|
+ const [isCreateForm, setIsCreateForm] = useState(true)
|
|
|
+ const [isBulkModalOpen, setIsBulkModalOpen] = useState(false)
|
|
|
+
|
|
|
+ // 获取代金券批次列表
|
|
|
+ const { data: stockData } = useQuery({
|
|
|
+ queryKey: ['wechat-coupon-stocks-all'],
|
|
|
+ queryFn: async () => {
|
|
|
+ const res = await wechatCouponStockClient.$get({
|
|
|
+ query: { page: 1, pageSize: 100 },
|
|
|
+ })
|
|
|
+ if (res.status !== 200) throw new Error('获取批次失败')
|
|
|
+ return await res.json()
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ // 获取兑换码列表
|
|
|
+ const { data, isLoading, refetch } = useQuery({
|
|
|
+ queryKey: ['redemption-codes', searchParams],
|
|
|
+ queryFn: async () => {
|
|
|
+ const filters: any = {}
|
|
|
+ if (searchParams.status) filters.status = searchParams.status
|
|
|
+ if (searchParams.batchId) filters.batchId = Number(searchParams.batchId)
|
|
|
+
|
|
|
+ const res = await redemptionCodeClient.$get({
|
|
|
+ query: {
|
|
|
+ page: searchParams.page,
|
|
|
+ pageSize: searchParams.limit,
|
|
|
+ keyword: searchParams.search,
|
|
|
+ ...(Object.keys(filters).length > 0 && { filters: JSON.stringify(filters) }),
|
|
|
+ },
|
|
|
+ })
|
|
|
+ if (res.status !== 200) throw new Error('获取列表失败')
|
|
|
+ return await res.json()
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ // 表单实例
|
|
|
+ const createForm = useForm<CreateRequest>({
|
|
|
+ resolver: zodResolver(CreateRedemptionCodeDto),
|
|
|
+ defaultValues: {
|
|
|
+ code: '',
|
|
|
+ batchId: 0,
|
|
|
+ amount: 100,
|
|
|
+ validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
|
|
+ maxRedemptions: 1,
|
|
|
+ redemptionResult: '',
|
|
|
+ isActive: 1,
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const bulkForm = useForm({
|
|
|
+ defaultValues: {
|
|
|
+ count: 10,
|
|
|
+ batchId: 0,
|
|
|
+ amount: 100,
|
|
|
+ prefix: 'CODE',
|
|
|
+ validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
|
|
+ maxRedemptions: 1,
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const updateForm = useForm<UpdateRequest>({
|
|
|
+ resolver: zodResolver(UpdateRedemptionCodeDto),
|
|
|
+ defaultValues: {},
|
|
|
+ })
|
|
|
+
|
|
|
+ // 打开创建表单
|
|
|
+ const handleCreate = () => {
|
|
|
+ setEditingData(null)
|
|
|
+ setIsCreateForm(true)
|
|
|
+ createForm.reset()
|
|
|
+ setIsModalOpen(true)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 打开编辑表单
|
|
|
+ const handleEdit = (data: CodeResponse) => {
|
|
|
+ setEditingData(data)
|
|
|
+ setIsCreateForm(false)
|
|
|
+ updateForm.reset({
|
|
|
+ code: data.code,
|
|
|
+ batchId: data.batchId,
|
|
|
+ amount: data.amount,
|
|
|
+ validUntil: new Date(data.validUntil),
|
|
|
+ maxRedemptions: data.maxRedemptions,
|
|
|
+ redemptionResult: data.redemptionResult,
|
|
|
+ isActive: data.isActive,
|
|
|
+ })
|
|
|
+ setIsModalOpen(true)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 打开批量创建表单
|
|
|
+ const handleBulkCreate = () => {
|
|
|
+ bulkForm.reset()
|
|
|
+ setIsBulkModalOpen(true)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提交创建
|
|
|
+ const handleCreateSubmit = async (values: CreateRequest) => {
|
|
|
+ try {
|
|
|
+ const res = await redemptionCodeClient.$post({ json: values })
|
|
|
+ if (res.status !== 201) throw new Error('创建失败')
|
|
|
+ toast.success('创建成功')
|
|
|
+ setIsModalOpen(false)
|
|
|
+ refetch()
|
|
|
+ } catch (error) {
|
|
|
+ toast.error('创建失败,请重试')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提交批量创建
|
|
|
+ const handleBulkCreateSubmit = async (values: any) => {
|
|
|
+ try {
|
|
|
+ const res = await redemptionCodeClient['bulk-create']['$post']({ json: values })
|
|
|
+ if (res.status !== 201) throw new Error('批量创建失败')
|
|
|
+ const result = await res.json()
|
|
|
+ toast.success(`成功创建 ${result.count} 个兑换码`)
|
|
|
+ setIsBulkModalOpen(false)
|
|
|
+ refetch()
|
|
|
+ } catch (error) {
|
|
|
+ toast.error('批量创建失败,请重试')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提交更新
|
|
|
+ const handleUpdateSubmit = async (values: UpdateRequest) => {
|
|
|
+ if (!editingData) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await redemptionCodeClient[':id']['$put']({
|
|
|
+ param: { id: editingData.id },
|
|
|
+ json: values,
|
|
|
+ })
|
|
|
+ if (res.status !== 200) throw new Error('更新失败')
|
|
|
+ toast.success('更新成功')
|
|
|
+ setIsModalOpen(false)
|
|
|
+ refetch()
|
|
|
+ } catch (error) {
|
|
|
+ toast.error('更新失败,请重试')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理删除
|
|
|
+ const handleDelete = async (id: number) => {
|
|
|
+ try {
|
|
|
+ const res = await redemptionCodeClient[':id']['$delete']({ param: { id } })
|
|
|
+ if (res.status !== 200) throw new Error('删除失败')
|
|
|
+ toast.success('删除成功')
|
|
|
+ refetch()
|
|
|
+ } catch (error) {
|
|
|
+ toast.error('删除失败,请重试')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理搜索
|
|
|
+ const handleSearch = (e: React.FormEvent) => {
|
|
|
+ e.preventDefault()
|
|
|
+ setSearchParams(prev => ({ ...prev, page: 1 }))
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取状态样式
|
|
|
+ const getStatusBadge = (status: string) => {
|
|
|
+ const statusMap: Record<string, { label: string; variant: 'default' | 'secondary' | 'success' | 'warning' | 'destructive' }> = {
|
|
|
+ ACTIVE: { label: '有效', variant: 'success' },
|
|
|
+ USED: { label: '已使用', variant: 'default' },
|
|
|
+ EXPIRED: { label: '已过期', variant: 'destructive' },
|
|
|
+ DISABLED: { label: '已禁用', variant: 'secondary' },
|
|
|
+ }
|
|
|
+ const config = statusMap[status] || { label: status, variant: 'default' }
|
|
|
+ return <Badge variant={config.variant}>{config.label}</Badge>
|
|
|
+ }
|
|
|
+
|
|
|
+ // 导出兑换码
|
|
|
+ const handleExport = async () => {
|
|
|
+ try {
|
|
|
+ const res = await redemptionCodeClient['export']['$get']({
|
|
|
+ query: {
|
|
|
+ ...(searchParams.batchId && { batchId: searchParams.batchId }),
|
|
|
+ ...(searchParams.status && { status: searchParams.status }),
|
|
|
+ },
|
|
|
+ })
|
|
|
+ if (res.status !== 200) throw new Error('导出失败')
|
|
|
+
|
|
|
+ const blob = await res.blob()
|
|
|
+ const url = URL.createObjectURL(blob)
|
|
|
+ const a = document.createElement('a')
|
|
|
+ a.href = url
|
|
|
+ a.download = `兑换码_${format(new Date(), 'yyyyMMddHHmmss')}.xlsx`
|
|
|
+ a.click()
|
|
|
+ URL.revokeObjectURL(url)
|
|
|
+ toast.success('导出成功')
|
|
|
+ } catch (error) {
|
|
|
+ toast.error('导出失败,请重试')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="space-y-4">
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
+ <div>
|
|
|
+ <h1 className="text-2xl font-bold">兑换码管理</h1>
|
|
|
+ <p className="text-muted-foreground">管理兑换码的生成和使用记录</p>
|
|
|
+ </div>
|
|
|
+ <div className="flex gap-2">
|
|
|
+ <Button onClick={handleBulkCreate} variant="outline">
|
|
|
+ <Plus className="mr-2 h-4 w-4" />
|
|
|
+ 批量生成
|
|
|
+ </Button>
|
|
|
+ <Button onClick={handleCreate}>
|
|
|
+ <Plus className="mr-2 h-4 w-4" />
|
|
|
+ 创建兑换码
|
|
|
+ </Button>
|
|
|
+ <Button onClick={handleExport} variant="outline">
|
|
|
+ <Download className="mr-2 h-4 w-4" />
|
|
|
+ 导出
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle>兑换码列表</CardTitle>
|
|
|
+ <CardDescription>查看和管理所有兑换码</CardDescription>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <form onSubmit={handleSearch} className="flex gap-4 mb-4">
|
|
|
+ <div className="relative flex-1 max-w-sm">
|
|
|
+ <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
|
+ <Input
|
|
|
+ placeholder="搜索兑换码..."
|
|
|
+ value={searchParams.search}
|
|
|
+ onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
|
|
|
+ className="pl-8"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <Select
|
|
|
+ value={searchParams.batchId}
|
|
|
+ onValueChange={(value) => setSearchParams(prev => ({ ...prev, batchId: value, page: 1 }))}
|
|
|
+ >
|
|
|
+ <SelectTrigger className="w-[150px]">
|
|
|
+ <SelectValue placeholder="选择批次" />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ <SelectItem value="">全部批次</SelectItem>
|
|
|
+ {stockData?.data.map((stock) => (
|
|
|
+ <SelectItem key={stock.id} value={String(stock.id)}>
|
|
|
+ {stock.stockName}
|
|
|
+ </SelectItem>
|
|
|
+ ))}
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ <Select
|
|
|
+ value={searchParams.status}
|
|
|
+ onValueChange={(value) => setSearchParams(prev => ({ ...prev, status: value, page: 1 }))}
|
|
|
+ >
|
|
|
+ <SelectTrigger className="w-[150px]">
|
|
|
+ <SelectValue placeholder="全部状态" />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ <SelectItem value="">全部状态</SelectItem>
|
|
|
+ <SelectItem value="ACTIVE">有效</SelectItem>
|
|
|
+ <SelectItem value="USED">已使用</SelectItem>
|
|
|
+ <SelectItem value="EXPIRED">已过期</SelectItem>
|
|
|
+ <SelectItem value="DISABLED">已禁用</SelectItem>
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ <Button type="submit" variant="outline">
|
|
|
+ 搜索
|
|
|
+ </Button>
|
|
|
+ </form>
|
|
|
+
|
|
|
+ <div className="rounded-md border">
|
|
|
+ <Table>
|
|
|
+ <TableHeader>
|
|
|
+ <TableRow>
|
|
|
+ <TableHead>兑换码</TableHead>
|
|
|
+ <TableHead>批次名称</TableHead>
|
|
|
+ <TableHead>面额</TableHead>
|
|
|
+ <TableHead>状态</TableHead>
|
|
|
+ <TableHead>使用次数</TableHead>
|
|
|
+ <TableHead>有效期</TableHead>
|
|
|
+ <TableHead>创建时间</TableHead>
|
|
|
+ <TableHead>使用时间</TableHead>
|
|
|
+ <TableHead>使用结果</TableHead>
|
|
|
+ <TableHead className="text-right">操作</TableHead>
|
|
|
+ </TableRow>
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody>
|
|
|
+ {isLoading ? (
|
|
|
+ <TableRow>
|
|
|
+ <TableCell colSpan={10} className="text-center">
|
|
|
+ 加载中...
|
|
|
+ </TableCell>
|
|
|
+ </TableRow>
|
|
|
+ ) : (
|
|
|
+ data?.data.map((code) => (
|
|
|
+ <TableRow key={code.id}>
|
|
|
+ <TableCell className="font-medium font-mono">{code.code}</TableCell>
|
|
|
+ <TableCell>{code.batch?.stockName}</TableCell>
|
|
|
+ <TableCell>¥{(code.amount / 100).toFixed(2)}</TableCell>
|
|
|
+ <TableCell>{getStatusBadge(code.status)}</TableCell>
|
|
|
+ <TableCell>
|
|
|
+ {code.currentRedemptions}/{code.maxRedemptions}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ {format(new Date(code.validUntil), 'yyyy-MM-dd', { locale: zhCN })}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ {format(new Date(code.createdAt), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ {code.redeemedAt ? (
|
|
|
+ format(new Date(code.redeemedAt), 'yyyy-MM-dd HH:mm', { locale: zhCN })
|
|
|
+ ) : (
|
|
|
+ <span className="text-muted-foreground">未使用</span>
|
|
|
+ )}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ {code.redemptionResult || (
|
|
|
+ <span className="text-muted-foreground">无</span>
|
|
|
+ )}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell className="text-right">
|
|
|
+ <div className="flex justify-end gap-2">
|
|
|
+ <Button
|
|
|
+ variant="ghost"
|
|
|
+ size="icon"
|
|
|
+ onClick={() => handleEdit(code)}
|
|
|
+ >
|
|
|
+ <Edit className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ variant="ghost"
|
|
|
+ size="icon"
|
|
|
+ onClick={() => handleDelete(code.id)}
|
|
|
+ >
|
|
|
+ <Trash2 className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </TableCell>
|
|
|
+ </TableRow>
|
|
|
+ ))
|
|
|
+ )}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {data && (
|
|
|
+ <DataTablePagination
|
|
|
+ currentPage={searchParams.page}
|
|
|
+ totalPages={Math.ceil(data.pagination.total / searchParams.limit)}
|
|
|
+ pageSize={searchParams.limit}
|
|
|
+ totalItems={data.pagination.total}
|
|
|
+ onPageChange={(page) => setSearchParams(prev => ({ ...prev, page }))}
|
|
|
+ onPageSizeChange={(limit) => setSearchParams(prev => ({ ...prev, limit, page: 1 }))}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
|
|
+ <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
|
|
|
+ <DialogHeader>
|
|
|
+ <DialogTitle>{isCreateForm ? '创建兑换码' : '编辑兑换码'}</DialogTitle>
|
|
|
+ <DialogDescription>
|
|
|
+ {isCreateForm ? '创建一个新的兑换码' : '编辑现有兑换码信息'}
|
|
|
+ </DialogDescription>
|
|
|
+ </DialogHeader>
|
|
|
+
|
|
|
+ <Form {...(isCreateForm ? createForm : updateForm)}>
|
|
|
+ <form
|
|
|
+ onSubmit={(isCreateForm ? createForm : updateForm).handleSubmit(
|
|
|
+ isCreateForm ? handleCreateSubmit : handleUpdateSubmit
|
|
|
+ )}
|
|
|
+ className="space-y-4"
|
|
|
+ >
|
|
|
+ <FormField
|
|
|
+ control={(isCreateForm ? createForm : updateForm).control}
|
|
|
+ name="code"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel className="flex items-center">
|
|
|
+ 兑换码
|
|
|
+ <span className="text-red-500 ml-1">*</span>
|
|
|
+ </FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input placeholder="请输入兑换码" {...field} />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ control={(isCreateForm ? createForm : updateForm).control}
|
|
|
+ name="batchId"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel className="flex items-center">
|
|
|
+ 代金券批次
|
|
|
+ <span className="text-red-500 ml-1">*</span>
|
|
|
+ </FormLabel>
|
|
|
+ <Select
|
|
|
+ onValueChange={(value) => field.onChange(Number(value))}
|
|
|
+ value={String(field.value)}
|
|
|
+ >
|
|
|
+ <FormControl>
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue placeholder="请选择批次" />
|
|
|
+ </SelectTrigger>
|
|
|
+ </FormControl>
|
|
|
+ <SelectContent>
|
|
|
+ {stockData?.data.map((stock) => (
|
|
|
+ <SelectItem key={stock.id} value={String(stock.id)}>
|
|
|
+ {stock.stockName}
|
|
|
+ </SelectItem>
|
|
|
+ ))}
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ control={(isCreateForm ? createForm : updateForm).control}
|
|
|
+ name="amount"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel className="flex items-center">
|
|
|
+ 面额(分)
|
|
|
+ <span className="text-red-500 ml-1">*</span>
|
|
|
+ </FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input
|
|
|
+ type="number"
|
|
|
+ placeholder="请输入面额"
|
|
|
+ {...field}
|
|
|
+ onChange={(e) => field.onChange(Number(e.target.value))}
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ control={(isCreateForm ? createForm : updateForm).control}
|
|
|
+ name="maxRedemptions"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel className="flex items-center">
|
|
|
+ 最大使用次数
|
|
|
+ <span className="text-red-500 ml-1">*</span>
|
|
|
+ </FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input
|
|
|
+ type="number"
|
|
|
+ placeholder="请输入最大使用次数"
|
|
|
+ {...field}
|
|
|
+ onChange={(e) => field.onChange(Number(e.target.value))}
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ control={(isCreateForm ? createForm : updateForm).control}
|
|
|
+ name="validUntil"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>有效期至</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input
|
|
|
+ type="date"
|
|
|
+ {...field}
|
|
|
+ value={field.value ? format(new Date(field.value), 'yyyy-MM-dd') : ''}
|
|
|
+ onChange={(e) => field.onChange(new Date(e.target.value))}
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ control={(isCreateForm ? createForm : updateForm).control}
|
|
|
+ name="isActive"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>状态</FormLabel>
|
|
|
+ <Select onValueChange={(value) => field.onChange(Number(value))} value={String(field.value)}>
|
|
|
+ <FormControl>
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue placeholder="请选择状态" />
|
|
|
+ </SelectTrigger>
|
|
|
+ </FormControl>
|
|
|
+ <SelectContent>
|
|
|
+ <SelectItem value="1">启用</SelectItem>
|
|
|
+ <SelectItem value="0">禁用</SelectItem>
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ control={(isCreateForm ? createForm : updateForm).control}
|
|
|
+ name="redemptionResult"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>使用结果</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Textarea
|
|
|
+ placeholder="请输入使用结果"
|
|
|
+ rows={3}
|
|
|
+ {...field}
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <DialogFooter>
|
|
|
+ <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
|
|
|
+ 取消
|
|
|
+ </Button>
|
|
|
+ <Button type="submit">
|
|
|
+ {isCreateForm ? '创建' : '更新'}
|
|
|
+ </Button>
|
|
|
+ </DialogFooter>
|
|
|
+ </form>
|
|
|
+ </Form>
|
|
|
+ </DialogContent>
|
|
|
+ </Dialog>
|
|
|
+
|
|
|
+ <Dialog open={isBulkModalOpen} onOpenChange={setIsBulkModalOpen}>
|
|
|
+ <DialogContent className="sm:max-w-[500px]">
|
|
|
+ <DialogHeader>
|
|
|
+ <DialogTitle>批量生成兑换码</DialogTitle>
|
|
|
+ <DialogDescription>
|
|
|
+ 批量生成多个兑换码
|
|
|
+ </DialogDescription>
|
|
|
+ </DialogHeader>
|
|
|
+
|
|
|
+ <Form {...bulkForm}>
|
|
|
+ <form
|
|
|
+ onSubmit={bulkForm.handleSubmit(handleBulkCreateSubmit)}
|
|
|
+ className="space-y-4"
|
|
|
+ >
|
|
|
+ <FormField
|
|
|
+ control={bulkForm.control}
|
|
|
+ name="count"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel className="flex items-center">
|
|
|
+ 生成数量
|
|
|
+ <span className="text-red-500 ml-1">*</span>
|
|
|
+ </FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input
|
|
|
+ type="number"
|
|
|
+ placeholder="请输入生成数量"
|
|
|
+ {...field}
|
|
|
+ onChange={(e) => field.onChange(Number(e.target.value))}
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ control={bulkForm.control}
|
|
|
+ name="batchId"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel className="flex items-center">
|
|
|
+ 代金券批次
|
|
|
+ <span className="text-red-500 ml-1">*</span>
|
|
|
+ </FormLabel>
|
|
|
+ <Select
|
|
|
+ onValueChange={(value) => field.onChange(Number(value))}
|
|
|
+ value={String(field.value)}
|
|
|
+ >
|
|
|
+ <FormControl>
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue placeholder="请选择批次" />
|
|
|
+ </SelectTrigger>
|
|
|
+ </FormControl>
|
|
|
+ <SelectContent>
|
|
|
+ {stockData?.data.map((stock) => (
|
|
|
+ <SelectItem key={stock.id} value={String(stock.id)}>
|
|
|
+ {stock.stockName}
|
|
|
+ </SelectItem>
|
|
|
+ ))}
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ control={bulkForm.control}
|
|
|
+ name="amount"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel className="flex items-center">
|
|
|
+ 面额(分)
|
|
|
+ <span className="text-red-500 ml-1">*</span>
|
|
|
+ </FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input
|
|
|
+ type="number"
|
|
|
+ placeholder="请输入面额"
|
|
|
+ {...field}
|
|
|
+ onChange={(e) => field.onChange(Number(e.target.value))}
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ control={bulkForm.control}
|
|
|
+ name="prefix"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>前缀</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input placeholder="请输入兑换码前缀" {...field} />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ control={bulkForm.control}
|
|
|
+ name="validUntil"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>有效期至</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input
|
|
|
+ type="date"
|
|
|
+ {...field}
|
|
|
+ value={field.value ? format(new Date(field.value), 'yyyy-MM-dd') : ''}
|
|
|
+ onChange={(e) => field.onChange(new Date(e.target.value))}
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <DialogFooter>
|
|
|
+ <Button type="button" variant="outline" onClick={() => setIsBulkModalOpen(false)}>
|
|
|
+ 取消
|
|
|
+ </Button>
|
|
|
+ <Button type="submit">
|
|
|
+ 批量生成
|
|
|
+ </Button>
|
|
|
+ </DialogFooter>
|
|
|
+ </form>
|
|
|
+ </Form>
|
|
|
+ </DialogContent>
|
|
|
+ </Dialog>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|