2026-08-31 14:49:28 +08:00
|
|
|
|
import { Lock, Trash2, Unlock } from "lucide-react"
|
2026-09-18 13:17:23 +08:00
|
|
|
|
import { useCallback, useEffect, useState } from "react"
|
|
|
|
|
|
import { toast } from "sonner"
|
|
|
|
|
|
import {
|
|
|
|
|
|
createWhiteIp,
|
|
|
|
|
|
deleteWhiteIp,
|
|
|
|
|
|
fetchWhiteList,
|
|
|
|
|
|
lockWhiteIp,
|
|
|
|
|
|
type WhiteActionResult,
|
|
|
|
|
|
} from "@/api/whitelist"
|
2026-08-31 14:49:28 +08:00
|
|
|
|
import DataTable from "@/components/data-table"
|
2026-07-30 13:55:28 +08:00
|
|
|
|
import Page from "@/components/page"
|
2026-08-31 14:49:28 +08:00
|
|
|
|
import { Badge } from "@/components/ui/badge"
|
|
|
|
|
|
import { Button } from "@/components/ui/button"
|
|
|
|
|
|
import { Input } from "@/components/ui/input"
|
2026-09-18 13:17:23 +08:00
|
|
|
|
import type { ApiResponse } from "@/lib/api"
|
2026-08-31 14:49:28 +08:00
|
|
|
|
import type { WhiteIpItem } from "@/lib/models/whitelist"
|
2026-09-18 13:17:23 +08:00
|
|
|
|
import { formatUnixTime } from "@/lib/utils"
|
2026-07-30 13:55:28 +08:00
|
|
|
|
|
|
|
|
|
|
export default function WhitelistPage() {
|
2026-08-31 14:49:28 +08:00
|
|
|
|
const [ip, setIp] = useState("")
|
|
|
|
|
|
const [list, setList] = useState<WhiteIpItem[]>([])
|
2026-09-18 13:17:23 +08:00
|
|
|
|
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")
|
|
|
|
|
|
}, [])
|
2026-08-31 14:49:28 +08:00
|
|
|
|
|
2026-09-18 13:17:23 +08:00
|
|
|
|
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 () => {
|
2026-08-31 14:49:28 +08:00
|
|
|
|
const value = ip.trim()
|
2026-09-18 13:17:23 +08:00
|
|
|
|
if (!value) {
|
|
|
|
|
|
toast.error("请输入IP地址")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
setSaving(true)
|
|
|
|
|
|
await runAction(() => createWhiteIp(value), "添加成功")
|
|
|
|
|
|
setSaving(false)
|
2026-08-31 14:49:28 +08:00
|
|
|
|
setIp("")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:17:23 +08:00
|
|
|
|
const handleLock = (item: WhiteIpItem) => {
|
|
|
|
|
|
const locked = item.isLocked === "已锁定"
|
|
|
|
|
|
runAction(
|
|
|
|
|
|
() => lockWhiteIp(item.id, locked ? 0 : 1),
|
|
|
|
|
|
locked ? "解锁成功" : "锁定成功",
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDelete = (item: WhiteIpItem) => {
|
|
|
|
|
|
runAction(() => deleteWhiteIp(item.id), "删除成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-31 14:49:28 +08:00
|
|
|
|
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"
|
|
|
|
|
|
/>
|
2026-09-18 13:17:23 +08:00
|
|
|
|
<Button onClick={handleSave} disabled={saving}>
|
|
|
|
|
|
保存
|
|
|
|
|
|
</Button>
|
2026-08-31 14:49:28 +08:00
|
|
|
|
</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}
|
2026-09-18 13:17:23 +08:00
|
|
|
|
status={status}
|
2026-08-31 14:49:28 +08:00
|
|
|
|
columns={[
|
2026-09-18 13:17:23 +08:00
|
|
|
|
{ accessorKey: "ip", header: "IP地址" },
|
2026-08-31 14:49:28 +08:00
|
|
|
|
{
|
2026-09-18 13:17:23 +08:00
|
|
|
|
accessorKey: "description",
|
|
|
|
|
|
header: "备注",
|
|
|
|
|
|
cell: ({ row }) => row.original.description || "-",
|
2026-08-31 14:49:28 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
accessorKey: "createTime",
|
|
|
|
|
|
header: "设置时间",
|
2026-09-18 13:17:23 +08:00
|
|
|
|
cell: ({ row }) => formatUnixTime(row.original.createTime),
|
2026-08-31 14:49:28 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
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
|
2026-09-18 13:17:23 +08:00
|
|
|
|
const isLocked = item.isLocked === "已锁定"
|
2026-08-31 14:49:28 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="sm"
|
2026-09-18 13:17:23 +08:00
|
|
|
|
onClick={() => handleLock(item)}
|
2026-08-31 14:49:28 +08:00
|
|
|
|
>
|
2026-09-18 13:17:23 +08:00
|
|
|
|
{isLocked ? (
|
2026-08-31 14:49:28 +08:00
|
|
|
|
<Unlock className="mr-1 h-3 w-3" />
|
2026-09-18 13:17:23 +08:00
|
|
|
|
) : (
|
|
|
|
|
|
<Lock className="mr-1 h-3 w-3" />
|
2026-08-31 14:49:28 +08:00
|
|
|
|
)}
|
2026-09-18 13:17:23 +08:00
|
|
|
|
{isLocked ? "解锁" : "锁定"}
|
2026-08-31 14:49:28 +08:00
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="destructive"
|
|
|
|
|
|
size="sm"
|
2026-09-18 13:17:23 +08:00
|
|
|
|
onClick={() => handleDelete(item)}
|
2026-08-31 14:49:28 +08:00
|
|
|
|
>
|
|
|
|
|
|
<Trash2 className="mr-1 h-3 w-3" />
|
|
|
|
|
|
删除
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Page>
|
|
|
|
|
|
)
|
2026-07-30 13:55:28 +08:00
|
|
|
|
}
|