diff --git a/src/components/proxy/proxy-chain.tsx b/src/components/proxy/proxy-chain.tsx
index 651fcbc9..ea19e931 100644
--- a/src/components/proxy/proxy-chain.tsx
+++ b/src/components/proxy/proxy-chain.tsx
@@ -31,7 +31,7 @@ import {
Typography,
useTheme,
} from "@mui/material";
-import { useCallback, useEffect, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import {
@@ -196,9 +196,10 @@ export const ProxyChain = ({
const theme = useTheme();
const { t } = useTranslation();
const { proxies } = useAppData();
- const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
- const [isConnected, setIsConnected] = useState(false);
+ const markUnsavedChanges = useCallback(() => {
+ onMarkUnsavedChanges?.();
+ }, [onMarkUnsavedChanges]);
// 获取当前代理信息以检查连接状态
const { data: currentProxies, mutate: mutateProxies } = useSWR(
@@ -211,52 +212,26 @@ export const ProxyChain = ({
},
);
- // 检查连接状态
- useEffect(() => {
+ const isConnected = useMemo(() => {
if (!currentProxies || proxyChain.length < 2) {
- setIsConnected(false);
- return;
+ return false;
}
- // 获取用户配置的最后一个节点
const lastNode = proxyChain[proxyChain.length - 1];
- // 根据模式确定要检查的代理组和当前选中的代理
if (mode === "global") {
- // 全局模式:检查 global 对象
- if (!currentProxies.global || !currentProxies.global.now) {
- setIsConnected(false);
- return;
- }
-
- // 检查当前选中的代理是否是配置的最后一个节点
- if (currentProxies.global.now === lastNode.name) {
- setIsConnected(true);
- } else {
- setIsConnected(false);
- }
- } else {
- // 规则模式:检查指定的代理组
- if (!selectedGroup) {
- setIsConnected(false);
- return;
- }
-
- const proxyChainGroup = currentProxies.groups.find(
- (group) => group.name === selectedGroup,
- );
- if (!proxyChainGroup || !proxyChainGroup.now) {
- setIsConnected(false);
- return;
- }
-
- // 检查当前选中的代理是否是配置的最后一个节点
- if (proxyChainGroup.now === lastNode.name) {
- setIsConnected(true);
- } else {
- setIsConnected(false);
- }
+ return currentProxies.global?.now === lastNode.name;
}
+
+ if (!selectedGroup || !Array.isArray(currentProxies.groups)) {
+ return false;
+ }
+
+ const proxyChainGroup = currentProxies.groups.find(
+ (group) => group.name === selectedGroup,
+ );
+
+ return proxyChainGroup?.now === lastNode.name;
}, [currentProxies, proxyChain, mode, selectedGroup]);
// 监听链的变化,但排除从配置加载的情况
@@ -267,10 +242,10 @@ export const ProxyChain = ({
chainLengthRef.current !== proxyChain.length &&
chainLengthRef.current !== 0
) {
- setHasUnsavedChanges(true);
+ markUnsavedChanges();
}
chainLengthRef.current = proxyChain.length;
- }, [proxyChain.length]);
+ }, [proxyChain.length, markUnsavedChanges]);
const sensors = useSensors(
useSensor(PointerSensor),
@@ -288,26 +263,21 @@ export const ProxyChain = ({
const newIndex = proxyChain.findIndex((item) => item.id === over?.id);
onUpdateChain(arrayMove(proxyChain, oldIndex, newIndex));
- setHasUnsavedChanges(true);
+ markUnsavedChanges();
}
},
- [proxyChain, onUpdateChain],
+ [proxyChain, onUpdateChain, markUnsavedChanges],
);
const handleRemoveProxy = useCallback(
(id: string) => {
const newChain = proxyChain.filter((item) => item.id !== id);
onUpdateChain(newChain);
- setHasUnsavedChanges(true);
+ markUnsavedChanges();
},
- [proxyChain, onUpdateChain],
+ [proxyChain, onUpdateChain, markUnsavedChanges],
);
- const handleClearAll = useCallback(() => {
- onUpdateChain([]);
- setHasUnsavedChanges(true);
- }, [onUpdateChain]);
-
const handleConnect = useCallback(async () => {
if (isConnected) {
// 如果已连接,则断开连接
@@ -327,10 +297,6 @@ export const ProxyChain = ({
// 清空链式代理配置UI
// onUpdateChain([]);
- // setHasUnsavedChanges(false);
-
- // 强制更新连接状态
- setIsConnected(false);
} catch (error) {
console.error("Failed to disconnect from proxy chain:", error);
alert(t("Failed to disconnect from proxy chain") || "断开链式代理失败");
@@ -372,9 +338,6 @@ export const ProxyChain = ({
// 刷新代理信息以更新连接状态
mutateProxies();
-
- // 清除未保存标记
- setHasUnsavedChanges(false);
console.log("Successfully connected to proxy chain");
} catch (error) {
console.error("Failed to connect to proxy chain:", error);
@@ -411,7 +374,6 @@ export const ProxyChain = ({
delay: undefined,
})) || [];
onUpdateChain(chainItems);
- setHasUnsavedChanges(false);
} catch (parseError) {
console.error("Failed to parse YAML:", parseError);
onUpdateChain([]);
@@ -435,7 +397,6 @@ export const ProxyChain = ({
delay: undefined,
})) || [];
onUpdateChain(chainItems);
- setHasUnsavedChanges(false);
} catch (jsonError) {
console.error("Failed to parse as JSON either:", jsonError);
onUpdateChain([]);
@@ -448,7 +409,6 @@ export const ProxyChain = ({
} else if (chainConfigData === "") {
// Empty string means no proxies available, show empty state
onUpdateChain([]);
- setHasUnsavedChanges(false);
}
}, [chainConfigData, onUpdateChain]);
@@ -519,7 +479,6 @@ export const ProxyChain = ({
onClick={() => {
updateProxyChainConfigInRuntime(null);
onUpdateChain([]);
- setHasUnsavedChanges(false);
}}
sx={{
color: theme.palette.error.main,
diff --git a/src/components/proxy/proxy-groups.tsx b/src/components/proxy/proxy-groups.tsx
index b548681d..25861183 100644
--- a/src/components/proxy/proxy-groups.tsx
+++ b/src/components/proxy/proxy-groups.tsx
@@ -46,6 +46,8 @@ interface ProxyChainItem {
delay?: number;
}
+const VirtuosoFooter = () =>
;
+
export const ProxyGroups = (props: Props) => {
const { t } = useTranslation();
const { mode, isChainMode = false, chainConfigData } = props;
@@ -61,23 +63,25 @@ export const ProxyGroups = (props: Props) => {
const { verge } = useVerge();
const { proxies: proxiesData } = useAppData();
+ const groups = proxiesData?.groups;
+ const availableGroups = useMemo(() => groups ?? [], [groups]);
- // 当链式代理模式且规则模式下,如果没有选择代理组,默认选择第一个
- useEffect(() => {
- if (
- isChainMode &&
- mode === "rule" &&
- !selectedGroup &&
- proxiesData?.groups?.length > 0
- ) {
- setSelectedGroup(proxiesData.groups[0].name);
+ const defaultRuleGroup = useMemo(() => {
+ if (isChainMode && mode === "rule" && availableGroups.length > 0) {
+ return availableGroups[0].name;
}
- }, [isChainMode, mode, selectedGroup, proxiesData]);
+ return null;
+ }, [availableGroups, isChainMode, mode]);
+
+ const activeSelectedGroup = useMemo(
+ () => selectedGroup ?? defaultRuleGroup,
+ [selectedGroup, defaultRuleGroup],
+ );
const { renderList, onProxies, onHeadState } = useRenderList(
mode,
isChainMode,
- selectedGroup,
+ activeSelectedGroup,
);
const getGroupHeadState = useCallback(
@@ -112,6 +116,8 @@ export const ProxyGroups = (props: Props) => {
useEffect(() => {
if (renderList.length === 0) return;
+ let restoreTimer: ReturnType | null = null;
+
try {
const savedPositions = localStorage.getItem("proxy-scroll-positions");
if (savedPositions) {
@@ -120,7 +126,7 @@ export const ProxyGroups = (props: Props) => {
const savedPosition = positions[mode];
if (savedPosition !== undefined) {
- setTimeout(() => {
+ restoreTimer = setTimeout(() => {
virtuosoRef.current?.scrollTo({
top: savedPosition,
behavior: "auto",
@@ -131,6 +137,12 @@ export const ProxyGroups = (props: Props) => {
} catch (e) {
console.error("Error restoring scroll position:", e);
}
+
+ return () => {
+ if (restoreTimer) {
+ clearTimeout(restoreTimer);
+ }
+ };
}, [mode, renderList.length]);
// 改为使用节流函数保存滚动位置
@@ -150,25 +162,30 @@ export const ProxyGroups = (props: Props) => {
);
// 使用改进的滚动处理
- const handleScroll = useCallback(
- throttle((e: any) => {
- const scrollTop = e.target.scrollTop;
- setShowScrollTop(scrollTop > 100);
- // 使用稳定的节流来保存位置,而不是setTimeout
- saveScrollPosition(scrollTop);
- }, 500), // 增加到500ms以确保平滑滚动
+ const handleScroll = useMemo(
+ () =>
+ throttle((event: Event) => {
+ const target = event.target as HTMLElement | null;
+ const scrollTop = target?.scrollTop ?? 0;
+ setShowScrollTop(scrollTop > 100);
+ // 使用稳定的节流来保存位置,而不是setTimeout
+ saveScrollPosition(scrollTop);
+ }, 500), // 增加到500ms以确保平滑滚动
[saveScrollPosition],
);
// 添加和清理滚动事件监听器
useEffect(() => {
- if (!scrollerRef.current) return;
- scrollerRef.current.addEventListener("scroll", handleScroll, {
- passive: true,
- });
+ const node = scrollerRef.current;
+ if (!node) return;
+
+ const listener = handleScroll as EventListener;
+ const options: AddEventListenerOptions = { passive: true };
+
+ node.addEventListener("scroll", listener, options);
return () => {
- scrollerRef.current?.removeEventListener("scroll", handleScroll);
+ node.removeEventListener("scroll", listener, options);
};
}, [handleScroll]);
@@ -186,18 +203,14 @@ export const ProxyGroups = (props: Props) => {
setDuplicateWarning({ open: false, message: "" });
}, []);
- // 获取当前选中的代理组信息
- const getCurrentGroup = useCallback(() => {
- if (!selectedGroup || !proxiesData?.groups) return null;
- return proxiesData.groups.find(
- (group: any) => group.name === selectedGroup,
+ const currentGroup = useMemo(() => {
+ if (!activeSelectedGroup) return null;
+ return (
+ availableGroups.find(
+ (group: any) => group.name === activeSelectedGroup,
+ ) ?? null
);
- }, [selectedGroup, proxiesData]);
-
- // 获取可用的代理组列表
- const getAvailableGroups = useCallback(() => {
- return proxiesData?.groups || [];
- }, [proxiesData]);
+ }, [activeSelectedGroup, availableGroups]);
// 处理代理组选择菜单
const handleGroupMenuOpen = (event: React.MouseEvent) => {
@@ -220,9 +233,6 @@ export const ProxyGroups = (props: Props) => {
}
};
- const currentGroup = getCurrentGroup();
- const availableGroups = getAvailableGroups();
-
const handleChangeProxy = useCallback(
(group: IProxyGroupItem, proxy: IProxyItem) => {
if (isChainMode) {
@@ -472,7 +482,7 @@ export const ProxyGroups = (props: Props) => {
scrollerRef.current = ref as Element;
}}
components={{
- Footer: () => ,
+ Footer: VirtuosoFooter,
}}
initialScrollTop={scrollPositionRef.current[mode]}
computeItemKey={(index) => renderList[index].key}
@@ -498,7 +508,7 @@ export const ProxyGroups = (props: Props) => {
onUpdateChain={setProxyChain}
chainConfigData={chainConfigData}
mode={mode}
- selectedGroup={selectedGroup}
+ selectedGroup={activeSelectedGroup}
/>
@@ -530,11 +540,11 @@ export const ProxyGroups = (props: Props) => {
},
}}
>
- {availableGroups.map((group: any, _index: number) => (
+ {availableGroups.map((group: any) => (