完善充值记录和短效套餐页面
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { fetchScoreList } from "@/api/funds"
|
||||
import DataTable from "@/components/data-table"
|
||||
@@ -58,18 +58,31 @@ export default function FundsPage() {
|
||||
const [status, setStatus] = useState<"load" | "done" | "fail">("load")
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
// 总数恰为 50 整数倍时最后一页也是 50 条,翻到空页后锁死最大页码
|
||||
const [maxPage, setMaxPage] = useState<number | null>(null)
|
||||
const seqRef = useRef(0)
|
||||
|
||||
const load = useCallback(async (targetPage: number) => {
|
||||
const seq = ++seqRef.current
|
||||
setStatus("load")
|
||||
const res = await fetchScoreList(targetPage)
|
||||
if (res.success) {
|
||||
setData(res.data)
|
||||
setHasMore(res.data.length >= SCORE_PAGE_SIZE)
|
||||
setStatus("done")
|
||||
} else {
|
||||
console.log(res, 'datadatadatadatadata');
|
||||
|
||||
if (seq !== seqRef.current) return
|
||||
if (!res.success) {
|
||||
setStatus("fail")
|
||||
toast.error(res.message)
|
||||
return
|
||||
}
|
||||
if (res.data.length === 0 && targetPage > 1) {
|
||||
setMaxPage(targetPage - 1)
|
||||
setPage(targetPage - 1)
|
||||
load(targetPage - 1)
|
||||
return
|
||||
}
|
||||
setData(res.data)
|
||||
setHasMore(res.data.length >= SCORE_PAGE_SIZE)
|
||||
setStatus("done")
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -82,10 +95,13 @@ export default function FundsPage() {
|
||||
load(nextPage)
|
||||
}
|
||||
|
||||
// 接口不返回总条数:还有下一页时多加 1 条保证可翻页,否则按累计条数展示
|
||||
const total = hasMore
|
||||
? page * SCORE_PAGE_SIZE + 1
|
||||
: (page - 1) * SCORE_PAGE_SIZE + data.length
|
||||
const canNext = maxPage !== null ? page < maxPage : hasMore
|
||||
const total =
|
||||
maxPage !== null
|
||||
? maxPage * SCORE_PAGE_SIZE
|
||||
: canNext
|
||||
? page * SCORE_PAGE_SIZE + 1
|
||||
: (page - 1) * SCORE_PAGE_SIZE + data.length
|
||||
|
||||
return (
|
||||
<Page>
|
||||
|
||||
@@ -1,34 +1,56 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { fetchHttpRechargeList } from "@/api/httpRecharge"
|
||||
import DataTable from "@/components/data-table"
|
||||
import Page from "@/components/page"
|
||||
import type { HttpRechargeItem } from "@/lib/models/httpRecharge"
|
||||
import {
|
||||
formatPayStatus,
|
||||
type HttpRechargeItem,
|
||||
} from "@/lib/models/httpRecharge"
|
||||
|
||||
export default function HttpRechargePage() {
|
||||
const [data, setData] = useState<HttpRechargeItem[]>([])
|
||||
const [status, setStatus] = useState<"load" | "done" | "fail">("load")
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setStatus("load")
|
||||
const res = await fetchHttpRechargeList()
|
||||
console.log(res, 'fewewf');
|
||||
|
||||
if (!res.success) {
|
||||
setStatus("fail")
|
||||
return
|
||||
}
|
||||
setData(res.data)
|
||||
setStatus("done")
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
const columns = useMemo<ColumnDef<HttpRechargeItem>[]>(
|
||||
() => [
|
||||
{ accessorKey: "create_time", header: "交易时间" },
|
||||
{ accessorKey: "ju_money", header: "充值H币" },
|
||||
{ accessorKey: "pay_money", header: "支付金额" },
|
||||
{ accessorKey: "pay_type", header: "充值渠道" },
|
||||
{
|
||||
accessorKey: "pay_status",
|
||||
header: "支付状态",
|
||||
cell: ({ row }) => formatPayStatus(row.original.pay_status),
|
||||
},
|
||||
{ accessorKey: "order_no", header: "订单编号" },
|
||||
{ accessorKey: "ali_wx_no", header: "支付宝/微信订单号" },
|
||||
],
|
||||
[],
|
||||
)
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="bg-white rounded-lg p-4">
|
||||
<h3 className="font-semibold text-lg">HTTP 充值记录</h3>
|
||||
<DataTable<HttpRechargeItem>
|
||||
data={[]}
|
||||
status="done"
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "create_time",
|
||||
header: "交易时间",
|
||||
},
|
||||
{
|
||||
accessorKey: "ju_money",
|
||||
header: "充值H币",
|
||||
},
|
||||
{
|
||||
accessorKey: "pay_type",
|
||||
header: "充值渠道",
|
||||
},
|
||||
{
|
||||
accessorKey: "pay_money",
|
||||
header: "充值金额",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<h3 className="font-semibold text-lg mb-4">HTTP 充值记录</h3>
|
||||
<DataTable data={data} status={status} columns={columns} />
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
|
||||
@@ -13,6 +13,14 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import {
|
||||
type ApiLinkConfig,
|
||||
type DxPackage,
|
||||
@@ -69,6 +77,151 @@ const SEPARATOR_OPTIONS = [
|
||||
{ value: "6", label: "自定义:" },
|
||||
]
|
||||
|
||||
const API_PARAM_DOCS: {
|
||||
name: string
|
||||
type: string
|
||||
required: string
|
||||
desc: string
|
||||
}[] = [
|
||||
{
|
||||
name: "pt",
|
||||
type: "int",
|
||||
required: "是",
|
||||
desc: "套餐类型,0或12-储值套餐,21-短效包天,22-短效包量,23-短效无限量,29-终身免费VIP套餐,31-长效套餐",
|
||||
},
|
||||
{ name: "num", type: "int", required: "是", desc: "提取IP数量" },
|
||||
{
|
||||
name: "gt",
|
||||
type: "int",
|
||||
required: "否",
|
||||
desc: "提取时长 0-默认 1-1分钟以上 2-2分钟以上 3-3分钟以上 4-4分钟以上",
|
||||
},
|
||||
{
|
||||
name: "regions",
|
||||
type: "string",
|
||||
required: "否",
|
||||
desc: "多个用英文逗号换行,不传表示随机",
|
||||
},
|
||||
{ name: "isp", type: "int", required: "是", desc: "0:不限 4:联通 2:电信" },
|
||||
{ name: "port", type: "int", required: "是", desc: "IP协议 1:HTTP 2:SOCKS5" },
|
||||
{
|
||||
name: "time",
|
||||
type: "int",
|
||||
required: "否",
|
||||
desc: "有效时长 1-0.1至1分钟 5-1至5分钟 25-5至25分钟 180-25至180分钟 360-3至6小时",
|
||||
},
|
||||
{
|
||||
name: "type",
|
||||
type: "int",
|
||||
required: "否",
|
||||
desc: "数据格式:1:TXT 2:JSON 3:HTML",
|
||||
},
|
||||
{ name: "pack", type: "int", required: "否", desc: "用户套餐ID" },
|
||||
{
|
||||
name: "ts",
|
||||
type: "int",
|
||||
required: "否",
|
||||
desc: "是否显示IP过期时间: 1显示 0不显示",
|
||||
},
|
||||
{
|
||||
name: "lb",
|
||||
type: "int",
|
||||
required: "否",
|
||||
desc: "换行符(1:\\r\\n 2:/br 3:\\r 4:\\n 5:\\t 6:自定义)",
|
||||
},
|
||||
{ name: "clb", type: "string", required: "否", desc: "自定义换行符" },
|
||||
{
|
||||
name: "sp",
|
||||
type: "int",
|
||||
required: "否",
|
||||
desc: "分隔符(1:竖线| 2:斜线/ 3:冒号 6:自定义)",
|
||||
},
|
||||
{ name: "csp", type: "string", required: "否", desc: "自定义分隔符" },
|
||||
{
|
||||
name: "distinct",
|
||||
type: "int",
|
||||
required: "否",
|
||||
desc: "去重选择(2:严格去重。0:不去重)",
|
||||
},
|
||||
{
|
||||
name: "aw",
|
||||
type: "int",
|
||||
required: "否",
|
||||
desc: "是否将调用者自动添加到IP白名单(0:不自动添加。1:自动添加)",
|
||||
},
|
||||
{
|
||||
name: "at",
|
||||
type: "int",
|
||||
required: "否",
|
||||
desc: "鉴权模式 1-白名单,2-白名单+账密严格鉴权,3-仅账密鉴权(无限量不可用)",
|
||||
},
|
||||
]
|
||||
|
||||
const RESPONSE_FIELD_DOCS: { name: string; type: string; desc: string }[] = [
|
||||
{ name: "success", type: "bool", desc: "是否成功" },
|
||||
{ name: "code", type: "int", desc: "0表示成功,其他参见《错误代码》表" },
|
||||
{ name: "ip", type: "string", desc: "IP" },
|
||||
{ name: "port", type: "int", desc: "端口号" },
|
||||
]
|
||||
|
||||
const ERROR_CODE_DOCS: { code: string; msg: string; desc: string }[] = [
|
||||
{
|
||||
code: "111",
|
||||
msg: "请在1秒后再次请求",
|
||||
desc: "提取链接请求太过频繁,超出限制",
|
||||
},
|
||||
{
|
||||
code: "113",
|
||||
msg: "请将192.168.0.0设置为白名单!",
|
||||
desc: "白名单未添加/白名单掉了",
|
||||
},
|
||||
{ code: "114", msg: "余额不足", desc: "账户金额消耗完毕" },
|
||||
{
|
||||
code: "115",
|
||||
msg: "请更换地区等条件重新生成api链接地址",
|
||||
desc: "没有资源或没有符合条件的数据",
|
||||
},
|
||||
{
|
||||
code: "116",
|
||||
msg: "您的套餐今日已到达上限!",
|
||||
desc: "套餐内IP数量消耗完毕",
|
||||
},
|
||||
{
|
||||
code: "117",
|
||||
msg: "您的套餐pack传参有误!请检测您现在的ip是否在套餐所在账户!",
|
||||
desc: "检测本地白名单是不是在账户下",
|
||||
},
|
||||
{
|
||||
code: "118",
|
||||
msg: "您的账户异常,请联系客服!",
|
||||
desc: "账户处于被禁用状态",
|
||||
},
|
||||
{ code: "121", msg: "您的该套餐已经过期了!", desc: "套餐过期" },
|
||||
{
|
||||
code: "401",
|
||||
msg: "IP:1.1.1.1:2000 Authentication failed!",
|
||||
desc: "白名单错误/使用的IP已经过期",
|
||||
},
|
||||
{
|
||||
code: "403",
|
||||
msg: "Sorry!The site has been banned!",
|
||||
desc: "客户目标网站异常,联系客服处理",
|
||||
},
|
||||
]
|
||||
|
||||
const RESPONSE_SAMPLE = `{
|
||||
"code": 0,
|
||||
"msg": "提示信息",
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"ip": "49.68.68.197", // 隧道ip(代理ip)
|
||||
"port": 33220, // 代理端口
|
||||
"expireTime": "2019-05-24 08:58:31"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
type FieldRowProps = {
|
||||
label: string
|
||||
children: React.ReactNode
|
||||
@@ -179,8 +332,10 @@ export default function ShortApiLink({ packages }: { packages: DxPackage[] }) {
|
||||
push("t", token.token)
|
||||
}
|
||||
push("at", config.at)
|
||||
push("sp", config.sp)
|
||||
push("csp", config.csp)
|
||||
if (config.at !== 1) {
|
||||
push("sp", config.sp)
|
||||
push("csp", config.csp)
|
||||
}
|
||||
if (regions.length > 0) push("regions", regions.join(","))
|
||||
return `${API_LINK_BASE}?${params.join("&")}`
|
||||
}
|
||||
@@ -431,7 +586,7 @@ export default function ShortApiLink({ packages }: { packages: DxPackage[] }) {
|
||||
</FieldRow>
|
||||
)}
|
||||
|
||||
{config.at !== 1 && !isUnlimited && (
|
||||
{!isUnlimited && (
|
||||
<FieldRow label="分隔符">
|
||||
<RadioGroup
|
||||
value={String(config.sp)}
|
||||
@@ -485,6 +640,81 @@ export default function ShortApiLink({ packages }: { packages: DxPackage[] }) {
|
||||
{currentPackage.durationType}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="rounded-lg border border-dashed border-gray-200 p-4 flex flex-col gap-3">
|
||||
<h4 className="font-semibold">请求参数注释</h4>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>名称</TableHead>
|
||||
<TableHead>类型</TableHead>
|
||||
<TableHead>必选</TableHead>
|
||||
<TableHead>说明</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{API_PARAM_DOCS.map(param => (
|
||||
<TableRow key={param.name}>
|
||||
<TableCell className="font-mono">{param.name}</TableCell>
|
||||
<TableCell>{param.type}</TableCell>
|
||||
<TableCell>{param.required}</TableCell>
|
||||
<TableCell className="whitespace-normal min-w-96">
|
||||
{param.desc}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-dashed border-gray-200 p-4 flex flex-col gap-3">
|
||||
<h4 className="font-semibold">返回结果示例</h4>
|
||||
<pre className="rounded-md bg-gray-50 border border-gray-200 p-3 text-xs overflow-x-auto text-gray-700">
|
||||
<code>{RESPONSE_SAMPLE}</code>
|
||||
</pre>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-24">名称</TableHead>
|
||||
<TableHead className="w-24">类型</TableHead>
|
||||
<TableHead>说明</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{RESPONSE_FIELD_DOCS.map(field => (
|
||||
<TableRow key={field.name}>
|
||||
<TableCell className="font-mono">{field.name}</TableCell>
|
||||
<TableCell>{field.type}</TableCell>
|
||||
<TableCell className="whitespace-normal">
|
||||
{field.desc}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-dashed border-gray-200 p-4 flex flex-col gap-3">
|
||||
<h4 className="font-semibold">错误代码</h4>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-32">错误编码</TableHead>
|
||||
<TableHead>错误提示</TableHead>
|
||||
<TableHead>说明</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ERROR_CODE_DOCS.map(err => (
|
||||
<TableRow key={err.code}>
|
||||
<TableCell className="font-mono">code:{err.code}</TableCell>
|
||||
<TableCell>{err.msg}</TableCell>
|
||||
<TableCell className="whitespace-normal">{err.desc}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export default function ShortPage() {
|
||||
const loadPackages = useCallback(async () => {
|
||||
setStatus("load")
|
||||
const res = await fetchDxPackages()
|
||||
console.log(res, 'resresresres');
|
||||
if (!res.success) {
|
||||
setStatus("fail")
|
||||
return
|
||||
@@ -44,6 +45,7 @@ export default function ShortPage() {
|
||||
const loadOrders = useCallback(async () => {
|
||||
setOrderStatus("load")
|
||||
const res = await fetchDxOrders()
|
||||
|
||||
if (!res.success) {
|
||||
setOrderStatus("fail")
|
||||
return
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import type { ApiResponse } from "@/lib/api"
|
||||
import type { ScoreItem } from "@/lib/models/funds"
|
||||
import { phpPost } from "./http"
|
||||
import { phpFetch } from "./http"
|
||||
|
||||
export async function fetchScoreList(
|
||||
page: number,
|
||||
): Promise<ApiResponse<ScoreItem[]>> {
|
||||
const res = await phpPost<ScoreItem[] | unknown>("/user/UserScore/getList", {
|
||||
cookie: document.cookie,
|
||||
page,
|
||||
})
|
||||
const query = page > 1 ? `?page=${page}` : ""
|
||||
const res = await phpFetch<ScoreItem[] | { data?: unknown }>(
|
||||
"POST",
|
||||
`/user/UserScore/getList${query}`,
|
||||
{ cookie: document.cookie },
|
||||
{ credentials: "include" },
|
||||
)
|
||||
if (!res.success) return res
|
||||
return { success: true, data: Array.isArray(res.data) ? res.data : [] }
|
||||
if (Array.isArray(res.data)) return { success: true, data: res.data }
|
||||
return {
|
||||
success: true,
|
||||
data: Array.isArray(res.data?.data) ? res.data.data : [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,9 @@ export async function phpFetch<T = RawHttpResponse>(
|
||||
message: "请求失败",
|
||||
}
|
||||
}
|
||||
const data = (await response.json()) as T
|
||||
const text = await response.text()
|
||||
// 老 PHP 接口异常时可能返回空响应体,直接 json() 会抛错
|
||||
const data = text ? (JSON.parse(text) as T) : ({} as T)
|
||||
return { success: true, data }
|
||||
} catch (e) {
|
||||
if (controller.signal.aborted) {
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import type { ApiResponse } from "@/lib/api"
|
||||
import type { HttpRechargeItem } from "@/lib/models/httpRecharge"
|
||||
import { callByUser } from "./base"
|
||||
import { isArray, phpPost } from "./http"
|
||||
|
||||
// ==================== HTTP 充值记录 ====================
|
||||
// TODO: 接口路径待后端确认(旧后台为 php-api /http/* 充值列表接口)。
|
||||
|
||||
export async function fetchHttpRechargeList(
|
||||
params: Record<string, unknown>,
|
||||
): Promise<ApiResponse<HttpRechargeItem[]>> {
|
||||
return await callByUser<HttpRechargeItem[]>(
|
||||
"/api/course/v1/order/HttpRecharges",
|
||||
params,
|
||||
"GET",
|
||||
export async function fetchHttpRechargeList(): Promise<
|
||||
ApiResponse<HttpRechargeItem[]>
|
||||
> {
|
||||
const res = await phpPost<HttpRechargeItem[] | { data?: unknown }>(
|
||||
"/http/user/charge_list",
|
||||
{ cookie: document.cookie },
|
||||
)
|
||||
if (!res.success) return res
|
||||
if (isArray<HttpRechargeItem>(res.data)) {
|
||||
return { success: true, data: res.data }
|
||||
}
|
||||
const list = isArray<HttpRechargeItem>(res.data?.data) ? res.data.data : []
|
||||
return { success: true, data: list }
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ export async function createRechargeOrder(data: {
|
||||
}
|
||||
|
||||
// 轮询充值支付状态:返回 true 表示支付成功
|
||||
// 新后端 /user/IsPay 响应体:{ Code: 0|1, Message: "" },Code 本身即支付状态
|
||||
// 响应体:{ Code: 0|1, Message: "" },Code 本身即支付状态
|
||||
// 兼容旧格式 { Code: 10000, Data: 0|1 }
|
||||
export async function checkRechargeStatus(
|
||||
orderNo: string,
|
||||
|
||||
@@ -16,4 +16,5 @@ export type ScoreItem = {
|
||||
RestAmount2: string
|
||||
}
|
||||
|
||||
// 服务端固定每页条数(接口不返回总条数)
|
||||
export const SCORE_PAGE_SIZE = 50
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
export type HttpRechargeItem = {
|
||||
create_time: string
|
||||
ju_money: number
|
||||
id: string
|
||||
user_id: string
|
||||
ju_money: string | number
|
||||
pay_money: string | number
|
||||
pay_type: string
|
||||
pay_money: number
|
||||
pay_status: string | number
|
||||
channel?: string | null
|
||||
order_no: string
|
||||
ali_wx_no: string
|
||||
create_time: string
|
||||
update_time: string
|
||||
}
|
||||
|
||||
export const HTTP_RECHARGE_PAY_STATUS_LABELS: Record<number, string> = {
|
||||
0: "未支付",
|
||||
1: "支付成功",
|
||||
}
|
||||
|
||||
export function formatPayStatus(value: string | number): string {
|
||||
return HTTP_RECHARGE_PAY_STATUS_LABELS[Number(value)] ?? String(value)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ export type DxPackage = {
|
||||
expireTime: string
|
||||
isExpired?: boolean | number | string
|
||||
ipAmount?: number
|
||||
userId?: number
|
||||
poolType?: number
|
||||
usedAmount?: number
|
||||
transferable?: number
|
||||
createTime?: number
|
||||
}
|
||||
|
||||
// 短效购买记录
|
||||
@@ -21,6 +26,16 @@ export type DxOrder = {
|
||||
durationType: string
|
||||
money: number | string
|
||||
create_time: string
|
||||
id?: string
|
||||
user_id?: string
|
||||
order_status?: number | string
|
||||
game_type?: string | null
|
||||
ip_count?: number | string | null
|
||||
order_id?: string
|
||||
payCash?: string
|
||||
serial?: string
|
||||
status?: number | string
|
||||
update_time?: string
|
||||
}
|
||||
|
||||
// 长效套餐(已开通)
|
||||
@@ -214,11 +229,11 @@ export const IP_STATE_LABELS: Record<number, string> = {
|
||||
|
||||
// ==================== 字段格式化 ====================
|
||||
|
||||
// 后端的 isExpired 可能返回 true/1/"1",统一转布尔
|
||||
// 后端的 isExpired 可能返回 true/1/"1",也可能直接返回"已过期"/"未过期"字符串
|
||||
export function parseExpired(
|
||||
value: boolean | number | string | undefined,
|
||||
): boolean {
|
||||
return value === true || value === 1 || value === "1"
|
||||
return value === true || value === 1 || value === "1" || value === "已过期"
|
||||
}
|
||||
|
||||
export function formatExpired(
|
||||
|
||||
Reference in New Issue
Block a user