2026-08-31 14:49:28 +08:00
|
|
|
|
import type { ColumnDef } from "@tanstack/react-table"
|
|
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react"
|
|
|
|
|
|
import { useNavigate } from "react-router-dom"
|
|
|
|
|
|
import { toast } from "sonner"
|
|
|
|
|
|
import {
|
|
|
|
|
|
fetchCxOrders,
|
|
|
|
|
|
fetchCxPackages,
|
|
|
|
|
|
fetchLangIpList,
|
|
|
|
|
|
fetchLangIps,
|
|
|
|
|
|
fetchSwitchList,
|
|
|
|
|
|
} from "@/api/http"
|
|
|
|
|
|
import DataTable from "@/components/data-table"
|
2026-08-18 13:33:22 +08:00
|
|
|
|
import Page from "@/components/page"
|
2026-08-31 14:49:28 +08:00
|
|
|
|
import { Button } from "@/components/ui/button"
|
|
|
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
|
|
|
|
import type {
|
|
|
|
|
|
CxOrder,
|
|
|
|
|
|
CxPackage,
|
|
|
|
|
|
LangIpItem,
|
|
|
|
|
|
SwitchRecord,
|
|
|
|
|
|
} from "@/lib/models/package"
|
|
|
|
|
|
import {
|
|
|
|
|
|
formatAbroad,
|
2026-09-18 13:17:23 +08:00
|
|
|
|
formatExpired,
|
2026-08-31 14:49:28 +08:00
|
|
|
|
formatIpState,
|
2026-09-18 13:17:23 +08:00
|
|
|
|
formatLineType,
|
2026-08-31 14:49:28 +08:00
|
|
|
|
formatOrderTypeLabel,
|
2026-09-18 13:17:23 +08:00
|
|
|
|
formatShareType,
|
|
|
|
|
|
formatSwitchReason,
|
2026-08-31 14:49:28 +08:00
|
|
|
|
LONG_ORDER_TYPE_LABELS,
|
|
|
|
|
|
parseExpired,
|
|
|
|
|
|
} from "@/lib/models/package"
|
2026-09-18 13:17:23 +08:00
|
|
|
|
import { downloadTextFile, estimateTotal, formatUnixTime } from "@/lib/utils"
|
2026-08-31 14:49:28 +08:00
|
|
|
|
import LongRenewDialog, { type LongRenewSource } from "./long-renew-dialog"
|
|
|
|
|
|
import LongSwitchDialog from "./long-switch-dialog"
|
|
|
|
|
|
|
|
|
|
|
|
const PAGE_SIZE = 50
|
2026-08-18 13:33:22 +08:00
|
|
|
|
|
|
|
|
|
|
export default function LongPage() {
|
2026-08-31 14:49:28 +08:00
|
|
|
|
const navigate = useNavigate()
|
|
|
|
|
|
const [activeTab, setActiveTab] = useState("ips")
|
|
|
|
|
|
|
|
|
|
|
|
// 已开通长效
|
|
|
|
|
|
const [ipList, setIpList] = useState<LangIpItem[]>([])
|
|
|
|
|
|
const [ipStatus, setIpStatus] = useState<"load" | "done" | "fail">("load")
|
|
|
|
|
|
const [ipPage, setIpPage] = useState(1)
|
|
|
|
|
|
const [ipTotal, setIpTotal] = useState(0)
|
|
|
|
|
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({})
|
|
|
|
|
|
|
|
|
|
|
|
// 全部套餐
|
|
|
|
|
|
const [packages, setPackages] = useState<CxPackage[]>([])
|
|
|
|
|
|
const [packageStatus, setPackageStatus] = useState<"load" | "done" | "fail">(
|
|
|
|
|
|
"load",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 购买记录
|
|
|
|
|
|
const [orders, setOrders] = useState<CxOrder[]>([])
|
|
|
|
|
|
const [orderStatus, setOrderStatus] = useState<"load" | "done" | "fail">(
|
|
|
|
|
|
"load",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// IP切换记录
|
|
|
|
|
|
const [switchList, setSwitchList] = useState<SwitchRecord[]>([])
|
|
|
|
|
|
const [switchStatus, setSwitchStatus] = useState<"load" | "done" | "fail">(
|
|
|
|
|
|
"load",
|
|
|
|
|
|
)
|
|
|
|
|
|
const [switchPage, setSwitchPage] = useState(1)
|
|
|
|
|
|
const [switchTotal, setSwitchTotal] = useState(0)
|
|
|
|
|
|
|
|
|
|
|
|
// 弹窗
|
|
|
|
|
|
const [renewSource, setRenewSource] = useState<LongRenewSource | null>(null)
|
|
|
|
|
|
const [switchTarget, setSwitchTarget] = useState<{
|
|
|
|
|
|
ip: LangIpItem
|
|
|
|
|
|
pop: 0 | 1
|
|
|
|
|
|
} | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
const loadIps = useCallback(async (page: number) => {
|
|
|
|
|
|
setIpStatus("load")
|
|
|
|
|
|
const res = await fetchLangIpList({
|
|
|
|
|
|
page,
|
|
|
|
|
|
limit: PAGE_SIZE,
|
|
|
|
|
|
packId: "",
|
|
|
|
|
|
isAbroad: "",
|
|
|
|
|
|
isExpired: 0,
|
|
|
|
|
|
state: -1,
|
|
|
|
|
|
ip: "",
|
|
|
|
|
|
groupId: "",
|
|
|
|
|
|
gameId: "",
|
|
|
|
|
|
cityCode: "",
|
|
|
|
|
|
orderBy: 0,
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!res.success) {
|
|
|
|
|
|
setIpStatus("fail")
|
|
|
|
|
|
toast.error(res.message)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
setIpList(res.data.items)
|
|
|
|
|
|
setIpTotal(
|
|
|
|
|
|
estimateTotal(
|
|
|
|
|
|
res.data.pager.totalCount,
|
|
|
|
|
|
res.data.pager.currentPage || page,
|
|
|
|
|
|
PAGE_SIZE,
|
|
|
|
|
|
res.data.items.length,
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
setIpStatus("done")
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
const loadPackages = useCallback(async () => {
|
|
|
|
|
|
setPackageStatus("load")
|
|
|
|
|
|
const res = await fetchCxPackages()
|
|
|
|
|
|
if (!res.success) {
|
|
|
|
|
|
setPackageStatus("fail")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
setPackages(res.data)
|
|
|
|
|
|
setPackageStatus("done")
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
const loadOrders = useCallback(async () => {
|
|
|
|
|
|
setOrderStatus("load")
|
|
|
|
|
|
const res = await fetchCxOrders()
|
|
|
|
|
|
if (!res.success) {
|
|
|
|
|
|
setOrderStatus("fail")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
setOrders(res.data)
|
|
|
|
|
|
setOrderStatus("done")
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
const loadSwitch = useCallback(async (page: number) => {
|
|
|
|
|
|
setSwitchStatus("load")
|
|
|
|
|
|
const res = await fetchSwitchList({
|
|
|
|
|
|
page,
|
|
|
|
|
|
limit: PAGE_SIZE,
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!res.success) {
|
|
|
|
|
|
setSwitchStatus("fail")
|
|
|
|
|
|
toast.error(res.message)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
setSwitchList(res.data.items)
|
|
|
|
|
|
setSwitchTotal(
|
|
|
|
|
|
estimateTotal(
|
|
|
|
|
|
res.data.pager.totalCount,
|
|
|
|
|
|
res.data.pager.currentPage || page,
|
|
|
|
|
|
PAGE_SIZE,
|
|
|
|
|
|
res.data.items.length,
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
setSwitchStatus("done")
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-09-11 17:06:29 +08:00
|
|
|
|
loadIps(1)
|
2026-08-31 14:49:28 +08:00
|
|
|
|
}, [loadIps])
|
|
|
|
|
|
|
|
|
|
|
|
const handleTabChange = (value: string) => {
|
|
|
|
|
|
setActiveTab(value)
|
2026-09-11 17:06:29 +08:00
|
|
|
|
if (value === "packages" && !packages.length) loadPackages()
|
|
|
|
|
|
if (value === "orders" && !orders.length) loadOrders()
|
|
|
|
|
|
if (value === "switch" && !switchList.length) loadSwitch(1)
|
2026-08-31 14:49:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handlePageChange = (page: number) => {
|
|
|
|
|
|
setIpPage(page)
|
|
|
|
|
|
setRowSelection({})
|
2026-09-11 17:06:29 +08:00
|
|
|
|
loadIps(page)
|
2026-08-31 14:49:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleSwitchPageChange = (page: number) => {
|
|
|
|
|
|
setSwitchPage(page)
|
2026-09-11 17:06:29 +08:00
|
|
|
|
loadSwitch(page)
|
2026-08-31 14:49:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleExtract = async () => {
|
|
|
|
|
|
const ids = Object.keys(rowSelection)
|
|
|
|
|
|
if (ids.length === 0) {
|
|
|
|
|
|
toast.error("请选择IP!")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const res = await fetchLangIps(ids.join(","))
|
|
|
|
|
|
if (!res.success) {
|
|
|
|
|
|
toast.error(res.message)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (res.data.length === 0) {
|
|
|
|
|
|
toast.error("提取失败,请稍后重试")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const content = res.data
|
|
|
|
|
|
.map(
|
|
|
|
|
|
ip =>
|
|
|
|
|
|
`${ip.ip}|${ip.httpPort}|${ip.socks5Port}|${ip.username}|${ip.password}`,
|
|
|
|
|
|
)
|
|
|
|
|
|
.join("\n")
|
|
|
|
|
|
downloadTextFile("ip.txt", content)
|
|
|
|
|
|
toast.success("提取成功,已开始下载")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ipColumns = useMemo<ColumnDef<LangIpItem>[]>(
|
|
|
|
|
|
() => [
|
|
|
|
|
|
{
|
|
|
|
|
|
accessorKey: "isRelayed",
|
|
|
|
|
|
header: "模式",
|
|
|
|
|
|
cell: ({ row }) => (row.original.isRelayed ? "中继" : "直连"),
|
|
|
|
|
|
},
|
|
|
|
|
|
{ accessorKey: "ip", header: "节点入口IP" },
|
|
|
|
|
|
{ accessorKey: "realIp", header: "隧道出口IP" },
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "ports",
|
|
|
|
|
|
header: "HTTP/SK5端口",
|
|
|
|
|
|
cell: ({ row }) =>
|
|
|
|
|
|
`${row.original.httpPort}/${row.original.socks5Port}`,
|
|
|
|
|
|
},
|
|
|
|
|
|
{ accessorKey: "area", header: "归属地" },
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "bandwidth",
|
|
|
|
|
|
header: "带宽",
|
|
|
|
|
|
cell: ({ row }) =>
|
2026-09-18 13:17:23 +08:00
|
|
|
|
`${row.original.bandwidth}M/${formatLineType(row.original.lineType)}`,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
accessorKey: "shareType",
|
|
|
|
|
|
header: "线路",
|
|
|
|
|
|
cell: ({ row }) => formatShareType(row.original.shareType),
|
2026-08-31 14:49:28 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
accessorKey: "state",
|
|
|
|
|
|
header: "状态",
|
|
|
|
|
|
cell: ({ row }) => formatIpState(row.original.state),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "account",
|
|
|
|
|
|
header: "账号/密码",
|
|
|
|
|
|
cell: ({ row }) => `${row.original.username}/${row.original.password}`,
|
|
|
|
|
|
},
|
2026-09-18 13:17:23 +08:00
|
|
|
|
{
|
|
|
|
|
|
accessorKey: "expireTime",
|
|
|
|
|
|
header: "过期时间",
|
|
|
|
|
|
cell: ({ row }) => formatUnixTime(row.original.expireTime),
|
|
|
|
|
|
},
|
2026-08-31 14:49:28 +08:00
|
|
|
|
{
|
|
|
|
|
|
accessorKey: "isExpired",
|
|
|
|
|
|
header: "是否过期",
|
|
|
|
|
|
cell: ({ row }) =>
|
|
|
|
|
|
parseExpired(row.original.isExpired) ? "已过期" : "未过期",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "actions",
|
|
|
|
|
|
header: "操作",
|
|
|
|
|
|
cell: ({ row }) => {
|
|
|
|
|
|
const ip = row.original
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => setRenewSource({ kind: "ip", ip })}
|
|
|
|
|
|
className="bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
|
|
|
|
|
>
|
|
|
|
|
|
续费
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
{ip.isAbroad === 1 && ip.isRelayed === 1 && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => setSwitchTarget({ ip, pop: 1 })}
|
|
|
|
|
|
>
|
|
|
|
|
|
切换入口
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => setSwitchTarget({ ip, pop: 0 })}
|
|
|
|
|
|
>
|
|
|
|
|
|
切换节点
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
[],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const packageColumns = useMemo<ColumnDef<CxPackage>[]>(
|
|
|
|
|
|
() => [
|
2026-09-18 13:17:23 +08:00
|
|
|
|
{
|
|
|
|
|
|
accessorKey: "name",
|
|
|
|
|
|
header: "套餐名称",
|
|
|
|
|
|
cell: ({ row }) => row.original.name || "-",
|
|
|
|
|
|
},
|
2026-08-31 14:49:28 +08:00
|
|
|
|
{
|
|
|
|
|
|
accessorKey: "isAbroad",
|
|
|
|
|
|
header: "套餐类型",
|
|
|
|
|
|
cell: ({ row }) => formatAbroad(row.original.isAbroad),
|
|
|
|
|
|
},
|
2026-09-18 13:17:23 +08:00
|
|
|
|
{
|
|
|
|
|
|
accessorKey: "shareType",
|
|
|
|
|
|
header: "模式",
|
|
|
|
|
|
cell: ({ row }) => formatShareType(row.original.shareType),
|
|
|
|
|
|
},
|
2026-08-31 14:49:28 +08:00
|
|
|
|
{ accessorKey: "ipAmount", header: "可用IP数量(个)" },
|
2026-09-18 13:17:23 +08:00
|
|
|
|
{
|
|
|
|
|
|
accessorKey: "isExpired",
|
|
|
|
|
|
header: "是否过期",
|
|
|
|
|
|
cell: ({ row }) => formatExpired(row.original.isExpired),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
accessorKey: "expireTime",
|
|
|
|
|
|
header: "过期时间",
|
|
|
|
|
|
cell: ({ row }) => formatUnixTime(row.original.expireTime),
|
|
|
|
|
|
},
|
2026-08-31 14:49:28 +08:00
|
|
|
|
{
|
|
|
|
|
|
id: "actions",
|
|
|
|
|
|
header: "续费",
|
|
|
|
|
|
cell: ({ row }) => (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() =>
|
|
|
|
|
|
setRenewSource({ kind: "package", pkg: row.original })
|
|
|
|
|
|
}
|
|
|
|
|
|
className="bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
|
|
|
|
|
>
|
|
|
|
|
|
续费
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
[],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const orderColumns = useMemo<ColumnDef<CxOrder>[]>(
|
|
|
|
|
|
() => [
|
|
|
|
|
|
{ accessorKey: "order_no", header: "订单号" },
|
|
|
|
|
|
{
|
|
|
|
|
|
accessorKey: "order_type",
|
|
|
|
|
|
header: "订单类型",
|
|
|
|
|
|
cell: ({ row }) =>
|
|
|
|
|
|
formatOrderTypeLabel(LONG_ORDER_TYPE_LABELS, row.original.order_type),
|
|
|
|
|
|
},
|
|
|
|
|
|
{ accessorKey: "game_type", header: "游戏类型" },
|
|
|
|
|
|
{ accessorKey: "ip_count", header: "IP数量" },
|
|
|
|
|
|
{ accessorKey: "money", header: "实付金额" },
|
|
|
|
|
|
{ accessorKey: "create_time", header: "支付时间" },
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "rebuy",
|
|
|
|
|
|
header: "再次购买",
|
|
|
|
|
|
cell: () => (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => navigate("/product/http")}
|
|
|
|
|
|
>
|
|
|
|
|
|
再次购买
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
[navigate],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const switchColumns = useMemo<ColumnDef<SwitchRecord>[]>(
|
|
|
|
|
|
() => [
|
|
|
|
|
|
{ accessorKey: "oldIp", header: "原IP" },
|
|
|
|
|
|
{ accessorKey: "oldArea", header: "原属地" },
|
|
|
|
|
|
{ accessorKey: "newIp", header: "切换后IP" },
|
|
|
|
|
|
{ accessorKey: "newArea", header: "切换后属地" },
|
2026-09-18 13:17:23 +08:00
|
|
|
|
{
|
|
|
|
|
|
accessorKey: "reason",
|
|
|
|
|
|
header: "切换方式",
|
|
|
|
|
|
cell: ({ row }) => formatSwitchReason(row.original.reason),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
accessorKey: "createTime",
|
|
|
|
|
|
header: "切换时间",
|
|
|
|
|
|
cell: ({ row }) => formatUnixTime(row.original.createTime),
|
|
|
|
|
|
},
|
2026-08-31 14:49:28 +08:00
|
|
|
|
],
|
|
|
|
|
|
[],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Page>
|
|
|
|
|
|
<div className="bg-white rounded-lg p-4">
|
|
|
|
|
|
<h3 className="font-semibold text-lg mb-4">长效套餐</h3>
|
|
|
|
|
|
<Tabs value={activeTab} onValueChange={handleTabChange}>
|
|
|
|
|
|
<TabsList>
|
|
|
|
|
|
<TabsTrigger value="ips">已开通长效</TabsTrigger>
|
|
|
|
|
|
<TabsTrigger value="packages">全部套餐</TabsTrigger>
|
|
|
|
|
|
<TabsTrigger value="orders">购买记录</TabsTrigger>
|
|
|
|
|
|
<TabsTrigger value="switch">IP切换记录</TabsTrigger>
|
|
|
|
|
|
</TabsList>
|
|
|
|
|
|
|
|
|
|
|
|
<TabsContent value="ips" className="pt-4 flex flex-col gap-3">
|
|
|
|
|
|
<div className="flex items-center gap-4 flex-wrap">
|
|
|
|
|
|
<Button
|
2026-09-11 17:06:29 +08:00
|
|
|
|
onClick={() => handleExtract()}
|
2026-08-31 14:49:28 +08:00
|
|
|
|
className="bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
|
|
|
|
|
>
|
|
|
|
|
|
批量提取IP
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<span className="text-xs text-gray-500">
|
|
|
|
|
|
格式:IP|http端口|socks5端口|账号|密码
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<DataTable
|
|
|
|
|
|
data={ipList}
|
|
|
|
|
|
status={ipStatus}
|
|
|
|
|
|
columns={ipColumns}
|
|
|
|
|
|
enableSelection
|
|
|
|
|
|
rowSelection={rowSelection}
|
|
|
|
|
|
onSelectionChange={setRowSelection}
|
|
|
|
|
|
getRowId={row => String(row.id)}
|
|
|
|
|
|
pagination={{
|
2026-09-11 17:06:29 +08:00
|
|
|
|
page: ipPage,
|
2026-08-31 14:49:28 +08:00
|
|
|
|
size: PAGE_SIZE,
|
|
|
|
|
|
total: ipTotal,
|
|
|
|
|
|
sizeOptions: [PAGE_SIZE],
|
|
|
|
|
|
onPageChange: handlePageChange,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
|
|
<TabsContent value="packages" className="pt-4">
|
|
|
|
|
|
<DataTable
|
|
|
|
|
|
data={packages}
|
|
|
|
|
|
status={packageStatus}
|
|
|
|
|
|
columns={packageColumns}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
|
|
<TabsContent value="orders" className="pt-4">
|
|
|
|
|
|
<DataTable
|
|
|
|
|
|
data={orders}
|
|
|
|
|
|
status={orderStatus}
|
|
|
|
|
|
columns={orderColumns}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
|
|
<TabsContent value="switch" className="pt-4">
|
|
|
|
|
|
<DataTable
|
|
|
|
|
|
data={switchList}
|
|
|
|
|
|
status={switchStatus}
|
|
|
|
|
|
columns={switchColumns}
|
|
|
|
|
|
pagination={{
|
2026-09-11 17:06:29 +08:00
|
|
|
|
page: switchPage,
|
2026-08-31 14:49:28 +08:00
|
|
|
|
size: PAGE_SIZE,
|
|
|
|
|
|
total: switchTotal,
|
|
|
|
|
|
sizeOptions: [PAGE_SIZE],
|
|
|
|
|
|
onPageChange: handleSwitchPageChange,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
</Tabs>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<LongRenewDialog
|
|
|
|
|
|
open={renewSource !== null}
|
|
|
|
|
|
source={renewSource}
|
|
|
|
|
|
onClose={() => setRenewSource(null)}
|
|
|
|
|
|
onSuccess={() => {
|
2026-09-11 17:06:29 +08:00
|
|
|
|
loadIps(ipPage)
|
|
|
|
|
|
loadPackages()
|
2026-08-31 14:49:28 +08:00
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<LongSwitchDialog
|
|
|
|
|
|
open={switchTarget !== null}
|
|
|
|
|
|
ip={switchTarget?.ip ?? null}
|
|
|
|
|
|
pop={switchTarget?.pop ?? 0}
|
|
|
|
|
|
onClose={() => setSwitchTarget(null)}
|
2026-09-11 17:06:29 +08:00
|
|
|
|
onSuccess={() => loadIps(ipPage)}
|
2026-08-31 14:49:28 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</Page>
|
|
|
|
|
|
)
|
2026-08-18 13:33:22 +08:00
|
|
|
|
}
|