mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-11 11:46:37 +08:00
style: 美化用户隧道权限分配表单UI并统一代码格式
- 使用HeroUI Checkbox组件替换原生checkbox - 重构隧道列表为card-based tile风格 - 添加选中/未选中/已分配状态视觉区分 - 应用eslint --fix代码格式统一 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
+320
-275
@@ -3,18 +3,23 @@ import { Card, CardBody, CardHeader } from "@heroui/card";
|
||||
import { Button } from "@heroui/button";
|
||||
import { Input } from "@heroui/input";
|
||||
import { Select, SelectItem } from "@heroui/select";
|
||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/modal";
|
||||
import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
} from "@heroui/modal";
|
||||
import { Chip } from "@heroui/chip";
|
||||
import { Spinner } from "@heroui/spinner";
|
||||
import toast from 'react-hot-toast';
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
|
||||
import {
|
||||
createSpeedLimit,
|
||||
getSpeedLimitList,
|
||||
updateSpeedLimit,
|
||||
deleteSpeedLimit,
|
||||
getTunnelList
|
||||
import {
|
||||
createSpeedLimit,
|
||||
getSpeedLimitList,
|
||||
updateSpeedLimit,
|
||||
deleteSpeedLimit,
|
||||
getTunnelList,
|
||||
} from "@/api";
|
||||
|
||||
interface SpeedLimitRule {
|
||||
@@ -46,7 +51,7 @@ export default function LimitPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [rules, setRules] = useState<SpeedLimitRule[]>([]);
|
||||
const [tunnels, setTunnels] = useState<Tunnel[]>([]);
|
||||
|
||||
|
||||
// 模态框状态
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
@@ -54,18 +59,18 @@ export default function LimitPage() {
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const [ruleToDelete, setRuleToDelete] = useState<SpeedLimitRule | null>(null);
|
||||
|
||||
|
||||
// 表单状态
|
||||
const [form, setForm] = useState<SpeedLimitForm>({
|
||||
name: '',
|
||||
name: "",
|
||||
speed: 100,
|
||||
tunnelId: null,
|
||||
tunnelName: '',
|
||||
status: 1
|
||||
tunnelName: "",
|
||||
status: 1,
|
||||
});
|
||||
|
||||
|
||||
// 表单验证错误
|
||||
const [errors, setErrors] = useState<{[key: string]: string}>({});
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
@@ -77,23 +82,23 @@ export default function LimitPage() {
|
||||
try {
|
||||
const [rulesRes, tunnelsRes] = await Promise.all([
|
||||
getSpeedLimitList(),
|
||||
getTunnelList()
|
||||
getTunnelList(),
|
||||
]);
|
||||
|
||||
|
||||
if (rulesRes.code === 0) {
|
||||
setRules(rulesRes.data || []);
|
||||
} else {
|
||||
toast.error(rulesRes.msg || '获取限速规则失败');
|
||||
toast.error(rulesRes.msg || "获取限速规则失败");
|
||||
}
|
||||
|
||||
|
||||
if (tunnelsRes.code === 0) {
|
||||
setTunnels(tunnelsRes.data || []);
|
||||
} else {
|
||||
console.warn('获取隧道列表失败:', tunnelsRes.msg);
|
||||
console.warn("获取隧道列表失败:", tunnelsRes.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
toast.error('加载数据失败');
|
||||
console.error("加载数据失败:", error);
|
||||
toast.error("加载数据失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -101,23 +106,24 @@ export default function LimitPage() {
|
||||
|
||||
// 表单验证
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: {[key: string]: string} = {};
|
||||
|
||||
const newErrors: { [key: string]: string } = {};
|
||||
|
||||
if (!form.name.trim()) {
|
||||
newErrors.name = '请输入规则名称';
|
||||
newErrors.name = "请输入规则名称";
|
||||
} else if (form.name.length < 2 || form.name.length > 50) {
|
||||
newErrors.name = '规则名称长度应在2-50个字符之间';
|
||||
newErrors.name = "规则名称长度应在2-50个字符之间";
|
||||
}
|
||||
|
||||
|
||||
if (!form.speed || form.speed < 1) {
|
||||
newErrors.speed = '请输入有效的速度限制(≥1 Mbps)';
|
||||
newErrors.speed = "请输入有效的速度限制(≥1 Mbps)";
|
||||
}
|
||||
|
||||
|
||||
if (!form.tunnelId) {
|
||||
newErrors.tunnelId = '请选择要绑定的隧道';
|
||||
newErrors.tunnelId = "请选择要绑定的隧道";
|
||||
}
|
||||
|
||||
|
||||
setErrors(newErrors);
|
||||
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
@@ -125,11 +131,11 @@ export default function LimitPage() {
|
||||
const handleAdd = () => {
|
||||
setIsEdit(false);
|
||||
setForm({
|
||||
name: '',
|
||||
name: "",
|
||||
speed: 100,
|
||||
tunnelId: null,
|
||||
tunnelName: '',
|
||||
status: 1
|
||||
tunnelName: "",
|
||||
status: 1,
|
||||
});
|
||||
setErrors({});
|
||||
setModalOpen(true);
|
||||
@@ -144,7 +150,7 @@ export default function LimitPage() {
|
||||
speed: rule.speed,
|
||||
tunnelId: rule.tunnelId,
|
||||
tunnelName: rule.tunnelName,
|
||||
status: rule.status
|
||||
status: rule.status,
|
||||
});
|
||||
setErrors({});
|
||||
setModalOpen(true);
|
||||
@@ -159,20 +165,21 @@ export default function LimitPage() {
|
||||
// 确认删除规则
|
||||
const confirmDelete = async () => {
|
||||
if (!ruleToDelete) return;
|
||||
|
||||
|
||||
setDeleteLoading(true);
|
||||
try {
|
||||
const res = await deleteSpeedLimit(ruleToDelete.id);
|
||||
|
||||
if (res.code === 0) {
|
||||
toast.success('删除成功');
|
||||
toast.success("删除成功");
|
||||
setDeleteModalOpen(false);
|
||||
loadData();
|
||||
} else {
|
||||
toast.error(res.msg || '删除失败');
|
||||
toast.error(res.msg || "删除失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
toast.error('删除失败');
|
||||
console.error("删除失败:", error);
|
||||
toast.error("删除失败");
|
||||
} finally {
|
||||
setDeleteLoading(false);
|
||||
}
|
||||
@@ -181,27 +188,29 @@ export default function LimitPage() {
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) return;
|
||||
|
||||
|
||||
setSubmitLoading(true);
|
||||
try {
|
||||
let res;
|
||||
|
||||
if (isEdit) {
|
||||
res = await updateSpeedLimit(form);
|
||||
} else {
|
||||
const { id, ...createData } = form;
|
||||
|
||||
res = await createSpeedLimit(createData);
|
||||
}
|
||||
|
||||
|
||||
if (res.code === 0) {
|
||||
toast.success(isEdit ? '修改成功' : '创建成功');
|
||||
toast.success(isEdit ? "修改成功" : "创建成功");
|
||||
setModalOpen(false);
|
||||
loadData();
|
||||
} else {
|
||||
toast.error(res.msg || '操作失败');
|
||||
toast.error(res.msg || "操作失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
toast.error('操作失败');
|
||||
console.error("提交失败:", error);
|
||||
toast.error("操作失败");
|
||||
} finally {
|
||||
setSubmitLoading(false);
|
||||
}
|
||||
@@ -209,269 +218,305 @@ export default function LimitPage() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="flex items-center gap-3">
|
||||
<Spinner size="sm" />
|
||||
<span className="text-default-600">正在加载...</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="flex items-center gap-3">
|
||||
<Spinner size="sm" />
|
||||
<span className="text-default-600">正在加载...</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<div className="px-3 lg:px-6 py-8">
|
||||
{/* 页面头部 */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex-1">
|
||||
</div>
|
||||
<div className="px-3 lg:px-6 py-8">
|
||||
{/* 页面头部 */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex-1" />
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="primary"
|
||||
onPress={handleAdd}
|
||||
|
||||
<Button size="sm" variant="flat" color="primary" onPress={handleAdd}>
|
||||
新增
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 统一卡片网格 */}
|
||||
{rules.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||
{rules.map((rule) => (
|
||||
<Card
|
||||
key={rule.id}
|
||||
className="shadow-sm border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
新增
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 统一卡片网格 */}
|
||||
{rules.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||
{rules.map((rule) => (
|
||||
<Card key={rule.id} className="shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex justify-between items-start w-full">
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground">{rule.name}</h3>
|
||||
</div>
|
||||
<Chip
|
||||
color={rule.status === 1 ? "success" : "danger"}
|
||||
variant="flat"
|
||||
size="sm"
|
||||
>
|
||||
{rule.status === 1 ? '运行' : '异常'}
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex justify-between items-start w-full">
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground">
|
||||
{rule.name}
|
||||
</h3>
|
||||
</div>
|
||||
<Chip
|
||||
color={rule.status === 1 ? "success" : "danger"}
|
||||
variant="flat"
|
||||
size="sm"
|
||||
>
|
||||
{rule.status === 1 ? "运行" : "异常"}
|
||||
</Chip>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody className="pt-0">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-small text-default-600">
|
||||
速度限制
|
||||
</span>
|
||||
<Chip color="secondary" size="sm" variant="flat">
|
||||
{rule.speed} Mbps
|
||||
</Chip>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody className="pt-0">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-small text-default-600">速度限制</span>
|
||||
<Chip color="secondary" variant="flat" size="sm">
|
||||
{rule.speed} Mbps
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-small text-default-600">
|
||||
绑定隧道
|
||||
</span>
|
||||
{rule.tunnelName ? (
|
||||
<Chip color="primary" size="sm" variant="flat">
|
||||
{rule.tunnelName}
|
||||
</Chip>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-small text-default-600">绑定隧道</span>
|
||||
{rule.tunnelName ? (
|
||||
<Chip color="primary" variant="flat" size="sm">
|
||||
{rule.tunnelName}
|
||||
</Chip>
|
||||
) : (
|
||||
<span className="text-default-400 text-small">未绑定</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-default-400 text-small">
|
||||
未绑定
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button
|
||||
className="flex-1"
|
||||
color="primary"
|
||||
onPress={() => handleEdit(rule)}
|
||||
className="flex-1"
|
||||
size="sm"
|
||||
startContent={
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="danger"
|
||||
onPress={() => handleDelete(rule)}
|
||||
className="flex-1"
|
||||
startContent={
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z" clipRule="evenodd" />
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 012 0v4a1 1 0 11-2 0V7zM12 7a1 1 0 012 0v4a1 1 0 11-2 0V7z" clipRule="evenodd" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
/* 空状态 */
|
||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<CardBody className="text-center py-16">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="w-16 h-16 bg-default-100 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-default-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 6v6l4 2m6-6a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-foreground">暂无限速规则</h3>
|
||||
<p className="text-default-500 text-sm mt-1">还没有创建任何限速规则,点击上方按钮开始创建</p>
|
||||
onPress={() => handleEdit(rule)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="danger"
|
||||
onPress={() => handleDelete(rule)}
|
||||
className="flex-1"
|
||||
startContent={
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 012 0v4a1 1 0 11-2 0V7zM12 7a1 1 0 012 0v4a1 1 0 11-2 0V7z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
/* 空状态 */
|
||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<CardBody className="text-center py-16">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="w-16 h-16 bg-default-100 rounded-full flex items-center justify-center">
|
||||
<svg
|
||||
className="w-8 h-8 text-default-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M12 6v6l4 2m6-6a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
暂无限速规则
|
||||
</h3>
|
||||
<p className="text-default-500 text-sm mt-1">
|
||||
还没有创建任何限速规则,点击上方按钮开始创建
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 新增/编辑模态框 */}
|
||||
<Modal
|
||||
isOpen={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
size="2xl"
|
||||
{/* 新增/编辑模态框 */}
|
||||
<Modal
|
||||
isOpen={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
size="2xl"
|
||||
scrollBehavior="outside"
|
||||
backdrop="blur"
|
||||
placement="center"
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
<h2 className="text-xl font-bold">
|
||||
{isEdit ? '编辑限速规则' : '新增限速规则'}
|
||||
</h2>
|
||||
<p className="text-small text-default-500">
|
||||
{isEdit ? '修改现有限速规则的配置信息' : '创建新的限速规则并绑定到隧道'}
|
||||
</p>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
<h2 className="text-xl font-bold">
|
||||
{isEdit ? "编辑限速规则" : "新增限速规则"}
|
||||
</h2>
|
||||
<p className="text-small text-default-500">
|
||||
{isEdit
|
||||
? "修改现有限速规则的配置信息"
|
||||
: "创建新的限速规则并绑定到隧道"}
|
||||
</p>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
errorMessage={errors.name}
|
||||
isInvalid={!!errors.name}
|
||||
label="规则名称"
|
||||
placeholder="请输入限速规则名称"
|
||||
value={form.name}
|
||||
variant="bordered"
|
||||
onChange={(e) => setForm(prev => ({ ...prev, name: e.target.value }))}
|
||||
isInvalid={!!errors.name}
|
||||
errorMessage={errors.name}
|
||||
variant="bordered"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="速度限制"
|
||||
placeholder="请输入速度限制"
|
||||
type="number"
|
||||
value={form.speed.toString()}
|
||||
onChange={(e) => setForm(prev => ({ ...prev, speed: parseInt(e.target.value) || 0 }))}
|
||||
isInvalid={!!errors.speed}
|
||||
errorMessage={errors.speed}
|
||||
variant="bordered"
|
||||
endContent={
|
||||
/>
|
||||
|
||||
<Input
|
||||
endContent={
|
||||
<div className="pointer-events-none flex items-center">
|
||||
<span className="text-default-400 text-small">Mbps</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="绑定隧道"
|
||||
placeholder="请选择要绑定的隧道"
|
||||
selectedKeys={form.tunnelId ? [form.tunnelId.toString()] : []}
|
||||
onSelectionChange={(keys) => {
|
||||
const selectedKey = Array.from(keys)[0] as string;
|
||||
if (selectedKey) {
|
||||
const selectedTunnel = tunnels.find(tunnel => tunnel.id === parseInt(selectedKey));
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
tunnelId: parseInt(selectedKey),
|
||||
tunnelName: selectedTunnel?.name || ''
|
||||
}));
|
||||
} else {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
tunnelId: null,
|
||||
tunnelName: ''
|
||||
}));
|
||||
}
|
||||
}}
|
||||
isInvalid={!!errors.tunnelId}
|
||||
errorMessage={errors.tunnelId}
|
||||
errorMessage={errors.speed}
|
||||
isInvalid={!!errors.speed}
|
||||
label="速度限制"
|
||||
placeholder="请输入速度限制"
|
||||
type="number"
|
||||
value={form.speed.toString()}
|
||||
variant="bordered"
|
||||
isDisabled={isEdit}
|
||||
description={isEdit ? "编辑时无法修改绑定隧道" : undefined}
|
||||
>
|
||||
{tunnels.map((tunnel) => (
|
||||
<SelectItem key={tunnel.id}>
|
||||
{tunnel.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={handleSubmit}
|
||||
isLoading={submitLoading}
|
||||
>
|
||||
{isEdit ? '保存修改' : '创建规则'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
onChange={(e) => setForm(prev => ({ ...prev, speed: parseInt(e.target.value) || 0 }))}
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 删除确认模态框 */}
|
||||
<Modal
|
||||
isOpen={deleteModalOpen}
|
||||
onOpenChange={setDeleteModalOpen}
|
||||
size="2xl"
|
||||
<Select
|
||||
label="绑定隧道"
|
||||
placeholder="请选择要绑定的隧道"
|
||||
selectedKeys={
|
||||
form.tunnelId ? [form.tunnelId.toString()] : []
|
||||
}
|
||||
onSelectionChange={(keys) => {
|
||||
const selectedKey = Array.from(keys)[0] as string;
|
||||
|
||||
if (selectedKey) {
|
||||
const selectedTunnel = tunnels.find(
|
||||
(tunnel) => tunnel.id === parseInt(selectedKey),
|
||||
);
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
tunnelId: parseInt(selectedKey),
|
||||
tunnelName: selectedTunnel?.name || "",
|
||||
}));
|
||||
} else {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
tunnelId: null,
|
||||
tunnelName: "",
|
||||
}));
|
||||
}
|
||||
}}
|
||||
isInvalid={!!errors.tunnelId}
|
||||
errorMessage={errors.tunnelId}
|
||||
variant="bordered"
|
||||
isDisabled={isEdit}
|
||||
description={isEdit ? "编辑时无法修改绑定隧道" : undefined}
|
||||
>
|
||||
{tunnels.map((tunnel) => (
|
||||
<SelectItem key={tunnel.id}>{tunnel.name}</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={handleSubmit}
|
||||
isLoading={submitLoading}
|
||||
>
|
||||
{isEdit ? "保存修改" : "创建规则"}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
{/* 删除确认模态框 */}
|
||||
<Modal
|
||||
isOpen={deleteModalOpen}
|
||||
onOpenChange={setDeleteModalOpen}
|
||||
size="2xl"
|
||||
scrollBehavior="outside"
|
||||
backdrop="blur"
|
||||
placement="center"
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
<h2 className="text-lg font-bold text-danger">确认删除</h2>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className="text-default-600">
|
||||
确定要删除限速规则 <span className="font-semibold text-foreground">"{ruleToDelete?.name}"</span> 吗?
|
||||
</p>
|
||||
<p className="text-small text-default-500 mt-2">
|
||||
此操作无法撤销,删除后该规则将永久消失。
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
onPress={confirmDelete}
|
||||
isLoading={deleteLoading}
|
||||
>
|
||||
确认删除
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
<h2 className="text-lg font-bold text-danger">确认删除</h2>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className="text-default-600">
|
||||
确定要删除限速规则{" "}
|
||||
<span className="font-semibold text-foreground">
|
||||
"{ruleToDelete?.name}"
|
||||
</span>{" "}
|
||||
吗?
|
||||
</p>
|
||||
<p className="text-small text-default-500 mt-2">
|
||||
此操作无法撤销,删除后该规则将永久消失。
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
onPress={confirmDelete}
|
||||
isLoading={deleteLoading}
|
||||
>
|
||||
确认删除
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user