补充个人中心下的页面
This commit is contained in:
@@ -1,5 +1,440 @@
|
||||
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"
|
||||
import Page from "@/components/page"
|
||||
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,
|
||||
formatIpState,
|
||||
formatOrderTypeLabel,
|
||||
LONG_ORDER_TYPE_LABELS,
|
||||
parseExpired,
|
||||
} from "@/lib/models/package"
|
||||
import { downloadTextFile, estimateTotal } from "@/lib/utils"
|
||||
import LongRenewDialog, { type LongRenewSource } from "./long-renew-dialog"
|
||||
import LongSwitchDialog from "./long-switch-dialog"
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
export default function LongPage() {
|
||||
return <Page>长效管理</Page>
|
||||
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(() => {
|
||||
void loadIps(1)
|
||||
}, [loadIps])
|
||||
|
||||
const handleTabChange = (value: string) => {
|
||||
setActiveTab(value)
|
||||
if (value === "packages" && !packages.length) void loadPackages()
|
||||
if (value === "orders" && !orders.length) void loadOrders()
|
||||
if (value === "switch" && !switchList.length) void loadSwitch(1)
|
||||
}
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setIpPage(page)
|
||||
setRowSelection({})
|
||||
void loadIps(page)
|
||||
}
|
||||
|
||||
const handleSwitchPageChange = (page: number) => {
|
||||
setSwitchPage(page)
|
||||
void loadSwitch(page)
|
||||
}
|
||||
|
||||
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 }) =>
|
||||
`${row.original.bandwidth}M/${row.original.lineType}`,
|
||||
},
|
||||
{ accessorKey: "shareType", header: "线路" },
|
||||
{
|
||||
accessorKey: "state",
|
||||
header: "状态",
|
||||
cell: ({ row }) => formatIpState(row.original.state),
|
||||
},
|
||||
{
|
||||
id: "account",
|
||||
header: "账号/密码",
|
||||
cell: ({ row }) => `${row.original.username}/${row.original.password}`,
|
||||
},
|
||||
{ accessorKey: "expireTime", header: "过期时间" },
|
||||
{
|
||||
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>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "isAbroad",
|
||||
header: "套餐类型",
|
||||
cell: ({ row }) => formatAbroad(row.original.isAbroad),
|
||||
},
|
||||
{ accessorKey: "shareType", header: "模式" },
|
||||
{ accessorKey: "ipAmount", header: "可用IP数量(个)" },
|
||||
{ accessorKey: "expireTime", header: "过期时间" },
|
||||
{
|
||||
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: "切换后属地" },
|
||||
{ accessorKey: "reason", header: "切换方式" },
|
||||
{ accessorKey: "createTime", header: "切换时间" },
|
||||
],
|
||||
[],
|
||||
)
|
||||
|
||||
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
|
||||
onClick={() => void handleExtract()}
|
||||
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={{
|
||||
page: ipPage - 1,
|
||||
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={{
|
||||
page: switchPage - 1,
|
||||
size: PAGE_SIZE,
|
||||
total: switchTotal,
|
||||
sizeOptions: [PAGE_SIZE],
|
||||
onPageChange: handleSwitchPageChange,
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<LongRenewDialog
|
||||
open={renewSource !== null}
|
||||
source={renewSource}
|
||||
onClose={() => setRenewSource(null)}
|
||||
onSuccess={() => {
|
||||
void loadIps(ipPage)
|
||||
void loadPackages()
|
||||
}}
|
||||
/>
|
||||
|
||||
<LongSwitchDialog
|
||||
open={switchTarget !== null}
|
||||
ip={switchTarget?.ip ?? null}
|
||||
pop={switchTarget?.pop ?? 0}
|
||||
onClose={() => setSwitchTarget(null)}
|
||||
onSuccess={() => void loadIps(ipPage)}
|
||||
/>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user