270 lines
8.5 KiB
TypeScript
270 lines
8.5 KiB
TypeScript
"use client"
|
|
import { zodResolver } from "@hookform/resolvers/zod"
|
|
import { format } from "date-fns"
|
|
import { useRouter, useSearchParams } from "next/navigation"
|
|
import { Suspense, useState } from "react"
|
|
import { Controller, useForm } from "react-hook-form"
|
|
import { z } from "zod"
|
|
import { getBatch } from "@/actions/batch"
|
|
import { DataTable, useDataTable } from "@/components/data-table"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
Field,
|
|
FieldError,
|
|
FieldGroup,
|
|
FieldLabel,
|
|
} from "@/components/ui/field"
|
|
import { Input } from "@/components/ui/input"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
import type { Batch } from "@/models/batch"
|
|
|
|
type APIFilterParams = {
|
|
phone?: string
|
|
batch_no?: string
|
|
resource_no?: string
|
|
prov?: string
|
|
city?: string
|
|
isp?: string
|
|
created_at_start?: Date
|
|
created_at_end?: Date
|
|
}
|
|
|
|
const filterSchema = z
|
|
.object({
|
|
user_phone: z.string().optional(),
|
|
resource_no: z.string().optional(),
|
|
batch_no: z.string().optional(),
|
|
prov: z.string().optional(),
|
|
city: z.string().optional(),
|
|
isp: z.string().optional(),
|
|
created_at_start: z.string().optional(),
|
|
created_at_end: z.string().optional(),
|
|
})
|
|
.superRefine((data, ctx) => {
|
|
if (data.created_at_start && data.created_at_end) {
|
|
const start = new Date(data.created_at_start)
|
|
const end = new Date(data.created_at_end)
|
|
|
|
if (end < start) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "结束时间不能早于开始时间",
|
|
path: ["created_at_end"],
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
type FilterSchema = z.infer<typeof filterSchema>
|
|
|
|
export default function BatchPage() {
|
|
const searchParams = useSearchParams()
|
|
const router = useRouter()
|
|
const userId = searchParams.get("userId")
|
|
const userPhone = searchParams.get("phone")
|
|
const [filters, setFilters] = useState<APIFilterParams>({})
|
|
|
|
const { control, handleSubmit, reset } = useForm<FilterSchema>({
|
|
resolver: zodResolver(filterSchema),
|
|
defaultValues: {
|
|
resource_no: "",
|
|
batch_no: "",
|
|
prov: "",
|
|
city: "",
|
|
isp: "all",
|
|
created_at_start: "",
|
|
created_at_end: "",
|
|
},
|
|
})
|
|
|
|
const table = useDataTable<Batch>((page, size) =>
|
|
getBatch({ page, size, user_id: Number(userId), ...filters }),
|
|
)
|
|
|
|
const onFilter = handleSubmit(data => {
|
|
const result: APIFilterParams = {}
|
|
if (data.user_phone?.trim()) result.phone = data.user_phone.trim()
|
|
if (data.batch_no?.trim()) result.batch_no = data.batch_no.trim()
|
|
if (data.resource_no?.trim()) result.resource_no = data.resource_no.trim()
|
|
if (data.prov?.trim()) result.prov = data.prov.trim()
|
|
if (data.city?.trim()) result.city = data.city.trim()
|
|
if (data.isp && data.isp !== "all") result.isp = data.isp
|
|
if (data.created_at_start)
|
|
result.created_at_start = new Date(data.created_at_start)
|
|
if (data.created_at_end)
|
|
result.created_at_end = new Date(data.created_at_end)
|
|
|
|
setFilters(result)
|
|
table.pagination.onPageChange(1)
|
|
})
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<Button
|
|
onClick={() => {
|
|
router.back()
|
|
}}
|
|
className="gap-2"
|
|
>
|
|
返回上一级
|
|
</Button>
|
|
<span className="ml-2 text-gray-600">用户会员号: {userPhone}</span>
|
|
|
|
<form onSubmit={onFilter} className="bg-white p-4 rounded-lg">
|
|
<div className="flex flex-wrap items-end gap-4">
|
|
<Controller
|
|
name="batch_no"
|
|
control={control}
|
|
render={({ field, fieldState }) => (
|
|
<Field
|
|
data-invalid={fieldState.invalid}
|
|
className="w-40 flex-none"
|
|
>
|
|
<FieldLabel>批次号</FieldLabel>
|
|
<Input {...field} placeholder="请输入批次号" />
|
|
<FieldError>{fieldState.error?.message}</FieldError>
|
|
</Field>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="resource_no"
|
|
control={control}
|
|
render={({ field, fieldState }) => (
|
|
<Field
|
|
data-invalid={fieldState.invalid}
|
|
className="w-40 flex-none"
|
|
>
|
|
<FieldLabel>套餐号</FieldLabel>
|
|
<Input {...field} placeholder="请输入套餐号" />
|
|
<FieldError>{fieldState.error?.message}</FieldError>
|
|
</Field>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="prov"
|
|
control={control}
|
|
render={({ field, fieldState }) => (
|
|
<Field
|
|
data-invalid={fieldState.invalid}
|
|
className="w-32 flex-none"
|
|
>
|
|
<FieldLabel>省份</FieldLabel>
|
|
<Input {...field} placeholder="请输入省份" />
|
|
<FieldError>{fieldState.error?.message}</FieldError>
|
|
</Field>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="city"
|
|
control={control}
|
|
render={({ field, fieldState }) => (
|
|
<Field
|
|
data-invalid={fieldState.invalid}
|
|
className="w-32 flex-none"
|
|
>
|
|
<FieldLabel>城市</FieldLabel>
|
|
<Input {...field} placeholder="请输入城市" />
|
|
<FieldError>{fieldState.error?.message}</FieldError>
|
|
</Field>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="isp"
|
|
control={control}
|
|
render={({ field, fieldState }) => (
|
|
<Field data-invalid={fieldState.invalid} className="w-32">
|
|
<FieldLabel>运营商</FieldLabel>
|
|
<Select value={field.value} onValueChange={field.onChange}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="全部" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">全部</SelectItem>
|
|
<SelectItem value="1">电信</SelectItem>
|
|
<SelectItem value="2">联通</SelectItem>
|
|
<SelectItem value="3">移动</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FieldError>{fieldState.error?.message}</FieldError>
|
|
</Field>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="created_at_start"
|
|
control={control}
|
|
render={({ field, fieldState }) => (
|
|
<Field
|
|
data-invalid={fieldState.invalid}
|
|
className="w-40 flex-none"
|
|
>
|
|
<FieldLabel>开始时间</FieldLabel>
|
|
<Input type="date" {...field} />
|
|
<FieldError>{fieldState.error?.message}</FieldError>
|
|
</Field>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="created_at_end"
|
|
control={control}
|
|
render={({ field, fieldState }) => (
|
|
<Field
|
|
data-invalid={fieldState.invalid}
|
|
className="w-40 flex-none"
|
|
>
|
|
<FieldLabel>结束时间</FieldLabel>
|
|
<Input type="date" {...field} />
|
|
<FieldError>{fieldState.error?.message}</FieldError>
|
|
</Field>
|
|
)}
|
|
/>
|
|
</div>
|
|
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
|
<Button type="submit">搜索</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => {
|
|
reset()
|
|
setFilters({})
|
|
table.pagination.onPageChange(1)
|
|
}}
|
|
>
|
|
重置
|
|
</Button>
|
|
</FieldGroup>
|
|
</form>
|
|
|
|
<Suspense fallback={<div>Loading...</div>}>
|
|
<DataTable<Batch>
|
|
{...table}
|
|
columns={[
|
|
{
|
|
header: "会员号",
|
|
accessorFn: row => row.user?.phone || "",
|
|
},
|
|
{ header: "套餐号", accessorKey: "resource.resource_no" },
|
|
{ header: "批次号", accessorKey: "batch_no" },
|
|
{ header: "省份", accessorKey: "prov" },
|
|
{ header: "城市", accessorKey: "city" },
|
|
{ header: "提取IP", accessorKey: "ip" },
|
|
{ header: "运营商", accessorKey: "isp" },
|
|
{ header: "提取数量", accessorKey: "count" },
|
|
{
|
|
header: "提取时间",
|
|
accessorKey: "time",
|
|
cell: ({ row }) =>
|
|
format(new Date(row.original.time), "yyyy-MM-dd HH:mm"),
|
|
},
|
|
]}
|
|
/>
|
|
</Suspense>
|
|
</div>
|
|
)
|
|
}
|