Files
juip-web/src/admin/channels/page.tsx
2026-09-18 13:17:23 +08:00

302 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ColumnDef } from "@tanstack/react-table"
import { SearchIcon } from "lucide-react"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Link } from "react-router-dom"
import { toast } from "sonner"
import { fetchMyAccounts } from "@/api/account"
import DataTable from "@/components/data-table"
import Page from "@/components/page"
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
}
const INITIAL_QUERY: ChannelQuery = {
expiredDay: "-100",
productId: "0",
packageName: "0",
keyword: "",
}
const PAGE_SIZE = 10
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,
}
}
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>
),
},
]
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)
const [selection, setSelection] = useState<Record<string, boolean>>({})
const selectedProduct = useMemo(
() =>
products.find(item => String(item.Product.Id) === query.productId) ??
null,
[products, query.productId],
)
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)
}
setPage(1)
setSelection({})
}, [])
const loadedRef = useRef(false)
useEffect(() => {
if (loadedRef.current) return
loadedRef.current = true
load(toParams(INITIAL_QUERY))
}, [load])
const applyFilters = (patch: Partial<ChannelQuery>) => {
const next = { ...query, ...patch }
setQuery(next)
load(toParams(next))
}
const handleReset = () => {
setQuery(INITIAL_QUERY)
load(toParams(INITIAL_QUERY))
}
const paged = useMemo(
() => data.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
[data, page],
)
return (
<Page>
<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
value={query.productId}
onValueChange={value =>
applyFilters({ productId: value, packageName: "0" })
}
>
<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 variant="outline" onClick={handleReset}>
重置
</Button>
</div>
</div>
<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("续费功能开发中,敬请期待")}
>
续费
</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 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}
enableSelection
rowSelection={selection}
onSelectionChange={setSelection}
getRowId={row => String(row.Id)}
pagination={{
page,
size: PAGE_SIZE,
total: data.length,
onPageChange: setPage,
}}
/>
</div>
</Page>
)
}