时间显示到秒 & 表格之间的跳转页面

This commit is contained in:
Eamon
2026-05-13 14:57:27 +08:00
parent bc29a025b0
commit 284b0d6afe
25 changed files with 442 additions and 240 deletions

View File

@@ -2,7 +2,16 @@ import type { PageRecord } from "@/lib/api"
import type { User } from "@/models/user"
import { callByUser } from "./base"
export async function getPageCusts(params: { page: number; size: number }) {
export async function getPageCusts(params: {
page: number
size: number
account?: string
name?: string
identified?: boolean
enabled?: boolean
created_at_start?: Date
created_at_end?: Date
}) {
return callByUser<PageRecord<User>>("/api/admin/user/page", params)
}
export async function updateCust(data: {

View File

@@ -6,7 +6,6 @@ import {
ChevronsRight,
CircleDollarSign,
ClipboardList,
ComputerIcon,
ContactRound,
DollarSign,
DoorClosedIcon,
@@ -24,7 +23,7 @@ import {
} from "lucide-react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { createContext, type ReactNode, Suspense, useContext, useState } from "react"
import { createContext, type ReactNode, useContext, useState } from "react"
import { twJoin } from "tailwind-merge"
import { Auth } from "@/components/auth"
import { Button } from "@/components/ui/button"
@@ -307,7 +306,7 @@ export default function Navigation() {
)}
>
{/*Logo 区域 */}
<Suspense><Logo collapsed={collapsed} /></Suspense>
<Logo collapsed={collapsed} />
{/* Navigation Menu */}
<ScrollArea className="flex-1 py-3 overflow-hidden">

View File

@@ -1,10 +1,15 @@
"use client"
import { ComputerIcon } from "lucide-react"
import { useEffect, useState } from "react"
import { getNodeEnv } from "@/actions/env"
import { cn } from "@/lib/utils"
export default async function Logo(props: { collapsed: boolean }) {
const env = await getNodeEnv()
export default function Logo(props: { collapsed: boolean }) {
const [env, setEnv] = useState<string>("")
useEffect(() => {
getNodeEnv().then(setEnv)
}, [])
return (
<div
className={cn(

View File

@@ -18,11 +18,9 @@ import type { Admin } from "@/models/admin"
export default function Appbar(props: { admin: Admin }) {
const router = useRouter()
const [showDropdown, setShowDropdown] = useState(false)
const [showNotifications, setShowNotifications] = useState(false)
const pathname = usePathname()
const dropdownRef = useRef<HTMLDivElement>(null)
const notificationRef = useRef<HTMLDivElement>(null)
// 处理点击外部关闭下拉菜单
useEffect(() => {
@@ -33,12 +31,6 @@ export default function Appbar(props: { admin: Admin }) {
) {
setShowDropdown(false)
}
if (
notificationRef.current &&
!notificationRef.current.contains(event.target as Node)
) {
setShowNotifications(false)
}
}
document.addEventListener("mousedown", handleClickOutside)
@@ -51,7 +43,7 @@ export default function Appbar(props: { admin: Admin }) {
const filteredPaths = paths.filter(path => !hiddenSegments.includes(path))
const breadcrumbs = [
{ path: "/", label: "首页" },
...filteredPaths.map((path, index) => {
...filteredPaths.map(path => {
const originalIndex = paths.findIndex(p => p === path)
const url = `/${paths.slice(0, originalIndex + 1).join("/")}`
const label = getBreadcrumbLabel(path)

View File

@@ -1,19 +1,15 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { format } from "date-fns"
import { Suspense, useCallback, useState } from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { Suspense, useCallback } from "react"
import { Controller, useForm } from "react-hook-form"
import z from "zod"
import { getPageBalance } from "@/actions/balance"
import { DataTable, useDataTable } from "@/components/data-table"
import { Page } from "@/components/page"
import { Button } from "@/components/ui/button"
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import { Field, FieldError, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import type { Balance } from "@/models/balance"
@@ -51,12 +47,14 @@ const filterSchema = z
type FormValues = z.infer<typeof filterSchema>
export default function BalancePage() {
const [filters, setFilters] = useState<FilterValues>({})
const { control, handleSubmit, reset } = useForm<FormValues>({
const searchParams = useSearchParams()
const billNo = searchParams.get("bill_no")
const router = useRouter()
const { control, handleSubmit, reset, getValues } = useForm<FormValues>({
resolver: zodResolver(filterSchema),
defaultValues: {
phone: "",
bill_no: "",
bill_no: billNo || "",
admin_id: "",
created_at_start: "",
created_at_end: "",
@@ -64,21 +62,23 @@ export default function BalancePage() {
})
const fetchUsers = useCallback(
(page: number, size: number) => getPageBalance({ page, size, ...filters }),
[filters],
(page: number, size: number) => {
const result: FilterValues = {}
const filters = getValues()
if (filters.phone?.trim()) result.user_phone = filters.phone.trim()
if (filters.bill_no?.trim()) result.bill_no = filters.bill_no.trim()
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 getPageBalance({ page, size, ...result })
},
[getValues],
)
const table = useDataTable<Balance>(fetchUsers)
const onFilter = handleSubmit(data => {
const result: FilterValues = {}
if (data.phone?.trim()) result.user_phone = data.phone.trim()
if (data.bill_no?.trim()) result.bill_no = data.bill_no.trim()
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)
const onFilter = handleSubmit(() => {
table.pagination.onPageChange(1)
})
@@ -148,8 +148,14 @@ export default function BalancePage() {
type="button"
variant="outline"
onClick={() => {
reset()
setFilters({})
reset({
phone: "",
bill_no: "",
admin_id: "",
created_at_start: "",
created_at_end: "",
})
router.replace("./balance")
table.pagination.onPageChange(1)
}}
>
@@ -218,7 +224,10 @@ export default function BalancePage() {
header: "创建时间",
accessorKey: "created_at",
cell: ({ row }) =>
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
format(
new Date(row.original.created_at),
"yyyy-MM-dd HH:mm:ss",
),
},
]}
/>

View File

@@ -1,7 +1,9 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { format } from "date-fns"
import { Suspense, useState } from "react"
import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation"
import { Suspense } from "react"
import { Controller, useForm } from "react-hook-form"
import { z } from "zod"
import { getPageBatch } from "@/actions/batch"
@@ -64,14 +66,17 @@ const filterSchema = z
type FilterSchema = z.infer<typeof filterSchema>
export default function BatchPage() {
const [filters, setFilters] = useState<APIFilterParams>({})
const { control, handleSubmit, reset } = useForm<FilterSchema>({
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>({
resolver: zodResolver(filterSchema),
defaultValues: {
user_phone: "",
batch_no: "",
batch_no: batchNo || "",
prov: "",
resource_no: resourceNo || "",
city: "",
isp: "all",
created_at_start: "",
@@ -79,24 +84,25 @@ export default function BatchPage() {
},
})
const table = useDataTable<Batch>((page, size) =>
getPageBatch({ page, size, ...filters }),
)
const onFilter = handleSubmit(data => {
const table = useDataTable<Batch>((page, size) => {
const result: APIFilterParams = {}
if (data.user_phone?.trim()) result.user_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)
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 })
})
setFilters(result)
const onFilter = handleSubmit(() => {
table.pagination.onPageChange(1)
})
@@ -237,8 +243,17 @@ export default function BatchPage() {
type="button"
variant="outline"
onClick={() => {
reset()
setFilters({})
reset({
user_phone: "",
batch_no: "",
prov: "",
resource_no: "",
city: "",
isp: "all",
created_at_start: "",
created_at_end: "",
})
router.replace("./batch")
table.pagination.onPageChange(1)
}}
>
@@ -255,7 +270,23 @@ export default function BatchPage() {
header: "会员号",
accessorFn: row => row.user?.phone || "",
},
{ header: "套餐号", accessorKey: "resource.resource_no" },
{
header: "套餐号",
accessorKey: "resource.resource_no",
cell: ({ row }) => {
const resourceNo = row.original.resource?.resource_no
return (
<Link
href={`/channel?resource_no=${resourceNo}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline cursor-pointer"
>
{resourceNo}
</Link>
)
},
},
{ header: "提取编号", accessorKey: "batch_no" },
{ header: "省份", accessorKey: "prov" },
{ header: "城市", accessorKey: "city" },
@@ -266,7 +297,7 @@ export default function BatchPage() {
header: "提取时间",
accessorKey: "time",
cell: ({ row }) =>
format(new Date(row.original.time), "yyyy-MM-dd HH:mm"),
format(new Date(row.original.time), "yyyy-MM-dd HH:mm:ss"),
},
]}
/>

View File

@@ -2,6 +2,8 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { format } from "date-fns"
import { CreditCard, Wallet } from "lucide-react"
import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation"
import { Suspense, useEffect, useState } from "react"
import { Controller, useForm } from "react-hook-form"
import { toast } from "sonner"
@@ -77,17 +79,21 @@ const filterSchema = z
type FilterSchema = z.infer<typeof filterSchema>
export default function BillingPage() {
const [filters, setFilters] = useState<FilterValues>({})
const searchParams = useSearchParams()
const innerNo = searchParams.get("inner_no")
console.log(innerNo, "innerNo")
const [skuOptions, setSkuOptions] = useState<SkuOption[]>([])
const [loading, setLoading] = useState(true)
const [skuProductCode, setSkuProductCode] = useState<ProductCode>(
ProductCode.All,
)
const { control, handleSubmit, reset } = useForm<FilterSchema>({
const router = useRouter()
const { control, handleSubmit, reset, getValues } = useForm<FilterSchema>({
resolver: zodResolver(filterSchema),
defaultValues: {
bill_no: "",
inner_no: "",
inner_no: innerNo || "",
created_at_start: "",
created_at_end: "",
phone: "",
@@ -125,31 +131,58 @@ export default function BillingPage() {
})
}, [skuProductCode])
const table = useDataTable<Billing>((page, size) =>
getPageBill({ page, size, ...filters }),
)
const onFilter = handleSubmit(data => {
const loadData = (page: number, size: number) => {
const result: FilterValues = {}
if (data.phone?.trim()) result.user_phone = data.phone.trim()
if (data.inner_no?.trim()) result.trade_inner_no = data.inner_no.trim()
if (data.bill_no?.trim()) result.bill_no = data.bill_no.trim()
if (data.resource_no?.trim()) result.resource_no = data.resource_no.trim()
if (data.product_code && data.product_code !== ProductCode.All) {
result.product_code = data.product_code
}
if (data.sku_code && data.sku_code !== "all") {
result.sku_code = data.sku_code
}
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)
if (innerNo) {
result.trade_inner_no = innerNo
} else {
const filters = getValues()
if (filters.phone?.trim()) result.user_phone = filters.phone.trim()
if (filters.inner_no?.trim())
result.trade_inner_no = filters.inner_no.trim()
if (filters.bill_no?.trim()) result.bill_no = filters.bill_no.trim()
if (filters.resource_no?.trim())
result.resource_no = filters.resource_no.trim()
if (filters.product_code && filters.product_code !== ProductCode.All) {
result.product_code = filters.product_code
}
if (filters.sku_code && filters.sku_code !== "all") {
result.sku_code = filters.sku_code
}
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 getPageBill({
page,
size,
...result,
})
}
const clearFilter = () => {
router.replace("/billing")
reset({
bill_no: "",
inner_no: "",
created_at_start: "",
created_at_end: "",
phone: "",
resource_no: "",
sku_code: "all",
product_code: "",
})
setSkuProductCode(ProductCode.All)
table.pagination.onPageChange(1)
}
const table = useDataTable<Billing>(loadData)
const onFilter = handleSubmit(() => {
table.pagination.onPageChange(1)
})
return (
<Page>
<form onSubmit={onFilter} className="bg-card p-4 rounded-lg">
@@ -310,16 +343,7 @@ export default function BillingPage() {
<FieldGroup className="flex-row justify-start mt-4 gap-2">
<Button type="submit"></Button>
<Button
type="button"
variant="outline"
onClick={() => {
reset()
setSkuProductCode(ProductCode.All)
setFilters({})
table.pagination.onPageChange(1)
}}
>
<Button type="button" variant="outline" onClick={clearFilter}>
</Button>
</FieldGroup>
@@ -333,9 +357,31 @@ export default function BillingPage() {
header: "创建时间",
accessorKey: "created_at",
cell: ({ row }) =>
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
format(
new Date(row.original.created_at),
"yyyy-MM-dd HH:mm:ss",
),
},
{
header: "套餐号",
accessorKey: "resource.resource_no",
cell: ({ row }) => {
const resource_no = row.original.resource?.resource_no
return resource_no ? (
<Link
href={`/resources?resource_no=${resource_no}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline cursor-pointer"
>
{resource_no}
</Link>
) : (
<span></span>
)
},
},
{ header: "套餐号", accessorKey: "resource.resource_no" },
{ header: "会员号", accessorFn: row => row.user?.phone || "" },
{
header: "账单详情",
@@ -412,7 +458,23 @@ export default function BillingPage() {
)
},
},
{ header: "账单号", accessorKey: "bill_no" },
{
header: "账单号",
accessorKey: "bill_no",
cell: ({ row }) => {
const billNo = row.original.bill_no
return (
<Link
href={`./balance?bill_no=${billNo}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline cursor-pointer"
>
{billNo}
</Link>
)
},
},
{
header: "订单号",
accessorKey: "trade.inner_no",

View File

@@ -1,7 +1,9 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { format } from "date-fns"
import { Suspense, useState } from "react"
import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation"
import { Suspense } from "react"
import { Controller, useForm } from "react-hook-form"
import { z } from "zod"
import { getPageChannel } from "@/actions/channel"
@@ -64,14 +66,15 @@ const ispMap: Record<number, string> = {
}
export default function ChannelPage() {
const [filters, setFilters] = useState<FilterValues>({})
const { control, handleSubmit, reset } = useForm<FilterSchema>({
const searchParams = useSearchParams()
const resourceNo = searchParams.get("resource_no")
const router = useRouter()
const { control, handleSubmit, reset, getValues } = useForm<FilterSchema>({
resolver: zodResolver(filterSchema),
defaultValues: {
batch_no: "",
user_phone: "",
resource_no: "",
resource_no: resourceNo || "",
proxy_port: "",
proxy_host: "",
node_ip: "",
@@ -80,24 +83,26 @@ export default function ChannelPage() {
},
})
const table = useDataTable<Channel>((page, size) =>
getPageChannel({ page, size, ...filters }),
)
const onFilter = handleSubmit(data => {
const table = useDataTable<Channel>((page, size) => {
const result: FilterValues = {}
if (data.batch_no?.trim()) result.batch_no = data.batch_no.trim()
if (data.user_phone?.trim()) result.user_phone = data.user_phone.trim()
if (data.resource_no?.trim()) result.resource_no = data.resource_no.trim()
if (data.proxy_host?.trim()) result.proxy_host = data.proxy_host.trim()
if (data.proxy_port?.trim())
result.proxy_port = Number(data.proxy_port.trim())
if (data.node_ip?.trim()) result.node_ip = data.node_ip.trim()
if (data.expired_at_start)
result.expired_at_start = new Date(data.expired_at_start)
if (data.expired_at_end)
result.expired_at_end = new Date(data.expired_at_end)
setFilters(result)
const filters = getValues()
if (filters.batch_no?.trim()) result.batch_no = filters.batch_no.trim()
if (filters.user_phone?.trim())
result.user_phone = filters.user_phone.trim()
if (filters.resource_no?.trim())
result.resource_no = filters.resource_no.trim()
if (filters.proxy_host?.trim())
result.proxy_host = filters.proxy_host.trim()
if (filters.proxy_port) result.proxy_port = Number(filters.proxy_port)
if (filters.node_ip?.trim()) result.node_ip = filters.node_ip.trim()
if (filters.expired_at_start)
result.expired_at_start = new Date(filters.expired_at_start)
if (filters.expired_at_end)
result.expired_at_end = new Date(filters.expired_at_end)
return getPageChannel({ page, size, ...result })
})
const onFilter = handleSubmit(() => {
table.pagination.onPageChange(1)
})
@@ -225,8 +230,17 @@ export default function ChannelPage() {
type="button"
variant="outline"
onClick={() => {
reset()
setFilters({})
reset({
batch_no: "",
user_phone: "",
resource_no: "",
proxy_port: "",
proxy_host: "",
node_ip: "",
expired_at_start: "",
expired_at_end: "",
})
router.replace("./channel")
table.pagination.onPageChange(1)
}}
>
@@ -244,7 +258,23 @@ export default function ChannelPage() {
accessorFn: row => row.user?.phone || "-",
},
{ header: "套餐号", accessorKey: "resource.resource_no" },
{ header: "提取编号", accessorKey: "batch_no" },
{
header: "提取编号",
accessorKey: "batch_no",
cell: ({ row }) => {
const batchNo = row.original.batch_no
return (
<Link
href={`./batch?batch_no=${batchNo}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline cursor-pointer"
>
{batchNo}
</Link>
)
},
},
{
header: "节点",
accessorFn: row => row.ip || row.edge_ref || row.edge_id,
@@ -318,7 +348,10 @@ export default function ChannelPage() {
header: "过期时间",
accessorKey: "expired_at",
cell: ({ row }) =>
format(new Date(row.original.expired_at), "yyyy-MM-dd HH:mm"),
format(
new Date(row.original.expired_at),
"yyyy-MM-dd HH:mm:ss",
),
},
]}
/>

View File

@@ -213,7 +213,10 @@ export default function BalancePage() {
header: "创建时间",
accessorKey: "created_at",
cell: ({ row }) =>
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
format(
new Date(row.original.created_at),
"yyyy-MM-dd HH:mm:ss",
),
},
]}
/>

View File

@@ -261,7 +261,7 @@ export default function BatchPage() {
header: "提取时间",
accessorKey: "time",
cell: ({ row }) =>
format(new Date(row.original.time), "yyyy-MM-dd HH:mm"),
format(new Date(row.original.time), "yyyy-MM-dd HH:mm:ss"),
},
]}
/>

View File

@@ -335,7 +335,10 @@ export default function BillingPage() {
header: "创建时间",
accessorKey: "created_at",
cell: ({ row }) =>
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
format(
new Date(row.original.created_at),
"yyyy-MM-dd HH:mm:ss",
),
},
{ header: "套餐号", accessorKey: "resource.resource_no" },
{

View File

@@ -319,7 +319,10 @@ export default function ChannelPage() {
header: "过期时间",
accessorKey: "expired_at",
cell: ({ row }) =>
format(new Date(row.original.expired_at), "yyyy-MM-dd HH:mm"),
format(
new Date(row.original.expired_at),
"yyyy-MM-dd HH:mm:ss",
),
},
]}
/>

View File

@@ -99,7 +99,7 @@ export default function CouponPage() {
header: "发放时间",
accessorKey: "issued_at",
// cell: ({ row }) =>
// format(new Date(row.original.issued_at), "yyyy-MM-dd HH:mm"),
// format(new Date(row.original.issued_at), "yyyy-MM-dd HH:mm:ss"),
},
{
header: "备注",

View File

@@ -150,7 +150,10 @@ export default function UserQueryPage() {
header: "创建时间",
accessorKey: "created_at",
cell: ({ row }) =>
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
format(
new Date(row.original.created_at),
"yyyy-MM-dd HH:mm:ss",
),
},
{
header: "客户来源",
@@ -215,7 +218,7 @@ export default function UserQueryPage() {
row.original.last_login
? format(
new Date(row.original.last_login),
"yyyy-MM-dd HH:mm",
"yyyy-MM-dd HH:mm:ss",
)
: "",
},

View File

@@ -158,7 +158,7 @@ function ExpireBadge({ expireAt }: { expireAt: Date | null | undefined }) {
// 格式化日期
function formatDateTime(date: Date | null | undefined) {
if (!date) return "-"
return format(date, "yyyy-MM-dd HH:mm")
return format(date, "yyyy-MM-dd HH:mm:ss")
}
// 计算今日使用量

View File

@@ -252,7 +252,10 @@ export default function TradePage() {
header: "创建时间",
accessorKey: "created_at",
cell: ({ row }) =>
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
format(
new Date(row.original.created_at),
"yyyy-MM-dd HH:mm:ss",
),
},
{
header: "订单号",

View File

@@ -84,11 +84,14 @@ export default function CouponPage() {
if (coupon.expire_type === 2 && coupon.expire_in) {
const expireDate = new Date(coupon.created_at)
expireDate.setDate(expireDate.getDate() + coupon.expire_in)
return format(expireDate, "yyyy-MM-dd HH:mm")
return format(expireDate, "yyyy-MM-dd HH:mm:ss")
}
if (coupon.expire_type === 1 && coupon.expire_at) {
return format(new Date(coupon.expire_at), "yyyy-MM-dd HH:mm")
return format(
new Date(coupon.expire_at),
"yyyy-MM-dd HH:mm:ss",
)
}
return <span></span>
},
@@ -97,7 +100,10 @@ export default function CouponPage() {
header: "创建时间",
accessorKey: "created_at",
cell: ({ row }) =>
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
format(
new Date(row.original.created_at),
"yyyy-MM-dd HH:mm:ss",
),
},
{
id: "action",

View File

@@ -308,7 +308,7 @@ export function ReleaseCoupon(props: {
{user.last_login
? format(
new Date(user.last_login),
"yyyy-MM-dd HH:mm",
"yyyy-MM-dd HH:mm:ss",
)
: "-"}
</span>

View File

@@ -3,7 +3,7 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { format } from "date-fns"
import { useRouter } from "next/navigation"
import { Suspense, useCallback, useState } from "react"
import { Suspense, useCallback } from "react"
import { Controller, useForm } from "react-hook-form"
import { z } from "zod"
import { getPageCusts } from "@/actions/cust"
@@ -82,9 +82,8 @@ const filterSchema = z
type FormValues = z.infer<typeof filterSchema>
export default function CustPage() {
const [filters, setFilters] = useState<FilterValues>({})
const router = useRouter()
const { control, handleSubmit, reset } = useForm<FormValues>({
const { control, handleSubmit, reset, getValues } = useForm<FormValues>({
resolver: zodResolver(filterSchema),
defaultValues: {
account: "",
@@ -97,21 +96,27 @@ export default function CustPage() {
})
const fetchUsers = useCallback(
(page: number, size: number) => getPageCusts({ page, size, ...filters }),
[filters],
(page: number, size: number) => {
const result: FilterValues = {}
const filters = getValues()
if (filters.account?.trim()) result.account = filters.account.trim()
if (filters.name?.trim()) result.name = filters.name.trim()
if (filters.identified && filters.identified !== "all")
result.identified = filters.identified === "1"
if (filters.enabled && filters.enabled !== "all")
result.enabled = filters.enabled === "1"
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 getPageCusts({ page, size, ...result })
},
[getValues],
)
const table = useDataTable<User>(fetchUsers)
const onFilter = handleSubmit(data => {
const result: FilterValues = {}
if (data.account?.trim()) result.account = data.account.trim()
if (data.name?.trim()) result.name = data.name.trim()
if (data.identified && data.identified !== "all")
result.identified = data.identified === "1"
if (data.enabled && data.enabled !== "all")
result.enabled = data.enabled === "1"
setFilters(result)
const onFilter = handleSubmit(() => {
table.pagination.onPageChange(1)
})
@@ -219,8 +224,14 @@ export default function CustPage() {
type="button"
variant="outline"
onClick={() => {
reset()
setFilters({})
reset({
account: "",
name: "",
identified: "all",
enabled: "all",
created_at_start: "",
created_at_end: "",
})
table.pagination.onPageChange(1)
}}
>
@@ -240,7 +251,10 @@ export default function CustPage() {
header: "创建时间",
accessorKey: "created_at",
cell: ({ row }) =>
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
format(
new Date(row.original.created_at),
"yyyy-MM-dd HH:mm:ss",
),
},
// { header: "邮箱", accessorKey: "email" },
{
@@ -311,7 +325,7 @@ export default function CustPage() {
row.original.last_login
? format(
new Date(row.original.last_login),
"yyyy-MM-dd HH:mm",
"yyyy-MM-dd HH:mm:ss",
)
: "",
},

View File

@@ -49,13 +49,19 @@ export default function DiscountPage() {
header: "创建时间",
accessorKey: "created_at",
cell: ({ row }) =>
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
format(
new Date(row.original.created_at),
"yyyy-MM-dd HH:mm:ss",
),
},
{
header: "更新时间",
accessorKey: "updated_at",
cell: ({ row }) =>
format(new Date(row.original.updated_at), "yyyy-MM-dd HH:mm"),
format(
new Date(row.original.updated_at),
"yyyy-MM-dd HH:mm:ss",
),
},
{
id: "action",

View File

@@ -105,7 +105,10 @@ export default function GatewayPage() {
header: "创建时间",
accessorKey: "created_at",
cell: ({ row }) =>
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
format(
new Date(row.original.created_at),
"yyyy-MM-dd HH:mm:ss",
),
},
{
id: "action",

View File

@@ -144,11 +144,11 @@ function ProductSkus(props: {
{ header: "最低购买数量", accessorKey: "count_min" },
{
header: "创建时间",
accessorFn: row => format(row.created_at, "yyyy-MM-dd HH:mm"),
accessorFn: row => format(row.created_at, "yyyy-MM-dd HH:mm:ss"),
},
{
header: "更新时间",
accessorFn: row => format(row.updated_at, "yyyy-MM-dd HH:mm"),
accessorFn: row => format(row.updated_at, "yyyy-MM-dd HH:mm:ss"),
},
{
id: "action",

View File

@@ -2,6 +2,8 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { format, isBefore, isSameDay } from "date-fns"
import { Box, Loader2, Timer } from "lucide-react"
import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation"
import { Suspense, useCallback, useMemo, useState } from "react"
import { Controller, useForm } from "react-hook-form"
import { toast } from "sonner"
@@ -156,8 +158,8 @@ function ExpireBadge({ expireAt }: { expireAt: Date | null | undefined }) {
// 格式化日期
function formatDateTime(date: Date | null | undefined) {
if (!date) return "-"
return format(date, "yyyy-MM-dd HH:mm")
if (!date) return ""
return format(date, "yyyy-MM-dd HH:mm:ss")
}
// 计算今日使用量
@@ -200,15 +202,17 @@ interface ResourceListProps {
}
function ResourceList({ resourceType }: ResourceListProps) {
const searchParams = useSearchParams()
const resourceNo = searchParams.get("resource_no")
const isLong = resourceType === "long"
const listFn = isLong ? listResourceLong : listResourceShort
const [filters, setFilters] = useState<FilterParams>({})
const [updatingId, setUpdatingId] = useState<number | null>(null)
const { control, handleSubmit, reset } = useForm<FormValues>({
const router = useRouter()
const { control, handleSubmit, reset, getValues } = useForm<FormValues>({
resolver: zodResolver(filterSchema),
defaultValues: {
user_phone: "",
resource_no: "",
resource_no: resourceNo || "",
status: "all",
type: "all",
created_at_start: "",
@@ -219,16 +223,35 @@ function ResourceList({ resourceType }: ResourceListProps) {
const fetchResources = useCallback(
(page: number, size: number) => {
return listFn({ page, size, ...filters })
const result: FilterParams = {}
const filters = getValues()
if (filters.user_phone?.trim())
result.user_phone = filters.user_phone.trim()
if (filters.resource_no?.trim())
result.resource_no = filters.resource_no.trim()
if (filters.status && filters.status !== "all") {
result.active = filters.status === "0"
}
if (filters.type && filters.type !== "all") {
result.mode = Number(filters.type)
}
if (filters.expired && filters.expired !== "all") {
result.expired = filters.expired === "1"
}
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 listFn({ page, size, ...result })
},
[listFn, filters],
[listFn, getValues],
)
const table = useDataTable<Resources>(fetchResources)
const refreshTable = useCallback(() => {
setFilters(prev => ({ ...prev }))
}, [])
// const refreshTable = useCallback(() => {
// setFilters(prev => ({ ...prev }))
// }, [])
const handleStatusChange = useCallback(
async (resource: Resources, newStatusValue: string) => {
@@ -243,7 +266,7 @@ function ResourceList({ resourceType }: ResourceListProps) {
toast.success("更新成功", {
description: `资源状态已更新为${newActive ? "启用" : "禁用"}`,
})
refreshTable()
table.refresh()
} catch (error) {
console.error("更新状态失败:", error)
toast.error("更新失败", {
@@ -253,7 +276,7 @@ function ResourceList({ resourceType }: ResourceListProps) {
setUpdatingId(null)
}
},
[refreshTable],
[table],
)
const handleCheckipChange = useCallback(
async (resource: Resources) => {
@@ -267,7 +290,7 @@ function ResourceList({ resourceType }: ResourceListProps) {
toast.success("更新成功", {
description: `IP检查已${newCheckip ? "启用IP检查" : "停用IP检查"}`,
})
refreshTable()
table.refresh()
} catch (error) {
console.error("更新IP检查状态失败:", error)
toast.error("更新失败", {
@@ -277,27 +300,9 @@ function ResourceList({ resourceType }: ResourceListProps) {
setUpdatingId(null)
}
},
[refreshTable],
[table],
)
const onFilter = handleSubmit(data => {
const result: FilterParams = {}
if (data.user_phone?.trim()) result.user_phone = data.user_phone.trim()
if (data.resource_no?.trim()) result.resource_no = data.resource_no.trim()
if (data.status && data.status !== "all") {
result.active = data.status === "0"
}
if (data.type && data.type !== "all") {
result.mode = Number(data.type)
}
if (data.expired && data.expired !== "all") {
result.expired = data.expired === "1"
}
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)
const onFilter = handleSubmit(() => {
table.pagination.onPageChange(1)
})
@@ -317,7 +322,14 @@ function ResourceList({ resourceType }: ResourceListProps) {
<div className="flex flex-col gap-1">
<div>{name}</div>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">{resourceNo}</span>
<Link
href={`/batch?resource_no=${resourceNo}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-gray-500"
>
{resourceNo}
</Link>
<ExpireBadge expireAt={expireAt} />
</div>
</div>
@@ -576,8 +588,16 @@ function ResourceList({ resourceType }: ResourceListProps) {
type="button"
variant="outline"
onClick={() => {
reset()
setFilters({})
router.replace("./resources")
reset({
user_phone: "",
resource_no: "",
status: "all",
type: "all",
created_at_start: "",
created_at_end: "",
expired: "all",
})
table.pagination.onPageChange(1)
}}
>

View File

@@ -2,6 +2,7 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { format } from "date-fns"
import { CheckCircle, Clock, XCircle } from "lucide-react"
import Link from "next/link"
import { Suspense, useCallback, useState } from "react"
import { Controller, useForm } from "react-hook-form"
import { z } from "zod"
@@ -9,12 +10,7 @@ import { getPageTrade } from "@/actions/trade"
import { DataTable, useDataTable } from "@/components/data-table"
import { Page } from "@/components/page"
import { Button } from "@/components/ui/button"
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import { Field, FieldError, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import {
Select,
@@ -69,7 +65,6 @@ type FilterSchema = z.infer<typeof filterSchema>
export default function TradePage() {
const [filters, setFilters] = useState<FilterValues>({})
const { control, handleSubmit, reset } = useForm<FilterSchema>({
resolver: zodResolver(filterSchema),
defaultValues: {
@@ -120,10 +115,7 @@ export default function TradePage() {
name="user_phone"
control={control}
render={({ field, fieldState }) => (
<Field
data-invalid={fieldState.invalid}
className="w-40 flex"
>
<Field data-invalid={fieldState.invalid} className="w-40 flex">
<FieldLabel></FieldLabel>
<Input {...field} placeholder="请输入会员号" />
<FieldError>{fieldState.error?.message}</FieldError>
@@ -135,10 +127,7 @@ export default function TradePage() {
name="inner_no"
control={control}
render={({ field, fieldState }) => (
<Field
data-invalid={fieldState.invalid}
className="w-40 flex"
>
<Field data-invalid={fieldState.invalid} className="w-40 flex">
<FieldLabel></FieldLabel>
<Input {...field} placeholder="请输入订单号" />
<FieldError>{fieldState.error?.message}</FieldError>
@@ -217,10 +206,7 @@ export default function TradePage() {
name="created_at_start"
control={control}
render={({ field, fieldState }) => (
<Field
data-invalid={fieldState.invalid}
className="w-40 flex"
>
<Field data-invalid={fieldState.invalid} className="w-40 flex">
<FieldLabel></FieldLabel>
<Input type="date" {...field} />
<FieldError>{fieldState.error?.message}</FieldError>
@@ -232,10 +218,7 @@ export default function TradePage() {
name="created_at_end"
control={control}
render={({ field, fieldState }) => (
<Field
data-invalid={fieldState.invalid}
className="w-40 flex"
>
<Field data-invalid={fieldState.invalid} className="w-40 flex">
<FieldLabel></FieldLabel>
<Input type="date" {...field} />
<FieldError>{fieldState.error?.message}</FieldError>
@@ -266,10 +249,29 @@ export default function TradePage() {
header: "创建时间",
accessorKey: "created_at",
cell: ({ row }) =>
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
format(
new Date(row.original.created_at),
"yyyy-MM-dd HH:mm:ss",
),
},
{ header: "会员号", accessorFn: row => row.user?.phone || "" },
{ header: "订单号", accessorKey: "inner_no" },
{
header: "订单号",
accessorKey: "inner_no",
cell: ({ row }) => {
const innerNo = row.original.inner_no
return (
<Link
href={`/billing?inner_no=${innerNo}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline cursor-pointer"
>
{innerNo}
</Link>
)
},
},
{ header: "购买套餐", accessorKey: "subject" },
{
header: "支付金额",

View File

@@ -10,11 +10,7 @@ import { DataTable, useDataTable } from "@/components/data-table"
import { Page } from "@/components/page"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Field,
FieldError,
FieldLabel,
} from "@/components/ui/field"
import { Field, FieldError, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { useFetch } from "@/hooks/data"
import { ScopeUserWriteBind } from "@/lib/scopes"
@@ -67,10 +63,7 @@ export default function UserPage() {
name="phone"
control={control}
render={({ field, fieldState }) => (
<Field
data-invalid={fieldState.invalid}
className="w-40 flex"
>
<Field data-invalid={fieldState.invalid} className="w-40 flex">
<FieldLabel></FieldLabel>
<Input {...field} placeholder="请输入手机号" />
<FieldError>{fieldState.error?.message}</FieldError>
@@ -161,7 +154,7 @@ export default function UserPage() {
row.original.last_login
? format(
new Date(row.original.last_login),
"yyyy-MM-dd HH:mm",
"yyyy-MM-dd HH:mm:ss",
)
: "",
},
@@ -174,7 +167,10 @@ export default function UserPage() {
header: "创建时间",
accessorKey: "created_at",
cell: ({ row }) =>
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
format(
new Date(row.original.created_at),
"yyyy-MM-dd HH:mm:ss",
),
},
{
id: "action",