fix: homepage proxy card handle direct mode

This commit is contained in:
Tunglies 2025-03-20 21:51:12 +08:00
parent 108840c4be
commit a06597a3a6

View File

@ -53,11 +53,16 @@ function convertDelayColor(delayValue: number) {
const mainColor = colorStr.split(".")[0]; const mainColor = colorStr.split(".")[0];
switch (mainColor) { switch (mainColor) {
case "success": return "success"; case "success":
case "warning": return "warning"; return "success";
case "error": return "error"; case "warning":
case "primary": return "primary"; return "warning";
default: return "default"; case "error":
return "error";
case "primary":
return "primary";
default:
return "default";
} }
} }
@ -79,7 +84,7 @@ function getSignalIcon(delay: number) {
// 简单的防抖函数 // 简单的防抖函数
function debounce(fn: Function, ms = 100) { function debounce(fn: Function, ms = 100) {
let timeoutId: ReturnType<typeof setTimeout>; let timeoutId: ReturnType<typeof setTimeout>;
return function(this: any, ...args: any[]) { return function (this: any, ...args: any[]) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), ms); timeoutId = setTimeout(() => fn.apply(this, args), ms);
}; };
@ -87,7 +92,8 @@ function debounce(fn: Function, ms = 100) {
export const CurrentProxyCard = () => { export const CurrentProxyCard = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentProxy, primaryGroupName, mode, refreshProxy } = useCurrentProxy(); const { currentProxy, primaryGroupName, mode, refreshProxy } =
useCurrentProxy();
const navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme(); const theme = useTheme();
const { verge } = useVerge(); const { verge } = useVerge();
@ -135,133 +141,141 @@ export const CurrentProxyCard = () => {
useEffect(() => { useEffect(() => {
// 根据模式确定初始组 // 根据模式确定初始组
if (isGlobalMode) { if (isGlobalMode) {
setState(prev => ({ setState((prev) => ({
...prev, ...prev,
selection: { selection: {
...prev.selection, ...prev.selection,
group: "GLOBAL" group: "GLOBAL",
} },
})); }));
} else if (isDirectMode) { } else if (isDirectMode) {
setState(prev => ({ setState((prev) => ({
...prev, ...prev,
selection: { selection: {
...prev.selection, ...prev.selection,
group: "DIRECT" group: "DIRECT",
} },
})); }));
} else { } else {
const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP); const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP);
setState(prev => ({ setState((prev) => ({
...prev, ...prev,
selection: { selection: {
...prev.selection, ...prev.selection,
group: savedGroup || primaryGroupName || "" group: savedGroup || primaryGroupName || "",
} },
})); }));
} }
}, [isGlobalMode, isDirectMode, primaryGroupName]); }, [isGlobalMode, isDirectMode, primaryGroupName]);
// 带锁的代理数据获取函数,防止并发请求 // 带锁的代理数据获取函数,防止并发请求
const fetchProxyData = useCallback(async (force = false) => { const fetchProxyData = useCallback(
// 防止重复请求 async (force = false) => {
if (isRefreshingRef.current) { // 防止重复请求
pendingRefreshRef.current = true; if (isRefreshingRef.current) {
return; pendingRefreshRef.current = true;
} return;
// 检查刷新间隔
const now = Date.now();
if (!force && now - lastRefreshRef.current < 1000) {
return;
}
isRefreshingRef.current = true;
lastRefreshRef.current = now;
try {
const data = await getProxies();
// 过滤和格式化组
const filteredGroups = data.groups
.filter(g => g.name !== "DIRECT" && g.name !== "REJECT")
.map(g => ({
name: g.name,
now: g.now || "",
all: g.all.map(p => p.name),
}));
// 使用函数式更新确保状态更新的原子性
setState(prev => {
let newProxy = "";
let newDisplayProxy = null;
let newGroup = prev.selection.group;
// 根据模式确定新代理
if (isDirectMode) {
newGroup = "DIRECT";
newProxy = "DIRECT";
newDisplayProxy = data.records?.DIRECT || null;
} else if (isGlobalMode && data.global) {
newGroup = "GLOBAL";
newProxy = data.global.now || "";
newDisplayProxy = data.records?.[newProxy] || null;
} else {
// 普通模式 - 检查当前选择的组是否存在
const currentGroup = filteredGroups.find(g => g.name === prev.selection.group);
// 如果当前组不存在或为空,自动选择第一个组
if (!currentGroup && filteredGroups.length > 0) {
newGroup = filteredGroups[0].name;
const firstGroup = filteredGroups[0];
newProxy = firstGroup.now;
newDisplayProxy = data.records?.[newProxy] || null;
// 保存到本地存储
if (!isGlobalMode && !isDirectMode) {
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
if (newProxy) {
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
}
}
} else if (currentGroup) {
// 使用当前组的代理
newProxy = currentGroup.now;
newDisplayProxy = data.records?.[newProxy] || null;
}
}
// 返回新状态
return {
proxyData: {
groups: filteredGroups,
records: data.records || {},
globalProxy: data.global?.now || "",
directProxy: data.records?.DIRECT || null,
},
selection: {
group: newGroup,
proxy: newProxy
},
displayProxy: newDisplayProxy
};
});
} catch (error) {
console.error("获取代理信息失败", error);
} finally {
isRefreshingRef.current = false;
// 处理待处理的刷新请求
if (pendingRefreshRef.current) {
pendingRefreshRef.current = false;
setTimeout(() => fetchProxyData(), 100);
} }
}
}, [isGlobalMode, isDirectMode]); // 检查刷新间隔
const now = Date.now();
if (!force && now - lastRefreshRef.current < 1000) {
return;
}
isRefreshingRef.current = true;
lastRefreshRef.current = now;
try {
const data = await getProxies();
// 过滤和格式化组
const filteredGroups = data.groups
.filter((g) => g.name !== "DIRECT" && g.name !== "REJECT")
.map((g) => ({
name: g.name,
now: g.now || "",
all: g.all.map((p) => p.name),
}));
// 使用函数式更新确保状态更新的原子性
setState((prev) => {
let newProxy = "";
let newDisplayProxy = null;
let newGroup = prev.selection.group;
// 根据模式确定新代理
if (isDirectMode) {
newGroup = "DIRECT";
newProxy = "DIRECT";
newDisplayProxy = data.records?.DIRECT || null;
} else if (isGlobalMode && data.global) {
newGroup = "GLOBAL";
newProxy = data.global.now || "";
newDisplayProxy = data.records?.[newProxy] || null;
} else {
// 普通模式 - 检查当前选择的组是否存在
const currentGroup = filteredGroups.find(
(g) => g.name === prev.selection.group,
);
// 如果当前组不存在或为空,自动选择第一个组
if (!currentGroup && filteredGroups.length > 0) {
newGroup = filteredGroups[0].name;
const firstGroup = filteredGroups[0];
newProxy = firstGroup.now;
newDisplayProxy = data.records?.[newProxy] || null;
// 保存到本地存储
if (!isGlobalMode && !isDirectMode) {
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
if (newProxy) {
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
}
}
} else if (currentGroup) {
// 使用当前组的代理
newProxy = currentGroup.now;
newDisplayProxy = data.records?.[newProxy] || null;
}
}
// 返回新状态
return {
proxyData: {
groups: filteredGroups,
records: data.records || {},
globalProxy: data.global?.now || "",
directProxy: data.records?.DIRECT || null,
},
selection: {
group: newGroup,
proxy: newProxy,
},
displayProxy: newDisplayProxy,
};
});
} catch (error) {
console.error("获取代理信息失败", error);
} finally {
isRefreshingRef.current = false;
// 处理待处理的刷新请求
if (pendingRefreshRef.current) {
pendingRefreshRef.current = false;
setTimeout(() => fetchProxyData(), 100);
}
}
},
[isGlobalMode, isDirectMode],
);
// 响应 currentProxy 变化 // 响应 currentProxy 变化
useEffect(() => { useEffect(() => {
if (currentProxy && (!state.displayProxy || currentProxy.name !== state.displayProxy.name)) { if (
currentProxy &&
(!state.displayProxy || currentProxy.name !== state.displayProxy.name)
) {
fetchProxyData(true); fetchProxyData(true);
} }
}, [currentProxy, fetchProxyData, state.displayProxy]); }, [currentProxy, fetchProxyData, state.displayProxy]);
@ -269,11 +283,11 @@ export const CurrentProxyCard = () => {
// 平滑的定期刷新,使用固定间隔 // 平滑的定期刷新,使用固定间隔
useEffect(() => { useEffect(() => {
fetchProxyData(); fetchProxyData();
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
fetchProxyData(); fetchProxyData();
}, 3000); // 使用固定的3秒间隔平衡响应速度和性能 }, 3000); // 使用固定的3秒间隔平衡响应速度和性能
return () => clearInterval(intervalId); return () => clearInterval(intervalId);
}, [fetchProxyData]); }, [fetchProxyData]);
@ -285,14 +299,16 @@ export const CurrentProxyCard = () => {
if (isGlobalMode && state.proxyData.records) { if (isGlobalMode && state.proxyData.records) {
// 全局模式下的选项 // 全局模式下的选项
return Object.keys(state.proxyData.records) return Object.keys(state.proxyData.records)
.filter(name => name !== "DIRECT" && name !== "REJECT") .filter((name) => name !== "DIRECT" && name !== "REJECT")
.map(name => ({ name })); .map((name) => ({ name }));
} }
// 普通模式 // 普通模式
const group = state.proxyData.groups.find(g => g.name === state.selection.group); const group = state.proxyData.groups.find(
(g) => g.name === state.selection.group,
);
if (group) { if (group) {
return group.all.map(name => ({ name })); return group.all.map((name) => ({ name }));
} }
return []; return [];
}, [isDirectMode, isGlobalMode, state.proxyData, state.selection.group]); }, [isDirectMode, isGlobalMode, state.proxyData, state.selection.group]);
@ -302,88 +318,103 @@ export const CurrentProxyCard = () => {
debounce((updateFn: (prev: ProxyState) => ProxyState) => { debounce((updateFn: (prev: ProxyState) => ProxyState) => {
setState(updateFn); setState(updateFn);
}, 50), }, 50),
[] [],
); );
// 处理代理组变更 // 处理代理组变更
const handleGroupChange = useCallback((event: SelectChangeEvent) => { const handleGroupChange = useCallback(
if (isGlobalMode || isDirectMode) return; (event: SelectChangeEvent) => {
if (isGlobalMode || isDirectMode) return;
const newGroup = event.target.value;
const newGroup = event.target.value;
// 保存到本地存储
localStorage.setItem(STORAGE_KEY_GROUP, newGroup); // 保存到本地存储
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
// 获取该组当前选中的代理
setState(prev => { // 获取该组当前选中的代理
const group = prev.proxyData.groups.find(g => g.name === newGroup); setState((prev) => {
if (group) { const group = prev.proxyData.groups.find((g) => g.name === newGroup);
if (group) {
return {
...prev,
selection: {
group: newGroup,
proxy: group.now,
},
displayProxy: prev.proxyData.records[group.now] || null,
};
}
return { return {
...prev, ...prev,
selection: { selection: {
...prev.selection,
group: newGroup, group: newGroup,
proxy: group.now
}, },
displayProxy: prev.proxyData.records[group.now] || null
}; };
} });
return { },
[isGlobalMode, isDirectMode],
);
// 处理代理节点变更
const handleProxyChange = useCallback(
async (event: SelectChangeEvent) => {
if (isDirectMode) return;
const newProxy = event.target.value;
const currentGroup = state.selection.group;
const previousProxy = state.selection.proxy;
// 立即更新UI优化体验
debouncedSetState((prev: ProxyState) => ({
...prev, ...prev,
selection: { selection: {
...prev.selection, ...prev.selection,
group: newGroup proxy: newProxy,
} },
}; displayProxy: prev.proxyData.records[newProxy] || null,
}); }));
}, [isGlobalMode, isDirectMode]);
// 处理代理节点变更 // 非特殊模式下保存到本地存储
const handleProxyChange = useCallback(async (event: SelectChangeEvent) => { if (!isGlobalMode && !isDirectMode) {
if (isDirectMode) return; localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
const newProxy = event.target.value;
const currentGroup = state.selection.group;
const previousProxy = state.selection.proxy;
// 立即更新UI优化体验
debouncedSetState((prev: ProxyState) => ({
...prev,
selection: {
...prev.selection,
proxy: newProxy
},
displayProxy: prev.proxyData.records[newProxy] || null
}));
// 非特殊模式下保存到本地存储
if (!isGlobalMode && !isDirectMode) {
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
}
try {
// 更新代理设置
await updateProxy(currentGroup, newProxy);
// 自动关闭连接设置
if (verge?.auto_close_connection && previousProxy) {
getConnections().then(({ connections }) => {
connections.forEach(conn => {
if (conn.chains.includes(previousProxy)) {
deleteConnection(conn.id);
}
});
});
} }
// 刷新代理信息,使用较短的延迟 try {
setTimeout(() => { // 更新代理设置
refreshProxy(); await updateProxy(currentGroup, newProxy);
fetchProxyData(true);
}, 200); // 自动关闭连接设置
} catch (error) { if (verge?.auto_close_connection && previousProxy) {
console.error("更新代理失败", error); getConnections().then(({ connections }) => {
} connections.forEach((conn) => {
}, [isDirectMode, isGlobalMode, state.proxyData.records, state.selection, verge?.auto_close_connection, refreshProxy, fetchProxyData, debouncedSetState]); if (conn.chains.includes(previousProxy)) {
deleteConnection(conn.id);
}
});
});
}
// 刷新代理信息,使用较短的延迟
setTimeout(() => {
refreshProxy();
fetchProxyData(true);
}, 200);
} catch (error) {
console.error("更新代理失败", error);
}
},
[
isDirectMode,
isGlobalMode,
state.proxyData.records,
state.selection,
verge?.auto_close_connection,
refreshProxy,
fetchProxyData,
debouncedSetState,
],
);
// 导航到代理页面 // 导航到代理页面
const goToProxies = useCallback(() => { const goToProxies = useCallback(() => {
@ -392,35 +423,38 @@ export const CurrentProxyCard = () => {
// 获取要显示的代理节点 // 获取要显示的代理节点
const proxyToDisplay = state.displayProxy || currentProxy; const proxyToDisplay = state.displayProxy || currentProxy;
// 获取当前节点的延迟 // 获取当前节点的延迟
const currentDelay = proxyToDisplay const currentDelay = proxyToDisplay
? delayManager.getDelayFix(proxyToDisplay, state.selection.group) ? delayManager.getDelayFix(proxyToDisplay, state.selection.group)
: -1; : -1;
// 获取信号图标 // 获取信号图标
const signalInfo = getSignalIcon(currentDelay); const signalInfo = getSignalIcon(currentDelay);
// 自定义渲染选择框中的值 // 自定义渲染选择框中的值
const renderProxyValue = useCallback((selected: string) => { const renderProxyValue = useCallback(
if (!selected || !state.proxyData.records[selected]) return selected; (selected: string) => {
if (!selected || !state.proxyData.records[selected]) return selected;
const delayValue = delayManager.getDelayFix( const delayValue = delayManager.getDelayFix(
state.proxyData.records[selected], state.proxyData.records[selected],
state.selection.group state.selection.group,
); );
return ( return (
<Box sx={{ display: "flex", justifyContent: "space-between" }}> <Box sx={{ display: "flex", justifyContent: "space-between" }}>
<Typography noWrap>{selected}</Typography> <Typography noWrap>{selected}</Typography>
<Chip <Chip
size="small" size="small"
label={delayManager.formatDelay(delayValue)} label={delayManager.formatDelay(delayValue)}
color={convertDelayColor(delayValue)} color={convertDelayColor(delayValue)}
/> />
</Box> </Box>
); );
}, [state.proxyData.records, state.selection.group]); },
[state.proxyData.records, state.selection.group],
);
return ( return (
<EnhancedCard <EnhancedCard
@ -471,22 +505,48 @@ export const CurrentProxyCard = () => {
{proxyToDisplay.name} {proxyToDisplay.name}
</Typography> </Typography>
<Box sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}> <Box
<Typography variant="caption" color="text.secondary" sx={{ mr: 1 }}> sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}
>
<Typography
variant="caption"
color="text.secondary"
sx={{ mr: 1 }}
>
{proxyToDisplay.type} {proxyToDisplay.type}
</Typography> </Typography>
{isGlobalMode && ( {isGlobalMode && (
<Chip size="small" label={t("Global Mode")} color="primary" sx={{ mr: 0.5 }} /> <Chip
size="small"
label={t("Global Mode")}
color="primary"
sx={{ mr: 0.5 }}
/>
)} )}
{isDirectMode && ( {isDirectMode && (
<Chip size="small" label={t("Direct Mode")} color="success" sx={{ mr: 0.5 }} /> <Chip
size="small"
label={t("Direct Mode")}
color="success"
sx={{ mr: 0.5 }}
/>
)} )}
{/* 节点特性 */} {/* 节点特性 */}
{proxyToDisplay.udp && <Chip size="small" label="UDP" variant="outlined" />} {proxyToDisplay.udp && (
{proxyToDisplay.tfo && <Chip size="small" label="TFO" variant="outlined" />} <Chip size="small" label="UDP" variant="outlined" />
{proxyToDisplay.xudp && <Chip size="small" label="XUDP" variant="outlined" />} )}
{proxyToDisplay.mptcp && <Chip size="small" label="MPTCP" variant="outlined" />} {proxyToDisplay.tfo && (
{proxyToDisplay.smux && <Chip size="small" label="SMUX" variant="outlined" />} <Chip size="small" label="TFO" variant="outlined" />
)}
{proxyToDisplay.xudp && (
<Chip size="small" label="XUDP" variant="outlined" />
)}
{proxyToDisplay.mptcp && (
<Chip size="small" label="MPTCP" variant="outlined" />
)}
{proxyToDisplay.smux && (
<Chip size="small" label="SMUX" variant="outlined" />
)}
</Box> </Box>
</Box> </Box>
@ -500,7 +560,12 @@ export const CurrentProxyCard = () => {
)} )}
</Box> </Box>
{/* 代理组选择器 */} {/* 代理组选择器 */}
<FormControl fullWidth variant="outlined" size="small" sx={{ mb: 1.5 }}> <FormControl
fullWidth
variant="outlined"
size="small"
sx={{ mb: 1.5 }}
>
<InputLabel id="proxy-group-select-label">{t("Group")}</InputLabel> <InputLabel id="proxy-group-select-label">{t("Group")}</InputLabel>
<Select <Select
labelId="proxy-group-select-label" labelId="proxy-group-select-label"
@ -535,39 +600,41 @@ export const CurrentProxyCard = () => {
}, },
}} }}
> >
{proxyOptions.map((proxy) => { {isDirectMode
const delayValue = delayManager.getDelayFix( ? null
state.proxyData.records[proxy.name], : proxyOptions.map((proxy) => {
state.selection.group const delayValue = delayManager.getDelayFix(
); state.proxyData.records[proxy.name],
return ( state.selection.group,
<MenuItem );
key={proxy.name} return (
value={proxy.name} <MenuItem
sx={{ key={proxy.name}
display: "flex", value={proxy.name}
justifyContent: "space-between", sx={{
alignItems: "center", display: "flex",
width: "100%", justifyContent: "space-between",
pr: 1, alignItems: "center",
}} width: "100%",
> pr: 1,
<Typography noWrap sx={{ flex: 1, mr: 1 }}> }}
{proxy.name} >
</Typography> <Typography noWrap sx={{ flex: 1, mr: 1 }}>
<Chip {proxy.name}
size="small" </Typography>
label={delayManager.formatDelay(delayValue)} <Chip
color={convertDelayColor(delayValue)} size="small"
sx={{ label={delayManager.formatDelay(delayValue)}
minWidth: "60px", color={convertDelayColor(delayValue)}
height: "22px", sx={{
flexShrink: 0, minWidth: "60px",
}} height: "22px",
/> flexShrink: 0,
</MenuItem> }}
); />
})} </MenuItem>
);
})}
</Select> </Select>
</FormControl> </FormControl>
</Box> </Box>