更新http管理二级路由页面
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import type { ColumnDef } from "@tanstack/react-table"
|
import type { ColumnDef } from "@tanstack/react-table"
|
||||||
import { SearchIcon } from "lucide-react"
|
import { SearchIcon } from "lucide-react"
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { fetchMyAccounts } from "@/api/account"
|
import { fetchMyAccounts } from "@/api/account"
|
||||||
import DataTable from "@/components/data-table"
|
import DataTable from "@/components/data-table"
|
||||||
@@ -113,6 +114,7 @@ export default function ChannelsPage() {
|
|||||||
const [data, setData] = useState<MyAccountItem[]>([])
|
const [data, setData] = useState<MyAccountItem[]>([])
|
||||||
const [status, setStatus] = useState<"load" | "done" | "fail">("load")
|
const [status, setStatus] = useState<"load" | "done" | "fail">("load")
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
|
const [selection, setSelection] = useState<Record<string, boolean>>({})
|
||||||
|
|
||||||
const selectedProduct = useMemo(
|
const selectedProduct = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -132,6 +134,7 @@ export default function ChannelsPage() {
|
|||||||
toast.error(res.message)
|
toast.error(res.message)
|
||||||
}
|
}
|
||||||
setPage(1)
|
setPage(1)
|
||||||
|
setSelection({})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const loadedRef = useRef(false)
|
const loadedRef = useRef(false)
|
||||||
@@ -146,7 +149,10 @@ export default function ChannelsPage() {
|
|||||||
setQuery(next)
|
setQuery(next)
|
||||||
load(toParams(next))
|
load(toParams(next))
|
||||||
}
|
}
|
||||||
|
const handleReset = () => {
|
||||||
|
setQuery(INITIAL_QUERY)
|
||||||
|
load(toParams(INITIAL_QUERY))
|
||||||
|
}
|
||||||
const paged = useMemo(
|
const paged = useMemo(
|
||||||
() => data.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
|
() => data.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
|
||||||
[data, page],
|
[data, page],
|
||||||
@@ -154,80 +160,121 @@ export default function ChannelsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="bg-white rounded-lg p-4 flex flex-wrap items-end gap-x-4 gap-y-3">
|
<div className="bg-white rounded-lg flex flex-col">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-col items-center bg-amber-100 p-3 gap-2">
|
||||||
<span className="text-sm text-gray-500 whitespace-nowrap">
|
<p className="text-sm text-gray-600">
|
||||||
过期时间:
|
为了给您带来更好的服务体验,请完善qq号和微信号。
|
||||||
</span>
|
<Link
|
||||||
|
to="/admin"
|
||||||
|
className="text-blue-600 hover:text-blue-700 underline-offset-2 hover:underline"
|
||||||
|
>
|
||||||
|
立即完善》
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-end p-4 gap-x-4 gap-y-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-gray-500 whitespace-nowrap">
|
||||||
|
过期时间:
|
||||||
|
</span>
|
||||||
|
<Select
|
||||||
|
value={query.expiredDay}
|
||||||
|
onValueChange={value => applyFilters({ expiredDay: value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-40">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{EXPIRED_DAY_OPTIONS.map(option => (
|
||||||
|
<SelectItem key={option.value} value={String(option.value)}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={query.expiredDay}
|
value={query.productId}
|
||||||
onValueChange={value => applyFilters({ expiredDay: value })}
|
onValueChange={value =>
|
||||||
|
applyFilters({ productId: value, packageName: "0" })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-40">
|
<SelectTrigger className="w-36">
|
||||||
<SelectValue />
|
<SelectValue placeholder="全部产品" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{EXPIRED_DAY_OPTIONS.map(option => (
|
<SelectItem value="0">全部产品</SelectItem>
|
||||||
<SelectItem key={option.value} value={String(option.value)}>
|
{products.map(item => (
|
||||||
{option.label}
|
<SelectItem key={item.Product.Id} value={String(item.Product.Id)}>
|
||||||
|
{item.Product.Name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={query.packageName}
|
||||||
|
onValueChange={value => applyFilters({ packageName: value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-36">
|
||||||
|
<SelectValue placeholder="全部套餐" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="0">全部套餐</SelectItem>
|
||||||
|
{selectedProduct?.Packages.filter(pkg => pkg.Status === 1).map(
|
||||||
|
pkg => (
|
||||||
|
<SelectItem key={pkg.Id} value={pkg.Name}>
|
||||||
|
{pkg.Name}
|
||||||
|
</SelectItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
value={query.keyword}
|
||||||
|
onChange={e => setQuery({ ...query, keyword: e.target.value })}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === "Enter") applyFilters({})
|
||||||
|
}}
|
||||||
|
placeholder="账号"
|
||||||
|
className="w-40"
|
||||||
|
/>
|
||||||
|
<Button onClick={() => applyFilters({})}>
|
||||||
|
<SearchIcon data-icon="inline-start" />
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={handleReset}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Select
|
<div className="flex flex-wrap items-center gap-4 pt-2 p-4">
|
||||||
value={query.productId}
|
<Button
|
||||||
onValueChange={value =>
|
className="bg-red-600 hover:bg-red-700 text-white"
|
||||||
applyFilters({ productId: value, packageName: "0" })
|
onClick={() => toast.info("续费功能开发中,敬请期待")}
|
||||||
}
|
>
|
||||||
>
|
续费
|
||||||
<SelectTrigger className="w-36">
|
|
||||||
<SelectValue placeholder="全部产品" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="0">全部产品</SelectItem>
|
|
||||||
{products.map(item => (
|
|
||||||
<SelectItem key={item.Product.Id} value={String(item.Product.Id)}>
|
|
||||||
{item.Product.Name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
value={query.packageName}
|
|
||||||
onValueChange={value => applyFilters({ packageName: value })}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-36">
|
|
||||||
<SelectValue placeholder="全部套餐" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="0">全部套餐</SelectItem>
|
|
||||||
{selectedProduct?.Packages.filter(pkg => pkg.Status === 1).map(
|
|
||||||
pkg => (
|
|
||||||
<SelectItem key={pkg.Id} value={pkg.Name}>
|
|
||||||
{pkg.Name}
|
|
||||||
</SelectItem>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Input
|
|
||||||
value={query.keyword}
|
|
||||||
onChange={e => setQuery({ ...query, keyword: e.target.value })}
|
|
||||||
onKeyDown={e => {
|
|
||||||
if (e.key === "Enter") applyFilters({})
|
|
||||||
}}
|
|
||||||
placeholder="账号"
|
|
||||||
className="w-40"
|
|
||||||
/>
|
|
||||||
<Button onClick={() => applyFilters({})}>
|
|
||||||
<SearchIcon data-icon="inline-start" />
|
|
||||||
查询
|
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
onClick={() => toast.info("退货功能开发中,敬请期待")}
|
||||||
|
>
|
||||||
|
退货
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="bg-amber-500 hover:bg-amber-600 text-white"
|
||||||
|
onClick={() => toast.info("批量修改密码功能开发中,敬请期待")}
|
||||||
|
>
|
||||||
|
批量修改密码
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm text-red-500">
|
||||||
|
*批量续费可通过筛选和搜索功能精准定位账号
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -237,6 +284,10 @@ export default function ChannelsPage() {
|
|||||||
data={paged}
|
data={paged}
|
||||||
status={status}
|
status={status}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
enableSelection
|
||||||
|
rowSelection={selection}
|
||||||
|
onSelectionChange={setSelection}
|
||||||
|
getRowId={row => String(row.Id)}
|
||||||
pagination={{
|
pagination={{
|
||||||
page,
|
page,
|
||||||
size: PAGE_SIZE,
|
size: PAGE_SIZE,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default function HttpRechargePage() {
|
|||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setStatus("load")
|
setStatus("load")
|
||||||
const res = await fetchHttpRechargeList()
|
const res = await fetchHttpRechargeList()
|
||||||
console.log(res, 'fewewf');
|
console.log(res, "fewewf")
|
||||||
|
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
setStatus("fail")
|
setStatus("fail")
|
||||||
|
|||||||
@@ -4,16 +4,29 @@ import AccountCell from "@/components/composites/accountCell"
|
|||||||
import OrderFilterBar from "@/components/composites/orderFilterBar"
|
import OrderFilterBar from "@/components/composites/orderFilterBar"
|
||||||
import DataTable from "@/components/data-table"
|
import DataTable from "@/components/data-table"
|
||||||
import Page from "@/components/page"
|
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 { useProducts } from "@/lib/hooks/useProducts"
|
||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
formatMoney,
|
formatMoney,
|
||||||
formatOrderType,
|
formatOrderType,
|
||||||
|
type MyOrderQueryParams,
|
||||||
|
type OrderFilters,
|
||||||
type OrderItem,
|
type OrderItem,
|
||||||
totalConnections,
|
totalConnections,
|
||||||
} from "@/lib/models/order"
|
} from "@/lib/models/order"
|
||||||
|
|
||||||
|
const mapOrderFilters = (
|
||||||
|
filters: OrderFilters,
|
||||||
|
): Partial<MyOrderQueryParams> => ({
|
||||||
|
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<OrderItem>[] = [
|
const columns: ColumnDef<OrderItem>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "CreateTime",
|
accessorKey: "CreateTime",
|
||||||
@@ -71,8 +84,19 @@ const columns: ColumnDef<OrderItem>[] = [
|
|||||||
|
|
||||||
export default function OrdersPage() {
|
export default function OrdersPage() {
|
||||||
const products = useProducts()
|
const products = useProducts()
|
||||||
const { filters, patch, load, total, status, page, setPage, paged } =
|
const {
|
||||||
useMyOrderPage(fetchMyOrders)
|
filters,
|
||||||
|
patch,
|
||||||
|
load,
|
||||||
|
reset,
|
||||||
|
total,
|
||||||
|
status,
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
setPage,
|
||||||
|
changeSize,
|
||||||
|
paged,
|
||||||
|
} = useMyOrderPage(fetchMyOrders, mapOrderFilters)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
@@ -80,7 +104,8 @@ export default function OrdersPage() {
|
|||||||
filters={filters}
|
filters={filters}
|
||||||
products={products}
|
products={products}
|
||||||
onPatch={patch}
|
onPatch={patch}
|
||||||
onSearch={() => load()}
|
onSearch={load}
|
||||||
|
onReset={reset}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
||||||
@@ -91,9 +116,10 @@ export default function OrdersPage() {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={{
|
pagination={{
|
||||||
page,
|
page,
|
||||||
size: MY_ORDER_PAGE_SIZE,
|
size,
|
||||||
total,
|
total,
|
||||||
onPageChange: setPage,
|
onPageChange: setPage,
|
||||||
|
onSizeChange: changeSize,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { ColumnDef } from "@tanstack/react-table"
|
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 DataTable from "@/components/data-table"
|
||||||
import Page from "@/components/page"
|
import Page from "@/components/page"
|
||||||
import {
|
import {
|
||||||
@@ -9,39 +11,11 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
import {
|
import type { HttpUsedLogItem, HttpUsedSummary } from "@/lib/models/record"
|
||||||
formatPackType,
|
import { formatPackType, HTTP_USE_PACK_TYPE_OPTIONS } from "@/lib/models/record"
|
||||||
HTTP_USE_PACK_TYPE_OPTIONS,
|
import { estimateTotal, formatUnixTime } from "@/lib/utils"
|
||||||
type HttpUsedLogItem,
|
|
||||||
} from "@/lib/models/record"
|
|
||||||
import { MOCK_USED_LOGS, MOCK_USED_SUMMARY } from "./mock"
|
|
||||||
|
|
||||||
// TODO: 后端使用记录接口就绪后,将 mock 替换为 src/api/record.ts 的
|
const PAGE_SIZE_OPTIONS = [10, 20, 50]
|
||||||
// fetchHttpUsedSummary / fetchHttpUsedLogs(服务端分页)
|
|
||||||
|
|
||||||
const columns: ColumnDef<HttpUsedLogItem>[] = [
|
|
||||||
{
|
|
||||||
accessorKey: "packType",
|
|
||||||
header: "使用类型",
|
|
||||||
cell: ({ row }) => formatPackType(row.original.packType),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "userIp",
|
|
||||||
header: "客户端IP",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "ip",
|
|
||||||
header: "使用IP",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "port",
|
|
||||||
header: "端口号",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "createTime",
|
|
||||||
header: "使用时间",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
function StatCard(props: {
|
function StatCard(props: {
|
||||||
label: string
|
label: string
|
||||||
@@ -61,38 +35,81 @@ function StatCard(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function RecordPage() {
|
export default function RecordPage() {
|
||||||
|
const [summary, setSummary] = useState<HttpUsedSummary | null>(null)
|
||||||
const [packType, setPackType] = useState(HTTP_USE_PACK_TYPE_OPTIONS[0])
|
const [packType, setPackType] = useState(HTTP_USE_PACK_TYPE_OPTIONS[0])
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [pageSize, setPageSize] = useState(10)
|
const [pageSize, setPageSize] = useState(10)
|
||||||
|
const [items, setItems] = useState<HttpUsedLogItem[]>([])
|
||||||
|
const [total, setTotal] = useState(0)
|
||||||
|
const [status, setStatus] = useState<"load" | "done" | "fail">("load")
|
||||||
|
|
||||||
const filtered = useMemo(
|
const loadLogs = useCallback(async () => {
|
||||||
() => MOCK_USED_LOGS.filter(log => log.packType === packType),
|
setStatus("load")
|
||||||
[packType],
|
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(
|
useEffect(() => {
|
||||||
() => filtered.slice((page - 1) * pageSize, page * pageSize),
|
fetchHttpUsedSummary().then(res => {
|
||||||
[filtered, page, pageSize],
|
if (res.success) setSummary(res.data)
|
||||||
)
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadLogs()
|
||||||
|
}, [loadLogs])
|
||||||
|
|
||||||
const handlePackTypeChange = (value: string) => {
|
const handlePackTypeChange = (value: string) => {
|
||||||
setPackType(Number(value))
|
setPackType(Number(value))
|
||||||
setPage(1)
|
setPage(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const columns = useMemo<ColumnDef<HttpUsedLogItem>[]>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
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 (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
||||||
<h3 className="font-semibold text-lg">包天包量使用概况</h3>
|
<h3 className="font-semibold text-lg">包天包量使用概况</h3>
|
||||||
<div className="grid grid-cols-4 sm:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||||
<StatCard
|
<StatCard
|
||||||
label="当日使用IP数量"
|
label="当日使用IP数量"
|
||||||
value={MOCK_USED_SUMMARY.todayShortPackIpUsed}
|
value={summary?.todayShortPackIpUsed ?? 0}
|
||||||
accent="amber"
|
accent="amber"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="累计使用IP数量"
|
label="累计使用IP数量"
|
||||||
value={MOCK_USED_SUMMARY.shortPackIpUsed}
|
value={summary?.shortPackIpUsed ?? 0}
|
||||||
accent="blue"
|
accent="blue"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,13 +136,14 @@ export default function RecordPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
data={paged}
|
data={items}
|
||||||
status="done"
|
status={status}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={{
|
pagination={{
|
||||||
page,
|
page,
|
||||||
size: pageSize,
|
size: pageSize,
|
||||||
total: filtered.length,
|
total,
|
||||||
|
sizeOptions: PAGE_SIZE_OPTIONS,
|
||||||
onPageChange: setPage,
|
onPageChange: setPage,
|
||||||
onSizeChange: size => {
|
onSizeChange: size => {
|
||||||
setPageSize(size)
|
setPageSize(size)
|
||||||
|
|||||||
@@ -4,15 +4,27 @@ import AccountCell from "@/components/composites/accountCell"
|
|||||||
import OrderFilterBar from "@/components/composites/orderFilterBar"
|
import OrderFilterBar from "@/components/composites/orderFilterBar"
|
||||||
import DataTable from "@/components/data-table"
|
import DataTable from "@/components/data-table"
|
||||||
import Page from "@/components/page"
|
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 { useProducts } from "@/lib/hooks/useProducts"
|
||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
formatMoney,
|
formatMoney,
|
||||||
|
formatOrderType,
|
||||||
|
type MyOrderQueryParams,
|
||||||
|
type OrderFilters,
|
||||||
type OrderItem,
|
type OrderItem,
|
||||||
totalConnections,
|
|
||||||
} from "@/lib/models/order"
|
} from "@/lib/models/order"
|
||||||
|
|
||||||
|
const mapRefundFilters = (
|
||||||
|
filters: OrderFilters,
|
||||||
|
): Partial<MyOrderQueryParams> => ({
|
||||||
|
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<OrderItem>[] = [
|
const columns: ColumnDef<OrderItem>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "CreateTime",
|
accessorKey: "CreateTime",
|
||||||
@@ -23,6 +35,11 @@ const columns: ColumnDef<OrderItem>[] = [
|
|||||||
accessorKey: "OrderNo",
|
accessorKey: "OrderNo",
|
||||||
header: "订单编号",
|
header: "订单编号",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "OrderType",
|
||||||
|
header: "类型",
|
||||||
|
cell: ({ row }) => formatOrderType(row.original.OrderType),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "ProductName",
|
accessorKey: "ProductName",
|
||||||
header: "产品",
|
header: "产品",
|
||||||
@@ -39,7 +56,7 @@ const columns: ColumnDef<OrderItem>[] = [
|
|||||||
{
|
{
|
||||||
accessorKey: "ConnectCount",
|
accessorKey: "ConnectCount",
|
||||||
header: "连接数",
|
header: "连接数",
|
||||||
cell: ({ row }) => totalConnections(row.original),
|
cell: ({ row }) => row.original.ConnectCount,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "DayCount",
|
accessorKey: "DayCount",
|
||||||
@@ -65,8 +82,19 @@ const columns: ColumnDef<OrderItem>[] = [
|
|||||||
|
|
||||||
export default function RefundOrdersPage() {
|
export default function RefundOrdersPage() {
|
||||||
const products = useProducts()
|
const products = useProducts()
|
||||||
const { filters, patch, load, total, status, page, setPage, paged } =
|
const {
|
||||||
useMyOrderPage(fetchMyRefundOrders)
|
filters,
|
||||||
|
patch,
|
||||||
|
load,
|
||||||
|
reset,
|
||||||
|
total,
|
||||||
|
status,
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
setPage,
|
||||||
|
changeSize,
|
||||||
|
paged,
|
||||||
|
} = useMyOrderPage(fetchMyRefundOrders, mapRefundFilters)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
@@ -75,7 +103,8 @@ export default function RefundOrdersPage() {
|
|||||||
products={products}
|
products={products}
|
||||||
showOrderType={false}
|
showOrderType={false}
|
||||||
onPatch={patch}
|
onPatch={patch}
|
||||||
onSearch={() => load()}
|
onSearch={load}
|
||||||
|
onReset={reset}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
||||||
@@ -86,9 +115,10 @@ export default function RefundOrdersPage() {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={{
|
pagination={{
|
||||||
page,
|
page,
|
||||||
size: MY_ORDER_PAGE_SIZE,
|
size,
|
||||||
total,
|
total,
|
||||||
onPageChange: setPage,
|
onPageChange: setPage,
|
||||||
|
onSizeChange: changeSize,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,12 +21,16 @@ import type {
|
|||||||
} from "@/lib/models/package"
|
} from "@/lib/models/package"
|
||||||
import {
|
import {
|
||||||
formatAbroad,
|
formatAbroad,
|
||||||
|
formatExpired,
|
||||||
formatIpState,
|
formatIpState,
|
||||||
|
formatLineType,
|
||||||
formatOrderTypeLabel,
|
formatOrderTypeLabel,
|
||||||
|
formatShareType,
|
||||||
|
formatSwitchReason,
|
||||||
LONG_ORDER_TYPE_LABELS,
|
LONG_ORDER_TYPE_LABELS,
|
||||||
parseExpired,
|
parseExpired,
|
||||||
} from "@/lib/models/package"
|
} 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 LongRenewDialog, { type LongRenewSource } from "./long-renew-dialog"
|
||||||
import LongSwitchDialog from "./long-switch-dialog"
|
import LongSwitchDialog from "./long-switch-dialog"
|
||||||
|
|
||||||
@@ -214,9 +218,13 @@ export default function LongPage() {
|
|||||||
id: "bandwidth",
|
id: "bandwidth",
|
||||||
header: "带宽",
|
header: "带宽",
|
||||||
cell: ({ row }) =>
|
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",
|
accessorKey: "state",
|
||||||
header: "状态",
|
header: "状态",
|
||||||
@@ -227,7 +235,11 @@ export default function LongPage() {
|
|||||||
header: "账号/密码",
|
header: "账号/密码",
|
||||||
cell: ({ row }) => `${row.original.username}/${row.original.password}`,
|
cell: ({ row }) => `${row.original.username}/${row.original.password}`,
|
||||||
},
|
},
|
||||||
{ accessorKey: "expireTime", header: "过期时间" },
|
{
|
||||||
|
accessorKey: "expireTime",
|
||||||
|
header: "过期时间",
|
||||||
|
cell: ({ row }) => formatUnixTime(row.original.expireTime),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "isExpired",
|
accessorKey: "isExpired",
|
||||||
header: "是否过期",
|
header: "是否过期",
|
||||||
@@ -274,14 +286,32 @@ export default function LongPage() {
|
|||||||
|
|
||||||
const packageColumns = useMemo<ColumnDef<CxPackage>[]>(
|
const packageColumns = useMemo<ColumnDef<CxPackage>[]>(
|
||||||
() => [
|
() => [
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: "套餐名称",
|
||||||
|
cell: ({ row }) => row.original.name || "-",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "isAbroad",
|
accessorKey: "isAbroad",
|
||||||
header: "套餐类型",
|
header: "套餐类型",
|
||||||
cell: ({ row }) => formatAbroad(row.original.isAbroad),
|
cell: ({ row }) => formatAbroad(row.original.isAbroad),
|
||||||
},
|
},
|
||||||
{ accessorKey: "shareType", header: "模式" },
|
{
|
||||||
|
accessorKey: "shareType",
|
||||||
|
header: "模式",
|
||||||
|
cell: ({ row }) => formatShareType(row.original.shareType),
|
||||||
|
},
|
||||||
{ accessorKey: "ipAmount", header: "可用IP数量(个)" },
|
{ 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",
|
id: "actions",
|
||||||
header: "续费",
|
header: "续费",
|
||||||
@@ -337,8 +367,16 @@ export default function LongPage() {
|
|||||||
{ accessorKey: "oldArea", header: "原属地" },
|
{ accessorKey: "oldArea", header: "原属地" },
|
||||||
{ accessorKey: "newIp", header: "切换后IP" },
|
{ accessorKey: "newIp", header: "切换后IP" },
|
||||||
{ accessorKey: "newArea", header: "切换后属地" },
|
{ 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),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default function ShortPage() {
|
|||||||
const loadPackages = useCallback(async () => {
|
const loadPackages = useCallback(async () => {
|
||||||
setStatus("load")
|
setStatus("load")
|
||||||
const res = await fetchDxPackages()
|
const res = await fetchDxPackages()
|
||||||
console.log(res, 'resresresres');
|
console.log(res, "resresresres")
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
setStatus("fail")
|
setStatus("fail")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import Page from "@/components/page"
|
|||||||
import { useList } from "@/lib/hooks/useList"
|
import { useList } from "@/lib/hooks/useList"
|
||||||
import type { RosOrderItem } from "@/lib/models/rosorder"
|
import type { RosOrderItem } from "@/lib/models/rosorder"
|
||||||
|
|
||||||
const PAGE_SIZE = 10
|
|
||||||
|
|
||||||
const columns: ColumnDef<RosOrderItem>[] = [
|
const columns: ColumnDef<RosOrderItem>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "username",
|
accessorKey: "username",
|
||||||
@@ -52,9 +50,8 @@ const columns: ColumnDef<RosOrderItem>[] = [
|
|||||||
export default function RosorderPage() {
|
export default function RosorderPage() {
|
||||||
const { data, status } = useList(fetchRosOrderList)
|
const { data, status } = useList(fetchRosOrderList)
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [pageSize, setPageSize] = useState(PAGE_SIZE)
|
const [pageSize, setPageSize] = useState(10)
|
||||||
|
|
||||||
// 接口返回全量列表,本地做前端分页
|
|
||||||
const paged = useMemo(
|
const paged = useMemo(
|
||||||
() => data.slice((page - 1) * pageSize, page * pageSize),
|
() => data.slice((page - 1) * pageSize, page * pageSize),
|
||||||
[data, page, pageSize],
|
[data, page, pageSize],
|
||||||
|
|||||||
@@ -1,29 +1,89 @@
|
|||||||
import { Lock, Trash2, Unlock } from "lucide-react"
|
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 DataTable from "@/components/data-table"
|
||||||
import Page from "@/components/page"
|
import Page from "@/components/page"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
|
import type { ApiResponse } from "@/lib/api"
|
||||||
import type { WhiteIpItem } from "@/lib/models/whitelist"
|
import type { WhiteIpItem } from "@/lib/models/whitelist"
|
||||||
|
import { formatUnixTime } from "@/lib/utils"
|
||||||
// TODO: 后端白名单接口就绪后,将 saveWhiteIp/fetchWhiteList 接入(见 src/api/whitelist.ts)
|
|
||||||
|
|
||||||
export default function WhitelistPage() {
|
export default function WhitelistPage() {
|
||||||
const [ip, setIp] = useState("")
|
const [ip, setIp] = useState("")
|
||||||
const [list, setList] = useState<WhiteIpItem[]>([])
|
const [list, setList] = useState<WhiteIpItem[]>([])
|
||||||
|
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<ApiResponse<WhiteActionResult>>,
|
||||||
|
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()
|
const value = ip.trim()
|
||||||
if (!value) return
|
if (!value) {
|
||||||
// TODO: await saveWhiteIp(value)
|
toast.error("请输入IP地址")
|
||||||
setList(prev => [
|
return
|
||||||
{ id: Date.now(), ip: value, createTime: "", isLocked: "未锁定" },
|
}
|
||||||
...prev,
|
setSaving(true)
|
||||||
])
|
await runAction(() => createWhiteIp(value), "添加成功")
|
||||||
|
setSaving(false)
|
||||||
setIp("")
|
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 (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="bg-white rounded-lg p-4">
|
<div className="bg-white rounded-lg p-4">
|
||||||
@@ -36,7 +96,9 @@ export default function WhitelistPage() {
|
|||||||
placeholder="请输入IP地址并点击保存白名单"
|
placeholder="请输入IP地址并点击保存白名单"
|
||||||
className="max-w-sm"
|
className="max-w-sm"
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleSave}>保存</Button>
|
<Button onClick={handleSave} disabled={saving}>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -48,15 +110,18 @@ export default function WhitelistPage() {
|
|||||||
|
|
||||||
<DataTable<WhiteIpItem>
|
<DataTable<WhiteIpItem>
|
||||||
data={list}
|
data={list}
|
||||||
status="done"
|
status={status}
|
||||||
columns={[
|
columns={[
|
||||||
|
{ accessorKey: "ip", header: "IP地址" },
|
||||||
{
|
{
|
||||||
accessorKey: "ip",
|
accessorKey: "description",
|
||||||
header: "IP地址",
|
header: "备注",
|
||||||
|
cell: ({ row }) => row.original.description || "-",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "createTime",
|
accessorKey: "createTime",
|
||||||
header: "设置时间",
|
header: "设置时间",
|
||||||
|
cell: ({ row }) => formatUnixTime(row.original.createTime),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "isLocked",
|
accessorKey: "isLocked",
|
||||||
@@ -75,41 +140,25 @@ export default function WhitelistPage() {
|
|||||||
header: "操作",
|
header: "操作",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const item = row.original
|
const item = row.original
|
||||||
// TODO: 接入 lockWhiteIp / deleteWhiteIp
|
const isLocked = item.isLocked === "已锁定"
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() =>
|
onClick={() => handleLock(item)}
|
||||||
setList(prev =>
|
|
||||||
prev.map(i =>
|
|
||||||
i.id === item.id
|
|
||||||
? {
|
|
||||||
...i,
|
|
||||||
isLocked:
|
|
||||||
i.isLocked === "未锁定"
|
|
||||||
? "已锁定"
|
|
||||||
: "未锁定",
|
|
||||||
}
|
|
||||||
: i,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{item.isLocked === "未锁定" ? (
|
{isLocked ? (
|
||||||
<Lock className="mr-1 h-3 w-3" />
|
|
||||||
) : (
|
|
||||||
<Unlock className="mr-1 h-3 w-3" />
|
<Unlock className="mr-1 h-3 w-3" />
|
||||||
|
) : (
|
||||||
|
<Lock className="mr-1 h-3 w-3" />
|
||||||
)}
|
)}
|
||||||
{item.isLocked === "未锁定" ? "锁定" : "解锁"}
|
{isLocked ? "解锁" : "锁定"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() =>
|
onClick={() => handleDelete(item)}
|
||||||
setList(prev => prev.filter(i => i.id !== item.id))
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Trash2 className="mr-1 h-3 w-3" />
|
<Trash2 className="mr-1 h-3 w-3" />
|
||||||
删除
|
删除
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import { Lock, Minus, Plus, Trash2, Unlock } from "lucide-react"
|
import { Lock, Minus, Plus, Trash2, Unlock } from "lucide-react"
|
||||||
import { useState } from "react"
|
import { useCallback, useEffect, useState } from "react"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import {
|
||||||
|
createWhiteIpSu,
|
||||||
|
deleteWhiteIpSu,
|
||||||
|
fetchUnlimitedAvailable,
|
||||||
|
fetchWhiteLimitSu,
|
||||||
|
fetchWhiteListSu,
|
||||||
|
lockWhiteIpSu,
|
||||||
|
setWhiteLimitSu,
|
||||||
|
type WhiteActionResult,
|
||||||
|
} from "@/api/whitelist"
|
||||||
import DataTable from "@/components/data-table"
|
import DataTable from "@/components/data-table"
|
||||||
import Page from "@/components/page"
|
import Page from "@/components/page"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
@@ -12,75 +23,144 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import type { WhiteIpItem } from "@/lib/models/whitelist"
|
import type { ApiResponse } from "@/lib/api"
|
||||||
|
import type { UnlimitedAvailable, WhiteIpItem } from "@/lib/models/whitelist"
|
||||||
// TODO: 后端白名单接口就绪后,将 fetchWhiteList / setWhiteLimitMax 接入(见 src/api/whitelist.ts)
|
import { formatUnixTime } from "@/lib/utils"
|
||||||
|
|
||||||
const clampLimit = (value: number) => Math.min(20, Math.max(1, value))
|
const clampLimit = (value: number) => Math.min(20, Math.max(1, value))
|
||||||
|
|
||||||
const discountOf = (limit: number) => Math.max(6, 11 - limit)
|
function calcRemain(
|
||||||
|
expire: number,
|
||||||
|
maxWhitelist: number,
|
||||||
|
newMaxWhitelist: number,
|
||||||
|
): number {
|
||||||
|
const now = Math.floor(Date.now() / 1000)
|
||||||
|
const prev = expire || now
|
||||||
|
const remain = prev - now
|
||||||
|
const multiple = maxWhitelist / newMaxWhitelist
|
||||||
|
const oldDiscount = Math.max(0.6, 1 - 0.1 * (maxWhitelist - 1))
|
||||||
|
const newDiscount = Math.max(0.6, 1 - 0.1 * (newMaxWhitelist - 1))
|
||||||
|
const offset = oldDiscount / newDiscount
|
||||||
|
const fee = 99 / 100
|
||||||
|
return Math.floor(remain * multiple * offset * fee + now) * 1000
|
||||||
|
}
|
||||||
|
|
||||||
export default function WhitelistSUPage() {
|
export default function WhitelistSUPage() {
|
||||||
const [ip, setIp] = useState("")
|
const [ip, setIp] = useState("")
|
||||||
const [list, setList] = useState<WhiteIpItem[]>([])
|
const [list, setList] = useState<WhiteIpItem[]>([])
|
||||||
const [whiteLimit, setWhiteLimit] = useState(5)
|
const [status, setStatus] = useState<"load" | "done" | "fail">("load")
|
||||||
const [tempWhiteLimit, setTempWhiteLimit] = useState(5)
|
const [whiteLimit, setWhiteLimit] = useState(0)
|
||||||
const [tempDiscount, setTempDiscount] = useState(10)
|
const [available, setAvailable] = useState<UnlimitedAvailable | null>(null)
|
||||||
|
const [tempWhiteLimit, setTempWhiteLimit] = useState(1)
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
// 调整后到期时间(演示用固定 +30 天)
|
const load = useCallback(async () => {
|
||||||
const newExpire = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
|
setStatus("load")
|
||||||
|
const res = await fetchWhiteListSu()
|
||||||
|
if (!res.success) {
|
||||||
|
setStatus("fail")
|
||||||
|
toast.error(res.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setList(res.data)
|
||||||
|
setStatus("done")
|
||||||
|
}, [])
|
||||||
|
|
||||||
const setTempWhiteLimitHandler = (value: number) => {
|
const loadLimit = useCallback(async () => {
|
||||||
const clamped = clampLimit(value)
|
const res = await fetchWhiteLimitSu()
|
||||||
setTempWhiteLimit(clamped)
|
if (res.success) setWhiteLimit(res.data)
|
||||||
setTempDiscount(discountOf(clamped))
|
}, [])
|
||||||
}
|
|
||||||
|
|
||||||
const increaseLimit = () => setTempWhiteLimitHandler(tempWhiteLimit + 1)
|
const loadAvailable = useCallback(async () => {
|
||||||
|
const res = await fetchUnlimitedAvailable()
|
||||||
|
if (res.success) setAvailable(res.data)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const decreaseLimit = () => setTempWhiteLimitHandler(tempWhiteLimit - 1)
|
useEffect(() => {
|
||||||
|
load()
|
||||||
|
loadLimit()
|
||||||
|
loadAvailable()
|
||||||
|
}, [load, loadLimit, loadAvailable])
|
||||||
|
|
||||||
const openDialog = () => {
|
const runAction = useCallback(
|
||||||
setTempWhiteLimit(whiteLimit)
|
async (
|
||||||
setTempDiscount(discountOf(whiteLimit))
|
action: () => Promise<ApiResponse<WhiteActionResult>>,
|
||||||
setIsDialogOpen(true)
|
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 confirmSetLimit = () => {
|
const handleSaveIp = async () => {
|
||||||
setWhiteLimit(tempWhiteLimit)
|
|
||||||
setIsDialogOpen(false)
|
|
||||||
// TODO: await setWhiteLimitMax(tempWhiteLimit)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSaveIp = () => {
|
|
||||||
const value = ip.trim()
|
const value = ip.trim()
|
||||||
if (!value) return
|
if (!value) {
|
||||||
// TODO: await saveWhiteIp(value)
|
toast.error("请输入IP地址")
|
||||||
setList(prev => [
|
return
|
||||||
{ id: Date.now(), ip: value, createTime: "", isLocked: "未锁定" },
|
}
|
||||||
...prev,
|
setSaving(true)
|
||||||
])
|
await runAction(() => createWhiteIpSu(value), "添加成功")
|
||||||
|
setSaving(false)
|
||||||
setIp("")
|
setIp("")
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleLock = (item: WhiteIpItem) => {
|
const handleLock = (item: WhiteIpItem) => {
|
||||||
// TODO: await lockWhiteIp(item.id, item.isLocked !== "已锁定")
|
const locked = item.isLocked === "已锁定"
|
||||||
setList(prev =>
|
runAction(
|
||||||
prev.map(i =>
|
() => lockWhiteIpSu(item.id, locked ? 0 : 1),
|
||||||
i.id === item.id
|
locked ? "解锁成功" : "锁定成功",
|
||||||
? {
|
|
||||||
...i,
|
|
||||||
isLocked: i.isLocked === "未锁定" ? "已锁定" : "未锁定",
|
|
||||||
}
|
|
||||||
: i,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeIp = (item: WhiteIpItem) => {
|
const handleDelete = (item: WhiteIpItem) => {
|
||||||
// TODO: await deleteWhiteIp(item.id)
|
runAction(() => deleteWhiteIpSu(item.id), "删除成功")
|
||||||
setList(prev => prev.filter(i => i.id !== item.id))
|
}
|
||||||
|
|
||||||
|
const openDialog = () => {
|
||||||
|
setTempWhiteLimit(clampLimit(whiteLimit || 1))
|
||||||
|
setIsDialogOpen(true)
|
||||||
|
loadAvailable()
|
||||||
|
}
|
||||||
|
|
||||||
|
const increaseLimit = () => setTempWhiteLimit(prev => clampLimit(prev + 1))
|
||||||
|
|
||||||
|
const decreaseLimit = () => setTempWhiteLimit(prev => clampLimit(prev - 1))
|
||||||
|
|
||||||
|
const tempDiscount = Math.max(6, 11 - tempWhiteLimit)
|
||||||
|
const newExpre = calcRemain(
|
||||||
|
available?.expireTime ?? 0,
|
||||||
|
whiteLimit,
|
||||||
|
tempWhiteLimit,
|
||||||
|
)
|
||||||
|
|
||||||
|
const confirmSetLimit = async () => {
|
||||||
|
setSaving(true)
|
||||||
|
const res = await setWhiteLimitSu(tempWhiteLimit)
|
||||||
|
setSaving(false)
|
||||||
|
if (!res.success) {
|
||||||
|
toast.error(res.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { code, msg } = res.data
|
||||||
|
if (code !== undefined && code < 1) {
|
||||||
|
toast.error(msg || "设置失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toast.success("白名单上限设置成功")
|
||||||
|
setIsDialogOpen(false)
|
||||||
|
loadLimit()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -95,7 +175,9 @@ export default function WhitelistSUPage() {
|
|||||||
className="max-w-sm"
|
className="max-w-sm"
|
||||||
onKeyDown={e => e.key === "Enter" && handleSaveIp()}
|
onKeyDown={e => e.key === "Enter" && handleSaveIp()}
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleSaveIp}>保存</Button>
|
<Button onClick={handleSaveIp} disabled={saving}>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -110,15 +192,18 @@ export default function WhitelistSUPage() {
|
|||||||
|
|
||||||
<DataTable<WhiteIpItem>
|
<DataTable<WhiteIpItem>
|
||||||
data={list}
|
data={list}
|
||||||
status="done"
|
status={status}
|
||||||
columns={[
|
columns={[
|
||||||
|
{ accessorKey: "ip", header: "IP地址" },
|
||||||
{
|
{
|
||||||
accessorKey: "ip",
|
accessorKey: "description",
|
||||||
header: "IP地址",
|
header: "备注",
|
||||||
|
cell: ({ row }) => row.original.description || "-",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "createTime",
|
accessorKey: "createTime",
|
||||||
header: "设置时间",
|
header: "设置时间",
|
||||||
|
cell: ({ row }) => formatUnixTime(row.original.createTime),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "isLocked",
|
accessorKey: "isLocked",
|
||||||
@@ -137,24 +222,25 @@ export default function WhitelistSUPage() {
|
|||||||
header: "操作",
|
header: "操作",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const item = row.original
|
const item = row.original
|
||||||
|
const isLocked = item.isLocked === "已锁定"
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => toggleLock(item)}
|
onClick={() => handleLock(item)}
|
||||||
>
|
>
|
||||||
{item.isLocked === "未锁定" ? (
|
{isLocked ? (
|
||||||
<Lock className="mr-1 h-3 w-3" />
|
|
||||||
) : (
|
|
||||||
<Unlock className="mr-1 h-3 w-3" />
|
<Unlock className="mr-1 h-3 w-3" />
|
||||||
|
) : (
|
||||||
|
<Lock className="mr-1 h-3 w-3" />
|
||||||
)}
|
)}
|
||||||
{item.isLocked === "未锁定" ? "锁定" : "解锁"}
|
{isLocked ? "解锁" : "锁定"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => removeIp(item)}
|
onClick={() => handleDelete(item)}
|
||||||
>
|
>
|
||||||
<Trash2 className="mr-1 h-3 w-3" />
|
<Trash2 className="mr-1 h-3 w-3" />
|
||||||
删除
|
删除
|
||||||
@@ -168,56 +254,63 @@ export default function WhitelistSUPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 调整白名单上限弹窗 */}
|
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>调整白名单上限</DialogTitle>
|
<DialogTitle>调整白名单上限</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-4">
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex flex-col items-start gap-4 py-4">
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
variant="outline"
|
||||||
onClick={decreaseLimit}
|
size="sm"
|
||||||
disabled={tempWhiteLimit <= 1}
|
className="focus:outline-none focus:ring-0"
|
||||||
>
|
onClick={decreaseLimit}
|
||||||
<Minus className="h-4 w-4" />
|
disabled={tempWhiteLimit <= 1}
|
||||||
</Button>
|
>
|
||||||
<span className="w-12 text-center font-medium">
|
<Minus className="h-4 w-4" />
|
||||||
{tempWhiteLimit}
|
</Button>
|
||||||
</span>
|
<span className="w-12 text-center font-medium">
|
||||||
<Button
|
{tempWhiteLimit}
|
||||||
variant="outline"
|
</span>
|
||||||
size="sm"
|
<Button
|
||||||
onClick={increaseLimit}
|
variant="outline"
|
||||||
disabled={tempWhiteLimit >= 20}
|
size="sm"
|
||||||
>
|
className="focus:outline-none focus:ring-0"
|
||||||
<Plus className="h-4 w-4" />
|
onClick={increaseLimit}
|
||||||
</Button>
|
disabled={tempWhiteLimit >= 20}
|
||||||
</div>
|
>
|
||||||
<div className="text-sm">
|
<Plus className="h-4 w-4" />
|
||||||
{tempDiscount === 10 ? (
|
</Button>
|
||||||
<span>
|
|
||||||
价格<span className="text-red-500">无折扣</span>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-red-500">
|
|
||||||
当前已应用 {tempDiscount * 10}% 折扣
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
调整后到期时间:{newExpire.toLocaleString()}
|
<div className="ml-8">
|
||||||
|
{tempDiscount === 10 ? (
|
||||||
|
<span>
|
||||||
|
价格<span className="text-red-500">无折扣</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-red-500">
|
||||||
|
当前已应用 {tempDiscount * 10}% 折扣
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-8">
|
||||||
|
调整后到期时间:{new Date(newExpre).toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>
|
<Button variant="secondary" onClick={() => setIsDialogOpen(false)}>
|
||||||
关闭
|
关闭
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={confirmSetLimit}>确定</Button>
|
<Button onClick={confirmSetLimit} disabled={saving}>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
CLIENT_ID,
|
CLIENT_ID,
|
||||||
CLIENT_SECRET,
|
CLIENT_SECRET,
|
||||||
} from "@/lib/api"
|
} from "@/lib/api"
|
||||||
import { clearSession, getUserToken } from "./token"
|
import { getUserToken } from "./token"
|
||||||
|
|
||||||
type CallMode = "public" | "device" | "user"
|
type CallMode = "public" | "device" | "user"
|
||||||
|
|
||||||
@@ -76,7 +76,14 @@ async function request<R = undefined>(
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
status: response.status,
|
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<R = undefined>(
|
|||||||
auth = `Bearer ${token}`
|
auth = `Bearer ${token}`
|
||||||
} else if (mode === "device") {
|
} else if (mode === "device") {
|
||||||
if (attempt > 1) {
|
if (attempt > 1) {
|
||||||
// 上次请求 401,强制作废旧 token 再重新获取
|
|
||||||
deviceToken = null
|
deviceToken = null
|
||||||
deviceTokenExpire = 0
|
deviceTokenExpire = 0
|
||||||
}
|
}
|
||||||
@@ -229,14 +235,13 @@ async function call<R = undefined>(
|
|||||||
|
|
||||||
const resp = await request<R>(url, { method, body, auth })
|
const resp = await request<R>(url, { method, body, auth })
|
||||||
|
|
||||||
if (!resp.success && resp.status === 401) {
|
if (
|
||||||
if (mode === "user") {
|
!resp.success &&
|
||||||
clearSession()
|
resp.status === 401 &&
|
||||||
return resp
|
mode === "device" &&
|
||||||
}
|
attempt < maxAttempts
|
||||||
if (mode === "device" && attempt < maxAttempts) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,12 +160,13 @@ export function toNumber(value: unknown): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 解析服务端分页信息,缺失字段兜底
|
// 解析服务端分页信息,缺失字段兜底
|
||||||
|
// 兼容两套字段命名:文档的 total/maxResults 与旧接口的 totalCount/pageSize
|
||||||
export function parsePager(raw: unknown, itemCount: number): Pager {
|
export function parsePager(raw: unknown, itemCount: number): Pager {
|
||||||
const p = (raw ?? {}) as Record<string, unknown>
|
const p = (raw ?? {}) as Record<string, unknown>
|
||||||
return {
|
return {
|
||||||
currentPage: toNumber(p.currentPage) || 1,
|
currentPage: toNumber(p.currentPage) || 1,
|
||||||
pageSize: toNumber(p.pageSize) || itemCount,
|
pageSize: toNumber(p.maxResults ?? p.pageSize) || itemCount,
|
||||||
totalCount: toNumber(p.totalCount),
|
totalCount: toNumber(p.total ?? p.totalCount),
|
||||||
totalPage: toNumber(p.totalPage),
|
totalPage: toNumber(p.totalPage),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,7 +347,9 @@ export async function fetchCxPackages(): Promise<ApiResponse<CxPackage[]>> {
|
|||||||
cookie: document.cookie,
|
cookie: document.cookie,
|
||||||
})
|
})
|
||||||
if (!res.success) return res
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: isArray<CxPackage>(list) ? list : [],
|
data: isArray<CxPackage>(list) ? list : [],
|
||||||
@@ -367,9 +370,10 @@ export async function fetchCxOrders(): Promise<ApiResponse<CxOrder[]>> {
|
|||||||
export async function fetchLangIpList(
|
export async function fetchLangIpList(
|
||||||
query: Record<string, unknown>,
|
query: Record<string, unknown>,
|
||||||
): Promise<ApiResponse<LangIpListResult>> {
|
): Promise<ApiResponse<LangIpListResult>> {
|
||||||
|
|
||||||
const res = await phpPost<RawHttpResponse>("/http/user/lang_ip_list", {
|
const res = await phpPost<RawHttpResponse>("/http/user/lang_ip_list", {
|
||||||
cookie: document.cookie,
|
cookie: document.cookie,
|
||||||
data: query,
|
...query,
|
||||||
})
|
})
|
||||||
if (!res.success) return res
|
if (!res.success) return res
|
||||||
const d = unwrapD(res.data)
|
const d = unwrapD(res.data)
|
||||||
@@ -383,9 +387,10 @@ export async function fetchLangIpList(
|
|||||||
export async function fetchSwitchList(
|
export async function fetchSwitchList(
|
||||||
query: Record<string, unknown>,
|
query: Record<string, unknown>,
|
||||||
): Promise<ApiResponse<SwitchListResult>> {
|
): Promise<ApiResponse<SwitchListResult>> {
|
||||||
|
|
||||||
const res = await phpPost<RawHttpResponse>("/http/user/cx_switch", {
|
const res = await phpPost<RawHttpResponse>("/http/user/cx_switch", {
|
||||||
cookie: document.cookie,
|
cookie: document.cookie,
|
||||||
data: query,
|
...query,
|
||||||
})
|
})
|
||||||
if (!res.success) return res
|
if (!res.success) return res
|
||||||
const d = unwrapD(res.data)
|
const d = unwrapD(res.data)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type {
|
|||||||
CreateOrderData,
|
CreateOrderData,
|
||||||
CreateOrderRequest,
|
CreateOrderRequest,
|
||||||
CreateTestAccountRequest,
|
CreateTestAccountRequest,
|
||||||
MyOrderPage,
|
MyOrderListResult,
|
||||||
MyOrderQueryParams,
|
MyOrderQueryParams,
|
||||||
OrderItem,
|
OrderItem,
|
||||||
OrderQueryParams,
|
OrderQueryParams,
|
||||||
@@ -53,15 +53,43 @@ export async function fetchOrderPage(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 退款订单分页列表
|
// ==================== 个人中心订单/退货订单 ====================
|
||||||
export async function fetchRefundOrders(
|
|
||||||
params: OrderQueryParams,
|
// 响应结构:{ Code: 10000, data: [{ RowCount, List }] }
|
||||||
): Promise<ApiResponse<OrderItem[]>> {
|
async function fetchMyOrderPage(
|
||||||
return await callByUser<OrderItem[]>(
|
endpoint: string,
|
||||||
"/api/course/v1/order/RefundOrders",
|
params: MyOrderQueryParams,
|
||||||
params,
|
): Promise<ApiResponse<MyOrderListResult>> {
|
||||||
"GET",
|
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<ApiResponse<MyOrderListResult>> {
|
||||||
|
return fetchMyOrderPage("/user/ApiMyOrders", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 我的退货订单
|
||||||
|
export async function fetchMyRefundOrders(
|
||||||
|
params: MyOrderQueryParams,
|
||||||
|
): Promise<ApiResponse<MyOrderListResult>> {
|
||||||
|
return fetchMyOrderPage("/user/ApiMyRefundOrders", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优惠券订单分页列表
|
// 优惠券订单分页列表
|
||||||
@@ -108,43 +136,6 @@ export async function exportOrders(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchMyOrderPage(
|
|
||||||
endpoint: string,
|
|
||||||
params: MyOrderQueryParams,
|
|
||||||
): Promise<ApiResponse<MyOrderPage>> {
|
|
||||||
const res = await callPublic<MyOrderPage[] | MyOrderPage | 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 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<ApiResponse<MyOrderPage>> {
|
|
||||||
return fetchMyOrderPage("/user/ApiMyOrders", params)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 我的退货订单
|
|
||||||
export async function fetchMyRefundOrders(
|
|
||||||
params: MyOrderQueryParams,
|
|
||||||
): Promise<ApiResponse<MyOrderPage>> {
|
|
||||||
return fetchMyOrderPage("/user/ApiMyRefundOrders", params)
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RechargeOrderResult = {
|
export type RechargeOrderResult = {
|
||||||
OrderInfo: {
|
OrderInfo: {
|
||||||
OrderNo: string
|
OrderNo: string
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export async function fetchWindowProducts(): Promise<
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 产品+套餐(用于账号管理筛选,接口来源:juipnet MyAccounts.cshtml)
|
// 产品+套餐(用于账号管理筛选)
|
||||||
export async function fetchProductWithPackage(): Promise<
|
export async function fetchProductWithPackage(): Promise<
|
||||||
ApiResponse<ProductItem[]>
|
ApiResponse<ProductItem[]>
|
||||||
> {
|
> {
|
||||||
|
|||||||
@@ -2,42 +2,50 @@ import type { ApiResponse } from "@/lib/api"
|
|||||||
import type {
|
import type {
|
||||||
HttpUsedLogItem,
|
HttpUsedLogItem,
|
||||||
HttpUsedLogQuery,
|
HttpUsedLogQuery,
|
||||||
|
HttpUsedLogsResult,
|
||||||
HttpUsedSummary,
|
HttpUsedSummary,
|
||||||
} from "@/lib/models/record"
|
} from "@/lib/models/record"
|
||||||
import { isArray, phpPost, toNumber, unwrapD } from "./http"
|
import { isArray, parsePager, phpPost, toNumber, unwrapD } from "./http"
|
||||||
|
|
||||||
// ==================== HTTP 使用记录(php-api /http/user/*) ====================
|
// ==================== HTTP 使用记录(php-api /http/user/*) ====================
|
||||||
// TODO: 接口路径待后端确认,按现有 /http/user/* 命名规律推测。
|
|
||||||
|
|
||||||
|
// 包天包量使用概况
|
||||||
export async function fetchHttpUsedSummary(): Promise<
|
export async function fetchHttpUsedSummary(): Promise<
|
||||||
ApiResponse<HttpUsedSummary>
|
ApiResponse<HttpUsedSummary>
|
||||||
> {
|
> {
|
||||||
const res = await phpPost("/http/user/pack_used_summary", {
|
const res = await phpPost<Record<string, unknown>>("/http/user/dx_used", {
|
||||||
cookie: document.cookie,
|
cookie: document.cookie,
|
||||||
})
|
})
|
||||||
if (!res.success) return res
|
if (!res.success) return res
|
||||||
const d = unwrapD(res.data)
|
const raw = res.data ?? {}
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
todayShortPackIpUsed: toNumber(d.todayShortPackIpUsed),
|
todayCoinPackIpPaid:
|
||||||
shortPackIpUsed: toNumber(d.shortPackIpUsed),
|
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(
|
export async function fetchHttpUsedLogs(
|
||||||
query: HttpUsedLogQuery,
|
query: HttpUsedLogQuery,
|
||||||
): Promise<ApiResponse<HttpUsedLogItem[]>> {
|
): Promise<ApiResponse<HttpUsedLogsResult>> {
|
||||||
const res = await phpPost("/http/user/pack_used_log", {
|
const res = await phpPost<Record<string, unknown>>("/http/user/dx_used_log", {
|
||||||
cookie: document.cookie,
|
cookie: document.cookie,
|
||||||
data: query,
|
data: query,
|
||||||
})
|
})
|
||||||
if (!res.success) return res
|
if (!res.success) return res
|
||||||
const d = unwrapD(res.data)
|
const d = unwrapD(res.data)
|
||||||
const list = d.items ?? d.list ?? d.data
|
const items = isArray<HttpUsedLogItem>(d.items) ? d.items : []
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: isArray<HttpUsedLogItem>(list) ? list : [],
|
data: { items, pager: parsePager(d.pager, items.length) },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,118 @@
|
|||||||
import type { ApiResponse } from "@/lib/api"
|
import type { ApiResponse } from "@/lib/api"
|
||||||
import type { WhiteIpItem } from "@/lib/models/whitelist"
|
import type { UnlimitedAvailable, WhiteIpItem } from "@/lib/models/whitelist"
|
||||||
import { isArray, phpPost, unwrapD } from "./http"
|
import { isArray, phpPost, toNumber, unwrapD } from "./http"
|
||||||
|
|
||||||
// ==================== 白名单(php-api /http/user/*) ====================
|
type RawHttpResponse = Record<string, unknown>
|
||||||
// TODO: 后端接口路径待确认,以下路径按现有 /http/user/white_* 命名规律推测,
|
|
||||||
// 后端就绪后需按实际路径修正。
|
|
||||||
|
|
||||||
export async function fetchWhiteList(): Promise<ApiResponse<WhiteIpItem[]>> {
|
|
||||||
const res = await phpPost("/http/user/white_list", {
|
export type WhiteActionResult = {
|
||||||
|
code?: number
|
||||||
|
msg?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postWhiteAction(
|
||||||
|
path: string,
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
): Promise<ApiResponse<WhiteActionResult>> {
|
||||||
|
const res = await phpPost<RawHttpResponse>(path, {
|
||||||
cookie: document.cookie,
|
cookie: document.cookie,
|
||||||
|
data,
|
||||||
})
|
})
|
||||||
if (!res.success) return res
|
if (!res.success) return res
|
||||||
const d = unwrapD(res.data)
|
const raw = res.data ?? {}
|
||||||
const list = d.list ?? d.data ?? res.data?.data
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: isArray<WhiteIpItem>(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<ApiResponse<undefined>> {
|
// 白名单列表接口返回 data 直接为数组,兼容 { list: [...] } / { data: [...] }
|
||||||
return await phpPost("/http/user/white_save", {
|
function parseList(raw: unknown): WhiteIpItem[] {
|
||||||
cookie: document.cookie,
|
const value = raw as RawHttpResponse | undefined
|
||||||
ip,
|
const list = Array.isArray(raw) ? raw : (value?.list ?? value?.data)
|
||||||
})
|
return isArray<WhiteIpItem>(list) ? list : []
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteWhiteIp(
|
// ==================== 白名单(普通) ====================
|
||||||
id: number,
|
|
||||||
): Promise<ApiResponse<undefined>> {
|
export async function fetchWhiteList(): Promise<ApiResponse<WhiteIpItem[]>> {
|
||||||
return await phpPost("/http/user/white_delete", {
|
const res = await phpPost<RawHttpResponse>("/http/user/white_list", {
|
||||||
cookie: document.cookie,
|
cookie: document.cookie,
|
||||||
id,
|
|
||||||
})
|
})
|
||||||
|
if (!res.success) return res
|
||||||
|
return { success: true, data: parseList(res.data) }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function lockWhiteIp(
|
export function createWhiteIp(ip: string) {
|
||||||
id: number,
|
return postWhiteAction("/http/user/create_white_ip", { ip })
|
||||||
locked: boolean,
|
|
||||||
): Promise<ApiResponse<undefined>> {
|
|
||||||
return await phpPost("/http/user/white_lock", {
|
|
||||||
cookie: document.cookie,
|
|
||||||
id,
|
|
||||||
locked: locked ? 1 : 0,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setWhiteLimitMax(
|
export function lockWhiteIp(id: number, lock: 0 | 1) {
|
||||||
limit: number,
|
return postWhiteAction("/http/user/lock_ip", { id, lock })
|
||||||
): Promise<ApiResponse<undefined>> {
|
}
|
||||||
return await phpPost("/http/user/white_limit_set_su", {
|
|
||||||
cookie: document.cookie,
|
export function deleteWhiteIp(id: number) {
|
||||||
limit,
|
return postWhiteAction("/http/user/delete_ip", { id })
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// ==================== 白名单(短效无限量) ====================
|
||||||
|
|
||||||
|
export async function fetchWhiteListSu(): Promise<ApiResponse<WhiteIpItem[]>> {
|
||||||
|
const res = await phpPost<RawHttpResponse>("/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<ApiResponse<number>> {
|
||||||
|
const res = await phpPost<RawHttpResponse>("/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<UnlimitedAvailable>
|
||||||
|
> {
|
||||||
|
const res = await phpPost<RawHttpResponse>(
|
||||||
|
"/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),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,20 @@ export default function OrderFilterBar(props: {
|
|||||||
filters: OrderFilters
|
filters: OrderFilters
|
||||||
products: ProductItem[]
|
products: ProductItem[]
|
||||||
showOrderType?: boolean
|
showOrderType?: boolean
|
||||||
|
showPackageName?: boolean
|
||||||
onPatch: (patch: Partial<OrderFilters>) => void
|
onPatch: (patch: Partial<OrderFilters>) => void
|
||||||
onSearch: () => 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 (
|
return (
|
||||||
<div className="bg-white rounded-lg p-4 flex flex-wrap items-end gap-x-4 gap-y-3">
|
<div className="bg-white rounded-lg p-4 flex flex-wrap items-end gap-x-4 gap-y-3">
|
||||||
@@ -84,22 +94,24 @@ export default function OrderFilterBar(props: {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Select
|
{showPackageName && (
|
||||||
value={filters.packageName}
|
<Select
|
||||||
onValueChange={value => onPatch({ packageName: value })}
|
value={filters.packageName}
|
||||||
>
|
onValueChange={value => onPatch({ packageName: value })}
|
||||||
<SelectTrigger className="w-32">
|
>
|
||||||
<SelectValue placeholder="全部套餐" />
|
<SelectTrigger className="w-32">
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="全部套餐" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
<SelectItem value="0">全部套餐</SelectItem>
|
<SelectContent>
|
||||||
{PACKAGE_OPTIONS.map(pkg => (
|
<SelectItem value="0">全部套餐</SelectItem>
|
||||||
<SelectItem key={pkg} value={pkg}>
|
{PACKAGE_OPTIONS.map(pkg => (
|
||||||
{pkg}
|
<SelectItem key={pkg} value={pkg}>
|
||||||
</SelectItem>
|
{pkg}
|
||||||
))}
|
</SelectItem>
|
||||||
</SelectContent>
|
))}
|
||||||
</Select>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
@@ -115,6 +127,11 @@ export default function OrderFilterBar(props: {
|
|||||||
<SearchIcon data-icon="inline-start" />
|
<SearchIcon data-icon="inline-start" />
|
||||||
查询
|
查询
|
||||||
</Button>
|
</Button>
|
||||||
|
{onReset && (
|
||||||
|
<Button variant="outline" onClick={onReset}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
@@ -42,21 +40,6 @@ function Pagination({
|
|||||||
// 无数据时兜底为 1 页,避免除零与空分页器
|
// 无数据时兜底为 1 页,避免除零与空分页器
|
||||||
const totalPages = size > 0 ? Math.max(Math.ceil(total / size), 1) : 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 变化
|
// 同步外部 page 变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentPage(page)
|
setCurrentPage(page)
|
||||||
@@ -64,13 +47,12 @@ function Pagination({
|
|||||||
|
|
||||||
// 分页器逻辑
|
// 分页器逻辑
|
||||||
const generatePaginationItems = () => {
|
const generatePaginationItems = () => {
|
||||||
// 桌面端最多显示7个页码,移动端最多显示5个,其余用省略号
|
// 最多显示7个页码,其余用省略号
|
||||||
const SIBLINGS = isMobile ? 0 : 1 // 当前页左右各显示的页码数
|
const SIBLINGS = 1 // 当前页左右各显示的页码数
|
||||||
const MAX_VISIBLE = isMobile ? 5 : 7
|
|
||||||
const DOTS = -1 // 省略号标记
|
const DOTS = -1 // 省略号标记
|
||||||
|
|
||||||
if (totalPages <= MAX_VISIBLE) {
|
if (totalPages <= 7) {
|
||||||
// 总页数较少,全部显示
|
// 总页数少于7,全部显示
|
||||||
return Array.from({ length: totalPages }, (_, i) => i + 1)
|
return Array.from({ length: totalPages }, (_, i) => i + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,13 +121,8 @@ function Pagination({
|
|||||||
const paginationItems = generatePaginationItems()
|
const paginationItems = generatePaginationItems()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn("flex flex-wrap items-center gap-4", className)}>
|
||||||
className={cn(
|
<div className="flex-none flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
"flex flex-wrap items-center justify-center gap-2 md:justify-start md:gap-4",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex-none flex items-center gap-2 text-xs sm:text-sm text-muted-foreground">
|
|
||||||
共 {total} 条记录,每页
|
共 {total} 条记录,每页
|
||||||
<Select value={size.toString()} onValueChange={handlePageSizeChange}>
|
<Select value={size.toString()} onValueChange={handlePageSizeChange}>
|
||||||
<SelectTrigger className="h-8 w-20">
|
<SelectTrigger className="h-8 w-20">
|
||||||
@@ -168,7 +145,7 @@ function Pagination({
|
|||||||
<PaginationPrevious
|
<PaginationPrevious
|
||||||
onClick={() => handlePageChange(currentPage - 1)}
|
onClick={() => handlePageChange(currentPage - 1)}
|
||||||
className={
|
className={
|
||||||
currentPage <= 1 ? "opacity-50 pointer-events-none" : ""
|
currentPage === 1 ? "opacity-50 pointer-events-none" : ""
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
@@ -198,7 +175,7 @@ function Pagination({
|
|||||||
<PaginationNext
|
<PaginationNext
|
||||||
onClick={() => handlePageChange(currentPage + 1)}
|
onClick={() => handlePageChange(currentPage + 1)}
|
||||||
className={
|
className={
|
||||||
currentPage >= totalPages
|
currentPage === totalPages
|
||||||
? "opacity-50 pointer-events-none"
|
? "opacity-50 pointer-events-none"
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
@@ -216,7 +193,6 @@ function PaginationLayout({
|
|||||||
}: React.ComponentProps<"nav">) {
|
}: React.ComponentProps<"nav">) {
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
// role="navigation"
|
|
||||||
aria-label="pagination"
|
aria-label="pagination"
|
||||||
data-slot="pagination"
|
data-slot="pagination"
|
||||||
className={cn("flex-none", className)}
|
className={cn("flex-none", className)}
|
||||||
@@ -232,10 +208,7 @@ function PaginationContent({
|
|||||||
return (
|
return (
|
||||||
<ul
|
<ul
|
||||||
data-slot="pagination-content"
|
data-slot="pagination-content"
|
||||||
className={cn(
|
className={cn("flex flex-row items-center gap-1", className)}
|
||||||
"flex flex-row flex-wrap items-center justify-center gap-1",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -260,7 +233,7 @@ function PaginationLink({
|
|||||||
data-slot="pagination-link"
|
data-slot="pagination-link"
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center text-xs sm:text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-8 w-8 sm:h-9 sm:w-9 rounded-md border border-input hover:bg-secondary hover:text-secondary-foreground",
|
"inline-flex items-center justify-center text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-9 w-9 rounded-md border border-input hover:bg-secondary hover:text-secondary-foreground",
|
||||||
`bg-card`,
|
`bg-card`,
|
||||||
isActive &&
|
isActive &&
|
||||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground",
|
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground",
|
||||||
@@ -309,10 +282,7 @@ function PaginationEllipsis({
|
|||||||
<span
|
<span
|
||||||
aria-hidden
|
aria-hidden
|
||||||
data-slot="pagination-ellipsis"
|
data-slot="pagination-ellipsis"
|
||||||
className={cn(
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
"flex size-8 sm:size-9 items-center justify-center",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<MoreHorizontalIcon className="size-4" />
|
<MoreHorizontalIcon className="size-4" />
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export default function HttpPage() {
|
|||||||
const [customAmount, setCustomAmount] = useState("")
|
const [customAmount, setCustomAmount] = useState("")
|
||||||
const [customIpAmount, setCustomIpAmount] = useState("")
|
const [customIpAmount, setCustomIpAmount] = useState("")
|
||||||
|
|
||||||
// 未登录时跳转登录页,登录后回跳本页(参考 juipnet 产品页做法)
|
// 未登录时跳转登录页,登录后回跳本页
|
||||||
const ensureLogin = () => {
|
const ensureLogin = () => {
|
||||||
if (token !== null) return true
|
if (token !== null) return true
|
||||||
toast.error("请先登录")
|
toast.error("请先登录")
|
||||||
|
|||||||
@@ -3,24 +3,29 @@ import { toast } from "sonner"
|
|||||||
import type { ApiResponse } from "@/lib/api"
|
import type { ApiResponse } from "@/lib/api"
|
||||||
import {
|
import {
|
||||||
DEFAULT_FILTERS,
|
DEFAULT_FILTERS,
|
||||||
type MyOrderPage,
|
type MyOrderListResult,
|
||||||
type MyOrderQueryParams,
|
type MyOrderQueryParams,
|
||||||
type OrderFilters,
|
type OrderFilters,
|
||||||
type OrderItem,
|
type OrderItem,
|
||||||
} from "@/lib/models/order"
|
} from "@/lib/models/order"
|
||||||
import type { ListStatus } from "./useList"
|
import type { ListStatus } from "./useList"
|
||||||
|
|
||||||
const PAGE_SIZE = 10
|
const DEFAULT_SIZE = 10
|
||||||
|
|
||||||
// 订单管理/退货订单共用的筛选+分页逻辑(.NET /user/ApiMyOrders、ApiMyRefundOrders)
|
// 订单管理/退货订单共用
|
||||||
|
// 接口返回全量列表({ RowCount, List }),本地做前端分页
|
||||||
export function useMyOrderPage(
|
export function useMyOrderPage(
|
||||||
fetchPage: (params: MyOrderQueryParams) => Promise<ApiResponse<MyOrderPage>>,
|
fetchPage: (
|
||||||
|
params: MyOrderQueryParams,
|
||||||
|
) => Promise<ApiResponse<MyOrderListResult>>,
|
||||||
|
mapFilters: (filters: OrderFilters) => Partial<MyOrderQueryParams>,
|
||||||
) {
|
) {
|
||||||
const [filters, setFilters] = useState<OrderFilters>(DEFAULT_FILTERS)
|
const [filters, setFilters] = useState<OrderFilters>(DEFAULT_FILTERS)
|
||||||
const [data, setData] = useState<OrderItem[]>([])
|
const [list, setList] = useState<OrderItem[]>([])
|
||||||
const [total, setTotal] = useState(0)
|
const [total, setTotal] = useState(0)
|
||||||
const [status, setStatus] = useState<ListStatus>("load")
|
const [status, setStatus] = useState<ListStatus>("load")
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
|
const [size, setSize] = useState(DEFAULT_SIZE)
|
||||||
|
|
||||||
const patch = useCallback(
|
const patch = useCallback(
|
||||||
(partial: Partial<OrderFilters>) =>
|
(partial: Partial<OrderFilters>) =>
|
||||||
@@ -28,28 +33,31 @@ export function useMyOrderPage(
|
|||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const fetchWith = useCallback(
|
||||||
setStatus("load")
|
async (filterValues: OrderFilters) => {
|
||||||
const res = await fetchPage({
|
setStatus("load")
|
||||||
BTime: filters.bTime || undefined,
|
const res = await fetchPage(mapFilters(filterValues))
|
||||||
ETime: filters.eTime || undefined,
|
if (res.success) {
|
||||||
OrderType:
|
setList(res.data.List)
|
||||||
filters.orderType === "0" ? undefined : Number(filters.orderType),
|
setTotal(
|
||||||
ProductId:
|
res.data.RowCount > 0 ? res.data.RowCount : res.data.List.length,
|
||||||
filters.productId === "0" ? undefined : Number(filters.productId),
|
)
|
||||||
PackageId: filters.packageName === "0" ? undefined : filters.packageName,
|
setStatus("done")
|
||||||
KeyWord: filters.keyword.trim() || undefined,
|
} else {
|
||||||
})
|
setStatus("fail")
|
||||||
if (res.success) {
|
toast.error(res.message)
|
||||||
setData(res.data.List)
|
}
|
||||||
setTotal(res.data.RowCount)
|
setPage(1)
|
||||||
setStatus("done")
|
},
|
||||||
} else {
|
[fetchPage, mapFilters],
|
||||||
setStatus("fail")
|
)
|
||||||
toast.error(res.message)
|
|
||||||
}
|
const load = useCallback(() => fetchWith(filters), [fetchWith, filters])
|
||||||
setPage(1)
|
|
||||||
}, [fetchPage, filters])
|
const reset = useCallback(() => {
|
||||||
|
setFilters(DEFAULT_FILTERS)
|
||||||
|
fetchWith(DEFAULT_FILTERS)
|
||||||
|
}, [fetchWith])
|
||||||
|
|
||||||
const loadedRef = useRef(false)
|
const loadedRef = useRef(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -58,13 +66,28 @@ export function useMyOrderPage(
|
|||||||
load()
|
load()
|
||||||
}, [load])
|
}, [load])
|
||||||
|
|
||||||
// 接口返回全量列表(含 RowCount),本地做前端分页
|
|
||||||
const paged = useMemo(
|
const paged = useMemo(
|
||||||
() => data.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
|
() => list.slice((page - 1) * size, page * size),
|
||||||
[data, page],
|
[list, page, size],
|
||||||
)
|
)
|
||||||
|
|
||||||
return { filters, patch, load, data, total, status, page, setPage, paged }
|
const changeSize = useCallback((next: number) => {
|
||||||
}
|
setSize(next)
|
||||||
|
setPage(1)
|
||||||
|
}, [])
|
||||||
|
|
||||||
export { PAGE_SIZE as MY_ORDER_PAGE_SIZE }
|
return {
|
||||||
|
filters,
|
||||||
|
patch,
|
||||||
|
load,
|
||||||
|
reset,
|
||||||
|
list,
|
||||||
|
total,
|
||||||
|
status,
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
setPage,
|
||||||
|
changeSize,
|
||||||
|
paged,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
77
src/lib/hooks/useOrderPage.ts
Normal file
77
src/lib/hooks/useOrderPage.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import type { ApiResponse } from "@/lib/api"
|
||||||
|
import {
|
||||||
|
DEFAULT_FILTERS,
|
||||||
|
type OrderFilters,
|
||||||
|
type OrderItem,
|
||||||
|
type OrderPageData,
|
||||||
|
type OrderQueryParams,
|
||||||
|
} from "@/lib/models/order"
|
||||||
|
import type { ListStatus } from "./useList"
|
||||||
|
|
||||||
|
// 订单管理/退货订单共用的服务端分页逻辑
|
||||||
|
// (.NET /api/course/v1/order/Page、RefundOrders,返回 Data + TotalCount)
|
||||||
|
type FetchPage = (params: OrderQueryParams) => Promise<ApiResponse<OrderItem[]>>
|
||||||
|
|
||||||
|
export function useOrderPage(
|
||||||
|
fetchPage: FetchPage,
|
||||||
|
mapFilters: (filters: OrderFilters) => Partial<OrderQueryParams>,
|
||||||
|
) {
|
||||||
|
const [filters, setFilters] = useState<OrderFilters>(DEFAULT_FILTERS)
|
||||||
|
const [data, setData] = useState<OrderPageData>({
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0,
|
||||||
|
list: [],
|
||||||
|
})
|
||||||
|
const [status, setStatus] = useState<ListStatus>("load")
|
||||||
|
|
||||||
|
const patch = useCallback(
|
||||||
|
(partial: Partial<OrderFilters>) =>
|
||||||
|
setFilters(prev => ({ ...prev, ...partial })),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
const refresh = useCallback(
|
||||||
|
async (page: number, size: number) => {
|
||||||
|
setStatus("load")
|
||||||
|
const res = await fetchPage({
|
||||||
|
...mapFilters(filters),
|
||||||
|
PageIndex: page,
|
||||||
|
PageSize: size,
|
||||||
|
})
|
||||||
|
if (res.success) {
|
||||||
|
const totalPages = Math.max(Math.ceil((res.totalCount ?? 0) / size), 1)
|
||||||
|
// 数据变少导致当前页越界:回退到最后一页重新拉取
|
||||||
|
if (res.data.length === 0 && page > totalPages) {
|
||||||
|
await refresh(totalPages, size)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setData({
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
total: res.totalCount ?? 0,
|
||||||
|
list: res.data,
|
||||||
|
})
|
||||||
|
setStatus("done")
|
||||||
|
} else {
|
||||||
|
setStatus("fail")
|
||||||
|
toast.error(res.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[fetchPage, mapFilters, filters],
|
||||||
|
)
|
||||||
|
|
||||||
|
// 筛选查询:回到第 1 页
|
||||||
|
const load = useCallback(() => refresh(1, data.size), [refresh, data.size])
|
||||||
|
|
||||||
|
const loadedRef = useRef(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (loadedRef.current) return
|
||||||
|
loadedRef.current = true
|
||||||
|
refresh(1, data.size)
|
||||||
|
}, [refresh, data.size])
|
||||||
|
|
||||||
|
return { filters, patch, refresh, load, data, status }
|
||||||
|
}
|
||||||
@@ -16,5 +16,5 @@ export type ScoreItem = {
|
|||||||
RestAmount2: string
|
RestAmount2: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 服务端固定每页条数(接口不返回总条数)
|
// 每页条数
|
||||||
export const SCORE_PAGE_SIZE = 50
|
export const SCORE_PAGE_SIZE = 50
|
||||||
|
|||||||
@@ -1,22 +1,42 @@
|
|||||||
export type OrderItem = {
|
export type OrderItem = {
|
||||||
|
TenantId: number
|
||||||
|
UserId: number
|
||||||
|
UserName: string
|
||||||
Id: number
|
Id: number
|
||||||
OrderNo: string
|
OrderNo: string
|
||||||
OrderType: number
|
OrderType: number
|
||||||
ProductId: number
|
ProductId: number
|
||||||
ProductName: string
|
ProductName: string
|
||||||
|
PackageId: number
|
||||||
PackageName: string
|
PackageName: string
|
||||||
DayPrice: number
|
CouponId: number
|
||||||
|
CouponAmount: number
|
||||||
|
OrderName: string
|
||||||
|
OrderState: number
|
||||||
|
PayType: number
|
||||||
|
PayChannel: number
|
||||||
|
PayState: number
|
||||||
ConnectCount: number
|
ConnectCount: number
|
||||||
|
OrderAmount: number
|
||||||
|
PaymentAmount: number
|
||||||
|
AccountPayAmount: number
|
||||||
|
OtherPayAmount: number
|
||||||
|
DayPrice: number
|
||||||
|
DayCount: number
|
||||||
|
ClientType: number
|
||||||
|
ChannelType: number
|
||||||
AccountCount: number
|
AccountCount: number
|
||||||
Accounts: string
|
Accounts: string
|
||||||
OrderAmount: number
|
AccountPwd: string
|
||||||
CouponAmount: number
|
|
||||||
PaymentAmount: number
|
|
||||||
RefundCount: number
|
RefundCount: number
|
||||||
RefundAmount: number
|
RefundAmount: number
|
||||||
DayCount: number
|
StartTime: string
|
||||||
|
EndTime: string
|
||||||
|
BackAmount: number
|
||||||
|
agent_id: number
|
||||||
CreateTime: string
|
CreateTime: string
|
||||||
UpdateTime: string
|
UpdateTime: string
|
||||||
|
DeleteTag: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateOrderRequest = {
|
export type CreateOrderRequest = {
|
||||||
@@ -69,6 +89,13 @@ export type OrderQueryParams = {
|
|||||||
PackageNames?: string[]
|
PackageNames?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type OrderPageData = {
|
||||||
|
page: number
|
||||||
|
size: number
|
||||||
|
total: number
|
||||||
|
list: OrderItem[]
|
||||||
|
}
|
||||||
|
|
||||||
export type MyOrderQueryParams = {
|
export type MyOrderQueryParams = {
|
||||||
BTime?: string
|
BTime?: string
|
||||||
ETime?: string
|
ETime?: string
|
||||||
@@ -78,7 +105,7 @@ export type MyOrderQueryParams = {
|
|||||||
KeyWord?: string
|
KeyWord?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MyOrderPage = {
|
export type MyOrderListResult = {
|
||||||
RowCount: number
|
RowCount: number
|
||||||
List: OrderItem[]
|
List: OrderItem[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// 短效/长效套餐相关模型(与 php-api.juip.com 的 /http/user/* 接口对应)
|
|
||||||
|
|
||||||
// 短效套餐(已开通)
|
// 短效套餐(已开通)
|
||||||
export type DxPackage = {
|
export type DxPackage = {
|
||||||
id: number
|
id: number
|
||||||
@@ -41,57 +39,83 @@ export type DxOrder = {
|
|||||||
// 长效套餐(已开通)
|
// 长效套餐(已开通)
|
||||||
export type CxPackage = {
|
export type CxPackage = {
|
||||||
id: number
|
id: number
|
||||||
|
name?: string
|
||||||
isAbroad: number | string
|
isAbroad: number | string
|
||||||
isAbroad2?: number
|
isAbroad2?: number
|
||||||
shareType: number | string
|
shareType: number | string
|
||||||
ipAmount: number
|
ipAmount: number
|
||||||
expireTime: string
|
isExpired?: boolean | number | string
|
||||||
|
expireTime: number | string
|
||||||
|
transferable?: number
|
||||||
isRelayed?: number
|
isRelayed?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// 长效购买记录
|
// 长效购买记录
|
||||||
export type CxOrder = {
|
export type CxOrder = {
|
||||||
|
id?: string
|
||||||
|
user_id?: string
|
||||||
order_no: string
|
order_no: string
|
||||||
order_type: number | string
|
order_type: number | string
|
||||||
game_type: string
|
package_type?: string
|
||||||
ip_count: number
|
durationType?: string | null
|
||||||
|
order_status?: number | string
|
||||||
money: number | string
|
money: number | string
|
||||||
|
game_type: string
|
||||||
|
ip_count: number | string | null
|
||||||
|
order_id?: string
|
||||||
|
payCash?: string
|
||||||
|
serial?: string
|
||||||
|
status?: number | string
|
||||||
create_time: string
|
create_time: string
|
||||||
|
update_time?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已开通长效 IP
|
// 已开通长效 IP
|
||||||
export type LangIpItem = {
|
export type LangIpItem = {
|
||||||
id: number
|
id: number
|
||||||
packId?: number
|
packId?: number
|
||||||
isRelayed: number
|
groupId?: number
|
||||||
isAbroad: number
|
groupName?: string
|
||||||
|
gameId?: number
|
||||||
|
gameName?: string
|
||||||
ip: string
|
ip: string
|
||||||
realIp: string
|
index?: number
|
||||||
httpPort: number
|
httpPort: number
|
||||||
socks5Port: number
|
socks5Port: number
|
||||||
|
isp?: number
|
||||||
|
cityCode?: number
|
||||||
area: string
|
area: string
|
||||||
bandwidth: number
|
|
||||||
lineType: number | string
|
lineType: number | string
|
||||||
lineType2?: number
|
lineType2?: number
|
||||||
shareType: number | string
|
shareType: number | string
|
||||||
shareType2?: number
|
shareType2?: number
|
||||||
state: number | string
|
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
expireTime: string
|
isAbroad: number
|
||||||
|
isRelayed: number
|
||||||
|
expireTime: number | string
|
||||||
|
description?: string
|
||||||
|
bandwidth: number
|
||||||
isExpired: boolean | number | string
|
isExpired: boolean | number | string
|
||||||
gameId?: number
|
state: number | string
|
||||||
isp?: number
|
popState?: number | string
|
||||||
|
popCityCode?: number
|
||||||
|
popIsp?: number
|
||||||
|
popArea?: string
|
||||||
|
realIp: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 长效 IP 切换记录
|
// 长效 IP 切换记录
|
||||||
export type SwitchRecord = {
|
export type SwitchRecord = {
|
||||||
|
id?: number
|
||||||
|
packId?: number
|
||||||
oldIp: string
|
oldIp: string
|
||||||
oldArea: string
|
oldArea: string
|
||||||
newIp: string
|
newIp: string
|
||||||
newArea: string
|
newArea: string
|
||||||
reason: string
|
switchPrice?: number
|
||||||
createTime: string
|
reason: number | string
|
||||||
|
createTime: number | string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 服务端分页信息
|
// 服务端分页信息
|
||||||
@@ -227,6 +251,27 @@ export const IP_STATE_LABELS: Record<number, string> = {
|
|||||||
9: "封禁",
|
9: "封禁",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 线路共享类型
|
||||||
|
export const SHARE_TYPE_LABELS: Record<number, string> = {
|
||||||
|
1: "共享",
|
||||||
|
2: "独享",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 线路质量类型
|
||||||
|
export const LINE_TYPE_LABELS: Record<number, string> = {
|
||||||
|
1: "普通",
|
||||||
|
2: "优质",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 长效 IP 切换原因
|
||||||
|
export const SWITCH_REASON_LABELS: Record<number, string> = {
|
||||||
|
0: "系统自动切换",
|
||||||
|
1: "失效手动切换",
|
||||||
|
2: "免费手动切换",
|
||||||
|
5: "管理员免费切换",
|
||||||
|
6: "收费手动切换",
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 字段格式化 ====================
|
// ==================== 字段格式化 ====================
|
||||||
|
|
||||||
// 后端的 isExpired 可能返回 true/1/"1",也可能直接返回"已过期"/"未过期"字符串
|
// 后端的 isExpired 可能返回 true/1/"1",也可能直接返回"已过期"/"未过期"字符串
|
||||||
@@ -251,6 +296,21 @@ export function formatIpState(value: number | string): string {
|
|||||||
return IP_STATE_LABELS[num] ?? String(value)
|
return IP_STATE_LABELS[num] ?? String(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatShareType(value: number | string | undefined): string {
|
||||||
|
if (value === undefined || value === null || value === "") return ""
|
||||||
|
return SHARE_TYPE_LABELS[Number(value)] ?? String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatLineType(value: number | string | undefined): string {
|
||||||
|
if (value === undefined || value === null || value === "") return ""
|
||||||
|
return LINE_TYPE_LABELS[Number(value)] ?? String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSwitchReason(value: number | string | undefined): string {
|
||||||
|
if (value === undefined || value === null || value === "") return ""
|
||||||
|
return SWITCH_REASON_LABELS[Number(value)] ?? String(value)
|
||||||
|
}
|
||||||
|
|
||||||
export function formatOrderTypeLabel(
|
export function formatOrderTypeLabel(
|
||||||
labels: Record<number, string>,
|
labels: Record<number, string>,
|
||||||
value: number | string,
|
value: number | string,
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
|
import type { Pager } from "./package"
|
||||||
|
|
||||||
export type HttpUsedSummary = {
|
export type HttpUsedSummary = {
|
||||||
|
todayCoinPackIpPaid: string
|
||||||
|
todayCoinPackIpUsed: number
|
||||||
|
coinPackIpUsed: number
|
||||||
todayShortPackIpUsed: number
|
todayShortPackIpUsed: number
|
||||||
shortPackIpUsed: number
|
shortPackIpUsed: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HttpUsedLogItem = {
|
export type HttpUsedLogItem = {
|
||||||
|
id?: number
|
||||||
packType: number
|
packType: number
|
||||||
userIp: string
|
userIp: string
|
||||||
ip: string
|
ip: string
|
||||||
port: number
|
port: number
|
||||||
createTime: string
|
createTime: number | string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HttpUsedLogQuery = {
|
export type HttpUsedLogQuery = {
|
||||||
@@ -17,10 +23,16 @@ export type HttpUsedLogQuery = {
|
|||||||
packType: number
|
packType: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HttpUsedLogsResult = {
|
||||||
|
items: HttpUsedLogItem[]
|
||||||
|
pager: Pager
|
||||||
|
}
|
||||||
|
|
||||||
export const HTTP_USE_PACK_TYPE_LABELS: Record<number, string> = {
|
export const HTTP_USE_PACK_TYPE_LABELS: Record<number, string> = {
|
||||||
12: "储值套餐",
|
12: "储值套餐",
|
||||||
21: "短效包天套餐",
|
21: "短效包天套餐",
|
||||||
22: "短效包量套餐",
|
22: "短效包量套餐",
|
||||||
|
23: "短效无限量套餐",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HTTP_USE_PACK_TYPE_OPTIONS = [21, 22]
|
export const HTTP_USE_PACK_TYPE_OPTIONS = [21, 22]
|
||||||
|
|||||||
@@ -1,6 +1,41 @@
|
|||||||
export type WhiteIpItem = {
|
export type WhiteIpItem = {
|
||||||
id: number
|
id: number
|
||||||
ip: string
|
ip: string
|
||||||
createTime: string
|
description?: string
|
||||||
isLocked: string
|
isLocked: string
|
||||||
|
createTime: number | string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 短效无限量最新套餐详情
|
||||||
|
export type UnlimitedAvailable = {
|
||||||
|
packId: number
|
||||||
|
poolType: number
|
||||||
|
durationType: number
|
||||||
|
usage: number
|
||||||
|
expireTime: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 短效池类型
|
||||||
|
export const POOL_TYPE_LABELS: Record<number, string> = {
|
||||||
|
1: "经典池",
|
||||||
|
11: "白金池",
|
||||||
|
99: "全池",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有效期类型
|
||||||
|
export const DURATION_TYPE_LABELS: Record<number, string> = {
|
||||||
|
5: "1至5分钟",
|
||||||
|
25: "5至25分钟",
|
||||||
|
180: "25至180分钟",
|
||||||
|
360: "3至6小时",
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatPoolType(value: number | string | undefined): string {
|
||||||
|
if (value === undefined || value === null || value === "") return ""
|
||||||
|
return POOL_TYPE_LABELS[Number(value)] ?? String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDurationType(value: number | string | undefined): string {
|
||||||
|
if (value === undefined || value === null || value === "") return ""
|
||||||
|
return DURATION_TYPE_LABELS[Number(value)] ?? String(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,3 +69,19 @@ export function formatDate(value: string): string {
|
|||||||
const day = String(date.getDate()).padStart(2, "0")
|
const day = String(date.getDate()).padStart(2, "0")
|
||||||
return `${year}.${month}.${day}`
|
return `${year}.${month}.${day}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unix 时间戳(秒) → "YYYY-MM-DD HH:mm:ss";非数字内容原样返回
|
||||||
|
export function formatUnixTime(value: number | string | undefined): string {
|
||||||
|
if (value === undefined || value === null || value === "") return ""
|
||||||
|
const num = Number(value)
|
||||||
|
if (Number.isNaN(num) || num <= 0) return String(value)
|
||||||
|
const date = new Date(num * 1000)
|
||||||
|
if (Number.isNaN(date.getTime())) return String(value)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, "0")
|
||||||
|
const day = String(date.getDate()).padStart(2, "0")
|
||||||
|
const hours = String(date.getHours()).padStart(2, "0")
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, "0")
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, "0")
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user