From eee72ad74a46de74eb60fb76c2dc49959ba6d556 Mon Sep 17 00:00:00 2001 From: Eamon <17516219072@163.com> Date: Fri, 18 Sep 2026 13:17:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0http=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E4=BA=8C=E7=BA=A7=E8=B7=AF=E7=94=B1=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/admin/channels/page.tsx | 179 ++++++++---- src/admin/httpRecharge/page.tsx | 2 +- src/admin/orders/page.tsx | 36 ++- src/admin/record/mock.ts | 40 --- src/admin/record/page.tsx | 112 ++++--- src/admin/refundOrders/page.tsx | 44 ++- src/admin/resources/long.tsx | 54 +++- src/admin/resources/short.tsx | 2 +- src/admin/rosorder/page.tsx | 5 +- src/admin/whitelist/page.tsx | 123 +++++--- src/admin/whitelistSU/page.tsx | 289 ++++++++++++------- src/api/base.ts | 27 +- src/api/funds.ts | 2 +- src/api/http.ts | 15 +- src/api/order.ts | 85 +++--- src/api/product.ts | 2 +- src/api/record.ts | 28 +- src/api/whitelist.ts | 134 ++++++--- src/components/composites/orderFilterBar.tsx | 51 ++-- src/components/ui/pagination.tsx | 52 +--- src/home/product/http.tsx | 2 +- src/lib/hooks/useMyOrderPage.ts | 89 +++--- src/lib/hooks/useOrderPage.ts | 77 +++++ src/lib/models/funds.ts | 2 +- src/lib/models/order.ts | 39 ++- src/lib/models/package.ts | 90 +++++- src/lib/models/record.ts | 14 +- src/lib/models/whitelist.ts | 37 ++- src/lib/utils/index.ts | 16 + 29 files changed, 1109 insertions(+), 539 deletions(-) delete mode 100644 src/admin/record/mock.ts create mode 100644 src/lib/hooks/useOrderPage.ts diff --git a/src/admin/channels/page.tsx b/src/admin/channels/page.tsx index f3fc6e2..91d8a0d 100644 --- a/src/admin/channels/page.tsx +++ b/src/admin/channels/page.tsx @@ -1,6 +1,7 @@ import type { ColumnDef } from "@tanstack/react-table" import { SearchIcon } from "lucide-react" import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { Link } from "react-router-dom" import { toast } from "sonner" import { fetchMyAccounts } from "@/api/account" import DataTable from "@/components/data-table" @@ -113,6 +114,7 @@ export default function ChannelsPage() { const [data, setData] = useState([]) const [status, setStatus] = useState<"load" | "done" | "fail">("load") const [page, setPage] = useState(1) + const [selection, setSelection] = useState>({}) const selectedProduct = useMemo( () => @@ -132,6 +134,7 @@ export default function ChannelsPage() { toast.error(res.message) } setPage(1) + setSelection({}) }, []) const loadedRef = useRef(false) @@ -146,7 +149,10 @@ export default function ChannelsPage() { setQuery(next) load(toParams(next)) } - + const handleReset = () => { + setQuery(INITIAL_QUERY) + load(toParams(INITIAL_QUERY)) + } const paged = useMemo( () => data.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE), [data, page], @@ -154,80 +160,121 @@ export default function ChannelsPage() { return ( -
-
- - 过期时间: - +
+
+

+ 为了给您带来更好的服务体验,请完善qq号和微信号。 + + 立即完善》 + +

+
+ +
+
+ + 过期时间: + + +
+ + + + +
+ setQuery({ ...query, keyword: e.target.value })} + onKeyDown={e => { + if (e.key === "Enter") applyFilters({}) + }} + placeholder="账号" + className="w-40" + /> + + +
- - - - -
- setQuery({ ...query, keyword: e.target.value })} - onKeyDown={e => { - if (e.key === "Enter") applyFilters({}) - }} - placeholder="账号" - className="w-40" - /> - + + + + *批量续费可通过筛选和搜索功能精准定位账号 +
@@ -237,6 +284,10 @@ export default function ChannelsPage() { data={paged} status={status} columns={columns} + enableSelection + rowSelection={selection} + onSelectionChange={setSelection} + getRowId={row => String(row.Id)} pagination={{ page, size: PAGE_SIZE, diff --git a/src/admin/httpRecharge/page.tsx b/src/admin/httpRecharge/page.tsx index 924d4ee..0df8419 100644 --- a/src/admin/httpRecharge/page.tsx +++ b/src/admin/httpRecharge/page.tsx @@ -15,7 +15,7 @@ export default function HttpRechargePage() { const load = useCallback(async () => { setStatus("load") const res = await fetchHttpRechargeList() - console.log(res, 'fewewf'); + console.log(res, "fewewf") if (!res.success) { setStatus("fail") diff --git a/src/admin/orders/page.tsx b/src/admin/orders/page.tsx index cc635d5..668f92f 100644 --- a/src/admin/orders/page.tsx +++ b/src/admin/orders/page.tsx @@ -4,16 +4,29 @@ import AccountCell from "@/components/composites/accountCell" import OrderFilterBar from "@/components/composites/orderFilterBar" import DataTable from "@/components/data-table" import Page from "@/components/page" -import { MY_ORDER_PAGE_SIZE, useMyOrderPage } from "@/lib/hooks/useMyOrderPage" +import { useMyOrderPage } from "@/lib/hooks/useMyOrderPage" import { useProducts } from "@/lib/hooks/useProducts" import { formatDate, formatMoney, formatOrderType, + type MyOrderQueryParams, + type OrderFilters, type OrderItem, totalConnections, } from "@/lib/models/order" +const mapOrderFilters = ( + filters: OrderFilters, +): Partial => ({ + BTime: filters.bTime || undefined, + ETime: filters.eTime || undefined, + OrderType: filters.orderType === "0" ? undefined : Number(filters.orderType), + ProductId: filters.productId === "0" ? undefined : Number(filters.productId), + PackageId: filters.packageName === "0" ? undefined : filters.packageName, + KeyWord: filters.keyword.trim() || undefined, +}) + const columns: ColumnDef[] = [ { accessorKey: "CreateTime", @@ -71,8 +84,19 @@ const columns: ColumnDef[] = [ export default function OrdersPage() { const products = useProducts() - const { filters, patch, load, total, status, page, setPage, paged } = - useMyOrderPage(fetchMyOrders) + const { + filters, + patch, + load, + reset, + total, + status, + page, + size, + setPage, + changeSize, + paged, + } = useMyOrderPage(fetchMyOrders, mapOrderFilters) return ( @@ -80,7 +104,8 @@ export default function OrdersPage() { filters={filters} products={products} onPatch={patch} - onSearch={() => load()} + onSearch={load} + onReset={reset} />
@@ -91,9 +116,10 @@ export default function OrdersPage() { columns={columns} pagination={{ page, - size: MY_ORDER_PAGE_SIZE, + size, total, onPageChange: setPage, + onSizeChange: changeSize, }} />
diff --git a/src/admin/record/mock.ts b/src/admin/record/mock.ts deleted file mode 100644 index 9c2e754..0000000 --- a/src/admin/record/mock.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { HttpUsedLogItem, HttpUsedSummary } from "@/lib/models/record" - -export const MOCK_USED_SUMMARY: HttpUsedSummary = { - todayShortPackIpUsed: 3, - shortPackIpUsed: 45, -} - -const PACK_IP_PREFIX = [ - { packType: 21, prefix: "211.90.13" }, - { packType: 22, prefix: "112.65.28" }, -] - -function pad(value: number): string { - return String(value).padStart(2, "0") -} - -function buildLogs(count: number): HttpUsedLogItem[] { - const now = new Date() - const logs: HttpUsedLogItem[] = [] - - for (let i = 0; i < count; i += 1) { - const source = PACK_IP_PREFIX[i % PACK_IP_PREFIX.length] - const time = new Date(now.getTime() - i * 37 * 60 * 1000) - const createTime = - `${time.getFullYear()}-${pad(time.getMonth() + 1)}-${pad(time.getDate())} ` + - `${pad(time.getHours())}:${pad(time.getMinutes())}:${pad(time.getSeconds())}` - - logs.push({ - packType: source.packType, - userIp: `113.118.${(i % 200) + 10}.${(i % 250) + 3}`, - ip: `${source.prefix}.${(i % 240) + 10}`, - port: 6000 + (i % 300), - createTime, - }) - } - - return logs -} - -export const MOCK_USED_LOGS = buildLogs(56) diff --git a/src/admin/record/page.tsx b/src/admin/record/page.tsx index bafd1bc..1fd3a9b 100644 --- a/src/admin/record/page.tsx +++ b/src/admin/record/page.tsx @@ -1,5 +1,7 @@ import type { ColumnDef } from "@tanstack/react-table" -import { useMemo, useState } from "react" +import { useCallback, useEffect, useMemo, useState } from "react" +import { toast } from "sonner" +import { fetchHttpUsedLogs, fetchHttpUsedSummary } from "@/api/record" import DataTable from "@/components/data-table" import Page from "@/components/page" import { @@ -9,39 +11,11 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" -import { - formatPackType, - HTTP_USE_PACK_TYPE_OPTIONS, - type HttpUsedLogItem, -} from "@/lib/models/record" -import { MOCK_USED_LOGS, MOCK_USED_SUMMARY } from "./mock" +import type { HttpUsedLogItem, HttpUsedSummary } from "@/lib/models/record" +import { formatPackType, HTTP_USE_PACK_TYPE_OPTIONS } from "@/lib/models/record" +import { estimateTotal, formatUnixTime } from "@/lib/utils" -// TODO: 后端使用记录接口就绪后,将 mock 替换为 src/api/record.ts 的 -// fetchHttpUsedSummary / fetchHttpUsedLogs(服务端分页) - -const columns: ColumnDef[] = [ - { - accessorKey: "packType", - header: "使用类型", - cell: ({ row }) => formatPackType(row.original.packType), - }, - { - accessorKey: "userIp", - header: "客户端IP", - }, - { - accessorKey: "ip", - header: "使用IP", - }, - { - accessorKey: "port", - header: "端口号", - }, - { - accessorKey: "createTime", - header: "使用时间", - }, -] +const PAGE_SIZE_OPTIONS = [10, 20, 50] function StatCard(props: { label: string @@ -61,38 +35,81 @@ function StatCard(props: { } export default function RecordPage() { + const [summary, setSummary] = useState(null) const [packType, setPackType] = useState(HTTP_USE_PACK_TYPE_OPTIONS[0]) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(10) + const [items, setItems] = useState([]) + const [total, setTotal] = useState(0) + const [status, setStatus] = useState<"load" | "done" | "fail">("load") - const filtered = useMemo( - () => MOCK_USED_LOGS.filter(log => log.packType === packType), - [packType], - ) + const loadLogs = useCallback(async () => { + setStatus("load") + const res = await fetchHttpUsedLogs({ page, limit: pageSize, packType }) + if (!res.success) { + setStatus("fail") + toast.error(res.message) + return + } + setItems(res.data.items) + setTotal( + estimateTotal( + res.data.pager.totalCount, + res.data.pager.currentPage || page, + pageSize, + res.data.items.length, + ), + ) + setStatus("done") + }, [page, pageSize, packType]) - const paged = useMemo( - () => filtered.slice((page - 1) * pageSize, page * pageSize), - [filtered, page, pageSize], - ) + useEffect(() => { + fetchHttpUsedSummary().then(res => { + if (res.success) setSummary(res.data) + }) + }, []) + + useEffect(() => { + loadLogs() + }, [loadLogs]) const handlePackTypeChange = (value: string) => { setPackType(Number(value)) setPage(1) } + const columns = useMemo[]>( + () => [ + { + accessorKey: "packType", + header: "使用类型", + cell: ({ row }) => formatPackType(row.original.packType), + }, + { accessorKey: "userIp", header: "客户端IP" }, + { accessorKey: "ip", header: "使用IP" }, + { accessorKey: "port", header: "端口号" }, + { + accessorKey: "createTime", + header: "使用时间", + cell: ({ row }) => formatUnixTime(row.original.createTime), + }, + ], + [], + ) + return (

包天包量使用概况

-
+
@@ -119,13 +136,14 @@ export default function RecordPage() {
{ setPageSize(size) diff --git a/src/admin/refundOrders/page.tsx b/src/admin/refundOrders/page.tsx index f74a000..5fd1448 100644 --- a/src/admin/refundOrders/page.tsx +++ b/src/admin/refundOrders/page.tsx @@ -4,15 +4,27 @@ import AccountCell from "@/components/composites/accountCell" import OrderFilterBar from "@/components/composites/orderFilterBar" import DataTable from "@/components/data-table" import Page from "@/components/page" -import { MY_ORDER_PAGE_SIZE, useMyOrderPage } from "@/lib/hooks/useMyOrderPage" +import { useMyOrderPage } from "@/lib/hooks/useMyOrderPage" import { useProducts } from "@/lib/hooks/useProducts" import { formatDate, formatMoney, + formatOrderType, + type MyOrderQueryParams, + type OrderFilters, type OrderItem, - totalConnections, } from "@/lib/models/order" +const mapRefundFilters = ( + filters: OrderFilters, +): Partial => ({ + BTime: filters.bTime || undefined, + ETime: filters.eTime || undefined, + ProductId: filters.productId === "0" ? undefined : Number(filters.productId), + PackageId: filters.packageName === "0" ? undefined : filters.packageName, + KeyWord: filters.keyword.trim() || undefined, +}) + const columns: ColumnDef[] = [ { accessorKey: "CreateTime", @@ -23,6 +35,11 @@ const columns: ColumnDef[] = [ accessorKey: "OrderNo", header: "订单编号", }, + { + accessorKey: "OrderType", + header: "类型", + cell: ({ row }) => formatOrderType(row.original.OrderType), + }, { accessorKey: "ProductName", header: "产品", @@ -39,7 +56,7 @@ const columns: ColumnDef[] = [ { accessorKey: "ConnectCount", header: "连接数", - cell: ({ row }) => totalConnections(row.original), + cell: ({ row }) => row.original.ConnectCount, }, { accessorKey: "DayCount", @@ -65,8 +82,19 @@ const columns: ColumnDef[] = [ export default function RefundOrdersPage() { const products = useProducts() - const { filters, patch, load, total, status, page, setPage, paged } = - useMyOrderPage(fetchMyRefundOrders) + const { + filters, + patch, + load, + reset, + total, + status, + page, + size, + setPage, + changeSize, + paged, + } = useMyOrderPage(fetchMyRefundOrders, mapRefundFilters) return ( @@ -75,7 +103,8 @@ export default function RefundOrdersPage() { products={products} showOrderType={false} onPatch={patch} - onSearch={() => load()} + onSearch={load} + onReset={reset} />
@@ -86,9 +115,10 @@ export default function RefundOrdersPage() { columns={columns} pagination={{ page, - size: MY_ORDER_PAGE_SIZE, + size, total, onPageChange: setPage, + onSizeChange: changeSize, }} />
diff --git a/src/admin/resources/long.tsx b/src/admin/resources/long.tsx index be4d04f..f289525 100644 --- a/src/admin/resources/long.tsx +++ b/src/admin/resources/long.tsx @@ -21,12 +21,16 @@ import type { } from "@/lib/models/package" import { formatAbroad, + formatExpired, formatIpState, + formatLineType, formatOrderTypeLabel, + formatShareType, + formatSwitchReason, LONG_ORDER_TYPE_LABELS, parseExpired, } from "@/lib/models/package" -import { downloadTextFile, estimateTotal } from "@/lib/utils" +import { downloadTextFile, estimateTotal, formatUnixTime } from "@/lib/utils" import LongRenewDialog, { type LongRenewSource } from "./long-renew-dialog" import LongSwitchDialog from "./long-switch-dialog" @@ -214,9 +218,13 @@ export default function LongPage() { id: "bandwidth", header: "带宽", cell: ({ row }) => - `${row.original.bandwidth}M/${row.original.lineType}`, + `${row.original.bandwidth}M/${formatLineType(row.original.lineType)}`, + }, + { + accessorKey: "shareType", + header: "线路", + cell: ({ row }) => formatShareType(row.original.shareType), }, - { accessorKey: "shareType", header: "线路" }, { accessorKey: "state", header: "状态", @@ -227,7 +235,11 @@ export default function LongPage() { header: "账号/密码", cell: ({ row }) => `${row.original.username}/${row.original.password}`, }, - { accessorKey: "expireTime", header: "过期时间" }, + { + accessorKey: "expireTime", + header: "过期时间", + cell: ({ row }) => formatUnixTime(row.original.expireTime), + }, { accessorKey: "isExpired", header: "是否过期", @@ -274,14 +286,32 @@ export default function LongPage() { const packageColumns = useMemo[]>( () => [ + { + accessorKey: "name", + header: "套餐名称", + cell: ({ row }) => row.original.name || "-", + }, { accessorKey: "isAbroad", header: "套餐类型", cell: ({ row }) => formatAbroad(row.original.isAbroad), }, - { accessorKey: "shareType", header: "模式" }, + { + accessorKey: "shareType", + header: "模式", + cell: ({ row }) => formatShareType(row.original.shareType), + }, { accessorKey: "ipAmount", header: "可用IP数量(个)" }, - { accessorKey: "expireTime", header: "过期时间" }, + { + accessorKey: "isExpired", + header: "是否过期", + cell: ({ row }) => formatExpired(row.original.isExpired), + }, + { + accessorKey: "expireTime", + header: "过期时间", + cell: ({ row }) => formatUnixTime(row.original.expireTime), + }, { id: "actions", header: "续费", @@ -337,8 +367,16 @@ export default function LongPage() { { accessorKey: "oldArea", header: "原属地" }, { accessorKey: "newIp", header: "切换后IP" }, { accessorKey: "newArea", header: "切换后属地" }, - { accessorKey: "reason", header: "切换方式" }, - { accessorKey: "createTime", header: "切换时间" }, + { + accessorKey: "reason", + header: "切换方式", + cell: ({ row }) => formatSwitchReason(row.original.reason), + }, + { + accessorKey: "createTime", + header: "切换时间", + cell: ({ row }) => formatUnixTime(row.original.createTime), + }, ], [], ) diff --git a/src/admin/resources/short.tsx b/src/admin/resources/short.tsx index f21e91d..7590f37 100644 --- a/src/admin/resources/short.tsx +++ b/src/admin/resources/short.tsx @@ -33,7 +33,7 @@ export default function ShortPage() { const loadPackages = useCallback(async () => { setStatus("load") const res = await fetchDxPackages() - console.log(res, 'resresresres'); + console.log(res, "resresresres") if (!res.success) { setStatus("fail") return diff --git a/src/admin/rosorder/page.tsx b/src/admin/rosorder/page.tsx index 50e7e4f..9c23c45 100644 --- a/src/admin/rosorder/page.tsx +++ b/src/admin/rosorder/page.tsx @@ -6,8 +6,6 @@ import Page from "@/components/page" import { useList } from "@/lib/hooks/useList" import type { RosOrderItem } from "@/lib/models/rosorder" -const PAGE_SIZE = 10 - const columns: ColumnDef[] = [ { accessorKey: "username", @@ -52,9 +50,8 @@ const columns: ColumnDef[] = [ export default function RosorderPage() { const { data, status } = useList(fetchRosOrderList) const [page, setPage] = useState(1) - const [pageSize, setPageSize] = useState(PAGE_SIZE) + const [pageSize, setPageSize] = useState(10) - // 接口返回全量列表,本地做前端分页 const paged = useMemo( () => data.slice((page - 1) * pageSize, page * pageSize), [data, page, pageSize], diff --git a/src/admin/whitelist/page.tsx b/src/admin/whitelist/page.tsx index 39a6e2e..39d4b6f 100644 --- a/src/admin/whitelist/page.tsx +++ b/src/admin/whitelist/page.tsx @@ -1,29 +1,89 @@ import { Lock, Trash2, Unlock } from "lucide-react" -import { useState } from "react" +import { useCallback, useEffect, useState } from "react" +import { toast } from "sonner" +import { + createWhiteIp, + deleteWhiteIp, + fetchWhiteList, + lockWhiteIp, + type WhiteActionResult, +} from "@/api/whitelist" import DataTable from "@/components/data-table" import Page from "@/components/page" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" +import type { ApiResponse } from "@/lib/api" import type { WhiteIpItem } from "@/lib/models/whitelist" - -// TODO: 后端白名单接口就绪后,将 saveWhiteIp/fetchWhiteList 接入(见 src/api/whitelist.ts) +import { formatUnixTime } from "@/lib/utils" export default function WhitelistPage() { const [ip, setIp] = useState("") const [list, setList] = useState([]) + const [status, setStatus] = useState<"load" | "done" | "fail">("load") + const [saving, setSaving] = useState(false) - const handleSave = () => { + const load = useCallback(async () => { + setStatus("load") + const res = await fetchWhiteList() + if (!res.success) { + setStatus("fail") + toast.error(res.message) + return + } + setList(res.data) + setStatus("done") + }, []) + + useEffect(() => { + load() + }, [load]) + + const runAction = useCallback( + async ( + action: () => Promise>, + successMessage: string, + ) => { + const res = await action() + if (!res.success) { + toast.error(res.message) + return + } + const { code, msg } = res.data + if (code !== undefined && code < 1) { + toast.error(msg || "操作失败") + return + } + toast.success(successMessage) + load() + }, + [load], + ) + + const handleSave = async () => { const value = ip.trim() - if (!value) return - // TODO: await saveWhiteIp(value) - setList(prev => [ - { id: Date.now(), ip: value, createTime: "", isLocked: "未锁定" }, - ...prev, - ]) + if (!value) { + toast.error("请输入IP地址") + return + } + setSaving(true) + await runAction(() => createWhiteIp(value), "添加成功") + setSaving(false) setIp("") } + const handleLock = (item: WhiteIpItem) => { + const locked = item.isLocked === "已锁定" + runAction( + () => lockWhiteIp(item.id, locked ? 0 : 1), + locked ? "解锁成功" : "锁定成功", + ) + } + + const handleDelete = (item: WhiteIpItem) => { + runAction(() => deleteWhiteIp(item.id), "删除成功") + } + return (
@@ -36,7 +96,9 @@ export default function WhitelistPage() { placeholder="请输入IP地址并点击保存白名单" className="max-w-sm" /> - +
@@ -48,15 +110,18 @@ export default function WhitelistPage() { data={list} - status="done" + status={status} columns={[ + { accessorKey: "ip", header: "IP地址" }, { - accessorKey: "ip", - header: "IP地址", + accessorKey: "description", + header: "备注", + cell: ({ row }) => row.original.description || "-", }, { accessorKey: "createTime", header: "设置时间", + cell: ({ row }) => formatUnixTime(row.original.createTime), }, { accessorKey: "isLocked", @@ -75,41 +140,25 @@ export default function WhitelistPage() { header: "操作", cell: ({ row }) => { const item = row.original - // TODO: 接入 lockWhiteIp / deleteWhiteIp + const isLocked = item.isLocked === "已锁定" return (
+
@@ -110,15 +192,18 @@ export default function WhitelistSUPage() { data={list} - status="done" + status={status} columns={[ + { accessorKey: "ip", header: "IP地址" }, { - accessorKey: "ip", - header: "IP地址", + accessorKey: "description", + header: "备注", + cell: ({ row }) => row.original.description || "-", }, { accessorKey: "createTime", header: "设置时间", + cell: ({ row }) => formatUnixTime(row.original.createTime), }, { accessorKey: "isLocked", @@ -137,24 +222,25 @@ export default function WhitelistSUPage() { header: "操作", cell: ({ row }) => { const item = row.original + const isLocked = item.isLocked === "已锁定" return (
- {/* 调整白名单上限弹窗 */} + 调整白名单上限 -
-
-
- - - {tempWhiteLimit} - - -
-
- {tempDiscount === 10 ? ( - - 价格无折扣 - - ) : ( - - 当前已应用 {tempDiscount * 10}% 折扣 - - )} -
+ +
+ +
+ + + {tempWhiteLimit} + +
-
- 调整后到期时间:{newExpire.toLocaleString()} + +
+ {tempDiscount === 10 ? ( + + 价格无折扣 + + ) : ( + + 当前已应用 {tempDiscount * 10}% 折扣 + + )} +
+ +
+ 调整后到期时间:{new Date(newExpre).toLocaleString()}
+ - - +
diff --git a/src/api/base.ts b/src/api/base.ts index 3d9e326..3ed76ed 100644 --- a/src/api/base.ts +++ b/src/api/base.ts @@ -4,7 +4,7 @@ import { CLIENT_ID, CLIENT_SECRET, } from "@/lib/api" -import { clearSession, getUserToken } from "./token" +import { getUserToken } from "./token" type CallMode = "public" | "device" | "user" @@ -76,7 +76,14 @@ async function request( return { success: false, status: response.status, - message: json.message || json.error_description || "请求失败", + code: typeof json.Code === "number" ? json.Code : undefined, + message: + json.message || + json.Message || + json.error_description || + (typeof json.Code === "number" + ? `请求失败(错误码 ${json.Code})` + : "请求失败"), } } @@ -212,7 +219,6 @@ async function call( auth = `Bearer ${token}` } else if (mode === "device") { if (attempt > 1) { - // 上次请求 401,强制作废旧 token 再重新获取 deviceToken = null deviceTokenExpire = 0 } @@ -229,14 +235,13 @@ async function call( const resp = await request(url, { method, body, auth }) - if (!resp.success && resp.status === 401) { - if (mode === "user") { - clearSession() - return resp - } - if (mode === "device" && attempt < maxAttempts) { - continue - } + if ( + !resp.success && + resp.status === 401 && + mode === "device" && + attempt < maxAttempts + ) { + continue } return resp } diff --git a/src/api/funds.ts b/src/api/funds.ts index dfc26a2..0a4d565 100644 --- a/src/api/funds.ts +++ b/src/api/funds.ts @@ -18,4 +18,4 @@ export async function fetchScoreList( success: true, data: Array.isArray(res.data?.data) ? res.data.data : [], } -} +} \ No newline at end of file diff --git a/src/api/http.ts b/src/api/http.ts index 87032ab..d3e7dc5 100644 --- a/src/api/http.ts +++ b/src/api/http.ts @@ -160,12 +160,13 @@ export function toNumber(value: unknown): number { } // 解析服务端分页信息,缺失字段兜底 +// 兼容两套字段命名:文档的 total/maxResults 与旧接口的 totalCount/pageSize export function parsePager(raw: unknown, itemCount: number): Pager { const p = (raw ?? {}) as Record return { currentPage: toNumber(p.currentPage) || 1, - pageSize: toNumber(p.pageSize) || itemCount, - totalCount: toNumber(p.totalCount), + pageSize: toNumber(p.maxResults ?? p.pageSize) || itemCount, + totalCount: toNumber(p.total ?? p.totalCount), totalPage: toNumber(p.totalPage), } } @@ -346,7 +347,9 @@ export async function fetchCxPackages(): Promise> { cookie: document.cookie, }) if (!res.success) return res - const list = res.data?.data ?? res.data + const raw = res.data ?? {} + // 文档结构为 data.d.packs;兼容 data 直接为数组的情况 + const list = unwrapD(raw).packs ?? raw.data ?? raw return { success: true, data: isArray(list) ? list : [], @@ -367,9 +370,10 @@ export async function fetchCxOrders(): Promise> { export async function fetchLangIpList( query: Record, ): Promise> { + const res = await phpPost("/http/user/lang_ip_list", { cookie: document.cookie, - data: query, + ...query, }) if (!res.success) return res const d = unwrapD(res.data) @@ -383,9 +387,10 @@ export async function fetchLangIpList( export async function fetchSwitchList( query: Record, ): Promise> { + const res = await phpPost("/http/user/cx_switch", { cookie: document.cookie, - data: query, + ...query, }) if (!res.success) return res const d = unwrapD(res.data) diff --git a/src/api/order.ts b/src/api/order.ts index 41d6b4d..e7a4cef 100644 --- a/src/api/order.ts +++ b/src/api/order.ts @@ -3,7 +3,7 @@ import type { CreateOrderData, CreateOrderRequest, CreateTestAccountRequest, - MyOrderPage, + MyOrderListResult, MyOrderQueryParams, OrderItem, OrderQueryParams, @@ -53,15 +53,43 @@ export async function fetchOrderPage( ) } -// 退款订单分页列表 -export async function fetchRefundOrders( - params: OrderQueryParams, -): Promise> { - return await callByUser( - "/api/course/v1/order/RefundOrders", - params, - "GET", - ) +// ==================== 个人中心订单/退货订单 ==================== + +// 响应结构:{ Code: 10000, data: [{ RowCount, List }] } +async function fetchMyOrderPage( + endpoint: string, + params: MyOrderQueryParams, +): Promise> { + const res = await callPublic< + MyOrderListResult[] | MyOrderListResult | unknown + >(endpoint, params, "GET") + if (!res.success) return res + + const data = res.data + const raw = Array.isArray(data) ? data[0] : data + const page = + raw && typeof raw === "object" ? (raw as MyOrderListResult) : null + return { + success: true, + data: { + RowCount: Number(page?.RowCount ?? 0), + List: Array.isArray(page?.List) ? page.List : [], + }, + } +} + +// 我的订单(订单管理) +export async function fetchMyOrders( + params: MyOrderQueryParams, +): Promise> { + return fetchMyOrderPage("/user/ApiMyOrders", params) +} + +// 我的退货订单 +export async function fetchMyRefundOrders( + params: MyOrderQueryParams, +): Promise> { + return fetchMyOrderPage("/user/ApiMyRefundOrders", params) } // 优惠券订单分页列表 @@ -108,43 +136,6 @@ export async function exportOrders( ) } -async function fetchMyOrderPage( - endpoint: string, - params: MyOrderQueryParams, -): Promise> { - const res = await callPublic( - endpoint, - params, - "GET", - ) - if (!res.success) return res - - const data = res.data - const raw = Array.isArray(data) ? data[0] : data - const page = raw && typeof raw === "object" ? (raw as MyOrderPage) : null - return { - success: true, - data: { - RowCount: Number(page?.RowCount ?? 0), - List: Array.isArray(page?.List) ? page.List : [], - }, - } -} - -// 我的订单(订单管理) -export async function fetchMyOrders( - params: MyOrderQueryParams, -): Promise> { - return fetchMyOrderPage("/user/ApiMyOrders", params) -} - -// 我的退货订单 -export async function fetchMyRefundOrders( - params: MyOrderQueryParams, -): Promise> { - return fetchMyOrderPage("/user/ApiMyRefundOrders", params) -} - export type RechargeOrderResult = { OrderInfo: { OrderNo: string diff --git a/src/api/product.ts b/src/api/product.ts index 480ed3b..9be0515 100644 --- a/src/api/product.ts +++ b/src/api/product.ts @@ -30,7 +30,7 @@ export async function fetchWindowProducts(): Promise< ) } -// 产品+套餐(用于账号管理筛选,接口来源:juipnet MyAccounts.cshtml) +// 产品+套餐(用于账号管理筛选) export async function fetchProductWithPackage(): Promise< ApiResponse > { diff --git a/src/api/record.ts b/src/api/record.ts index 9a30e9a..88c4002 100644 --- a/src/api/record.ts +++ b/src/api/record.ts @@ -2,42 +2,50 @@ import type { ApiResponse } from "@/lib/api" import type { HttpUsedLogItem, HttpUsedLogQuery, + HttpUsedLogsResult, HttpUsedSummary, } from "@/lib/models/record" -import { isArray, phpPost, toNumber, unwrapD } from "./http" +import { isArray, parsePager, phpPost, toNumber, unwrapD } from "./http" // ==================== HTTP 使用记录(php-api /http/user/*) ==================== -// TODO: 接口路径待后端确认,按现有 /http/user/* 命名规律推测。 +// 包天包量使用概况 export async function fetchHttpUsedSummary(): Promise< ApiResponse > { - const res = await phpPost("/http/user/pack_used_summary", { + const res = await phpPost>("/http/user/dx_used", { cookie: document.cookie, }) if (!res.success) return res - const d = unwrapD(res.data) + const raw = res.data ?? {} return { success: true, data: { - todayShortPackIpUsed: toNumber(d.todayShortPackIpUsed), - shortPackIpUsed: toNumber(d.shortPackIpUsed), + todayCoinPackIpPaid: + typeof raw.todayCoinPackIpPaid === "string" + ? raw.todayCoinPackIpPaid + : String(raw.todayCoinPackIpPaid ?? ""), + todayCoinPackIpUsed: toNumber(raw.todayCoinPackIpUsed), + coinPackIpUsed: toNumber(raw.coinPackIpUsed), + todayShortPackIpUsed: toNumber(raw.todayShortPackIpUsed), + shortPackIpUsed: toNumber(raw.shortPackIpUsed), }, } } +// 短效套餐使用记录(服务端分页) export async function fetchHttpUsedLogs( query: HttpUsedLogQuery, -): Promise> { - const res = await phpPost("/http/user/pack_used_log", { +): Promise> { + const res = await phpPost>("/http/user/dx_used_log", { cookie: document.cookie, data: query, }) if (!res.success) return res const d = unwrapD(res.data) - const list = d.items ?? d.list ?? d.data + const items = isArray(d.items) ? d.items : [] return { success: true, - data: isArray(list) ? list : [], + data: { items, pager: parsePager(d.pager, items.length) }, } } diff --git a/src/api/whitelist.ts b/src/api/whitelist.ts index b875377..2628d0c 100644 --- a/src/api/whitelist.ts +++ b/src/api/whitelist.ts @@ -1,56 +1,118 @@ import type { ApiResponse } from "@/lib/api" -import type { WhiteIpItem } from "@/lib/models/whitelist" -import { isArray, phpPost, unwrapD } from "./http" +import type { UnlimitedAvailable, WhiteIpItem } from "@/lib/models/whitelist" +import { isArray, phpPost, toNumber, unwrapD } from "./http" -// ==================== 白名单(php-api /http/user/*) ==================== -// TODO: 后端接口路径待确认,以下路径按现有 /http/user/white_* 命名规律推测, -// 后端就绪后需按实际路径修正。 +type RawHttpResponse = Record -export async function fetchWhiteList(): Promise> { - const res = await phpPost("/http/user/white_list", { + +export type WhiteActionResult = { + code?: number + msg?: string +} + +async function postWhiteAction( + path: string, + data: Record, +): Promise> { + const res = await phpPost(path, { cookie: document.cookie, + data, }) if (!res.success) return res - const d = unwrapD(res.data) - const list = d.list ?? d.data ?? res.data?.data + const raw = res.data ?? {} return { success: true, - data: isArray(list) ? list : [], + data: { + code: raw.code === undefined ? undefined : toNumber(raw.code), + msg: typeof raw.msg === "string" ? raw.msg : undefined, + }, } } -export async function saveWhiteIp(ip: string): Promise> { - return await phpPost("/http/user/white_save", { - cookie: document.cookie, - ip, - }) +// 白名单列表接口返回 data 直接为数组,兼容 { list: [...] } / { data: [...] } +function parseList(raw: unknown): WhiteIpItem[] { + const value = raw as RawHttpResponse | undefined + const list = Array.isArray(raw) ? raw : (value?.list ?? value?.data) + return isArray(list) ? list : [] } -export async function deleteWhiteIp( - id: number, -): Promise> { - return await phpPost("/http/user/white_delete", { +// ==================== 白名单(普通) ==================== + +export async function fetchWhiteList(): Promise> { + const res = await phpPost("/http/user/white_list", { cookie: document.cookie, - id, }) + if (!res.success) return res + return { success: true, data: parseList(res.data) } } -export async function lockWhiteIp( - id: number, - locked: boolean, -): Promise> { - return await phpPost("/http/user/white_lock", { - cookie: document.cookie, - id, - locked: locked ? 1 : 0, - }) +export function createWhiteIp(ip: string) { + return postWhiteAction("/http/user/create_white_ip", { ip }) } -export async function setWhiteLimitMax( - limit: number, -): Promise> { - return await phpPost("/http/user/white_limit_set_su", { - cookie: document.cookie, - limit, - }) +export function lockWhiteIp(id: number, lock: 0 | 1) { + return postWhiteAction("/http/user/lock_ip", { id, lock }) +} + +export function deleteWhiteIp(id: number) { + return postWhiteAction("/http/user/delete_ip", { id }) +} + +// ==================== 白名单(短效无限量) ==================== + +export async function fetchWhiteListSu(): Promise> { + const res = await phpPost("/http/user/white_list_su", { + cookie: document.cookie, + }) + if (!res.success) return res + return { success: true, data: parseList(res.data) } +} + +export function createWhiteIpSu(ip: string) { + return postWhiteAction("/http/user/create_white_ip_su", { ip }) +} + +export function lockWhiteIpSu(id: number, lock: 0 | 1) { + return postWhiteAction("/http/user/lock_ip_su", { id, lock }) +} + +export function deleteWhiteIpSu(id: number) { + return postWhiteAction("/http/user/delete_ip_su", { id }) +} + +// 当前白名单数量 +export async function fetchWhiteLimitSu(): Promise> { + const res = await phpPost("/http/user/white_limit_get_su", { + cookie: document.cookie, + }) + if (!res.success) return res + return { success: true, data: toNumber(res.data?.data ?? res.data) } +} + +// 调整白名单上限 +export function setWhiteLimitSu(maxAmount: number) { + return postWhiteAction("/http/user/white_limit_set_su", { maxAmount }) +} + +// 获取最新套餐详情 +export async function fetchUnlimitedAvailable(): Promise< + ApiResponse +> { + const res = await phpPost( + "/http/user/get_unlimited_available", + { cookie: document.cookie }, + ) + if (!res.success) return res + // 文档结构为 data.d.{...};兼容 data 直接平铺字段的情况 + const merged = { ...(res.data ?? {}), ...unwrapD(res.data) } + return { + success: true, + data: { + packId: toNumber(merged.packId), + poolType: toNumber(merged.poolType), + durationType: toNumber(merged.durationType), + usage: toNumber(merged.usage), + expireTime: toNumber(merged.expireTime), + }, + } } diff --git a/src/components/composites/orderFilterBar.tsx b/src/components/composites/orderFilterBar.tsx index 178197a..de01c60 100644 --- a/src/components/composites/orderFilterBar.tsx +++ b/src/components/composites/orderFilterBar.tsx @@ -20,10 +20,20 @@ export default function OrderFilterBar(props: { filters: OrderFilters products: ProductItem[] showOrderType?: boolean + showPackageName?: boolean onPatch: (patch: Partial) => void onSearch: () => void + onReset?: () => void }) { - const { filters, products, showOrderType = true, onPatch, onSearch } = props + const { + filters, + products, + showOrderType = true, + showPackageName = true, + onPatch, + onSearch, + onReset, + } = props return (
@@ -84,22 +94,24 @@ export default function OrderFilterBar(props: { - + {showPackageName && ( + + )}
查询 + {onReset && ( + + )}
) diff --git a/src/components/ui/pagination.tsx b/src/components/ui/pagination.tsx index 0281493..a0fdcf4 100644 --- a/src/components/ui/pagination.tsx +++ b/src/components/ui/pagination.tsx @@ -1,5 +1,3 @@ -"use client" - import { ChevronLeftIcon, ChevronRightIcon, @@ -42,21 +40,6 @@ function Pagination({ // 无数据时兜底为 1 页,避免除零与空分页器 const totalPages = size > 0 ? Math.max(Math.ceil(total / size), 1) : 1 - // 移动端检测:小屏幕减少页码按钮数量,避免溢出 - const [isMobile, setIsMobile] = useState( - () => - typeof window !== "undefined" && - window.matchMedia("(max-width: 640px)").matches, - ) - - useEffect(() => { - const mql = window.matchMedia("(max-width: 640px)") - const handleChange = () => setIsMobile(mql.matches) - handleChange() - mql.addEventListener("change", handleChange) - return () => mql.removeEventListener("change", handleChange) - }, []) - // 同步外部 page 变化 useEffect(() => { setCurrentPage(page) @@ -64,13 +47,12 @@ function Pagination({ // 分页器逻辑 const generatePaginationItems = () => { - // 桌面端最多显示7个页码,移动端最多显示5个,其余用省略号 - const SIBLINGS = isMobile ? 0 : 1 // 当前页左右各显示的页码数 - const MAX_VISIBLE = isMobile ? 5 : 7 + // 最多显示7个页码,其余用省略号 + const SIBLINGS = 1 // 当前页左右各显示的页码数 const DOTS = -1 // 省略号标记 - if (totalPages <= MAX_VISIBLE) { - // 总页数较少,全部显示 + if (totalPages <= 7) { + // 总页数少于7,全部显示 return Array.from({ length: totalPages }, (_, i) => i + 1) } @@ -139,13 +121,8 @@ function Pagination({ const paginationItems = generatePaginationItems() return ( -
-
+
+
共 {total} 条记录,每页