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

177 lines
5.2 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 { Lock, Trash2, Unlock } from "lucide-react"
import { useCallback, useEffect, useState } from "react"
import { toast } from "sonner"
import {
createWhiteIp,
deleteWhiteIp,
fetchWhiteList,
lockWhiteIp,
type WhiteActionResult,
} from "@/api/whitelist"
import DataTable from "@/components/data-table"
import Page from "@/components/page"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import type { ApiResponse } from "@/lib/api"
import type { WhiteIpItem } from "@/lib/models/whitelist"
import { formatUnixTime } from "@/lib/utils"
export default function WhitelistPage() {
const [ip, setIp] = useState("")
const [list, setList] = useState<WhiteIpItem[]>([])
const [status, setStatus] = useState<"load" | "done" | "fail">("load")
const [saving, setSaving] = useState(false)
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()
if (!value) {
toast.error("请输入IP地址")
return
}
setSaving(true)
await runAction(() => createWhiteIp(value), "添加成功")
setSaving(false)
setIp("")
}
const handleLock = (item: WhiteIpItem) => {
const locked = item.isLocked === "已锁定"
runAction(
() => lockWhiteIp(item.id, locked ? 0 : 1),
locked ? "解锁成功" : "锁定成功",
)
}
const handleDelete = (item: WhiteIpItem) => {
runAction(() => deleteWhiteIp(item.id), "删除成功")
}
return (
<Page>
<div className="bg-white rounded-lg p-4">
<div className="space-y-4">
<div className="flex items-center gap-3">
<Input
value={ip}
onChange={e => setIp(e.target.value)}
onKeyDown={e => e.key === "Enter" && handleSave()}
placeholder="请输入IP地址并点击保存白名单"
className="max-w-sm"
/>
<Button onClick={handleSave} disabled={saving}>
保存
</Button>
</div>
<div className="flex items-center gap-3">
<h3 className="font-semibold text-lg">当前白名单列表</h3>
<Badge variant="secondary" className="text-yellow-600">
当前仅允许保留白名单数量:5个
</Badge>
</div>
<DataTable<WhiteIpItem>
data={list}
status={status}
columns={[
{ accessorKey: "ip", header: "IP地址" },
{
accessorKey: "description",
header: "备注",
cell: ({ row }) => row.original.description || "-",
},
{
accessorKey: "createTime",
header: "设置时间",
cell: ({ row }) => formatUnixTime(row.original.createTime),
},
{
accessorKey: "isLocked",
header: "锁定状态",
cell: ({ row }) => {
const isLocked = row.original.isLocked === "已锁定"
return (
<Badge variant={isLocked ? "destructive" : "outline"}>
{row.original.isLocked}
</Badge>
)
},
},
{
id: "actions",
header: "操作",
cell: ({ row }) => {
const item = row.original
const isLocked = item.isLocked === "已锁定"
return (
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleLock(item)}
>
{isLocked ? (
<Unlock className="mr-1 h-3 w-3" />
) : (
<Lock className="mr-1 h-3 w-3" />
)}
{isLocked ? "解锁" : "锁定"}
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => handleDelete(item)}
>
<Trash2 className="mr-1 h-3 w-3" />
删除
</Button>
</div>
)
},
},
]}
/>
</div>
</div>
</Page>
)
}