import { useState, useEffect, useMemo } from "react"; import toast from "react-hot-toast"; import { AnimatedPage, StaggerList, StaggerItem, } from "@/components/animated-page"; import { SearchBar } from "@/components/search-bar"; import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card"; import { Button } from "@/shadcn-bridge/heroui/button"; import { Input } from "@/shadcn-bridge/heroui/input"; import { Select, SelectItem } from "@/shadcn-bridge/heroui/select"; import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, } from "@/shadcn-bridge/heroui/modal"; import { Chip } from "@/shadcn-bridge/heroui/chip"; import { createSpeedLimit, getSpeedLimitList, updateSpeedLimit, deleteSpeedLimit, getTunnelList, } from "@/api"; import { PageLoadingState } from "@/components/page-state"; import { useLocalStorageState } from "@/hooks/use-local-storage-state"; interface SpeedLimitRule { id: number; name: string; speed: number; status: number; tunnelId: number; tunnelName: string; createdTime: string; updatedTime: string; } interface Tunnel { id: number; name: string; } interface SpeedLimitForm { id?: number; name: string; speed: number; tunnelId: number | null; tunnelName: string; status: number; } export default function LimitPage() { const [loading, setLoading] = useState(true); const [rules, setRules] = useState([]); const [tunnels, setTunnels] = useState([]); const [searchKeyword, setSearchKeyword] = useLocalStorageState( "limit-search-keyword", "", ); const [isSearchVisible, setIsSearchVisible] = useState(false); const filteredRules = useMemo(() => { if (!searchKeyword.trim()) return rules; const lowerKeyword = searchKeyword.toLowerCase(); return rules.filter( (r) => (r.name && r.name.toLowerCase().includes(lowerKeyword)) || (r.tunnelName && r.tunnelName.toLowerCase().includes(lowerKeyword)), ); }, [rules, searchKeyword]); // 模态框状态 const [modalOpen, setModalOpen] = useState(false); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [isEdit, setIsEdit] = useState(false); const [submitLoading, setSubmitLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false); const [ruleToDelete, setRuleToDelete] = useState(null); // 表单状态 const [form, setForm] = useState({ name: "", speed: 100, tunnelId: null, tunnelName: "", status: 1, }); // 表单验证错误 const [errors, setErrors] = useState<{ [key: string]: string }>({}); useEffect(() => { loadData(); }, []); // 加载所有数据 const loadData = async () => { setLoading(true); try { const [rulesRes, tunnelsRes] = await Promise.all([ getSpeedLimitList(), getTunnelList(), ]); if (rulesRes.code === 0) { setRules(rulesRes.data || []); } else { toast.error(rulesRes.msg || "获取限速规则失败"); } if (tunnelsRes.code === 0) { setTunnels(tunnelsRes.data || []); } else { } } catch { toast.error("加载数据失败"); } finally { setLoading(false); } }; // 表单验证 const validateForm = (): boolean => { const newErrors: { [key: string]: string } = {}; if (!form.name.trim()) { newErrors.name = "请输入规则名称"; } else if (form.name.length < 2 || form.name.length > 50) { newErrors.name = "规则名称长度应在2-50个字符之间"; } if (!form.speed || form.speed < 1) { newErrors.speed = "请输入有效的速度限制(≥1 Mbps)"; } if (!form.tunnelId) { newErrors.tunnelId = "请选择要绑定的隧道"; } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; // 新增规则 const handleAdd = () => { setIsEdit(false); setForm({ name: "", speed: 100, tunnelId: null, tunnelName: "", status: 1, }); setErrors({}); setModalOpen(true); }; // 编辑规则 const handleEdit = (rule: SpeedLimitRule) => { setIsEdit(true); setForm({ id: rule.id, name: rule.name, speed: rule.speed, tunnelId: rule.tunnelId, tunnelName: rule.tunnelName, status: rule.status, }); setErrors({}); setModalOpen(true); }; // 显示删除确认 const handleDelete = (rule: SpeedLimitRule) => { setRuleToDelete(rule); setDeleteModalOpen(true); }; // 确认删除规则 const confirmDelete = async () => { if (!ruleToDelete) return; setDeleteLoading(true); try { const res = await deleteSpeedLimit(ruleToDelete.id); if (res.code === 0) { toast.success("删除成功"); setDeleteModalOpen(false); loadData(); } else { toast.error(res.msg || "删除失败"); } } catch { toast.error("删除失败"); } finally { setDeleteLoading(false); } }; // 提交表单 const handleSubmit = async () => { if (!validateForm()) return; setSubmitLoading(true); try { let res: { code: number; msg: string }; if (isEdit) { res = await updateSpeedLimit(form); } else { const createData = { ...form }; delete createData.id; res = await createSpeedLimit(createData); } if (res.code === 0) { toast.success(isEdit ? "修改成功" : "创建成功"); setModalOpen(false); loadData(); } else { toast.error(res.msg || "操作失败"); } } catch { toast.error("操作失败"); } finally { setSubmitLoading(false); } }; if (loading) { return ; } return (
setIsSearchVisible(false)} onOpen={() => setIsSearchVisible(true)} />
{/* 统一卡片网格 */} {filteredRules.length > 0 ? ( {filteredRules.map((rule) => (

{rule.name}

{rule.status === 1 ? "运行" : "异常"}
速度限制 {rule.speed} Mbps
绑定隧道 {rule.tunnelName ? ( {rule.tunnelName} ) : ( 未绑定 )}
))}
) : ( /* 空状态 */

暂无限速规则

还没有创建任何限速规则,点击上方按钮开始创建

)} {/* 新增/编辑模态框 */} {(onClose) => ( <>

{isEdit ? "编辑限速规则" : "新增限速规则"}

{isEdit ? "修改现有限速规则的配置信息" : "创建新的限速规则并绑定到隧道"}

setForm((prev) => ({ ...prev, name: e.target.value })) } /> Mbps
} errorMessage={errors.speed} isInvalid={!!errors.speed} label="速度限制" placeholder="请输入速度限制" type="number" value={form.speed.toString()} variant="bordered" onChange={(e) => setForm((prev) => ({ ...prev, speed: parseInt(e.target.value) || 0, })) } />
)}
{/* 删除确认模态框 */} {(onClose) => ( <>

确认删除

确定要删除限速规则{" "} "{ruleToDelete?.name}" {" "} 吗?

此操作无法撤销,删除后该规则将永久消失。

)}
); }