fix: 保留用户/规则高级设置并增强节点运行时恢复 (#468)

* fix: 用户编辑时 maxConn 字段未正确回填

- User 接口添加 maxConn 类型定义
- handleEdit 中回填 maxConn 值
- normalizeUserItem 添加 maxConn 字段处理

解决编辑用户时最大连接数显示为 0 的问题

* fix: 保留转发设置并增强节点运行时恢复
This commit is contained in:
sagit
2026-04-25 18:12:13 +08:00
committed by GitHub
parent 9b923a2d0b
commit 3f374df724
30 changed files with 744 additions and 123 deletions
+18 -27
View File
@@ -5,7 +5,7 @@ import {
useNavigate,
Navigate,
} from "react-router-dom";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { AnimatePresence } from "framer-motion";
import IndexPage from "@/pages/index";
@@ -39,32 +39,7 @@ const ProtectedRoute = ({
skipLayout?: boolean;
}) => {
const isH5 = useH5Mode();
const navigate = useNavigate();
const [authenticated, setAuthenticated] = useState(() => isLoggedIn());
useEffect(() => {
const handleSessionChange = () => {
const loggedIn = isLoggedIn();
setAuthenticated(loggedIn);
if (!loggedIn) {
navigate("/", { replace: true });
}
};
window.addEventListener(SESSION_UPDATED_EVENT, handleSessionChange);
return () => {
window.removeEventListener(SESSION_UPDATED_EVENT, handleSessionChange);
};
}, [navigate]);
useEffect(() => {
if (!authenticated) {
navigate("/", { replace: true });
}
}, [authenticated, navigate]);
const authenticated = isLoggedIn();
if (!authenticated) {
return <Navigate replace to="/" />;
@@ -103,6 +78,22 @@ const LoginRoute = () => {
function App() {
const location = useLocation();
const navigate = useNavigate();
// 全局登录状态监听,当检测到未登录且不在首页时,跳转到首页
useEffect(() => {
const handleSessionUpdate = () => {
if (!isLoggedIn() && location.pathname !== "/") {
navigate("/", { replace: true });
}
};
window.addEventListener(SESSION_UPDATED_EVENT, handleSessionUpdate);
return () => {
window.removeEventListener(SESSION_UPDATED_EVENT, handleSessionUpdate);
};
}, [location.pathname, navigate]);
// 处理自定义背景图片
useEffect(() => {
+5 -1
View File
@@ -71,6 +71,8 @@ export interface ForwardApiItem {
userId?: number;
tunnelId?: number;
speedId?: number | null;
maxConn?: number;
proxyProtocol?: number;
inx?: number;
[key: string]: unknown;
}
@@ -201,7 +203,7 @@ export interface UserPackageInfoApiData {
num: number;
expTime?: string;
flowResetTime?: number;
maxConn?: number;
maxConn?: number;
[key: string]: unknown;
};
tunnelPermissions: UserTunnelPermissionApiItem[];
@@ -370,6 +372,8 @@ export interface ForwardMutationPayload {
remoteAddr?: string;
strategy?: string;
speedId?: number | null;
maxConn?: number;
proxyProtocol?: number;
}
export interface SpeedLimitMutationPayload {
-1
View File
@@ -241,7 +241,6 @@ export default function AdminLayout({
// 退出登录
const handleLogout = () => {
safeLogout();
navigate("/");
};
// 切换移动端菜单
@@ -1,5 +1,4 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import toast from "react-hot-toast";
import { Button } from "@/shadcn-bridge/heroui/button";
@@ -26,7 +25,6 @@ export default function ChangePasswordPage() {
});
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState<Partial<PasswordForm>>({});
const navigate = useNavigate();
const validateForm = (): boolean => {
const newErrors: Partial<PasswordForm> = {};
@@ -98,7 +96,6 @@ export default function ChangePasswordPage() {
const logout = () => {
safeLogout();
navigate("/");
};
const handleKeyPress = (e: React.KeyboardEvent) => {
-1
View File
@@ -50,7 +50,6 @@ export default function DashboardPage() {
const handleLogout = () => {
safeLogout();
toast.success("已退出登录");
navigate("/");
};
const {
+53 -13
View File
@@ -125,6 +125,7 @@ interface Forward {
userId?: number;
inx?: number;
speedId?: number | null;
proxyProtocol?: number;
}
interface Tunnel {
@@ -160,6 +161,7 @@ interface ForwardForm {
strategy: string;
speedId: number | null;
maxConn?: number;
proxyProtocol?: number;
}
interface ForwardUserGroup {
@@ -576,6 +578,11 @@ const mapForwardApiItems = (items: ForwardApiItem[]): Forward[] => {
typeof forward.speedId === "number" || forward.speedId === null
? forward.speedId
: undefined,
maxConn: typeof forward.maxConn === "number" ? forward.maxConn : undefined,
proxyProtocol:
typeof forward.proxyProtocol === "number"
? forward.proxyProtocol
: undefined,
serviceRunning: forward.status === 1,
}));
};
@@ -1310,6 +1317,7 @@ export default function ForwardPage() {
strategy: "fifo",
speedId: null,
maxConn: 0,
proxyProtocol: 0,
});
const [inIpTouched, setInIpTouched] = useState(false);
@@ -2097,6 +2105,7 @@ export default function ForwardPage() {
interfaceName: "",
strategy: "fifo",
speedId: null,
proxyProtocol: 0,
});
setErrors({});
setModalOpen(true);
@@ -2117,7 +2126,8 @@ export default function ForwardPage() {
interfaceName: forward.interfaceName || "",
strategy: forward.strategy || "fifo",
speedId: normalizeSpeedId(forward.speedId),
maxConn: forward.maxConn || 0,
maxConn: forward.maxConn ?? 0,
proxyProtocol: forward.proxyProtocol ?? 0,
});
setErrors({});
setModalOpen(true);
@@ -2247,6 +2257,7 @@ export default function ForwardPage() {
strategy: addressCount > 1 ? form.strategy : "fifo",
speedId: normalizedSpeedId,
maxConn: form.maxConn,
proxyProtocol: form.proxyProtocol,
};
res = await updateForward(updateData);
@@ -2260,11 +2271,11 @@ export default function ForwardPage() {
strategy: addressCount > 1 ? form.strategy : "fifo",
speedId: normalizedSpeedId,
maxConn: form.maxConn,
proxyProtocol: form.proxyProtocol,
};
res = await createForward(createData);
}
if (res.code === 0) {
const warningItems = Array.isArray((res as any).data?.warnings)
? (res as any).data.warnings
@@ -4742,8 +4753,6 @@ export default function ForwardPage() {
}
/>
<Select
description={
isEdit
@@ -4871,26 +4880,55 @@ export default function ForwardPage() {
<SelectItem key="hash">哈希模式 - IP哈希</SelectItem>
</Select>
)}
<Accordion className="px-0" variant="light">
<Accordion className="px-0" variant="light">
<AccordionItem
key="advanced"
aria-label="高级设置"
title={<span className="text-small text-default-500 font-medium">高级设置</span>}
title={
<span className="text-small text-default-500 font-medium">
高级设置
</span>
}
>
<div className="space-y-4 pb-2">
<Input
description="此设置优先于用户的全局连接数限制。0 表示不限制。"
label="最大连接数"
min="0"
placeholder="0 或空表示不限制"
type="number"
min="0"
value={form.maxConn === 0 ? "" : String(form.maxConn || "")}
value={
form.maxConn === 0 ? "" : String(form.maxConn || "")
}
variant="bordered"
onChange={(e) => {
const value = Math.max(Number(e.target.value) || 0, 0);
const value = Math.max(
Number(e.target.value) || 0,
0,
);
setForm((prev) => ({ ...prev, maxConn: value }));
}}
description="此设置优先于用户的全局连接数限制。0 表示不限制。"
variant="bordered"
/>
<Select
description="启用 PROXY protocol,用于透传客户端真实 IP"
label="Proxy Protocol"
placeholder="禁用"
selectedKeys={[String(form.proxyProtocol || 0)]}
variant="bordered"
onSelectionChange={(keys) => {
const selectedKey = Array.from(keys)[0] as string;
setForm((prev) => ({
...prev,
proxyProtocol: Number(selectedKey),
}));
}}
>
<SelectItem key="0">禁用</SelectItem>
<SelectItem key="1">Version 1</SelectItem>
<SelectItem key="2">Version 2</SelectItem>
</Select>
{isAdmin && (
<Select
label="规则限速"
@@ -4908,7 +4946,9 @@ export default function ForwardPage() {
setForm((prev) => ({
...prev,
speedId: selectedKey ? Number(selectedKey) : null,
speedId: selectedKey
? Number(selectedKey)
: null,
}));
}}
>
@@ -4925,7 +4965,7 @@ export default function ForwardPage() {
</div>
</AccordionItem>
</Accordion>
</div>
</div>
</ModalBody>
<ModalFooter>
<Button variant="light" onPress={onClose}>
-1
View File
@@ -127,7 +127,6 @@ export default function ProfilePage() {
// 退出登录
const handleLogout = () => {
safeLogout();
navigate("/", { replace: true });
};
// 密码表单验证
+8 -3
View File
@@ -160,6 +160,7 @@ const normalizeUserItem = (item: Partial<User>): User => {
monthlyUsedBytes: Number(item.monthlyUsedBytes ?? 0),
disabledByQuota: Number(item.disabledByQuota ?? 0),
quotaDisabledAt: Number(item.quotaDisabledAt ?? 0),
maxConn: item.maxConn != null ? Number(item.maxConn) : undefined,
};
};
@@ -515,7 +516,7 @@ export default function UserPage() {
num: 10,
expTime: null,
flowResetTime: 0,
maxConn: 0,
maxConn: 0,
groupIds: [],
});
onUserModalOpen();
@@ -545,6 +546,7 @@ export default function UserPage() {
num: user.num,
expTime: user.expTime ? new Date(user.expTime) : null,
flowResetTime: user.flowResetTime ?? 0,
maxConn: user.maxConn ?? 0,
groupIds: currentGroupIds,
});
onUserModalOpen();
@@ -1520,12 +1522,15 @@ export default function UserPage() {
/>
<Input
label="最大连接数"
min="0"
placeholder="0 或空表示不限制"
type="number"
min="0"
value={userForm.maxConn === 0 ? "" : String(userForm.maxConn || "")}
value={
userForm.maxConn === 0 ? "" : String(userForm.maxConn || "")
}
onChange={(e) => {
const value = Math.max(Number(e.target.value) || 0, 0);
setUserForm((prev) => ({ ...prev, maxConn: value }));
}}
/>
+1
View File
@@ -24,6 +24,7 @@ export interface User {
monthlyUsedBytes?: number;
disabledByQuota?: number;
quotaDisabledAt?: number;
maxConn?: number;
}
export interface UserGroup {
+1 -2
View File
@@ -2,9 +2,8 @@ import { clearSession } from "@/utils/session";
/**
* 安全退出登录函数
* 清除登录相关数据,并强制刷新跳转到首页
* 清除登录相关数据
*/
export const safeLogout = () => {
clearSession();
window.location.href = "/";
};