Files
juip-web/src/admin/channels/page.tsx

302 lines
8.5 KiB
TypeScript
Raw Normal View History

import type { ColumnDef } from "@tanstack/react-table"
import { SearchIcon } from "lucide-react"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
2026-09-18 13:17:23 +08:00
import { Link } from "react-router-dom"
2026-08-03 10:53:12 +08:00
import { toast } from "sonner"
import { fetchMyAccounts } from "@/api/account"
2026-08-03 10:53:12 +08:00
import DataTable from "@/components/data-table"
2026-07-30 13:55:28 +08:00
import Page from "@/components/page"
2026-08-03 10:53:12 +08:00
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { useProducts } from "@/lib/hooks/useProducts"
import {
EXPIRED_DAY_OPTIONS,
type MyAccountItem,
type MyAccountQueryParams,
} from "@/lib/models/account"
type ChannelQuery = {
expiredDay: string
productId: string
packageName: string
keyword: string
}
2026-08-03 10:53:12 +08:00
const INITIAL_QUERY: ChannelQuery = {
expiredDay: "-100",
productId: "0",
packageName: "0",
keyword: "",
}
2026-08-03 10:53:12 +08:00
const PAGE_SIZE = 10
2026-08-03 10:53:12 +08:00
function toParams(query: ChannelQuery): MyAccountQueryParams {
return {
ExpiredDay: Number(query.expiredDay),
ProductId: Number(query.productId) || undefined,
PackageId: query.packageName === "0" ? undefined : query.packageName,
Keyword: query.keyword.trim() || undefined,
2026-08-03 10:53:12 +08:00
}
}
2026-08-03 10:53:12 +08:00
const columns: ColumnDef<MyAccountItem>[] = [
{
accessorKey: "StartTime",
header: "开通时间",
},
{
accessorKey: "ProductName",
header: "产品",
},
{
accessorKey: "PackageName",
header: "套餐",
},
{
accessorKey: "Account",
header: "账号",
cell: ({ row }) => (
<span className="font-mono text-sm">{row.original.Account}</span>
),
},
{
accessorKey: "Pwd",
header: "密码",
cell: ({ row }) => (
<span className="font-mono text-sm">{row.original.Pwd}</span>
),
},
{
accessorKey: "ConnectCount",
header: "连接数",
},
{
accessorKey: "EndTime",
header: "到期时间",
},
{
accessorKey: "RestTime",
header: "剩余时间",
cell: ({ row }) => (
<span
className={
row.original.RestTime === "已过期" ? "text-red-500" : undefined
}
>
{row.original.RestTime}
</span>
),
},
{
accessorKey: "Status",
header: "状态",
cell: ({ row }) =>
row.original.Status === 1 ? (
<span className="text-emerald-600">正常</span>
) : (
<span className="text-red-500">已退货</span>
),
},
]
2026-08-03 10:53:12 +08:00
export default function ChannelsPage() {
const products = useProducts()
const [query, setQuery] = useState<ChannelQuery>(INITIAL_QUERY)
const [data, setData] = useState<MyAccountItem[]>([])
const [status, setStatus] = useState<"load" | "done" | "fail">("load")
const [page, setPage] = useState(1)
2026-09-18 13:17:23 +08:00
const [selection, setSelection] = useState<Record<string, boolean>>({})
const selectedProduct = useMemo(
() =>
products.find(item => String(item.Product.Id) === query.productId) ??
null,
[products, query.productId],
)
2026-08-03 10:53:12 +08:00
const load = useCallback(async (params: MyAccountQueryParams) => {
setStatus("load")
const res = await fetchMyAccounts(params)
if (res.success) {
setData(res.data)
setStatus("done")
} else {
setStatus("fail")
toast.error(res.message)
2026-08-03 10:53:12 +08:00
}
setPage(1)
2026-09-18 13:17:23 +08:00
setSelection({})
}, [])
const loadedRef = useRef(false)
useEffect(() => {
if (loadedRef.current) return
loadedRef.current = true
2026-09-11 17:06:29 +08:00
load(toParams(INITIAL_QUERY))
}, [load])
const applyFilters = (patch: Partial<ChannelQuery>) => {
const next = { ...query, ...patch }
setQuery(next)
2026-09-11 17:06:29 +08:00
load(toParams(next))
2026-08-03 10:53:12 +08:00
}
2026-09-18 13:17:23 +08:00
const handleReset = () => {
setQuery(INITIAL_QUERY)
load(toParams(INITIAL_QUERY))
}
const paged = useMemo(
() => data.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
[data, page],
)
2026-08-03 10:53:12 +08:00
return (
<Page>
2026-09-18 13:17:23 +08:00
<div className="bg-white rounded-lg flex flex-col">
<div className="flex flex-col items-center bg-amber-100 p-3 gap-2">
<p className="text-sm text-gray-600">
为了给您带来更好的服务体验,请完善qq号和微信号。
<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
2026-09-18 13:17:23 +08:00
value={query.productId}
onValueChange={value =>
applyFilters({ productId: value, packageName: "0" })
}
>
2026-09-18 13:17:23 +08:00
<SelectTrigger className="w-36">
<SelectValue placeholder="全部产品" />
</SelectTrigger>
<SelectContent>
2026-09-18 13:17:23 +08:00
<SelectItem value="0">全部产品</SelectItem>
{products.map(item => (
<SelectItem key={item.Product.Id} value={String(item.Product.Id)}>
{item.Product.Name}
</SelectItem>
))}
</SelectContent>
</Select>
2026-09-18 13:17:23 +08:00
<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>
2026-08-03 10:53:12 +08:00
2026-09-18 13:17:23 +08:00
<div className="flex flex-wrap items-center gap-4 pt-2 p-4">
<Button
className="bg-red-600 hover:bg-red-700 text-white"
onClick={() => toast.info("续费功能开发中,敬请期待")}
>
续费
2026-08-03 10:53:12 +08:00
</Button>
2026-09-18 13:17:23 +08:00
<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>
2026-08-03 10:53:12 +08:00
</div>
</div>
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
<h3 className="font-semibold text-lg">IP账号管理</h3>
<DataTable
data={paged}
status={status}
columns={columns}
2026-09-18 13:17:23 +08:00
enableSelection
rowSelection={selection}
onSelectionChange={setSelection}
getRowId={row => String(row.Id)}
2026-08-03 10:53:12 +08:00
pagination={{
2026-09-11 17:06:29 +08:00
page,
size: PAGE_SIZE,
total: data.length,
onPageChange: setPage,
2026-08-03 10:53:12 +08:00
}}
/>
</div>
</Page>
)
2026-07-30 13:55:28 +08:00
}