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

177 lines
5.2 KiB
TypeScript
Raw Normal View History

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
}