302 lines
8.5 KiB
TypeScript
302 lines
8.5 KiB
TypeScript
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>
|
||
)
|
||
}
|