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:
root
2026-02-04 02:22:28 +00:00
parent 72c2e28667
commit 6f6fececa8
31 changed files with 8080 additions and 5740 deletions
+320 -275
View File
@@ -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>
);
}
}