2025-12-30 18:35:37 +08:00
|
|
|
"use client"
|
2026-03-26 15:27:52 +08:00
|
|
|
import { zodResolver } from "@hookform/resolvers/zod"
|
2026-01-05 09:14:41 +08:00
|
|
|
import { format } from "date-fns"
|
2026-05-13 14:57:27 +08:00
|
|
|
import Link from "next/link"
|
|
|
|
|
import { useRouter, useSearchParams } from "next/navigation"
|
|
|
|
|
import { Suspense } from "react"
|
2026-03-26 15:27:52 +08:00
|
|
|
import { Controller, useForm } from "react-hook-form"
|
|
|
|
|
import { z } from "zod"
|
2025-12-30 18:35:37 +08:00
|
|
|
import { getPageBatch } from "@/actions/batch"
|
|
|
|
|
import { DataTable, useDataTable } from "@/components/data-table"
|
2026-04-11 17:12:16 +08:00
|
|
|
import { Page } from "@/components/page"
|
2026-03-26 15:27:52 +08:00
|
|
|
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"
|
2026-01-05 09:14:41 +08:00
|
|
|
import type { Batch } from "@/models/batch"
|
2025-12-30 18:35:37 +08:00
|
|
|
|
2026-03-26 15:27:52 +08:00
|
|
|
type APIFilterParams = {
|
2026-04-09 17:08:59 +08:00
|
|
|
user_phone?: string
|
2026-03-26 15:27:52 +08:00
|
|
|
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>
|
|
|
|
|
|
2026-01-05 09:14:41 +08:00
|
|
|
export default function BatchPage() {
|
2026-05-13 14:57:27 +08:00
|
|
|
const searchParams = useSearchParams()
|
|
|
|
|
const resourceNo = searchParams.get("resource_no")
|
|
|
|
|
const batchNo = searchParams.get("batch_no")
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
const { control, handleSubmit, reset, getValues } = useForm<FilterSchema>({
|
2026-03-26 15:27:52 +08:00
|
|
|
resolver: zodResolver(filterSchema),
|
|
|
|
|
defaultValues: {
|
|
|
|
|
user_phone: "",
|
2026-05-13 14:57:27 +08:00
|
|
|
batch_no: batchNo || "",
|
2026-03-26 15:27:52 +08:00
|
|
|
prov: "",
|
2026-05-13 14:57:27 +08:00
|
|
|
resource_no: resourceNo || "",
|
2026-03-26 15:27:52 +08:00
|
|
|
city: "",
|
|
|
|
|
isp: "all",
|
|
|
|
|
created_at_start: "",
|
|
|
|
|
created_at_end: "",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-13 14:57:27 +08:00
|
|
|
const table = useDataTable<Batch>((page, size) => {
|
2026-03-26 15:27:52 +08:00
|
|
|
const result: APIFilterParams = {}
|
2026-05-13 14:57:27 +08:00
|
|
|
const filters = getValues()
|
|
|
|
|
if (filters.user_phone?.trim())
|
|
|
|
|
result.user_phone = filters.user_phone.trim()
|
|
|
|
|
if (filters.batch_no?.trim()) result.batch_no = filters.batch_no.trim()
|
|
|
|
|
if (filters.resource_no?.trim())
|
|
|
|
|
result.resource_no = filters.resource_no.trim()
|
|
|
|
|
if (filters.prov?.trim()) result.prov = filters.prov.trim()
|
|
|
|
|
if (filters.city?.trim()) result.city = filters.city.trim()
|
|
|
|
|
if (filters.isp && filters.isp !== "all") result.isp = filters.isp
|
|
|
|
|
if (filters.created_at_start)
|
|
|
|
|
result.created_at_start = new Date(filters.created_at_start)
|
|
|
|
|
if (filters.created_at_end)
|
|
|
|
|
result.created_at_end = new Date(filters.created_at_end)
|
|
|
|
|
return getPageBatch({ page, size, ...result })
|
|
|
|
|
})
|
2026-03-26 15:27:52 +08:00
|
|
|
|
2026-05-13 14:57:27 +08:00
|
|
|
const onFilter = handleSubmit(() => {
|
2026-03-26 15:27:52 +08:00
|
|
|
table.pagination.onPageChange(1)
|
|
|
|
|
})
|
2025-12-30 18:35:37 +08:00
|
|
|
|
|
|
|
|
return (
|
2026-04-11 17:12:16 +08:00
|
|
|
<Page>
|
2026-04-11 17:07:03 +08:00
|
|
|
{/* 搜索表单 */}
|
2026-04-11 17:12:16 +08:00
|
|
|
<form onSubmit={onFilter} className="bg-card p-4 rounded-lg">
|
2026-03-26 15:27:52 +08:00
|
|
|
<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"
|
|
|
|
|
>
|
2026-04-23 11:04:57 +08:00
|
|
|
<FieldLabel>提取编号</FieldLabel>
|
2026-05-14 16:04:35 +08:00
|
|
|
<Input {...field} placeholder="请输入提取编号" clearable />
|
2026-03-26 15:27:52 +08:00
|
|
|
<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>
|
2026-05-14 16:04:35 +08:00
|
|
|
<Input {...field} placeholder="请输入套餐号" clearable />
|
2026-03-26 15:27:52 +08:00
|
|
|
<FieldError>{fieldState.error?.message}</FieldError>
|
|
|
|
|
</Field>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
<Controller
|
|
|
|
|
name="user_phone"
|
|
|
|
|
control={control}
|
|
|
|
|
render={({ field, fieldState }) => (
|
|
|
|
|
<Field
|
|
|
|
|
data-invalid={fieldState.invalid}
|
|
|
|
|
className="w-40 flex-none"
|
|
|
|
|
>
|
|
|
|
|
<FieldLabel>会员号</FieldLabel>
|
2026-05-14 16:04:35 +08:00
|
|
|
<Input {...field} placeholder="请输入会员号" clearable />
|
2026-03-26 15:27:52 +08:00
|
|
|
<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>
|
2026-05-14 16:04:35 +08:00
|
|
|
<Input {...field} placeholder="请输入省份" clearable />
|
2026-03-26 15:27:52 +08:00
|
|
|
<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>
|
2026-05-14 16:04:35 +08:00
|
|
|
<Input {...field} placeholder="请输入城市" clearable />
|
2026-03-26 15:27:52 +08:00
|
|
|
<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>
|
2026-05-15 16:54:58 +08:00
|
|
|
<Input type="date" {...field} />
|
2026-03-26 15:27:52 +08:00
|
|
|
<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>
|
2026-05-15 16:54:58 +08:00
|
|
|
<Input type="date" {...field} />
|
2026-03-26 15:27:52 +08:00
|
|
|
<FieldError>{fieldState.error?.message}</FieldError>
|
|
|
|
|
</Field>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
2026-04-11 17:07:03 +08:00
|
|
|
<Button type="submit">搜索</Button>
|
2026-03-26 15:27:52 +08:00
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => {
|
2026-05-13 14:57:27 +08:00
|
|
|
reset({
|
|
|
|
|
user_phone: "",
|
|
|
|
|
batch_no: "",
|
|
|
|
|
prov: "",
|
|
|
|
|
resource_no: "",
|
|
|
|
|
city: "",
|
|
|
|
|
isp: "all",
|
|
|
|
|
created_at_start: "",
|
|
|
|
|
created_at_end: "",
|
|
|
|
|
})
|
|
|
|
|
router.replace("./batch")
|
2026-03-26 15:27:52 +08:00
|
|
|
table.pagination.onPageChange(1)
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
重置
|
|
|
|
|
</Button>
|
|
|
|
|
</FieldGroup>
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
<Suspense fallback={<div>Loading...</div>}>
|
|
|
|
|
<DataTable<Batch>
|
|
|
|
|
{...table}
|
|
|
|
|
columns={[
|
|
|
|
|
{
|
|
|
|
|
header: "会员号",
|
2026-04-11 14:57:45 +08:00
|
|
|
accessorFn: row => row.user?.phone || "",
|
2026-03-26 15:27:52 +08:00
|
|
|
},
|
2026-05-13 14:57:27 +08:00
|
|
|
{
|
|
|
|
|
header: "套餐号",
|
|
|
|
|
accessorKey: "resource.resource_no",
|
|
|
|
|
cell: ({ row }) => {
|
|
|
|
|
const resourceNo = row.original.resource?.resource_no
|
2026-05-20 15:42:51 +08:00
|
|
|
const type = row.original.resource?.type
|
2026-05-13 14:57:27 +08:00
|
|
|
return (
|
|
|
|
|
<Link
|
2026-05-20 15:42:51 +08:00
|
|
|
href={`/resources?resource_no=${resourceNo}&type=${type}`}
|
2026-05-13 14:57:27 +08:00
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
2026-05-14 16:04:35 +08:00
|
|
|
className="text-blue-600"
|
2026-05-13 14:57:27 +08:00
|
|
|
>
|
|
|
|
|
{resourceNo}
|
|
|
|
|
</Link>
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-05-14 16:04:35 +08:00
|
|
|
{
|
|
|
|
|
header: "提取编号",
|
|
|
|
|
accessorKey: "batch_no",
|
|
|
|
|
cell: ({ row }) => {
|
|
|
|
|
const batch_no = row.original.batch_no
|
|
|
|
|
return (
|
|
|
|
|
<Link
|
|
|
|
|
href={`./channel?batch_no=${batch_no}`}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="text-blue-600"
|
|
|
|
|
>
|
|
|
|
|
{batch_no}
|
|
|
|
|
</Link>
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-03-26 15:27:52 +08:00
|
|
|
{ header: "省份", accessorKey: "prov" },
|
|
|
|
|
{ header: "城市", accessorKey: "city" },
|
2026-04-16 14:28:03 +08:00
|
|
|
{ header: "用户IP", accessorKey: "ip" },
|
2026-03-26 15:27:52 +08:00
|
|
|
{ header: "运营商", accessorKey: "isp" },
|
|
|
|
|
{ header: "提取数量", accessorKey: "count" },
|
|
|
|
|
{
|
|
|
|
|
header: "提取时间",
|
|
|
|
|
accessorKey: "time",
|
|
|
|
|
cell: ({ row }) =>
|
2026-05-13 14:57:27 +08:00
|
|
|
format(new Date(row.original.time), "yyyy-MM-dd HH:mm:ss"),
|
2026-03-26 15:27:52 +08:00
|
|
|
},
|
|
|
|
|
]}
|
|
|
|
|
/>
|
|
|
|
|
</Suspense>
|
2026-04-11 17:12:16 +08:00
|
|
|
</Page>
|
2025-12-30 18:35:37 +08:00
|
|
|
)
|
|
|
|
|
}
|