mirror of
https://github.com/clash-verge-rev/clash-verge-rev
synced 2025-05-05 06:53:44 +08:00
perf: optimize all home page components
This commit is contained in:
parent
6239f81f36
commit
105de99d06
@ -7,47 +7,94 @@ import { EnhancedCard } from "./enhanced-card";
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { getRules } from "@/services/api";
|
import { getRules } from "@/services/api";
|
||||||
import { getAppUptime } from "@/services/cmds";
|
import { getAppUptime } from "@/services/cmds";
|
||||||
import { useState } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
// 将毫秒转换为时:分:秒格式的函数
|
||||||
|
const formatUptime = (uptimeMs: number) => {
|
||||||
|
const hours = Math.floor(uptimeMs / 3600000);
|
||||||
|
const minutes = Math.floor((uptimeMs % 3600000) / 60000);
|
||||||
|
const seconds = Math.floor((uptimeMs % 60000) / 1000);
|
||||||
|
return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const ClashInfoCard = () => {
|
export const ClashInfoCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { clashInfo } = useClashInfo();
|
const { clashInfo } = useClashInfo();
|
||||||
const { version: clashVersion } = useClash();
|
const { version: clashVersion } = useClash();
|
||||||
|
|
||||||
// 计算运行时间
|
// 使用SWR获取应用运行时间,降低更新频率
|
||||||
const [uptime, setUptime] = useState("0:00:00");
|
const { data: uptimeMs = 0 } = useSWR(
|
||||||
|
|
||||||
// 使用SWR定期获取应用运行时间
|
|
||||||
useSWR(
|
|
||||||
"appUptime",
|
"appUptime",
|
||||||
async () => {
|
getAppUptime,
|
||||||
const uptimeMs = await getAppUptime();
|
|
||||||
// 将毫秒转换为时:分:秒格式
|
|
||||||
const hours = Math.floor(uptimeMs / 3600000);
|
|
||||||
const minutes = Math.floor((uptimeMs % 3600000) / 60000);
|
|
||||||
const seconds = Math.floor((uptimeMs % 60000) / 1000);
|
|
||||||
setUptime(
|
|
||||||
`${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`,
|
|
||||||
);
|
|
||||||
return uptimeMs;
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
refreshInterval: 1000, // 每秒更新一次
|
refreshInterval: 1000,
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
dedupingInterval: 500,
|
dedupingInterval: 1000,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 获取规则数
|
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
||||||
const { data: rulesData } = useSWR("getRules", getRules, {
|
const uptime = useMemo(() => formatUptime(uptimeMs), [uptimeMs]);
|
||||||
fallbackData: [],
|
|
||||||
suspense: false,
|
// 获取规则数据,只在组件加载时获取一次
|
||||||
|
const { data: rules = [] } = useSWR("getRules", getRules, {
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
errorRetryCount: 2,
|
errorRetryCount: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取规则数据
|
// 使用备忘录组件内容,减少重新渲染
|
||||||
const rules = rulesData || [];
|
const cardContent = useMemo(() => {
|
||||||
|
if (!clashInfo) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={1.5}>
|
||||||
|
<Stack direction="row" justifyContent="space-between">
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("Core Version")}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" fontWeight="medium">
|
||||||
|
{clashVersion || "-"}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Divider />
|
||||||
|
<Stack direction="row" justifyContent="space-between">
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("System Proxy Address")}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" fontWeight="medium">
|
||||||
|
{clashInfo.server || "-"}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Divider />
|
||||||
|
<Stack direction="row" justifyContent="space-between">
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("Mixed Port")}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" fontWeight="medium">
|
||||||
|
{clashInfo.mixed_port || "-"}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Divider />
|
||||||
|
<Stack direction="row" justifyContent="space-between">
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("Uptime")}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" fontWeight="medium">
|
||||||
|
{uptime}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Divider />
|
||||||
|
<Stack direction="row" justifyContent="space-between">
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("Rules Count")}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" fontWeight="medium">
|
||||||
|
{rules.length}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}, [clashInfo, clashVersion, t, uptime, rules.length]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
@ -56,54 +103,7 @@ export const ClashInfoCard = () => {
|
|||||||
iconColor="warning"
|
iconColor="warning"
|
||||||
action={null}
|
action={null}
|
||||||
>
|
>
|
||||||
{clashInfo && (
|
{cardContent}
|
||||||
<Stack spacing={1.5}>
|
|
||||||
<Stack direction="row" justifyContent="space-between">
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{t("Core Version")}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" fontWeight="medium">
|
|
||||||
{clashVersion || "-"}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
<Divider />
|
|
||||||
<Stack direction="row" justifyContent="space-between">
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{t("System Proxy Address")}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" fontWeight="medium">
|
|
||||||
{clashInfo.server || "-"}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
<Divider />
|
|
||||||
<Stack direction="row" justifyContent="space-between">
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{t("Mixed Port")}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" fontWeight="medium">
|
|
||||||
{clashInfo.mixed_port || "-"}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
<Divider />
|
|
||||||
<Stack direction="row" justifyContent="space-between">
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{t("Uptime")}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" fontWeight="medium">
|
|
||||||
{uptime}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
<Divider />
|
|
||||||
<Stack direction="row" justifyContent="space-between">
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{t("Rules Count")}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" fontWeight="medium">
|
|
||||||
{rules.length}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</EnhancedCard>
|
</EnhancedCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
MultipleStopRounded,
|
MultipleStopRounded,
|
||||||
DirectionsRounded,
|
DirectionsRounded,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
|
|
||||||
export const ClashModeCard = () => {
|
export const ClashModeCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -20,15 +20,13 @@ export const ClashModeCard = () => {
|
|||||||
const { data: clashConfig, mutate: mutateClash } = useSWR(
|
const { data: clashConfig, mutate: mutateClash } = useSWR(
|
||||||
"getClashConfig",
|
"getClashConfig",
|
||||||
getClashConfig,
|
getClashConfig,
|
||||||
{
|
{ revalidateOnFocus: false }
|
||||||
revalidateOnFocus: false,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 支持的模式列表 - 添加直连模式
|
// 支持的模式列表
|
||||||
const modeList = ["rule", "global", "direct"];
|
const modeList = useMemo(() => ["rule", "global", "direct"] as const, []);
|
||||||
|
|
||||||
// 本地状态记录当前模式,提供更快的UI响应
|
// 本地状态记录当前模式
|
||||||
const [localMode, setLocalMode] = useState<string>("rule");
|
const [localMode, setLocalMode] = useState<string>("rule");
|
||||||
|
|
||||||
// 当从API获取到当前模式时更新本地状态
|
// 当从API获取到当前模式时更新本地状态
|
||||||
@ -38,25 +36,27 @@ export const ClashModeCard = () => {
|
|||||||
}
|
}
|
||||||
}, [clashConfig]);
|
}, [clashConfig]);
|
||||||
|
|
||||||
|
// 模式图标映射
|
||||||
|
const modeIcons = useMemo(() => ({
|
||||||
|
rule: <MultipleStopRounded fontSize="small" />,
|
||||||
|
global: <LanguageRounded fontSize="small" />,
|
||||||
|
direct: <DirectionsRounded fontSize="small" />
|
||||||
|
}), []);
|
||||||
|
|
||||||
// 切换模式的处理函数
|
// 切换模式的处理函数
|
||||||
const onChangeMode = useLockFn(async (mode: string) => {
|
const onChangeMode = useLockFn(async (mode: string) => {
|
||||||
// 如果已经是当前模式,不做任何操作
|
|
||||||
if (mode === localMode) return;
|
if (mode === localMode) return;
|
||||||
|
|
||||||
// 立即更新本地UI状态
|
|
||||||
setLocalMode(mode);
|
setLocalMode(mode);
|
||||||
|
|
||||||
// 断开连接(如果启用了设置)
|
|
||||||
if (verge?.auto_close_connection) {
|
if (verge?.auto_close_connection) {
|
||||||
closeAllConnections();
|
closeAllConnections();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await patchClashMode(mode);
|
await patchClashMode(mode);
|
||||||
// 成功后刷新数据
|
|
||||||
mutateClash();
|
mutateClash();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 如果操作失败,恢复之前的状态
|
|
||||||
console.error("Failed to change mode:", error);
|
console.error("Failed to change mode:", error);
|
||||||
if (clashConfig?.mode) {
|
if (clashConfig?.mode) {
|
||||||
setLocalMode(clashConfig.mode.toLowerCase());
|
setLocalMode(clashConfig.mode.toLowerCase());
|
||||||
@ -64,32 +64,55 @@ export const ClashModeCard = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取模式对应的图标
|
// 按钮样式
|
||||||
const getModeIcon = (mode: string) => {
|
const buttonStyles = (mode: string) => ({
|
||||||
switch (mode) {
|
cursor: "pointer",
|
||||||
case "rule":
|
px: 2,
|
||||||
return <MultipleStopRounded fontSize="small" />;
|
py: 1.2,
|
||||||
case "global":
|
display: "flex",
|
||||||
return <LanguageRounded fontSize="small" />;
|
alignItems: "center",
|
||||||
case "direct":
|
justifyContent: "center",
|
||||||
return <DirectionsRounded fontSize="small" />;
|
gap: 1,
|
||||||
default:
|
bgcolor: mode === localMode ? "primary.main" : "background.paper",
|
||||||
return null;
|
color: mode === localMode ? "primary.contrastText" : "text.primary",
|
||||||
}
|
borderRadius: 1.5,
|
||||||
};
|
transition: "all 0.2s ease-in-out",
|
||||||
|
position: "relative",
|
||||||
|
overflow: "visible",
|
||||||
|
"&:hover": {
|
||||||
|
transform: "translateY(-1px)",
|
||||||
|
boxShadow: 1,
|
||||||
|
},
|
||||||
|
"&:active": {
|
||||||
|
transform: "translateY(1px)",
|
||||||
|
},
|
||||||
|
"&::after": mode === localMode
|
||||||
|
? {
|
||||||
|
content: '""',
|
||||||
|
position: "absolute",
|
||||||
|
bottom: -16,
|
||||||
|
left: "50%",
|
||||||
|
width: 2,
|
||||||
|
height: 16,
|
||||||
|
bgcolor: "primary.main",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
});
|
||||||
|
|
||||||
// 获取模式说明文字
|
// 描述样式
|
||||||
const getModeDescription = (mode: string) => {
|
const descriptionStyles = {
|
||||||
switch (mode) {
|
width: "95%",
|
||||||
case "rule":
|
textAlign: "center",
|
||||||
return t("Rule Mode Description");
|
color: "text.secondary",
|
||||||
case "global":
|
p: 0.8,
|
||||||
return t("Global Mode Description");
|
borderRadius: 1,
|
||||||
case "direct":
|
borderColor: "primary.main",
|
||||||
return t("Direct Mode Description");
|
borderWidth: 1,
|
||||||
default:
|
borderStyle: "solid",
|
||||||
return "";
|
backgroundColor: "background.paper",
|
||||||
}
|
wordBreak: "break-word",
|
||||||
|
hyphens: "auto",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -111,44 +134,9 @@ export const ClashModeCard = () => {
|
|||||||
key={mode}
|
key={mode}
|
||||||
elevation={mode === localMode ? 2 : 0}
|
elevation={mode === localMode ? 2 : 0}
|
||||||
onClick={() => onChangeMode(mode)}
|
onClick={() => onChangeMode(mode)}
|
||||||
sx={{
|
sx={buttonStyles(mode)}
|
||||||
cursor: "pointer",
|
|
||||||
px: 2,
|
|
||||||
py: 1.2,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
gap: 1,
|
|
||||||
bgcolor: mode === localMode ? "primary.main" : "background.paper",
|
|
||||||
color:
|
|
||||||
mode === localMode ? "primary.contrastText" : "text.primary",
|
|
||||||
borderRadius: 1.5,
|
|
||||||
transition: "all 0.2s ease-in-out",
|
|
||||||
position: "relative",
|
|
||||||
overflow: "visible",
|
|
||||||
"&:hover": {
|
|
||||||
transform: "translateY(-1px)",
|
|
||||||
boxShadow: 1,
|
|
||||||
},
|
|
||||||
"&:active": {
|
|
||||||
transform: "translateY(1px)",
|
|
||||||
},
|
|
||||||
"&::after":
|
|
||||||
mode === localMode
|
|
||||||
? {
|
|
||||||
content: '""',
|
|
||||||
position: "absolute",
|
|
||||||
bottom: -16,
|
|
||||||
left: "50%",
|
|
||||||
width: 2,
|
|
||||||
height: 16,
|
|
||||||
bgcolor: "primary.main",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{getModeIcon(mode)}
|
{modeIcons[mode]}
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{
|
sx={{
|
||||||
@ -173,77 +161,15 @@ export const ClashModeCard = () => {
|
|||||||
overflow: "visible",
|
overflow: "visible",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{localMode === "rule" && (
|
<Fade in={true} timeout={200}>
|
||||||
<Fade in={true} timeout={200}>
|
<Typography
|
||||||
<Typography
|
variant="caption"
|
||||||
variant="caption"
|
component="div"
|
||||||
component="div"
|
sx={descriptionStyles}
|
||||||
sx={{
|
>
|
||||||
width: "95%",
|
{t(`${localMode} Mode Description`)}
|
||||||
textAlign: "center",
|
</Typography>
|
||||||
color: "text.secondary",
|
</Fade>
|
||||||
p: 0.8,
|
|
||||||
borderRadius: 1,
|
|
||||||
borderColor: "primary.main",
|
|
||||||
borderWidth: 1,
|
|
||||||
borderStyle: "solid",
|
|
||||||
backgroundColor: "background.paper",
|
|
||||||
wordBreak: "break-word",
|
|
||||||
hyphens: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getModeDescription("rule")}
|
|
||||||
</Typography>
|
|
||||||
</Fade>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{localMode === "global" && (
|
|
||||||
<Fade in={true} timeout={200}>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
component="div"
|
|
||||||
sx={{
|
|
||||||
width: "95%",
|
|
||||||
textAlign: "center",
|
|
||||||
color: "text.secondary",
|
|
||||||
p: 0.8,
|
|
||||||
borderRadius: 1,
|
|
||||||
borderColor: "primary.main",
|
|
||||||
borderWidth: 1,
|
|
||||||
borderStyle: "solid",
|
|
||||||
backgroundColor: "background.paper",
|
|
||||||
wordBreak: "break-word",
|
|
||||||
hyphens: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getModeDescription("global")}
|
|
||||||
</Typography>
|
|
||||||
</Fade>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{localMode === "direct" && (
|
|
||||||
<Fade in={true} timeout={200}>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
component="div"
|
|
||||||
sx={{
|
|
||||||
width: "95%",
|
|
||||||
textAlign: "center",
|
|
||||||
color: "text.secondary",
|
|
||||||
p: 0.8,
|
|
||||||
borderRadius: 1,
|
|
||||||
borderColor: "primary.main",
|
|
||||||
borderWidth: 1,
|
|
||||||
borderStyle: "solid",
|
|
||||||
backgroundColor: "background.paper",
|
|
||||||
wordBreak: "break-word",
|
|
||||||
hyphens: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getModeDescription("direct")}
|
|
||||||
</Typography>
|
|
||||||
</Fade>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
SelectChangeEvent,
|
SelectChangeEvent,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo, useCallback, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
SignalWifi4Bar as SignalStrong,
|
SignalWifi4Bar as SignalStrong,
|
||||||
SignalWifi3Bar as SignalGood,
|
SignalWifi3Bar as SignalGood,
|
||||||
@ -45,17 +45,7 @@ interface ProxyOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 将delayManager返回的颜色格式转换为MUI Chip组件需要的格式
|
// 将delayManager返回的颜色格式转换为MUI Chip组件需要的格式
|
||||||
function convertDelayColor(
|
function convertDelayColor(delayValue: number) {
|
||||||
delayValue: number,
|
|
||||||
):
|
|
||||||
| "default"
|
|
||||||
| "success"
|
|
||||||
| "warning"
|
|
||||||
| "error"
|
|
||||||
| "primary"
|
|
||||||
| "secondary"
|
|
||||||
| "info"
|
|
||||||
| undefined {
|
|
||||||
const colorStr = delayManager.formatDelayColor(delayValue);
|
const colorStr = delayManager.formatDelayColor(delayValue);
|
||||||
if (!colorStr) return "default";
|
if (!colorStr) return "default";
|
||||||
|
|
||||||
@ -63,445 +53,365 @@ function convertDelayColor(
|
|||||||
const mainColor = colorStr.split(".")[0];
|
const mainColor = colorStr.split(".")[0];
|
||||||
|
|
||||||
switch (mainColor) {
|
switch (mainColor) {
|
||||||
case "success":
|
case "success": return "success";
|
||||||
return "success";
|
case "warning": return "warning";
|
||||||
case "warning":
|
case "error": return "error";
|
||||||
return "warning";
|
case "primary": return "primary";
|
||||||
case "error":
|
default: return "default";
|
||||||
return "error";
|
|
||||||
case "primary":
|
|
||||||
return "primary";
|
|
||||||
default:
|
|
||||||
return "default";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据延迟值获取合适的WiFi信号图标
|
// 根据延迟值获取合适的WiFi信号图标
|
||||||
function getSignalIcon(delay: number): {
|
function getSignalIcon(delay: number) {
|
||||||
icon: JSX.Element;
|
|
||||||
text: string;
|
|
||||||
color: string;
|
|
||||||
} {
|
|
||||||
if (delay < 0)
|
if (delay < 0)
|
||||||
return {
|
return { icon: <SignalNone />, text: "未测试", color: "text.secondary" };
|
||||||
icon: <SignalNone />,
|
|
||||||
text: "未测试",
|
|
||||||
color: "text.secondary",
|
|
||||||
};
|
|
||||||
if (delay >= 10000)
|
if (delay >= 10000)
|
||||||
return {
|
return { icon: <SignalError />, text: "超时", color: "error.main" };
|
||||||
icon: <SignalError />,
|
|
||||||
text: "超时",
|
|
||||||
color: "error.main",
|
|
||||||
};
|
|
||||||
if (delay >= 500)
|
if (delay >= 500)
|
||||||
return {
|
return { icon: <SignalWeak />, text: "延迟较高", color: "error.main" };
|
||||||
icon: <SignalWeak />,
|
|
||||||
text: "延迟较高",
|
|
||||||
color: "error.main",
|
|
||||||
};
|
|
||||||
if (delay >= 300)
|
if (delay >= 300)
|
||||||
return {
|
return { icon: <SignalMedium />, text: "延迟中等", color: "warning.main" };
|
||||||
icon: <SignalMedium />,
|
|
||||||
text: "延迟中等",
|
|
||||||
color: "warning.main",
|
|
||||||
};
|
|
||||||
if (delay >= 200)
|
if (delay >= 200)
|
||||||
return {
|
return { icon: <SignalGood />, text: "延迟良好", color: "info.main" };
|
||||||
icon: <SignalGood />,
|
return { icon: <SignalStrong />, text: "延迟极佳", color: "success.main" };
|
||||||
text: "延迟良好",
|
}
|
||||||
color: "info.main",
|
|
||||||
};
|
// 简单的防抖函数
|
||||||
return {
|
function debounce(fn: Function, ms = 100) {
|
||||||
icon: <SignalStrong />,
|
let timeoutId: ReturnType<typeof setTimeout>;
|
||||||
text: "延迟极佳",
|
return function(this: any, ...args: any[]) {
|
||||||
color: "success.main",
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => fn.apply(this, args), ms);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CurrentProxyCard = () => {
|
export const CurrentProxyCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentProxy, primaryGroupName, mode, refreshProxy } =
|
const { currentProxy, primaryGroupName, mode, refreshProxy } = useCurrentProxy();
|
||||||
useCurrentProxy();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
|
|
||||||
// 判断模式
|
// 判断模式
|
||||||
const isGlobalMode = mode === "global";
|
const isGlobalMode = mode === "global";
|
||||||
const isDirectMode = mode === "direct"; // 添加直连模式判断
|
const isDirectMode = mode === "direct";
|
||||||
|
|
||||||
// 从本地存储获取初始值,如果是特殊模式或没有存储值则使用默认值
|
// 使用 useRef 存储最后一次刷新时间和是否正在刷新
|
||||||
const getSavedGroup = () => {
|
const lastRefreshRef = useRef<number>(0);
|
||||||
// 全局模式使用 GLOBAL 组
|
const isRefreshingRef = useRef<boolean>(false);
|
||||||
if (isGlobalMode) {
|
const pendingRefreshRef = useRef<boolean>(false);
|
||||||
return "GLOBAL";
|
|
||||||
}
|
// 定义状态类型
|
||||||
// 直连模式使用 DIRECT
|
type ProxyState = {
|
||||||
if (isDirectMode) {
|
proxyData: {
|
||||||
return "DIRECT";
|
groups: { name: string; now: string; all: string[] }[];
|
||||||
}
|
records: Record<string, any>;
|
||||||
const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP);
|
globalProxy: string;
|
||||||
return savedGroup || primaryGroupName || "GLOBAL";
|
directProxy: any;
|
||||||
|
};
|
||||||
|
selection: {
|
||||||
|
group: string;
|
||||||
|
proxy: string;
|
||||||
|
};
|
||||||
|
displayProxy: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 状态管理
|
// 合并状态,减少状态更新次数
|
||||||
const [groups, setGroups] = useState<
|
const [state, setState] = useState<ProxyState>({
|
||||||
{ name: string; now: string; all: string[] }[]
|
proxyData: {
|
||||||
>([]);
|
groups: [],
|
||||||
const [selectedGroup, setSelectedGroup] = useState<string>(getSavedGroup());
|
records: {},
|
||||||
const [proxyOptions, setProxyOptions] = useState<ProxyOption[]>([]);
|
globalProxy: "",
|
||||||
const [selectedProxy, setSelectedProxy] = useState<string>("");
|
directProxy: null,
|
||||||
const [displayProxy, setDisplayProxy] = useState<any>(null);
|
},
|
||||||
const [records, setRecords] = useState<Record<string, any>>({});
|
selection: {
|
||||||
const [globalProxy, setGlobalProxy] = useState<string>(""); // 存储全局代理
|
group: "",
|
||||||
const [directProxy, setDirectProxy] = useState<any>(null); // 存储直连代理信息
|
proxy: "",
|
||||||
|
},
|
||||||
|
displayProxy: null,
|
||||||
|
});
|
||||||
|
|
||||||
// 保存选择的代理组到本地存储
|
// 初始化选择的组
|
||||||
useEffect(() => {
|
|
||||||
// 只有在普通模式下才保存到本地存储
|
|
||||||
if (selectedGroup && !isGlobalMode && !isDirectMode) {
|
|
||||||
localStorage.setItem(STORAGE_KEY_GROUP, selectedGroup);
|
|
||||||
}
|
|
||||||
}, [selectedGroup, isGlobalMode, isDirectMode]);
|
|
||||||
|
|
||||||
// 保存选择的代理节点到本地存储
|
|
||||||
useEffect(() => {
|
|
||||||
// 只有在普通模式下才保存到本地存储
|
|
||||||
if (selectedProxy && !isGlobalMode && !isDirectMode) {
|
|
||||||
localStorage.setItem(STORAGE_KEY_PROXY, selectedProxy);
|
|
||||||
}
|
|
||||||
}, [selectedProxy, isGlobalMode, isDirectMode]);
|
|
||||||
|
|
||||||
// 当模式变化时更新选择的组
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// 根据模式确定初始组
|
||||||
if (isGlobalMode) {
|
if (isGlobalMode) {
|
||||||
setSelectedGroup("GLOBAL");
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
selection: {
|
||||||
|
...prev.selection,
|
||||||
|
group: "GLOBAL"
|
||||||
|
}
|
||||||
|
}));
|
||||||
} else if (isDirectMode) {
|
} else if (isDirectMode) {
|
||||||
setSelectedGroup("DIRECT");
|
setState(prev => ({
|
||||||
} else if (primaryGroupName) {
|
...prev,
|
||||||
|
selection: {
|
||||||
|
...prev.selection,
|
||||||
|
group: "DIRECT"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP);
|
const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP);
|
||||||
setSelectedGroup(savedGroup || primaryGroupName);
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
selection: {
|
||||||
|
...prev.selection,
|
||||||
|
group: savedGroup || primaryGroupName || ""
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}, [isGlobalMode, isDirectMode, primaryGroupName]);
|
}, [isGlobalMode, isDirectMode, primaryGroupName]);
|
||||||
|
|
||||||
// 获取所有代理组和代理信息
|
// 带锁的代理数据获取函数,防止并发请求
|
||||||
useEffect(() => {
|
const fetchProxyData = useCallback(async (force = false) => {
|
||||||
const fetchProxies = async () => {
|
// 防止重复请求
|
||||||
try {
|
if (isRefreshingRef.current) {
|
||||||
const data = await getProxies();
|
pendingRefreshRef.current = true;
|
||||||
// 保存所有节点记录信息,用于显示详细节点信息
|
return;
|
||||||
setRecords(data.records);
|
|
||||||
|
|
||||||
// 检查并存储全局代理信息
|
|
||||||
if (data.global) {
|
|
||||||
setGlobalProxy(data.global.now || "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找并存储直连代理信息
|
|
||||||
if (data.records && data.records["DIRECT"]) {
|
|
||||||
setDirectProxy(data.records["DIRECT"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
}));
|
|
||||||
|
|
||||||
setGroups(filteredGroups);
|
|
||||||
|
|
||||||
// 直连模式处理
|
|
||||||
if (isDirectMode) {
|
|
||||||
// 直连模式下使用 DIRECT 节点
|
|
||||||
setSelectedGroup("DIRECT");
|
|
||||||
setSelectedProxy("DIRECT");
|
|
||||||
|
|
||||||
if (data.records && data.records["DIRECT"]) {
|
|
||||||
setDisplayProxy(data.records["DIRECT"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置仅包含 DIRECT 节点的选项
|
|
||||||
setProxyOptions([{ name: "DIRECT" }]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全局模式处理
|
|
||||||
if (isGlobalMode) {
|
|
||||||
// 在全局模式下,使用 GLOBAL 组和 data.global.now 作为选中节点
|
|
||||||
if (data.global) {
|
|
||||||
const globalNow = data.global.now || "";
|
|
||||||
setSelectedGroup("GLOBAL");
|
|
||||||
setSelectedProxy(globalNow);
|
|
||||||
|
|
||||||
if (globalNow && data.records[globalNow]) {
|
|
||||||
setDisplayProxy(data.records[globalNow]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置全局组的代理选项
|
|
||||||
const options = data.global.all.map((proxy) => ({
|
|
||||||
name: proxy.name,
|
|
||||||
}));
|
|
||||||
|
|
||||||
setProxyOptions(options);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 以下是普通模式的处理逻辑
|
|
||||||
let targetGroup = primaryGroupName;
|
|
||||||
|
|
||||||
// 非特殊模式下,尝试从本地存储获取上次选择的代理组
|
|
||||||
const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP);
|
|
||||||
targetGroup = savedGroup || primaryGroupName;
|
|
||||||
|
|
||||||
// 如果目标组在列表中,则选择它
|
|
||||||
if (targetGroup && filteredGroups.some((g) => g.name === targetGroup)) {
|
|
||||||
setSelectedGroup(targetGroup);
|
|
||||||
|
|
||||||
// 设置该组下的代理选项
|
|
||||||
const currentGroup = filteredGroups.find(
|
|
||||||
(g) => g.name === targetGroup,
|
|
||||||
);
|
|
||||||
if (currentGroup) {
|
|
||||||
// 创建代理选项
|
|
||||||
const options = currentGroup.all.map((proxyName) => {
|
|
||||||
return { name: proxyName };
|
|
||||||
});
|
|
||||||
|
|
||||||
setProxyOptions(options);
|
|
||||||
|
|
||||||
let targetProxy = currentGroup.now;
|
|
||||||
|
|
||||||
const savedProxy = localStorage.getItem(STORAGE_KEY_PROXY);
|
|
||||||
// 如果有保存的代理节点且该节点在当前组中,则选择它
|
|
||||||
if (savedProxy && currentGroup.all.includes(savedProxy)) {
|
|
||||||
targetProxy = savedProxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedProxy(targetProxy);
|
|
||||||
|
|
||||||
if (targetProxy && data.records[targetProxy]) {
|
|
||||||
setDisplayProxy(data.records[targetProxy]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (filteredGroups.length > 0) {
|
|
||||||
// 否则选择第一个组
|
|
||||||
setSelectedGroup(filteredGroups[0].name);
|
|
||||||
|
|
||||||
// 创建代理选项
|
|
||||||
const options = filteredGroups[0].all.map((proxyName) => {
|
|
||||||
return { name: proxyName };
|
|
||||||
});
|
|
||||||
|
|
||||||
setProxyOptions(options);
|
|
||||||
setSelectedProxy(filteredGroups[0].now);
|
|
||||||
|
|
||||||
// 更新显示的代理节点信息
|
|
||||||
if (filteredGroups[0].now && data.records[filteredGroups[0].now]) {
|
|
||||||
setDisplayProxy(data.records[filteredGroups[0].now]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取代理信息失败", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchProxies();
|
|
||||||
}, [primaryGroupName, isGlobalMode, isDirectMode]);
|
|
||||||
|
|
||||||
// 当选择的组发生变化时更新代理选项
|
|
||||||
useEffect(() => {
|
|
||||||
// 如果是特殊模式,已在 fetchProxies 中处理
|
|
||||||
if (isGlobalMode || isDirectMode) return;
|
|
||||||
|
|
||||||
const group = groups.find((g) => g.name === selectedGroup);
|
|
||||||
if (group && records) {
|
|
||||||
// 创建代理选项
|
|
||||||
const options = group.all.map((proxyName) => {
|
|
||||||
return { name: proxyName };
|
|
||||||
});
|
|
||||||
|
|
||||||
setProxyOptions(options);
|
|
||||||
|
|
||||||
let targetProxy = group.now;
|
|
||||||
|
|
||||||
const savedProxy = localStorage.getItem(STORAGE_KEY_PROXY);
|
|
||||||
// 如果保存的代理节点在当前组中,则选择它
|
|
||||||
if (savedProxy && group.all.includes(savedProxy)) {
|
|
||||||
targetProxy = savedProxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedProxy(targetProxy);
|
|
||||||
|
|
||||||
if (targetProxy && records[targetProxy]) {
|
|
||||||
setDisplayProxy(records[targetProxy]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [selectedGroup, groups, records, isGlobalMode, isDirectMode]);
|
|
||||||
|
|
||||||
// 刷新代理信息
|
// 检查刷新间隔
|
||||||
const refreshProxyData = async () => {
|
const now = Date.now();
|
||||||
|
if (!force && now - lastRefreshRef.current < 1000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRefreshingRef.current = true;
|
||||||
|
lastRefreshRef.current = now;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await getProxies();
|
const data = await getProxies();
|
||||||
// 更新所有代理记录
|
|
||||||
setRecords(data.records);
|
// 过滤和格式化组
|
||||||
|
|
||||||
// 更新代理组信息
|
|
||||||
const filteredGroups = data.groups
|
const filteredGroups = data.groups
|
||||||
.filter((g) => g.name !== "DIRECT" && g.name !== "REJECT")
|
.filter(g => g.name !== "DIRECT" && g.name !== "REJECT")
|
||||||
.map((g) => ({
|
.map(g => ({
|
||||||
name: g.name,
|
name: g.name,
|
||||||
now: g.now || "",
|
now: g.now || "",
|
||||||
all: g.all.map((p) => p.name),
|
all: g.all.map(p => p.name),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setGroups(filteredGroups);
|
// 使用函数式更新确保状态更新的原子性
|
||||||
|
setState(prev => {
|
||||||
|
let newProxy = "";
|
||||||
|
let newDisplayProxy = null;
|
||||||
|
let newGroup = prev.selection.group;
|
||||||
|
|
||||||
// 检查并更新全局代理信息
|
// 根据模式确定新代理
|
||||||
if (isGlobalMode && data.global) {
|
if (isDirectMode) {
|
||||||
const globalNow = data.global.now || "";
|
newGroup = "DIRECT";
|
||||||
setSelectedProxy(globalNow);
|
newProxy = "DIRECT";
|
||||||
|
newDisplayProxy = data.records?.DIRECT || null;
|
||||||
if (globalNow && data.records[globalNow]) {
|
} else if (isGlobalMode && data.global) {
|
||||||
setDisplayProxy(data.records[globalNow]);
|
newGroup = "GLOBAL";
|
||||||
}
|
newProxy = data.global.now || "";
|
||||||
|
newDisplayProxy = data.records?.[newProxy] || null;
|
||||||
// 更新全局组的代理选项
|
} else {
|
||||||
const options = data.global.all.map((proxy) => ({
|
// 普通模式 - 检查当前选择的组是否存在
|
||||||
name: proxy.name,
|
const currentGroup = filteredGroups.find(g => g.name === prev.selection.group);
|
||||||
}));
|
|
||||||
|
// 如果当前组不存在或为空,自动选择第一个组
|
||||||
setProxyOptions(options);
|
if (!currentGroup && filteredGroups.length > 0) {
|
||||||
}
|
newGroup = filteredGroups[0].name;
|
||||||
// 更新直连代理信息
|
const firstGroup = filteredGroups[0];
|
||||||
else if (isDirectMode && data.records["DIRECT"]) {
|
newProxy = firstGroup.now;
|
||||||
setDirectProxy(data.records["DIRECT"]);
|
newDisplayProxy = data.records?.[newProxy] || null;
|
||||||
setDisplayProxy(data.records["DIRECT"]);
|
|
||||||
}
|
// 保存到本地存储
|
||||||
// 更新普通模式下当前选中组的信息
|
if (!isGlobalMode && !isDirectMode) {
|
||||||
else {
|
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
||||||
const currentGroup = filteredGroups.find(
|
if (newProxy) {
|
||||||
(g) => g.name === selectedGroup,
|
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
|
||||||
);
|
}
|
||||||
if (currentGroup) {
|
|
||||||
// 如果当前选中的代理节点与组中的now不一致,则需要更新
|
|
||||||
if (currentGroup.now !== selectedProxy) {
|
|
||||||
setSelectedProxy(currentGroup.now);
|
|
||||||
|
|
||||||
if (data.records[currentGroup.now]) {
|
|
||||||
setDisplayProxy(data.records[currentGroup.now]);
|
|
||||||
}
|
}
|
||||||
|
} else if (currentGroup) {
|
||||||
|
// 使用当前组的代理
|
||||||
|
newProxy = currentGroup.now;
|
||||||
|
newDisplayProxy = data.records?.[newProxy] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新代理选项
|
|
||||||
const options = currentGroup.all.map((proxyName) => ({
|
|
||||||
name: proxyName,
|
|
||||||
}));
|
|
||||||
|
|
||||||
setProxyOptions(options);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// 返回新状态
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error("刷新代理信息失败", error);
|
console.error("获取代理信息失败", error);
|
||||||
|
} finally {
|
||||||
|
isRefreshingRef.current = false;
|
||||||
|
|
||||||
|
// 处理待处理的刷新请求
|
||||||
|
if (pendingRefreshRef.current) {
|
||||||
|
pendingRefreshRef.current = false;
|
||||||
|
setTimeout(() => fetchProxyData(), 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
}, [isGlobalMode, isDirectMode]);
|
||||||
|
|
||||||
// 每隔一段时间刷新代理信息 - 修改为在所有模式下都刷新
|
// 响应 currentProxy 变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 初始刷新一次
|
if (currentProxy && (!state.displayProxy || currentProxy.name !== state.displayProxy.name)) {
|
||||||
refreshProxyData();
|
fetchProxyData(true);
|
||||||
|
}
|
||||||
|
}, [currentProxy, fetchProxyData, state.displayProxy]);
|
||||||
|
|
||||||
// 定期刷新所有模式下的代理信息
|
// 平滑的定期刷新,使用固定间隔
|
||||||
const refreshInterval = setInterval(refreshProxyData, 2000);
|
useEffect(() => {
|
||||||
return () => clearInterval(refreshInterval);
|
fetchProxyData();
|
||||||
}, [isGlobalMode, isDirectMode, selectedGroup]); // 依赖项添加selectedGroup以便在切换组时重新设置定时器
|
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
fetchProxyData();
|
||||||
|
}, 3000); // 使用固定的3秒间隔,平衡响应速度和性能
|
||||||
|
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, [fetchProxyData]);
|
||||||
|
|
||||||
|
// 计算要显示的代理选项 - 使用 useMemo 优化
|
||||||
|
const proxyOptions = useMemo(() => {
|
||||||
|
if (isDirectMode) {
|
||||||
|
return [{ name: "DIRECT" }];
|
||||||
|
}
|
||||||
|
if (isGlobalMode && state.proxyData.records) {
|
||||||
|
// 全局模式下的选项
|
||||||
|
return Object.keys(state.proxyData.records)
|
||||||
|
.filter(name => name !== "DIRECT" && name !== "REJECT")
|
||||||
|
.map(name => ({ name }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通模式
|
||||||
|
const group = state.proxyData.groups.find(g => g.name === state.selection.group);
|
||||||
|
if (group) {
|
||||||
|
return group.all.map(name => ({ name }));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [isDirectMode, isGlobalMode, state.proxyData, state.selection.group]);
|
||||||
|
|
||||||
|
// 使用防抖包装状态更新,避免快速连续更新
|
||||||
|
const debouncedSetState = useCallback(
|
||||||
|
debounce((updateFn: (prev: ProxyState) => ProxyState) => {
|
||||||
|
setState(updateFn);
|
||||||
|
}, 50),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
// 处理代理组变更
|
// 处理代理组变更
|
||||||
const handleGroupChange = (event: SelectChangeEvent) => {
|
const handleGroupChange = useCallback((event: SelectChangeEvent) => {
|
||||||
// 特殊模式下不允许切换组
|
|
||||||
if (isGlobalMode || isDirectMode) return;
|
if (isGlobalMode || isDirectMode) return;
|
||||||
|
|
||||||
const newGroup = event.target.value;
|
const newGroup = event.target.value;
|
||||||
setSelectedGroup(newGroup);
|
|
||||||
};
|
// 保存到本地存储
|
||||||
|
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
||||||
|
|
||||||
|
// 获取该组当前选中的代理
|
||||||
|
setState(prev => {
|
||||||
|
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 {
|
||||||
|
...prev,
|
||||||
|
selection: {
|
||||||
|
...prev.selection,
|
||||||
|
group: newGroup
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [isGlobalMode, isDirectMode]);
|
||||||
|
|
||||||
// 处理代理节点变更
|
// 处理代理节点变更
|
||||||
const handleProxyChange = async (event: SelectChangeEvent) => {
|
const handleProxyChange = useCallback(async (event: SelectChangeEvent) => {
|
||||||
// 直连模式下不允许切换节点
|
|
||||||
if (isDirectMode) return;
|
if (isDirectMode) return;
|
||||||
|
|
||||||
const newProxy = event.target.value;
|
const newProxy = event.target.value;
|
||||||
const previousProxy = selectedProxy; // 保存变更前的代理节点名称
|
const currentGroup = state.selection.group;
|
||||||
|
const previousProxy = state.selection.proxy;
|
||||||
setSelectedProxy(newProxy);
|
|
||||||
|
// 立即更新UI,优化体验
|
||||||
// 更新显示的代理节点信息
|
debouncedSetState((prev: ProxyState) => ({
|
||||||
if (records[newProxy]) {
|
...prev,
|
||||||
setDisplayProxy(records[newProxy]);
|
selection: {
|
||||||
|
...prev.selection,
|
||||||
|
proxy: newProxy
|
||||||
|
},
|
||||||
|
displayProxy: prev.proxyData.records[newProxy] || null
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 非特殊模式下保存到本地存储
|
||||||
|
if (!isGlobalMode && !isDirectMode) {
|
||||||
|
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 更新代理设置
|
// 更新代理设置
|
||||||
await updateProxy(selectedGroup, newProxy);
|
await updateProxy(currentGroup, newProxy);
|
||||||
|
|
||||||
// 添加断开连接逻辑 - 与proxy-groups.tsx中的逻辑相同
|
// 自动关闭连接设置
|
||||||
if (verge?.auto_close_connection && previousProxy) {
|
if (verge?.auto_close_connection && previousProxy) {
|
||||||
getConnections().then(({ connections }) => {
|
getConnections().then(({ connections }) => {
|
||||||
connections.forEach((conn) => {
|
connections.forEach(conn => {
|
||||||
if (conn.chains.includes(previousProxy)) {
|
if (conn.chains.includes(previousProxy)) {
|
||||||
deleteConnection(conn.id);
|
deleteConnection(conn.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 刷新代理信息,使用较短的延迟
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
refreshProxy();
|
refreshProxy();
|
||||||
if (isGlobalMode || isDirectMode) {
|
fetchProxyData(true);
|
||||||
refreshProxyData(); // 特殊模式下额外刷新数据
|
}, 200);
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("更新代理失败", error);
|
console.error("更新代理失败", error);
|
||||||
}
|
}
|
||||||
};
|
}, [isDirectMode, isGlobalMode, state.proxyData.records, state.selection, verge?.auto_close_connection, refreshProxy, fetchProxyData, debouncedSetState]);
|
||||||
|
|
||||||
// 导航到代理页面
|
// 导航到代理页面
|
||||||
const goToProxies = () => {
|
const goToProxies = useCallback(() => {
|
||||||
// 修正路由路径,根据_routers.tsx配置,代理页面的路径是"/"
|
|
||||||
navigate("/");
|
navigate("/");
|
||||||
};
|
}, [navigate]);
|
||||||
|
|
||||||
// 获取要显示的代理节点
|
// 获取要显示的代理节点
|
||||||
const proxyToDisplay = displayProxy || currentProxy;
|
const proxyToDisplay = state.displayProxy || currentProxy;
|
||||||
|
|
||||||
// 获取当前节点的延迟
|
// 获取当前节点的延迟
|
||||||
const currentDelay = proxyToDisplay
|
const currentDelay = proxyToDisplay
|
||||||
? delayManager.getDelayFix(proxyToDisplay, selectedGroup)
|
? delayManager.getDelayFix(proxyToDisplay, state.selection.group)
|
||||||
: -1;
|
: -1;
|
||||||
|
|
||||||
// 获取信号图标
|
// 获取信号图标
|
||||||
const signalInfo = getSignalIcon(currentDelay);
|
const signalInfo = getSignalIcon(currentDelay);
|
||||||
|
|
||||||
// 自定义渲染选择框中的值
|
// 自定义渲染选择框中的值
|
||||||
const renderProxyValue = (selected: string) => {
|
const renderProxyValue = useCallback((selected: string) => {
|
||||||
if (!selected || !records[selected]) return selected;
|
if (!selected || !state.proxyData.records[selected]) return selected;
|
||||||
|
|
||||||
const delayValue = delayManager.getDelayFix(
|
const delayValue = delayManager.getDelayFix(
|
||||||
records[selected],
|
state.proxyData.records[selected],
|
||||||
selectedGroup,
|
state.selection.group
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography noWrap>{selected}</Typography>
|
<Typography noWrap>{selected}</Typography>
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
@ -510,7 +420,7 @@ export const CurrentProxyCard = () => {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
}, [state.proxyData.records, state.selection.group]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
@ -561,48 +471,22 @@ export const CurrentProxyCard = () => {
|
|||||||
{proxyToDisplay.name}
|
{proxyToDisplay.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box
|
<Box sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
|
||||||
sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}
|
<Typography variant="caption" color="text.secondary" sx={{ mr: 1 }}>
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ mr: 1 }}
|
|
||||||
>
|
|
||||||
{proxyToDisplay.type}
|
{proxyToDisplay.type}
|
||||||
</Typography>
|
</Typography>
|
||||||
{isGlobalMode && (
|
{isGlobalMode && (
|
||||||
<Chip
|
<Chip size="small" label={t("Global Mode")} color="primary" sx={{ mr: 0.5 }} />
|
||||||
size="small"
|
|
||||||
label={t("Global Mode")}
|
|
||||||
color="primary"
|
|
||||||
sx={{ mr: 0.5 }}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{isDirectMode && (
|
{isDirectMode && (
|
||||||
<Chip
|
<Chip size="small" label={t("Direct Mode")} color="success" sx={{ mr: 0.5 }} />
|
||||||
size="small"
|
|
||||||
label={t("Direct Mode")}
|
|
||||||
color="success"
|
|
||||||
sx={{ mr: 0.5 }}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{/* 节点特性 */}
|
{/* 节点特性 */}
|
||||||
{proxyToDisplay.udp && (
|
{proxyToDisplay.udp && <Chip size="small" label="UDP" variant="outlined" />}
|
||||||
<Chip size="small" label="UDP" variant="outlined" />
|
{proxyToDisplay.tfo && <Chip size="small" label="TFO" variant="outlined" />}
|
||||||
)}
|
{proxyToDisplay.xudp && <Chip size="small" label="XUDP" variant="outlined" />}
|
||||||
{proxyToDisplay.tfo && (
|
{proxyToDisplay.mptcp && <Chip size="small" label="MPTCP" variant="outlined" />}
|
||||||
<Chip size="small" label="TFO" variant="outlined" />
|
{proxyToDisplay.smux && <Chip size="small" label="SMUX" 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>
|
||||||
|
|
||||||
@ -610,31 +494,22 @@ export const CurrentProxyCard = () => {
|
|||||||
{proxyToDisplay && !isDirectMode && (
|
{proxyToDisplay && !isDirectMode && (
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
label={delayManager.formatDelay(
|
label={delayManager.formatDelay(currentDelay)}
|
||||||
delayManager.getDelayFix(proxyToDisplay, selectedGroup),
|
color={convertDelayColor(currentDelay)}
|
||||||
)}
|
|
||||||
color={convertDelayColor(
|
|
||||||
delayManager.getDelayFix(proxyToDisplay, selectedGroup),
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{/* 代理组选择器 */}
|
{/* 代理组选择器 */}
|
||||||
<FormControl
|
<FormControl fullWidth variant="outlined" size="small" sx={{ mb: 1.5 }}>
|
||||||
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"
|
||||||
value={selectedGroup}
|
value={state.selection.group}
|
||||||
onChange={handleGroupChange}
|
onChange={handleGroupChange}
|
||||||
label={t("Group")}
|
label={t("Group")}
|
||||||
disabled={isGlobalMode || isDirectMode} // 特殊模式下禁用选择器
|
disabled={isGlobalMode || isDirectMode}
|
||||||
>
|
>
|
||||||
{groups.map((group) => (
|
{state.proxyData.groups.map((group) => (
|
||||||
<MenuItem key={group.name} value={group.name}>
|
<MenuItem key={group.name} value={group.name}>
|
||||||
{group.name}
|
{group.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -647,10 +522,10 @@ export const CurrentProxyCard = () => {
|
|||||||
<InputLabel id="proxy-select-label">{t("Proxy")}</InputLabel>
|
<InputLabel id="proxy-select-label">{t("Proxy")}</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
labelId="proxy-select-label"
|
labelId="proxy-select-label"
|
||||||
value={selectedProxy}
|
value={state.selection.proxy}
|
||||||
onChange={handleProxyChange}
|
onChange={handleProxyChange}
|
||||||
label={t("Proxy")}
|
label={t("Proxy")}
|
||||||
disabled={isDirectMode} // 直连模式下禁用选择器
|
disabled={isDirectMode}
|
||||||
renderValue={renderProxyValue}
|
renderValue={renderProxyValue}
|
||||||
MenuProps={{
|
MenuProps={{
|
||||||
PaperProps: {
|
PaperProps: {
|
||||||
@ -662,8 +537,8 @@ export const CurrentProxyCard = () => {
|
|||||||
>
|
>
|
||||||
{proxyOptions.map((proxy) => {
|
{proxyOptions.map((proxy) => {
|
||||||
const delayValue = delayManager.getDelayFix(
|
const delayValue = delayManager.getDelayFix(
|
||||||
records[proxy.name],
|
state.proxyData.records[proxy.name],
|
||||||
selectedGroup,
|
state.selection.group
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -5,7 +5,6 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useCallback,
|
useCallback,
|
||||||
useMemo,
|
useMemo,
|
||||||
ReactElement,
|
|
||||||
useRef,
|
useRef,
|
||||||
memo,
|
memo,
|
||||||
} from "react";
|
} from "react";
|
||||||
@ -44,8 +43,8 @@ type TimeRange = 1 | 5 | 10; // 分钟
|
|||||||
type DataPoint = ITrafficItem & { name: string; timestamp: number };
|
type DataPoint = ITrafficItem & { name: string; timestamp: number };
|
||||||
|
|
||||||
// 控制帧率的工具函数
|
// 控制帧率的工具函数
|
||||||
const FPS_LIMIT = 30; // 限制最高30fps
|
const FPS_LIMIT = 1; // 限制为1fps,因为数据每秒才更新一次
|
||||||
const FRAME_MIN_TIME = 1000 / FPS_LIMIT; // 每帧最小时间间隔
|
const FRAME_MIN_TIME = 1000 / FPS_LIMIT; // 每帧最小时间间隔,即1000ms
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 增强型流量图表组件
|
* 增强型流量图表组件
|
||||||
@ -109,7 +108,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 初始化空数据缓冲区
|
// 初始化数据缓冲区
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 生成10分钟的初始数据点
|
// 生成10分钟的初始数据点
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@ -217,8 +216,9 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
|||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 直接更新ref,不触发重渲染
|
// 更新ref,但保持原数组大小
|
||||||
dataBufferRef.current = [...dataBufferRef.current.slice(1), newPoint];
|
const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
|
||||||
|
dataBufferRef.current = newBuffer;
|
||||||
|
|
||||||
// 使用节流更新显示数据
|
// 使用节流更新显示数据
|
||||||
throttledUpdateData();
|
throttledUpdateData();
|
||||||
@ -264,161 +264,23 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
|||||||
return t("{{time}} Minutes", { time: timeRange });
|
return t("{{time}} Minutes", { time: timeRange });
|
||||||
}, [timeRange, t]);
|
}, [timeRange, t]);
|
||||||
|
|
||||||
// 渲染图表内的标签
|
|
||||||
const renderInnerLabels = useCallback(() => (
|
|
||||||
<>
|
|
||||||
{/* 上传标签 - 右上角 */}
|
|
||||||
<text
|
|
||||||
x="98%"
|
|
||||||
y="7%"
|
|
||||||
textAnchor="end"
|
|
||||||
fill={colors.up}
|
|
||||||
fontSize={12}
|
|
||||||
fontWeight="bold"
|
|
||||||
>
|
|
||||||
{t("Upload")}
|
|
||||||
</text>
|
|
||||||
|
|
||||||
{/* 下载标签 - 右上角下方 */}
|
|
||||||
<text
|
|
||||||
x="98%"
|
|
||||||
y="16%"
|
|
||||||
textAnchor="end"
|
|
||||||
fill={colors.down}
|
|
||||||
fontSize={12}
|
|
||||||
fontWeight="bold"
|
|
||||||
>
|
|
||||||
{t("Download")}
|
|
||||||
</text>
|
|
||||||
</>
|
|
||||||
), [colors.up, colors.down, t]);
|
|
||||||
|
|
||||||
// 共享图表配置
|
// 共享图表配置
|
||||||
const commonProps = useMemo(() => ({
|
const chartConfig = useMemo(() => ({
|
||||||
data: displayData,
|
data: displayData,
|
||||||
margin: { top: 10, right: 20, left: 0, bottom: 0 },
|
margin: { top: 10, right: 20, left: 0, bottom: 0 },
|
||||||
}), [displayData]);
|
}), [displayData]);
|
||||||
|
|
||||||
// 曲线类型 - 使用平滑曲线
|
// 共享的线条/区域配置
|
||||||
const curveType = "basis";
|
const commonLineProps = useMemo(() => ({
|
||||||
|
dot: false,
|
||||||
|
strokeWidth: 2,
|
||||||
|
connectNulls: false,
|
||||||
|
activeDot: { r: 4, strokeWidth: 1 },
|
||||||
|
isAnimationActive: false, // 禁用动画以减少CPU使用
|
||||||
|
}), []);
|
||||||
|
|
||||||
// 共享图表子组件
|
// 曲线类型 - 使用线性曲线避免错位
|
||||||
const commonChildren = useMemo(() => (
|
const curveType = "monotone";
|
||||||
<>
|
|
||||||
<CartesianGrid
|
|
||||||
strokeDasharray="3 3"
|
|
||||||
vertical={false}
|
|
||||||
stroke={colors.grid}
|
|
||||||
opacity={0.3}
|
|
||||||
/>
|
|
||||||
<XAxis
|
|
||||||
dataKey="name"
|
|
||||||
tick={{ fontSize: 10, fill: colors.text }}
|
|
||||||
tickLine={{ stroke: colors.grid }}
|
|
||||||
axisLine={{ stroke: colors.grid }}
|
|
||||||
interval="preserveStart"
|
|
||||||
tickFormatter={formatXLabel}
|
|
||||||
minTickGap={timeRange === 1 ? 40 : 80}
|
|
||||||
tickCount={Math.min(6, timeRange * 2)}
|
|
||||||
domain={["dataMin", "dataMax"]}
|
|
||||||
scale="auto"
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
tickFormatter={formatYAxis}
|
|
||||||
tick={{ fontSize: 10, fill: colors.text }}
|
|
||||||
tickLine={{ stroke: colors.grid }}
|
|
||||||
axisLine={{ stroke: colors.grid }}
|
|
||||||
width={40}
|
|
||||||
domain={[0, "auto"]}
|
|
||||||
padding={{ top: 10, bottom: 0 }}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
formatter={formatTooltip}
|
|
||||||
labelFormatter={(label) => `${t("Time")}: ${label}`}
|
|
||||||
contentStyle={{
|
|
||||||
backgroundColor: colors.tooltip,
|
|
||||||
borderColor: colors.grid,
|
|
||||||
borderRadius: 4,
|
|
||||||
}}
|
|
||||||
itemStyle={{ color: colors.text }}
|
|
||||||
isAnimationActive={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 可点击的时间范围标签 */}
|
|
||||||
<g
|
|
||||||
className="time-range-selector"
|
|
||||||
onClick={handleTimeRangeClick}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
<text
|
|
||||||
x="1%"
|
|
||||||
y="6%"
|
|
||||||
textAnchor="start"
|
|
||||||
fill={theme.palette.text.secondary}
|
|
||||||
fontSize={11}
|
|
||||||
fontWeight="bold"
|
|
||||||
>
|
|
||||||
{getTimeRangeText()}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
</>
|
|
||||||
), [colors, formatXLabel, formatYAxis, formatTooltip, timeRange, theme.palette.text.secondary, handleTimeRangeClick, getTimeRangeText, t]);
|
|
||||||
|
|
||||||
// 渲染图表 - 线图或面积图
|
|
||||||
const renderChart = useCallback(() => {
|
|
||||||
// 共享的线条/区域配置
|
|
||||||
const commonLineProps = {
|
|
||||||
dot: false,
|
|
||||||
strokeWidth: 2,
|
|
||||||
connectNulls: false,
|
|
||||||
activeDot: { r: 4, strokeWidth: 1 },
|
|
||||||
isAnimationActive: false, // 禁用动画以减少CPU使用
|
|
||||||
};
|
|
||||||
|
|
||||||
return chartStyle === "line" ? (
|
|
||||||
<LineChart {...commonProps}>
|
|
||||||
{commonChildren}
|
|
||||||
<Line
|
|
||||||
type="basis"
|
|
||||||
{...commonLineProps}
|
|
||||||
dataKey="up"
|
|
||||||
name={t("Upload")}
|
|
||||||
stroke={colors.up}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
type="basis"
|
|
||||||
{...commonLineProps}
|
|
||||||
dataKey="down"
|
|
||||||
name={t("Download")}
|
|
||||||
stroke={colors.down}
|
|
||||||
/>
|
|
||||||
{renderInnerLabels()}
|
|
||||||
</LineChart>
|
|
||||||
) : (
|
|
||||||
<AreaChart {...commonProps}>
|
|
||||||
{commonChildren}
|
|
||||||
<Area
|
|
||||||
type="basis"
|
|
||||||
{...commonLineProps}
|
|
||||||
dataKey="up"
|
|
||||||
name={t("Upload")}
|
|
||||||
stroke={colors.up}
|
|
||||||
fill={colors.up}
|
|
||||||
fillOpacity={0.2}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
type="basis"
|
|
||||||
{...commonLineProps}
|
|
||||||
dataKey="down"
|
|
||||||
name={t("Download")}
|
|
||||||
stroke={colors.down}
|
|
||||||
fill={colors.down}
|
|
||||||
fillOpacity={0.3}
|
|
||||||
/>
|
|
||||||
{renderInnerLabels()}
|
|
||||||
</AreaChart>
|
|
||||||
);
|
|
||||||
}, [chartStyle, commonProps, commonChildren, renderInnerLabels, colors, t]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -434,7 +296,180 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
|||||||
onClick={toggleStyle}
|
onClick={toggleStyle}
|
||||||
>
|
>
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
{renderChart()}
|
{chartStyle === "line" ? (
|
||||||
|
<LineChart {...chartConfig}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={colors.grid} opacity={0.3} />
|
||||||
|
<XAxis
|
||||||
|
dataKey="name"
|
||||||
|
tick={{ fontSize: 10, fill: colors.text }}
|
||||||
|
tickLine={{ stroke: colors.grid }}
|
||||||
|
axisLine={{ stroke: colors.grid }}
|
||||||
|
interval="preserveStart"
|
||||||
|
tickFormatter={formatXLabel}
|
||||||
|
minTickGap={30}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tickFormatter={formatYAxis}
|
||||||
|
tick={{ fontSize: 10, fill: colors.text }}
|
||||||
|
tickLine={{ stroke: colors.grid }}
|
||||||
|
axisLine={{ stroke: colors.grid }}
|
||||||
|
width={40}
|
||||||
|
domain={[0, "auto"]}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
formatter={formatTooltip}
|
||||||
|
labelFormatter={(label) => `${t("Time")}: ${label}`}
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: colors.tooltip,
|
||||||
|
borderColor: colors.grid,
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
itemStyle={{ color: colors.text }}
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type={curveType}
|
||||||
|
{...commonLineProps}
|
||||||
|
dataKey="up"
|
||||||
|
name={t("Upload")}
|
||||||
|
stroke={colors.up}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type={curveType}
|
||||||
|
{...commonLineProps}
|
||||||
|
dataKey="down"
|
||||||
|
name={t("Download")}
|
||||||
|
stroke={colors.down}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 可点击的时间范围标签 */}
|
||||||
|
<text
|
||||||
|
x="1%"
|
||||||
|
y="6%"
|
||||||
|
textAnchor="start"
|
||||||
|
fill={theme.palette.text.secondary}
|
||||||
|
fontSize={11}
|
||||||
|
fontWeight="bold"
|
||||||
|
onClick={handleTimeRangeClick}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{getTimeRangeText()}
|
||||||
|
</text>
|
||||||
|
|
||||||
|
{/* 上传标签 - 右上角 */}
|
||||||
|
<text
|
||||||
|
x="98%"
|
||||||
|
y="7%"
|
||||||
|
textAnchor="end"
|
||||||
|
fill={colors.up}
|
||||||
|
fontSize={12}
|
||||||
|
fontWeight="bold"
|
||||||
|
>
|
||||||
|
{t("Upload")}
|
||||||
|
</text>
|
||||||
|
|
||||||
|
{/* 下载标签 - 右上角下方 */}
|
||||||
|
<text
|
||||||
|
x="98%"
|
||||||
|
y="16%"
|
||||||
|
textAnchor="end"
|
||||||
|
fill={colors.down}
|
||||||
|
fontSize={12}
|
||||||
|
fontWeight="bold"
|
||||||
|
>
|
||||||
|
{t("Download")}
|
||||||
|
</text>
|
||||||
|
</LineChart>
|
||||||
|
) : (
|
||||||
|
<AreaChart {...chartConfig}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={colors.grid} opacity={0.3} />
|
||||||
|
<XAxis
|
||||||
|
dataKey="name"
|
||||||
|
tick={{ fontSize: 10, fill: colors.text }}
|
||||||
|
tickLine={{ stroke: colors.grid }}
|
||||||
|
axisLine={{ stroke: colors.grid }}
|
||||||
|
interval="preserveStart"
|
||||||
|
tickFormatter={formatXLabel}
|
||||||
|
minTickGap={30}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tickFormatter={formatYAxis}
|
||||||
|
tick={{ fontSize: 10, fill: colors.text }}
|
||||||
|
tickLine={{ stroke: colors.grid }}
|
||||||
|
axisLine={{ stroke: colors.grid }}
|
||||||
|
width={40}
|
||||||
|
domain={[0, "auto"]}
|
||||||
|
padding={{ top: 10, bottom: 0 }}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
formatter={formatTooltip}
|
||||||
|
labelFormatter={(label) => `${t("Time")}: ${label}`}
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: colors.tooltip,
|
||||||
|
borderColor: colors.grid,
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
itemStyle={{ color: colors.text }}
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type={curveType}
|
||||||
|
{...commonLineProps}
|
||||||
|
dataKey="up"
|
||||||
|
name={t("Upload")}
|
||||||
|
stroke={colors.up}
|
||||||
|
fill={colors.up}
|
||||||
|
fillOpacity={0.2}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type={curveType}
|
||||||
|
{...commonLineProps}
|
||||||
|
dataKey="down"
|
||||||
|
name={t("Download")}
|
||||||
|
stroke={colors.down}
|
||||||
|
fill={colors.down}
|
||||||
|
fillOpacity={0.3}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 可点击的时间范围标签 */}
|
||||||
|
<text
|
||||||
|
x="1%"
|
||||||
|
y="6%"
|
||||||
|
textAnchor="start"
|
||||||
|
fill={theme.palette.text.secondary}
|
||||||
|
fontSize={11}
|
||||||
|
fontWeight="bold"
|
||||||
|
onClick={handleTimeRangeClick}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{getTimeRangeText()}
|
||||||
|
</text>
|
||||||
|
|
||||||
|
{/* 上传标签 - 右上角 */}
|
||||||
|
<text
|
||||||
|
x="98%"
|
||||||
|
y="7%"
|
||||||
|
textAnchor="end"
|
||||||
|
fill={colors.up}
|
||||||
|
fontSize={12}
|
||||||
|
fontWeight="bold"
|
||||||
|
>
|
||||||
|
{t("Upload")}
|
||||||
|
</text>
|
||||||
|
|
||||||
|
{/* 下载标签 - 右上角下方 */}
|
||||||
|
<text
|
||||||
|
x="98%"
|
||||||
|
y="16%"
|
||||||
|
textAnchor="end"
|
||||||
|
fill={colors.down}
|
||||||
|
fontSize={12}
|
||||||
|
fontWeight="bold"
|
||||||
|
>
|
||||||
|
{t("Download")}
|
||||||
|
</text>
|
||||||
|
</AreaChart>
|
||||||
|
)}
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useRef, useCallback, memo } from "react";
|
import { useState, useEffect, useRef, useCallback, memo, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
Typography,
|
Typography,
|
||||||
@ -62,6 +62,7 @@ declare global {
|
|||||||
|
|
||||||
// 控制更新频率
|
// 控制更新频率
|
||||||
const CONNECTIONS_UPDATE_INTERVAL = 5000; // 5秒更新一次连接数据
|
const CONNECTIONS_UPDATE_INTERVAL = 5000; // 5秒更新一次连接数据
|
||||||
|
const THROTTLE_TRAFFIC_UPDATE = 500; // 500ms节流流量数据更新
|
||||||
|
|
||||||
// 统计卡片组件 - 使用memo优化
|
// 统计卡片组件 - 使用memo优化
|
||||||
const CompactStatCard = memo(({
|
const CompactStatCard = memo(({
|
||||||
@ -74,18 +75,18 @@ const CompactStatCard = memo(({
|
|||||||
}: StatCardProps) => {
|
}: StatCardProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
// 获取调色板颜色
|
// 获取调色板颜色 - 使用useMemo避免重复计算
|
||||||
const getColorFromPalette = (colorName: string) => {
|
const colorValue = useMemo(() => {
|
||||||
const palette = theme.palette;
|
const palette = theme.palette;
|
||||||
if (
|
if (
|
||||||
colorName in palette &&
|
color in palette &&
|
||||||
palette[colorName as keyof typeof palette] &&
|
palette[color as keyof typeof palette] &&
|
||||||
"main" in (palette[colorName as keyof typeof palette] as PaletteColor)
|
"main" in (palette[color as keyof typeof palette] as PaletteColor)
|
||||||
) {
|
) {
|
||||||
return (palette[colorName as keyof typeof palette] as PaletteColor).main;
|
return (palette[color as keyof typeof palette] as PaletteColor).main;
|
||||||
}
|
}
|
||||||
return palette.primary.main;
|
return palette.primary.main;
|
||||||
};
|
}, [theme.palette, color]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
@ -94,14 +95,14 @@ const CompactStatCard = memo(({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
bgcolor: alpha(getColorFromPalette(color), 0.05),
|
bgcolor: alpha(colorValue, 0.05),
|
||||||
border: `1px solid ${alpha(getColorFromPalette(color), 0.15)}`,
|
border: `1px solid ${alpha(colorValue, 0.15)}`,
|
||||||
padding: "8px",
|
padding: "8px",
|
||||||
transition: "all 0.2s ease-in-out",
|
transition: "all 0.2s ease-in-out",
|
||||||
cursor: onClick ? "pointer" : "default",
|
cursor: onClick ? "pointer" : "default",
|
||||||
"&:hover": onClick ? {
|
"&:hover": onClick ? {
|
||||||
bgcolor: alpha(getColorFromPalette(color), 0.1),
|
bgcolor: alpha(colorValue, 0.1),
|
||||||
border: `1px solid ${alpha(getColorFromPalette(color), 0.3)}`,
|
border: `1px solid ${alpha(colorValue, 0.3)}`,
|
||||||
boxShadow: `0 4px 8px rgba(0,0,0,0.05)`,
|
boxShadow: `0 4px 8px rgba(0,0,0,0.05)`,
|
||||||
} : {},
|
} : {},
|
||||||
}}
|
}}
|
||||||
@ -119,8 +120,8 @@ const CompactStatCard = memo(({
|
|||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
borderRadius: "50%",
|
borderRadius: "50%",
|
||||||
bgcolor: alpha(getColorFromPalette(color), 0.1),
|
bgcolor: alpha(colorValue, 0.1),
|
||||||
color: getColorFromPalette(color),
|
color: colorValue,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
@ -156,24 +157,24 @@ export const EnhancedTrafficStats = () => {
|
|||||||
const pageVisible = useVisibility();
|
const pageVisible = useVisibility();
|
||||||
const [isDebug, setIsDebug] = useState(false);
|
const [isDebug, setIsDebug] = useState(false);
|
||||||
|
|
||||||
// 为流量数据和内存数据准备状态
|
// 使用单一状态对象减少状态更新次数
|
||||||
const [trafficData, setTrafficData] = useState<ITrafficItem>({
|
const [stats, setStats] = useState({
|
||||||
up: 0,
|
traffic: { up: 0, down: 0 },
|
||||||
down: 0,
|
memory: { inuse: 0, oslimit: undefined as number | undefined },
|
||||||
});
|
connections: { uploadTotal: 0, downloadTotal: 0, activeConnections: 0 },
|
||||||
const [memoryData, setMemoryData] = useState<MemoryUsage>({ inuse: 0 });
|
|
||||||
const [trafficStats, setTrafficStats] = useState<TrafficStatData>({
|
|
||||||
uploadTotal: 0,
|
|
||||||
downloadTotal: 0,
|
|
||||||
activeConnections: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 创建一个标记来追踪最后更新时间,用于节流
|
||||||
|
const lastUpdateRef = useRef({ traffic: 0 });
|
||||||
|
|
||||||
// 是否显示流量图表
|
// 是否显示流量图表
|
||||||
const trafficGraph = verge?.traffic_graph ?? true;
|
const trafficGraph = verge?.traffic_graph ?? true;
|
||||||
|
|
||||||
// WebSocket引用
|
// WebSocket引用
|
||||||
const trafficSocketRef = useRef<ReturnType<typeof createAuthSockette> | null>(null);
|
const socketRefs = useRef({
|
||||||
const memorySocketRef = useRef<ReturnType<typeof createAuthSockette> | null>(null);
|
traffic: null as ReturnType<typeof createAuthSockette> | null,
|
||||||
|
memory: null as ReturnType<typeof createAuthSockette> | null,
|
||||||
|
});
|
||||||
|
|
||||||
// 获取连接数据
|
// 获取连接数据
|
||||||
const fetchConnections = useCallback(async () => {
|
const fetchConnections = useCallback(async () => {
|
||||||
@ -191,11 +192,14 @@ export const EnhancedTrafficStats = () => {
|
|||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
setTrafficStats({
|
setStats(prev => ({
|
||||||
uploadTotal,
|
...prev,
|
||||||
downloadTotal,
|
connections: {
|
||||||
activeConnections: connections.connections.length,
|
uploadTotal,
|
||||||
});
|
downloadTotal,
|
||||||
|
activeConnections: connections.connections.length,
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to fetch connections:", err);
|
console.error("Failed to fetch connections:", err);
|
||||||
@ -204,11 +208,11 @@ export const EnhancedTrafficStats = () => {
|
|||||||
|
|
||||||
// 定期更新连接数据
|
// 定期更新连接数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pageVisible) {
|
if (!pageVisible) return;
|
||||||
fetchConnections();
|
|
||||||
const intervalId = setInterval(fetchConnections, CONNECTIONS_UPDATE_INTERVAL);
|
fetchConnections();
|
||||||
return () => clearInterval(intervalId);
|
const intervalId = setInterval(fetchConnections, CONNECTIONS_UPDATE_INTERVAL);
|
||||||
}
|
return () => clearInterval(intervalId);
|
||||||
}, [pageVisible, fetchConnections]);
|
}, [pageVisible, fetchConnections]);
|
||||||
|
|
||||||
// 检查是否支持调试
|
// 检查是否支持调试
|
||||||
@ -216,7 +220,7 @@ export const EnhancedTrafficStats = () => {
|
|||||||
isDebugEnabled().then((flag) => setIsDebug(flag));
|
isDebugEnabled().then((flag) => setIsDebug(flag));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 处理流量数据更新
|
// 处理流量数据更新 - 使用节流控制更新频率
|
||||||
const handleTrafficUpdate = useCallback((event: MessageEvent) => {
|
const handleTrafficUpdate = useCallback((event: MessageEvent) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data) as ITrafficItem;
|
const data = JSON.parse(event.data) as ITrafficItem;
|
||||||
@ -225,28 +229,40 @@ export const EnhancedTrafficStats = () => {
|
|||||||
typeof data.up === "number" &&
|
typeof data.up === "number" &&
|
||||||
typeof data.down === "number"
|
typeof data.down === "number"
|
||||||
) {
|
) {
|
||||||
|
// 使用节流控制更新频率
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastUpdateRef.current.traffic < THROTTLE_TRAFFIC_UPDATE) {
|
||||||
|
// 如果距离上次更新时间小于阈值,只更新图表不更新状态
|
||||||
|
if (trafficRef.current) {
|
||||||
|
trafficRef.current.appendData({
|
||||||
|
up: data.up,
|
||||||
|
down: data.down,
|
||||||
|
timestamp: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新最后更新时间
|
||||||
|
lastUpdateRef.current.traffic = now;
|
||||||
|
|
||||||
// 验证数据有效性,防止NaN
|
// 验证数据有效性,防止NaN
|
||||||
const safeUp = isNaN(data.up) ? 0 : data.up;
|
const safeUp = isNaN(data.up) ? 0 : data.up;
|
||||||
const safeDown = isNaN(data.down) ? 0 : data.down;
|
const safeDown = isNaN(data.down) ? 0 : data.down;
|
||||||
|
|
||||||
setTrafficData({
|
// 批量更新状态
|
||||||
up: safeUp,
|
setStats(prev => ({
|
||||||
down: safeDown,
|
...prev,
|
||||||
});
|
traffic: { up: safeUp, down: safeDown }
|
||||||
|
}));
|
||||||
|
|
||||||
// 更新图表数据
|
// 更新图表数据
|
||||||
if (trafficRef.current) {
|
if (trafficRef.current) {
|
||||||
trafficRef.current.appendData({
|
trafficRef.current.appendData({
|
||||||
up: safeUp,
|
up: safeUp,
|
||||||
down: safeDown,
|
down: safeDown,
|
||||||
timestamp: Date.now(),
|
timestamp: now,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清除之前可能存在的动画帧
|
|
||||||
if (window.animationFrameId) {
|
|
||||||
cancelAnimationFrame(window.animationFrameId);
|
|
||||||
window.animationFrameId = undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -259,74 +275,50 @@ export const EnhancedTrafficStats = () => {
|
|||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data) as MemoryUsage;
|
const data = JSON.parse(event.data) as MemoryUsage;
|
||||||
if (data && typeof data.inuse === "number") {
|
if (data && typeof data.inuse === "number") {
|
||||||
setMemoryData({
|
setStats(prev => ({
|
||||||
inuse: isNaN(data.inuse) ? 0 : data.inuse,
|
...prev,
|
||||||
oslimit: data.oslimit,
|
memory: {
|
||||||
});
|
inuse: isNaN(data.inuse) ? 0 : data.inuse,
|
||||||
|
oslimit: data.oslimit,
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Memory] 解析数据错误:", err);
|
console.error("[Memory] 解析数据错误:", err);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 使用 WebSocket 连接获取流量数据
|
// 使用 WebSocket 连接获取数据 - 合并流量和内存连接逻辑
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!clashInfo || !pageVisible) return;
|
if (!clashInfo || !pageVisible) return;
|
||||||
|
|
||||||
const { server, secret = "" } = clashInfo;
|
const { server, secret = "" } = clashInfo;
|
||||||
if (!server) return;
|
if (!server) return;
|
||||||
|
|
||||||
|
// 清理现有连接的函数
|
||||||
|
const cleanupSockets = () => {
|
||||||
|
Object.values(socketRefs.current).forEach(socket => {
|
||||||
|
if (socket) {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
socketRefs.current = { traffic: null, memory: null };
|
||||||
|
};
|
||||||
|
|
||||||
// 关闭现有连接
|
// 关闭现有连接
|
||||||
if (trafficSocketRef.current) {
|
cleanupSockets();
|
||||||
trafficSocketRef.current.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新连接
|
// 创建新连接
|
||||||
trafficSocketRef.current = createAuthSockette(`${server}/traffic`, secret, {
|
socketRefs.current.traffic = createAuthSockette(`${server}/traffic`, secret, {
|
||||||
onmessage: handleTrafficUpdate,
|
onmessage: handleTrafficUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
socketRefs.current.memory = createAuthSockette(`${server}/memory`, secret, {
|
||||||
if (trafficSocketRef.current) {
|
|
||||||
trafficSocketRef.current.close();
|
|
||||||
trafficSocketRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [clashInfo, pageVisible, handleTrafficUpdate]);
|
|
||||||
|
|
||||||
// 使用 WebSocket 连接获取内存数据
|
|
||||||
useEffect(() => {
|
|
||||||
if (!clashInfo || !pageVisible) return;
|
|
||||||
|
|
||||||
const { server, secret = "" } = clashInfo;
|
|
||||||
if (!server) return;
|
|
||||||
|
|
||||||
// 关闭现有连接
|
|
||||||
if (memorySocketRef.current) {
|
|
||||||
memorySocketRef.current.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新连接
|
|
||||||
memorySocketRef.current = createAuthSockette(`${server}/memory`, secret, {
|
|
||||||
onmessage: handleMemoryUpdate,
|
onmessage: handleMemoryUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return cleanupSockets;
|
||||||
if (memorySocketRef.current) {
|
}, [clashInfo, pageVisible, handleTrafficUpdate, handleMemoryUpdate]);
|
||||||
memorySocketRef.current.close();
|
|
||||||
memorySocketRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [clashInfo, pageVisible, handleMemoryUpdate]);
|
|
||||||
|
|
||||||
// 解析流量数据
|
|
||||||
const [up, upUnit] = parseTraffic(trafficData.up);
|
|
||||||
const [down, downUnit] = parseTraffic(trafficData.down);
|
|
||||||
const [inuse, inuseUnit] = parseTraffic(memoryData.inuse);
|
|
||||||
const [uploadTotal, uploadTotalUnit] = parseTraffic(trafficStats.uploadTotal);
|
|
||||||
const [downloadTotal, downloadTotalUnit] = parseTraffic(
|
|
||||||
trafficStats.downloadTotal,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 执行垃圾回收
|
// 执行垃圾回收
|
||||||
const handleGarbageCollection = useCallback(async () => {
|
const handleGarbageCollection = useCallback(async () => {
|
||||||
@ -340,8 +332,22 @@ export const EnhancedTrafficStats = () => {
|
|||||||
}
|
}
|
||||||
}, [isDebug]);
|
}, [isDebug]);
|
||||||
|
|
||||||
// 渲染流量图表
|
// 使用useMemo计算解析后的流量数据
|
||||||
const renderTrafficGraph = useCallback(() => {
|
const parsedData = useMemo(() => {
|
||||||
|
const [up, upUnit] = parseTraffic(stats.traffic.up);
|
||||||
|
const [down, downUnit] = parseTraffic(stats.traffic.down);
|
||||||
|
const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse);
|
||||||
|
const [uploadTotal, uploadTotalUnit] = parseTraffic(stats.connections.uploadTotal);
|
||||||
|
const [downloadTotal, downloadTotalUnit] = parseTraffic(stats.connections.downloadTotal);
|
||||||
|
|
||||||
|
return {
|
||||||
|
up, upUnit, down, downUnit, inuse, inuseUnit,
|
||||||
|
uploadTotal, uploadTotalUnit, downloadTotal, downloadTotalUnit
|
||||||
|
};
|
||||||
|
}, [stats]);
|
||||||
|
|
||||||
|
// 渲染流量图表 - 使用useMemo缓存渲染结果
|
||||||
|
const trafficGraphComponent = useMemo(() => {
|
||||||
if (!trafficGraph || !pageVisible) return null;
|
if (!trafficGraph || !pageVisible) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -382,58 +388,58 @@ export const EnhancedTrafficStats = () => {
|
|||||||
);
|
);
|
||||||
}, [trafficGraph, pageVisible, theme.palette.divider, isDebug]);
|
}, [trafficGraph, pageVisible, theme.palette.divider, isDebug]);
|
||||||
|
|
||||||
// 统计卡片配置
|
// 使用useMemo计算统计卡片配置
|
||||||
const statCards = [
|
const statCards = useMemo(() => [
|
||||||
{
|
{
|
||||||
icon: <ArrowUpwardRounded fontSize="small" />,
|
icon: <ArrowUpwardRounded fontSize="small" />,
|
||||||
title: t("Upload Speed"),
|
title: t("Upload Speed"),
|
||||||
value: up,
|
value: parsedData.up,
|
||||||
unit: `${upUnit}/s`,
|
unit: `${parsedData.upUnit}/s`,
|
||||||
color: "secondary" as const,
|
color: "secondary" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <ArrowDownwardRounded fontSize="small" />,
|
icon: <ArrowDownwardRounded fontSize="small" />,
|
||||||
title: t("Download Speed"),
|
title: t("Download Speed"),
|
||||||
value: down,
|
value: parsedData.down,
|
||||||
unit: `${downUnit}/s`,
|
unit: `${parsedData.downUnit}/s`,
|
||||||
color: "primary" as const,
|
color: "primary" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <LinkRounded fontSize="small" />,
|
icon: <LinkRounded fontSize="small" />,
|
||||||
title: t("Active Connections"),
|
title: t("Active Connections"),
|
||||||
value: trafficStats.activeConnections,
|
value: stats.connections.activeConnections,
|
||||||
unit: "",
|
unit: "",
|
||||||
color: "success" as const,
|
color: "success" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <CloudUploadRounded fontSize="small" />,
|
icon: <CloudUploadRounded fontSize="small" />,
|
||||||
title: t("Uploaded"),
|
title: t("Uploaded"),
|
||||||
value: uploadTotal,
|
value: parsedData.uploadTotal,
|
||||||
unit: uploadTotalUnit,
|
unit: parsedData.uploadTotalUnit,
|
||||||
color: "secondary" as const,
|
color: "secondary" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <CloudDownloadRounded fontSize="small" />,
|
icon: <CloudDownloadRounded fontSize="small" />,
|
||||||
title: t("Downloaded"),
|
title: t("Downloaded"),
|
||||||
value: downloadTotal,
|
value: parsedData.downloadTotal,
|
||||||
unit: downloadTotalUnit,
|
unit: parsedData.downloadTotalUnit,
|
||||||
color: "primary" as const,
|
color: "primary" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <MemoryRounded fontSize="small" />,
|
icon: <MemoryRounded fontSize="small" />,
|
||||||
title: t("Memory Usage"),
|
title: t("Memory Usage"),
|
||||||
value: inuse,
|
value: parsedData.inuse,
|
||||||
unit: inuseUnit,
|
unit: parsedData.inuseUnit,
|
||||||
color: "error" as const,
|
color: "error" as const,
|
||||||
onClick: isDebug ? handleGarbageCollection : undefined,
|
onClick: isDebug ? handleGarbageCollection : undefined,
|
||||||
},
|
},
|
||||||
];
|
], [t, parsedData, stats.connections.activeConnections, isDebug, handleGarbageCollection]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
|
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
|
||||||
<Grid size={12}>
|
<Grid size={12}>
|
||||||
{/* 流量图表区域 */}
|
{/* 流量图表区域 */}
|
||||||
{renderTrafficGraph()}
|
{trafficGraphComponent}
|
||||||
</Grid>
|
</Grid>
|
||||||
{/* 统计卡片区域 */}
|
{/* 统计卡片区域 */}
|
||||||
{statCards.map((card, index) => (
|
{statCards.map((card, index) => (
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
import { useState } from "react";
|
import { useMemo, useCallback, useState } from "react";
|
||||||
import { openWebUrl, updateProfile } from "@/services/cmds";
|
import { openWebUrl, updateProfile } from "@/services/cmds";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { Notice } from "@/components/base";
|
import { Notice } from "@/components/base";
|
||||||
@ -35,16 +35,16 @@ const round = keyframes`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// 辅助函数解析URL和过期时间
|
// 辅助函数解析URL和过期时间
|
||||||
function parseUrl(url?: string) {
|
const parseUrl = (url?: string) => {
|
||||||
if (!url) return "-";
|
if (!url) return "-";
|
||||||
if (url.startsWith("http")) return new URL(url).host;
|
if (url.startsWith("http")) return new URL(url).host;
|
||||||
return "local";
|
return "local";
|
||||||
}
|
};
|
||||||
|
|
||||||
function parseExpire(expire?: number) {
|
const parseExpire = (expire?: number) => {
|
||||||
if (!expire) return "-";
|
if (!expire) return "-";
|
||||||
return dayjs(expire * 1000).format("YYYY-MM-DD");
|
return dayjs(expire * 1000).format("YYYY-MM-DD");
|
||||||
}
|
};
|
||||||
|
|
||||||
// 使用类型定义,而不是导入
|
// 使用类型定义,而不是导入
|
||||||
interface ProfileExtra {
|
interface ProfileExtra {
|
||||||
@ -64,20 +64,178 @@ export interface ProfileItem {
|
|||||||
updated?: number;
|
updated?: number;
|
||||||
extra?: ProfileExtra;
|
extra?: ProfileExtra;
|
||||||
home?: string;
|
home?: string;
|
||||||
option?: any; // 添加option以兼容原始类型
|
option?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HomeProfileCardProps {
|
export interface HomeProfileCardProps {
|
||||||
current: ProfileItem | null | undefined;
|
current: ProfileItem | null | undefined;
|
||||||
|
onProfileUpdated?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HomeProfileCard = ({ current }: HomeProfileCardProps) => {
|
// 提取独立组件减少主组件复杂度
|
||||||
|
const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||||
|
current: ProfileItem;
|
||||||
|
onUpdateProfile: () => void;
|
||||||
|
updating: boolean;
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const usedTraffic = useMemo(() => {
|
||||||
|
if (!current.extra) return 0;
|
||||||
|
return current.extra.upload + current.extra.download;
|
||||||
|
}, [current.extra]);
|
||||||
|
|
||||||
|
const trafficPercentage = useMemo(() => {
|
||||||
|
if (!current.extra || !current.extra.total) return 1;
|
||||||
|
return Math.min(
|
||||||
|
Math.round((usedTraffic * 100) / (current.extra.total + 0.01)) + 1,
|
||||||
|
100
|
||||||
|
);
|
||||||
|
}, [current.extra, usedTraffic]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
{current.url && (
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
|
<DnsOutlined fontSize="small" color="action" />
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("From")}:{" "}
|
||||||
|
{current.home ? (
|
||||||
|
<Link
|
||||||
|
component="button"
|
||||||
|
fontWeight="medium"
|
||||||
|
onClick={() => current.home && openWebUrl(current.home)}
|
||||||
|
sx={{ display: "inline-flex", alignItems: "center" }}
|
||||||
|
>
|
||||||
|
{parseUrl(current.url)}
|
||||||
|
<LaunchOutlined
|
||||||
|
fontSize="inherit"
|
||||||
|
sx={{ ml: 0.5, fontSize: "0.8rem", opacity: 0.7 }}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Box component="span" fontWeight="medium">
|
||||||
|
{parseUrl(current.url)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{current.updated && (
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
|
<UpdateOutlined
|
||||||
|
fontSize="small"
|
||||||
|
color="action"
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
animation: updating ? `${round} 1.5s linear infinite` : "none",
|
||||||
|
}}
|
||||||
|
onClick={onUpdateProfile}
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ cursor: "pointer" }}
|
||||||
|
onClick={onUpdateProfile}
|
||||||
|
>
|
||||||
|
{t("Update Time")}:{" "}
|
||||||
|
<Box component="span" fontWeight="medium">
|
||||||
|
{dayjs(current.updated * 1000).format("YYYY-MM-DD HH:mm")}
|
||||||
|
</Box>
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{current.extra && (
|
||||||
|
<>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
|
<SpeedOutlined fontSize="small" color="action" />
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("Used / Total")}:{" "}
|
||||||
|
<Box component="span" fontWeight="medium">
|
||||||
|
{parseTraffic(usedTraffic)} / {parseTraffic(current.extra.total)}
|
||||||
|
</Box>
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{current.extra.expire > 0 && (
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
|
<EventOutlined fontSize="small" color="action" />
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("Expire Time")}:{" "}
|
||||||
|
<Box component="span" fontWeight="medium">
|
||||||
|
{parseExpire(current.extra.expire)}
|
||||||
|
</Box>
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box sx={{ mt: 1 }}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ mb: 0.5, display: "block" }}
|
||||||
|
>
|
||||||
|
{trafficPercentage}%
|
||||||
|
</Typography>
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={trafficPercentage}
|
||||||
|
sx={{
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: alpha(theme.palette.primary.main, 0.12),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提取空配置组件
|
||||||
|
const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
py: 2.4,
|
||||||
|
cursor: "pointer",
|
||||||
|
"&:hover": { bgcolor: "action.hover" },
|
||||||
|
borderRadius: 2,
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<CloudUploadOutlined
|
||||||
|
sx={{ fontSize: 60, color: "primary.main", mb: 2 }}
|
||||||
|
/>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
{t("Import")} {t("Profiles")}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("Click to import subscription")}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// 更新当前订阅
|
// 更新当前订阅
|
||||||
const [updating, setUpdating] = useState(false);
|
const [updating, setUpdating] = useState(false);
|
||||||
|
|
||||||
const onUpdateProfile = useLockFn(async () => {
|
const onUpdateProfile = useLockFn(async () => {
|
||||||
if (!current?.uid) return;
|
if (!current?.uid) return;
|
||||||
|
|
||||||
@ -85,6 +243,7 @@ export const HomeProfileCard = ({ current }: HomeProfileCardProps) => {
|
|||||||
try {
|
try {
|
||||||
await updateProfile(current.uid);
|
await updateProfile(current.uid);
|
||||||
Notice.success(t("Update subscription successfully"));
|
Notice.success(t("Update subscription successfully"));
|
||||||
|
onProfileUpdated?.();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Notice.error(err?.message || err.toString());
|
Notice.error(err?.message || err.toString());
|
||||||
} finally {
|
} finally {
|
||||||
@ -93,204 +252,71 @@ export const HomeProfileCard = ({ current }: HomeProfileCardProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 导航到订阅页面
|
// 导航到订阅页面
|
||||||
const goToProfiles = () => {
|
const goToProfiles = useCallback(() => {
|
||||||
navigate("/profile");
|
navigate("/profile");
|
||||||
};
|
}, [navigate]);
|
||||||
|
|
||||||
|
// 卡片标题
|
||||||
|
const cardTitle = useMemo(() => {
|
||||||
|
if (!current) return t("Profiles");
|
||||||
|
|
||||||
|
if (!current.home) return current.name;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
component="button"
|
||||||
|
variant="h6"
|
||||||
|
fontWeight="medium"
|
||||||
|
fontSize={18}
|
||||||
|
onClick={() => current.home && openWebUrl(current.home)}
|
||||||
|
sx={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
color: "inherit",
|
||||||
|
textDecoration: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{current.name}
|
||||||
|
<LaunchOutlined
|
||||||
|
fontSize="inherit"
|
||||||
|
sx={{ ml: 0.5, fontSize: "0.8rem", opacity: 0.7 }}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}, [current, t]);
|
||||||
|
|
||||||
|
// 卡片操作按钮
|
||||||
|
const cardAction = useMemo(() => {
|
||||||
|
if (!current) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={goToProfiles}
|
||||||
|
endIcon={<StorageOutlined fontSize="small" />}
|
||||||
|
sx={{ borderRadius: 1.5 }}
|
||||||
|
>
|
||||||
|
{t("Label-Profiles")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}, [current, goToProfiles, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
title={
|
title={cardTitle}
|
||||||
current ? (
|
|
||||||
current.home ? (
|
|
||||||
<Link
|
|
||||||
component="button"
|
|
||||||
variant="h6"
|
|
||||||
fontWeight="medium"
|
|
||||||
fontSize={18}
|
|
||||||
onClick={() => current.home && openWebUrl(current.home)}
|
|
||||||
sx={{
|
|
||||||
display: "inline-flex",
|
|
||||||
alignItems: "center",
|
|
||||||
color: "inherit",
|
|
||||||
textDecoration: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{current.name}
|
|
||||||
<LaunchOutlined
|
|
||||||
fontSize="inherit"
|
|
||||||
sx={{ ml: 0.5, fontSize: "0.8rem", opacity: 0.7 }}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
current.name
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
t("Profiles")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
icon={<CloudUploadOutlined />}
|
icon={<CloudUploadOutlined />}
|
||||||
iconColor="info"
|
iconColor="info"
|
||||||
action={
|
action={cardAction}
|
||||||
current && (
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
onClick={goToProfiles}
|
|
||||||
endIcon={<StorageOutlined fontSize="small" />}
|
|
||||||
sx={{ borderRadius: 1.5 }}
|
|
||||||
>
|
|
||||||
{t("Label-Profiles")}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{current ? (
|
{current ? (
|
||||||
// 已导入订阅,显示详情
|
<ProfileDetails
|
||||||
<Box>
|
current={current}
|
||||||
<Stack spacing={2}>
|
onUpdateProfile={onUpdateProfile}
|
||||||
{current.url && (
|
updating={updating}
|
||||||
<Stack direction="row" alignItems="center" spacing={1}>
|
/>
|
||||||
<DnsOutlined fontSize="small" color="action" />
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{t("From")}:{" "}
|
|
||||||
{current.home ? (
|
|
||||||
<Link
|
|
||||||
component="button"
|
|
||||||
fontWeight="medium"
|
|
||||||
onClick={() => current.home && openWebUrl(current.home)}
|
|
||||||
sx={{ display: "inline-flex", alignItems: "center" }}
|
|
||||||
>
|
|
||||||
{parseUrl(current.url)}
|
|
||||||
<LaunchOutlined
|
|
||||||
fontSize="inherit"
|
|
||||||
sx={{ ml: 0.5, fontSize: "0.8rem", opacity: 0.7 }}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Box component="span" fontWeight="medium">
|
|
||||||
{parseUrl(current.url)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{current.updated && (
|
|
||||||
<Stack direction="row" alignItems="center" spacing={1}>
|
|
||||||
<UpdateOutlined
|
|
||||||
fontSize="small"
|
|
||||||
color="action"
|
|
||||||
sx={{
|
|
||||||
cursor: "pointer",
|
|
||||||
animation: updating
|
|
||||||
? `${round} 1.5s linear infinite`
|
|
||||||
: "none",
|
|
||||||
}}
|
|
||||||
onClick={onUpdateProfile}
|
|
||||||
/>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ cursor: "pointer" }}
|
|
||||||
onClick={onUpdateProfile}
|
|
||||||
>
|
|
||||||
{t("Update Time")}:{" "}
|
|
||||||
<Box component="span" fontWeight="medium">
|
|
||||||
{dayjs(current.updated * 1000).format("YYYY-MM-DD HH:mm")}
|
|
||||||
</Box>
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{current.extra && (
|
|
||||||
<>
|
|
||||||
<Stack direction="row" alignItems="center" spacing={1}>
|
|
||||||
<SpeedOutlined fontSize="small" color="action" />
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{t("Used / Total")}:{" "}
|
|
||||||
<Box component="span" fontWeight="medium">
|
|
||||||
{parseTraffic(
|
|
||||||
current.extra.upload + current.extra.download,
|
|
||||||
)}{" "}
|
|
||||||
/ {parseTraffic(current.extra.total)}
|
|
||||||
</Box>
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{current.extra.expire > 0 && (
|
|
||||||
<Stack direction="row" alignItems="center" spacing={1}>
|
|
||||||
<EventOutlined fontSize="small" color="action" />
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{t("Expire Time")}:{" "}
|
|
||||||
<Box component="span" fontWeight="medium">
|
|
||||||
{parseExpire(current.extra.expire)}
|
|
||||||
</Box>
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box sx={{ mt: 1 }}>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ mb: 0.5, display: "block" }}
|
|
||||||
>
|
|
||||||
{Math.min(
|
|
||||||
Math.round(
|
|
||||||
((current.extra.download + current.extra.upload) *
|
|
||||||
100) /
|
|
||||||
(current.extra.total + 0.01),
|
|
||||||
) + 1,
|
|
||||||
100,
|
|
||||||
)}
|
|
||||||
%
|
|
||||||
</Typography>
|
|
||||||
<LinearProgress
|
|
||||||
variant="determinate"
|
|
||||||
value={Math.min(
|
|
||||||
Math.round(
|
|
||||||
((current.extra.download + current.extra.upload) *
|
|
||||||
100) /
|
|
||||||
(current.extra.total + 0.01),
|
|
||||||
) + 1,
|
|
||||||
100,
|
|
||||||
)}
|
|
||||||
sx={{
|
|
||||||
height: 8,
|
|
||||||
borderRadius: 4,
|
|
||||||
backgroundColor: alpha(theme.palette.primary.main, 0.12),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
) : (
|
) : (
|
||||||
// 未导入订阅,显示导入按钮
|
<EmptyProfile onClick={goToProfiles} />
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
py: 2.4,
|
|
||||||
cursor: "pointer",
|
|
||||||
"&:hover": { bgcolor: "action.hover" },
|
|
||||||
borderRadius: 2,
|
|
||||||
}}
|
|
||||||
onClick={goToProfiles}
|
|
||||||
>
|
|
||||||
<CloudUploadOutlined
|
|
||||||
sx={{ fontSize: 60, color: "primary.main", mb: 2 }}
|
|
||||||
/>
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
{t("Import")} {t("Profiles")}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{t("Click to import subscription")}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</EnhancedCard>
|
</EnhancedCard>
|
||||||
);
|
);
|
||||||
|
@ -15,15 +15,50 @@ import {
|
|||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { EnhancedCard } from "./enhanced-card";
|
import { EnhancedCard } from "./enhanced-card";
|
||||||
import { getIpInfo } from "@/services/api";
|
import { getIpInfo } from "@/services/api";
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback, memo } from "react";
|
||||||
|
|
||||||
// 定义刷新时间(秒)
|
// 定义刷新时间(秒)
|
||||||
const IP_REFRESH_SECONDS = 300;
|
const IP_REFRESH_SECONDS = 300;
|
||||||
|
|
||||||
|
// 提取InfoItem子组件并使用memo优化
|
||||||
|
const InfoItem = memo(({ label, value }: { label: string; value: string }) => (
|
||||||
|
<Box sx={{ mb: 0.7, display: "flex", alignItems: "flex-start" }}>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ minwidth: 60, mr: 0.5, flexShrink: 0, textAlign: "right" }}
|
||||||
|
>
|
||||||
|
{label}:
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
ml: 0.5,
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
whiteSpace: "normal",
|
||||||
|
flexGrow: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value || "Unknown"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
));
|
||||||
|
|
||||||
|
// 获取国旗表情
|
||||||
|
const getCountryFlag = (countryCode: string) => {
|
||||||
|
if (!countryCode) return "";
|
||||||
|
const codePoints = countryCode
|
||||||
|
.toUpperCase()
|
||||||
|
.split("")
|
||||||
|
.map((char) => 127397 + char.charCodeAt(0));
|
||||||
|
return String.fromCodePoint(...codePoints);
|
||||||
|
};
|
||||||
|
|
||||||
// IP信息卡片组件
|
// IP信息卡片组件
|
||||||
export const IpInfoCard = () => {
|
export const IpInfoCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const theme = useTheme();
|
|
||||||
const [ipInfo, setIpInfo] = useState<any>(null);
|
const [ipInfo, setIpInfo] = useState<any>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
@ -48,250 +83,241 @@ export const IpInfoCard = () => {
|
|||||||
// 组件加载时获取IP信息
|
// 组件加载时获取IP信息
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchIpInfo();
|
fetchIpInfo();
|
||||||
}, [fetchIpInfo]);
|
|
||||||
|
// 倒计时实现优化,减少不必要的重渲染
|
||||||
// 倒计时自动刷新
|
let timer: number | null = null;
|
||||||
useEffect(() => {
|
let currentCount = IP_REFRESH_SECONDS;
|
||||||
const timer = setInterval(() => {
|
|
||||||
setCountdown((prev) => {
|
// 只在必要时更新状态,减少重渲染次数
|
||||||
if (prev <= 1) {
|
const startCountdown = () => {
|
||||||
|
timer = window.setInterval(() => {
|
||||||
|
currentCount -= 1;
|
||||||
|
|
||||||
|
if (currentCount <= 0) {
|
||||||
fetchIpInfo();
|
fetchIpInfo();
|
||||||
return IP_REFRESH_SECONDS;
|
currentCount = IP_REFRESH_SECONDS;
|
||||||
}
|
}
|
||||||
return prev - 1;
|
|
||||||
});
|
// 每5秒或倒计时结束时才更新UI
|
||||||
}, 1000);
|
if (currentCount % 5 === 0 || currentCount <= 0) {
|
||||||
|
setCountdown(currentCount);
|
||||||
return () => clearInterval(timer);
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
startCountdown();
|
||||||
|
return () => {
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
};
|
||||||
}, [fetchIpInfo]);
|
}, [fetchIpInfo]);
|
||||||
|
|
||||||
// 刷新按钮点击处理
|
const toggleShowIp = useCallback(() => {
|
||||||
const handleRefresh = () => {
|
setShowIp(prev => !prev);
|
||||||
fetchIpInfo();
|
}, []);
|
||||||
};
|
|
||||||
|
|
||||||
// 切换显示/隐藏IP
|
// 渲染加载状态
|
||||||
const toggleShowIp = () => {
|
if (loading) {
|
||||||
setShowIp(!showIp);
|
return (
|
||||||
};
|
<EnhancedCard
|
||||||
|
title={t("IP Information")}
|
||||||
// 获取国旗表情
|
icon={<LocationOnOutlined />}
|
||||||
const getCountryFlag = (countryCode: string) => {
|
iconColor="info"
|
||||||
if (!countryCode) return "";
|
action={
|
||||||
const codePoints = countryCode
|
<IconButton size="small" onClick={fetchIpInfo} disabled={true}>
|
||||||
.toUpperCase()
|
<RefreshOutlined />
|
||||||
.split("")
|
</IconButton>
|
||||||
.map((char) => 127397 + char.charCodeAt(0));
|
}
|
||||||
return String.fromCodePoint(...codePoints);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 信息项组件 - 默认不换行,但在需要时可以换行
|
|
||||||
const InfoItem = ({ label, value }: { label: string; value: string }) => (
|
|
||||||
<Box sx={{ mb: 0.7, display: "flex", alignItems: "flex-start" }}>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{
|
|
||||||
minwidth: 60,
|
|
||||||
mr: 0.5,
|
|
||||||
flexShrink: 0,
|
|
||||||
textAlign: "right",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{label}:
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
</Typography>
|
<Skeleton variant="text" width="60%" height={32} />
|
||||||
<Typography
|
<Skeleton variant="text" width="80%" height={24} />
|
||||||
variant="body2"
|
<Skeleton variant="text" width="70%" height={24} />
|
||||||
sx={{
|
<Skeleton variant="text" width="50%" height={24} />
|
||||||
ml: 0.5,
|
</Box>
|
||||||
overflow: "hidden",
|
</EnhancedCard>
|
||||||
textOverflow: "ellipsis",
|
);
|
||||||
wordBreak: "break-word",
|
}
|
||||||
whiteSpace: "normal",
|
|
||||||
flexGrow: 1, // 让内容占用剩余空间
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{value || t("Unknown")}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// 渲染错误状态
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<EnhancedCard
|
||||||
|
title={t("IP Information")}
|
||||||
|
icon={<LocationOnOutlined />}
|
||||||
|
iconColor="info"
|
||||||
|
action={
|
||||||
|
<IconButton size="small" onClick={fetchIpInfo}>
|
||||||
|
<RefreshOutlined />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
height: "100%",
|
||||||
|
color: "error.main",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1" color="error">
|
||||||
|
{error}
|
||||||
|
</Typography>
|
||||||
|
<Button onClick={fetchIpInfo} sx={{ mt: 2 }}>
|
||||||
|
{t("Retry")}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</EnhancedCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染正常数据
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
title={t("IP Information")}
|
title={t("IP Information")}
|
||||||
icon={<LocationOnOutlined />}
|
icon={<LocationOnOutlined />}
|
||||||
iconColor="info"
|
iconColor="info"
|
||||||
action={
|
action={
|
||||||
<IconButton size="small" onClick={handleRefresh} disabled={loading}>
|
<IconButton size="small" onClick={fetchIpInfo}>
|
||||||
<RefreshOutlined />
|
<RefreshOutlined />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Box sx={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
<Box sx={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||||
{loading ? (
|
<Box
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
sx={{
|
||||||
<Skeleton variant="text" width="60%" height={34} />
|
display: "flex",
|
||||||
<Skeleton variant="text" width="80%" height={24} />
|
flexDirection: "row",
|
||||||
<Skeleton variant="text" width="70%" height={24} />
|
flex: 1,
|
||||||
<Skeleton variant="text" width="50%" height={24} />
|
overflow: "hidden",
|
||||||
</Box>
|
}}
|
||||||
) : error ? (
|
>
|
||||||
<Box
|
{/* 左侧:国家和IP地址 */}
|
||||||
sx={{
|
<Box sx={{ width: "40%", overflow: "hidden" }}>
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
height: "100%",
|
|
||||||
color: "error.main",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="body1" color="error">
|
|
||||||
{error}
|
|
||||||
</Typography>
|
|
||||||
<Button onClick={handleRefresh} sx={{ mt: 2 }}>
|
|
||||||
{t("Retry")}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
alignItems: "center",
|
||||||
flex: 1,
|
mb: 1,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 左侧:国家和IP地址 */}
|
<Box
|
||||||
<Box sx={{ width: "40%", overflow: "hidden" }}>
|
component="span"
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
mb: 1,
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
component="span"
|
|
||||||
sx={{
|
|
||||||
fontSize: "1.5rem",
|
|
||||||
mr: 1,
|
|
||||||
display: "inline-block",
|
|
||||||
width: 28,
|
|
||||||
textAlign: "center",
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getCountryFlag(ipInfo?.country_code)}
|
|
||||||
</Box>
|
|
||||||
<Typography
|
|
||||||
variant="subtitle1"
|
|
||||||
sx={{
|
|
||||||
fontWeight: "medium",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
maxWidth: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{ipInfo?.country || t("Unknown")}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box sx={{ display: "flex", alignItems: "center", mb: 1 }}>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ flexShrink: 0 }}
|
|
||||||
>
|
|
||||||
{t("IP")}:
|
|
||||||
</Typography>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
ml: 1,
|
|
||||||
overflow: "hidden",
|
|
||||||
maxWidth: "calc(100% - 30px)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
fontFamily: "monospace",
|
|
||||||
fontSize: "0.75rem",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
wordBreak: "break-all",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{showIp ? ipInfo?.ip : "••••••••••"}
|
|
||||||
</Typography>
|
|
||||||
<IconButton size="small" onClick={toggleShowIp}>
|
|
||||||
{showIp ? (
|
|
||||||
<VisibilityOffOutlined fontSize="small" />
|
|
||||||
) : (
|
|
||||||
<VisibilityOutlined fontSize="small" />
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<InfoItem
|
|
||||||
label={t("ASN")}
|
|
||||||
value={ipInfo?.asn ? `AS${ipInfo.asn}` : "N/A"}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 右侧:组织、ISP和位置信息 */}
|
|
||||||
<Box sx={{ width: "60%", overflow: "auto" }}>
|
|
||||||
<InfoItem label={t("ISP")} value={ipInfo?.isp} />
|
|
||||||
|
|
||||||
<InfoItem label={t("ORG")} value={ipInfo?.asn_organization} />
|
|
||||||
|
|
||||||
<InfoItem
|
|
||||||
label={t("Location")}
|
|
||||||
value={[ipInfo?.city, ipInfo?.region]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(", ")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<InfoItem label={t("Timezone")} value={ipInfo?.timezone} />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
mt: "auto",
|
|
||||||
pt: 0.5,
|
|
||||||
borderTop: 1,
|
|
||||||
borderColor: "divider",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
opacity: 0.7,
|
|
||||||
fontSize: "0.7rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="caption">
|
|
||||||
{t("Auto refresh")}: {countdown}s
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
sx={{
|
sx={{
|
||||||
textOverflow: "ellipsis",
|
fontSize: "1.5rem",
|
||||||
overflow: "hidden",
|
mr: 1,
|
||||||
whiteSpace: "nowrap",
|
display: "inline-block",
|
||||||
|
width: 28,
|
||||||
|
textAlign: "center",
|
||||||
|
flexShrink: 0,
|
||||||
|
fontFamily: '"twemoji mozilla", sans-serif',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{ipInfo?.country_code}, {ipInfo?.longitude?.toFixed(2)},{" "}
|
{getCountryFlag(ipInfo?.country_code)}
|
||||||
{ipInfo?.latitude?.toFixed(2)}
|
</Box>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
sx={{
|
||||||
|
fontWeight: "medium",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
maxWidth: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ipInfo?.country || t("Unknown")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
|
||||||
)}
|
<Box sx={{ display: "flex", alignItems: "center", mb: 1 }}>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
{t("IP")}:
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
ml: 1,
|
||||||
|
overflow: "hidden",
|
||||||
|
maxWidth: "calc(100% - 30px)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
wordBreak: "break-all",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showIp ? ipInfo?.ip : "••••••••••"}
|
||||||
|
</Typography>
|
||||||
|
<IconButton size="small" onClick={toggleShowIp}>
|
||||||
|
{showIp ? (
|
||||||
|
<VisibilityOffOutlined fontSize="small" />
|
||||||
|
) : (
|
||||||
|
<VisibilityOutlined fontSize="small" />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<InfoItem
|
||||||
|
label={t("ASN")}
|
||||||
|
value={ipInfo?.asn ? `AS${ipInfo.asn}` : "N/A"}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 右侧:组织、ISP和位置信息 */}
|
||||||
|
<Box sx={{ width: "60%", overflow: "auto" }}>
|
||||||
|
<InfoItem label={t("ISP")} value={ipInfo?.isp} />
|
||||||
|
<InfoItem label={t("ORG")} value={ipInfo?.asn_organization} />
|
||||||
|
<InfoItem
|
||||||
|
label={t("Location")}
|
||||||
|
value={[ipInfo?.city, ipInfo?.region]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(", ")}
|
||||||
|
/>
|
||||||
|
<InfoItem label={t("Timezone")} value={ipInfo?.timezone} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: "auto",
|
||||||
|
pt: 0.5,
|
||||||
|
borderTop: 1,
|
||||||
|
borderColor: "divider",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
opacity: 0.7,
|
||||||
|
fontSize: "0.7rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="caption">
|
||||||
|
{t("Auto refresh")}: {countdown}s
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
overflow: "hidden",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ipInfo?.country_code}, {ipInfo?.longitude?.toFixed(2)},{" "}
|
||||||
|
{ipInfo?.latitude?.toFixed(2)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</EnhancedCard>
|
</EnhancedCard>
|
||||||
);
|
);
|
||||||
|
@ -7,17 +7,16 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
alpha,
|
alpha,
|
||||||
useTheme,
|
useTheme,
|
||||||
Button,
|
|
||||||
Fade,
|
Fade,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useMemo, memo, FC } from "react";
|
||||||
import ProxyControlSwitches from "@/components/shared/ProxyControlSwitches";
|
import ProxyControlSwitches from "@/components/shared/ProxyControlSwitches";
|
||||||
import { Notice } from "@/components/base";
|
import { Notice } from "@/components/base";
|
||||||
import {
|
import {
|
||||||
LanguageRounded,
|
|
||||||
ComputerRounded,
|
ComputerRounded,
|
||||||
TroubleshootRounded,
|
TroubleshootRounded,
|
||||||
HelpOutlineRounded,
|
HelpOutlineRounded,
|
||||||
|
SvgIconComponent,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import {
|
import {
|
||||||
@ -26,15 +25,131 @@ import {
|
|||||||
getRunningMode,
|
getRunningMode,
|
||||||
} from "@/services/cmds";
|
} from "@/services/cmds";
|
||||||
|
|
||||||
export const ProxyTunCard = () => {
|
const LOCAL_STORAGE_TAB_KEY = "clash-verge-proxy-active-tab";
|
||||||
|
|
||||||
|
interface TabButtonProps {
|
||||||
|
isActive: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
icon: SvgIconComponent;
|
||||||
|
label: string;
|
||||||
|
hasIndicator?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抽取Tab组件以减少重复代码
|
||||||
|
const TabButton: FC<TabButtonProps> = memo(({
|
||||||
|
isActive,
|
||||||
|
onClick,
|
||||||
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
hasIndicator = false
|
||||||
|
}) => (
|
||||||
|
<Paper
|
||||||
|
elevation={isActive ? 2 : 0}
|
||||||
|
onClick={onClick}
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
px: 2,
|
||||||
|
py: 1,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 1,
|
||||||
|
bgcolor: isActive ? "primary.main" : "background.paper",
|
||||||
|
color: isActive ? "primary.contrastText" : "text.primary",
|
||||||
|
borderRadius: 1.5,
|
||||||
|
flex: 1,
|
||||||
|
maxWidth: 160,
|
||||||
|
transition: "all 0.2s ease-in-out",
|
||||||
|
position: "relative",
|
||||||
|
"&:hover": {
|
||||||
|
transform: "translateY(-1px)",
|
||||||
|
boxShadow: 1,
|
||||||
|
},
|
||||||
|
"&:after": isActive
|
||||||
|
? {
|
||||||
|
content: '""',
|
||||||
|
position: "absolute",
|
||||||
|
bottom: -9,
|
||||||
|
left: "50%",
|
||||||
|
width: 2,
|
||||||
|
height: 9,
|
||||||
|
bgcolor: "primary.main",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon fontSize="small" />
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ fontWeight: isActive ? 600 : 400 }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
{hasIndicator && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: "50%",
|
||||||
|
bgcolor: isActive ? "#fff" : "success.main",
|
||||||
|
position: "absolute",
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
));
|
||||||
|
|
||||||
|
interface TabDescriptionProps {
|
||||||
|
description: string;
|
||||||
|
tooltipTitle: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抽取描述文本组件
|
||||||
|
const TabDescription: FC<TabDescriptionProps> = memo(({ description, tooltipTitle }) => (
|
||||||
|
<Fade in={true} timeout={200}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
component="div"
|
||||||
|
sx={{
|
||||||
|
width: "95%",
|
||||||
|
textAlign: "center",
|
||||||
|
color: "text.secondary",
|
||||||
|
p: 0.8,
|
||||||
|
borderRadius: 1,
|
||||||
|
borderColor: "primary.main",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: "solid",
|
||||||
|
backgroundColor: "background.paper",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 0.5,
|
||||||
|
wordBreak: "break-word",
|
||||||
|
hyphens: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
<Tooltip title={tooltipTitle}>
|
||||||
|
<HelpOutlineRounded
|
||||||
|
sx={{ fontSize: 14, opacity: 0.7, flexShrink: 0 }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Typography>
|
||||||
|
</Fade>
|
||||||
|
));
|
||||||
|
|
||||||
|
export const ProxyTunCard: FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [activeTab, setActiveTab] = useState<string>(() =>
|
||||||
const [activeTab, setActiveTab] = useState<string>("system");
|
localStorage.getItem(LOCAL_STORAGE_TAB_KEY) || "system"
|
||||||
|
);
|
||||||
|
|
||||||
// 获取代理状态信息
|
// 获取代理状态信息
|
||||||
const { data: sysproxy } = useSWR("getSystemProxy", getSystemProxy);
|
const { data: sysproxy } = useSWR("getSystemProxy", getSystemProxy);
|
||||||
const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy);
|
|
||||||
const { data: runningMode } = useSWR("getRunningMode", getRunningMode);
|
const { data: runningMode } = useSWR("getRunningMode", getRunningMode);
|
||||||
|
|
||||||
// 是否以sidecar模式运行
|
// 是否以sidecar模式运行
|
||||||
@ -42,26 +157,34 @@ export const ProxyTunCard = () => {
|
|||||||
|
|
||||||
// 处理错误
|
// 处理错误
|
||||||
const handleError = (err: Error) => {
|
const handleError = (err: Error) => {
|
||||||
setError(err.message);
|
|
||||||
Notice.error(err.message || err.toString(), 3000);
|
Notice.error(err.message || err.toString(), 3000);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 用户提示文本
|
// 处理标签切换并保存到localStorage
|
||||||
const getTabDescription = (tab: string) => {
|
const handleTabChange = (tab: string) => {
|
||||||
switch (tab) {
|
setActiveTab(tab);
|
||||||
case "system":
|
localStorage.setItem(LOCAL_STORAGE_TAB_KEY, tab);
|
||||||
return sysproxy?.enable
|
|
||||||
? t("System Proxy Enabled")
|
|
||||||
: t("System Proxy Disabled");
|
|
||||||
case "tun":
|
|
||||||
return isSidecarMode
|
|
||||||
? t("TUN Mode Service Required")
|
|
||||||
: t("TUN Mode Intercept Info");
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 用户提示文本 - 使用useMemo避免重复计算
|
||||||
|
const tabDescription = useMemo(() => {
|
||||||
|
if (activeTab === "system") {
|
||||||
|
return {
|
||||||
|
text: sysproxy?.enable
|
||||||
|
? t("System Proxy Enabled")
|
||||||
|
: t("System Proxy Disabled"),
|
||||||
|
tooltip: t("System Proxy Info")
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
text: isSidecarMode
|
||||||
|
? t("TUN Mode Service Required")
|
||||||
|
: t("TUN Mode Intercept Info"),
|
||||||
|
tooltip: t("Tun Mode Info")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [activeTab, sysproxy?.enable, isSidecarMode, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||||||
{/* 选项卡 */}
|
{/* 选项卡 */}
|
||||||
@ -75,112 +198,19 @@ export const ProxyTunCard = () => {
|
|||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Paper
|
<TabButton
|
||||||
elevation={activeTab === "system" ? 2 : 0}
|
isActive={activeTab === "system"}
|
||||||
onClick={() => setActiveTab("system")}
|
onClick={() => handleTabChange("system")}
|
||||||
sx={{
|
icon={ComputerRounded}
|
||||||
cursor: "pointer",
|
label={t("System Proxy")}
|
||||||
px: 2,
|
hasIndicator={sysproxy?.enable}
|
||||||
py: 1,
|
/>
|
||||||
display: "flex",
|
<TabButton
|
||||||
alignItems: "center",
|
isActive={activeTab === "tun"}
|
||||||
justifyContent: "center",
|
onClick={() => handleTabChange("tun")}
|
||||||
gap: 1,
|
icon={TroubleshootRounded}
|
||||||
bgcolor:
|
label={t("Tun Mode")}
|
||||||
activeTab === "system" ? "primary.main" : "background.paper",
|
/>
|
||||||
color:
|
|
||||||
activeTab === "system" ? "primary.contrastText" : "text.primary",
|
|
||||||
borderRadius: 1.5,
|
|
||||||
flex: 1,
|
|
||||||
maxWidth: 160,
|
|
||||||
transition: "all 0.2s ease-in-out",
|
|
||||||
position: "relative",
|
|
||||||
"&:hover": {
|
|
||||||
transform: "translateY(-1px)",
|
|
||||||
boxShadow: 1,
|
|
||||||
},
|
|
||||||
"&:after":
|
|
||||||
activeTab === "system"
|
|
||||||
? {
|
|
||||||
content: '""',
|
|
||||||
position: "absolute",
|
|
||||||
bottom: -9,
|
|
||||||
left: "50%",
|
|
||||||
width: 2,
|
|
||||||
height: 9,
|
|
||||||
bgcolor: "primary.main",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ComputerRounded fontSize="small" />
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{ fontWeight: activeTab === "system" ? 600 : 400 }}
|
|
||||||
>
|
|
||||||
{t("System Proxy")}
|
|
||||||
</Typography>
|
|
||||||
{sysproxy?.enable && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
borderRadius: "50%",
|
|
||||||
bgcolor: activeTab === "system" ? "#fff" : "success.main",
|
|
||||||
position: "absolute",
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
<Paper
|
|
||||||
elevation={activeTab === "tun" ? 2 : 0}
|
|
||||||
onClick={() => setActiveTab("tun")}
|
|
||||||
sx={{
|
|
||||||
cursor: "pointer",
|
|
||||||
px: 2,
|
|
||||||
py: 1,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
gap: 1,
|
|
||||||
bgcolor: activeTab === "tun" ? "primary.main" : "background.paper",
|
|
||||||
color:
|
|
||||||
activeTab === "tun" ? "primary.contrastText" : "text.primary",
|
|
||||||
borderRadius: 1.5,
|
|
||||||
flex: 1,
|
|
||||||
maxWidth: 160,
|
|
||||||
transition: "all 0.2s ease-in-out",
|
|
||||||
position: "relative",
|
|
||||||
"&:hover": {
|
|
||||||
transform: "translateY(-1px)",
|
|
||||||
boxShadow: 1,
|
|
||||||
},
|
|
||||||
"&:after":
|
|
||||||
activeTab === "tun"
|
|
||||||
? {
|
|
||||||
content: '""',
|
|
||||||
position: "absolute",
|
|
||||||
bottom: -9,
|
|
||||||
left: "50%",
|
|
||||||
width: 2,
|
|
||||||
height: 9,
|
|
||||||
bgcolor: "primary.main",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TroubleshootRounded fontSize="small" />
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{ fontWeight: activeTab === "tun" ? 600 : 400 }}
|
|
||||||
>
|
|
||||||
{t("Tun Mode")}
|
|
||||||
</Typography>
|
|
||||||
</Paper>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{/* 说明文本区域 */}
|
{/* 说明文本区域 */}
|
||||||
@ -194,71 +224,10 @@ export const ProxyTunCard = () => {
|
|||||||
overflow: "visible",
|
overflow: "visible",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{activeTab === "system" && (
|
<TabDescription
|
||||||
<Fade in={true} timeout={200}>
|
description={tabDescription.text}
|
||||||
<Typography
|
tooltipTitle={tabDescription.tooltip}
|
||||||
variant="caption"
|
/>
|
||||||
component="div"
|
|
||||||
sx={{
|
|
||||||
width: "95%",
|
|
||||||
textAlign: "center",
|
|
||||||
color: "text.secondary",
|
|
||||||
p: 0.8,
|
|
||||||
borderRadius: 1,
|
|
||||||
borderColor: "primary.main",
|
|
||||||
borderWidth: 1,
|
|
||||||
borderStyle: "solid",
|
|
||||||
backgroundColor: "background.paper",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
gap: 0.5,
|
|
||||||
wordBreak: "break-word",
|
|
||||||
hyphens: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getTabDescription("system")}
|
|
||||||
<Tooltip title={t("System Proxy Info")}>
|
|
||||||
<HelpOutlineRounded
|
|
||||||
sx={{ fontSize: 14, opacity: 0.7, flexShrink: 0 }}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Typography>
|
|
||||||
</Fade>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === "tun" && (
|
|
||||||
<Fade in={true} timeout={200}>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
component="div"
|
|
||||||
sx={{
|
|
||||||
width: "95%",
|
|
||||||
textAlign: "center",
|
|
||||||
color: "text.secondary",
|
|
||||||
p: 0.8,
|
|
||||||
borderRadius: 1,
|
|
||||||
borderColor: "primary.main",
|
|
||||||
borderWidth: 1,
|
|
||||||
borderStyle: "solid",
|
|
||||||
backgroundColor: "background.paper",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
gap: 0.5,
|
|
||||||
wordBreak: "break-word",
|
|
||||||
hyphens: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getTabDescription("tun")}
|
|
||||||
<Tooltip title={t("Tun Mode Info")}>
|
|
||||||
<HelpOutlineRounded
|
|
||||||
sx={{ fontSize: 14, opacity: 0.7, flexShrink: 0 }}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Typography>
|
|
||||||
</Fade>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 控制开关部分 */}
|
{/* 控制开关部分 */}
|
||||||
|
@ -7,7 +7,7 @@ import useSWR from "swr";
|
|||||||
import { getRunningMode, getSystemInfo, installService } from "@/services/cmds";
|
import { getRunningMode, getSystemInfo, installService } from "@/services/cmds";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { version as appVersion } from "@root/package.json";
|
import { version as appVersion } from "@root/package.json";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
|
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { Notice } from "@/components/base";
|
import { Notice } from "@/components/base";
|
||||||
@ -17,99 +17,97 @@ export const SystemInfoCard = () => {
|
|||||||
const { verge, patchVerge } = useVerge();
|
const { verge, patchVerge } = useVerge();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// 系统信息状态
|
||||||
|
const [systemState, setSystemState] = useState({
|
||||||
|
osInfo: "",
|
||||||
|
lastCheckUpdate: "-",
|
||||||
|
});
|
||||||
|
|
||||||
// 获取运行模式
|
// 获取运行模式
|
||||||
const { data: runningMode = "sidecar", mutate: mutateRunningMode } = useSWR(
|
const { data: runningMode = "sidecar", mutate: mutateRunningMode } = useSWR(
|
||||||
"getRunningMode",
|
"getRunningMode",
|
||||||
getRunningMode,
|
getRunningMode,
|
||||||
|
{ suspense: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 获取系统信息
|
// 初始化系统信息
|
||||||
const [osInfo, setOsInfo] = useState<string>("");
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// 获取系统信息
|
||||||
getSystemInfo()
|
getSystemInfo()
|
||||||
.then((info) => {
|
.then((info) => {
|
||||||
const lines = info.split("\n");
|
const lines = info.split("\n");
|
||||||
if (lines.length > 0) {
|
if (lines.length > 0) {
|
||||||
// 提取系统名称和版本信息
|
const sysName = lines[0].split(": ")[1] || "";
|
||||||
const sysNameLine = lines[0]; // System Name: xxx
|
const sysVersion = lines[1].split(": ")[1] || "";
|
||||||
const sysVersionLine = lines[1]; // System Version: xxx
|
setSystemState(prev => ({ ...prev, osInfo: `${sysName} ${sysVersion}` }));
|
||||||
|
|
||||||
const sysName = sysNameLine.split(": ")[1] || "";
|
|
||||||
const sysVersion = sysVersionLine.split(": ")[1] || "";
|
|
||||||
|
|
||||||
setOsInfo(`${sysName} ${sysVersion}`);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(console.error);
|
||||||
console.error("Error getting system info:", err);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 获取最后检查更新时间
|
|
||||||
const [lastCheckUpdate, setLastCheckUpdate] = useState<string>("-");
|
|
||||||
|
|
||||||
// 在组件挂载时检查本地存储中的最后更新时间
|
|
||||||
useEffect(() => {
|
|
||||||
// 获取最后检查更新时间
|
// 获取最后检查更新时间
|
||||||
const lastCheck = localStorage.getItem("last_check_update");
|
const lastCheck = localStorage.getItem("last_check_update");
|
||||||
if (lastCheck) {
|
if (lastCheck) {
|
||||||
try {
|
try {
|
||||||
const timestamp = parseInt(lastCheck, 10);
|
const timestamp = parseInt(lastCheck, 10);
|
||||||
if (!isNaN(timestamp)) {
|
if (!isNaN(timestamp)) {
|
||||||
const date = new Date(timestamp);
|
setSystemState(prev => ({
|
||||||
setLastCheckUpdate(date.toLocaleString());
|
...prev,
|
||||||
|
lastCheckUpdate: new Date(timestamp).toLocaleString()
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error parsing last check update time", e);
|
console.error("Error parsing last check update time", e);
|
||||||
}
|
}
|
||||||
} else if (verge?.auto_check_update) {
|
} else if (verge?.auto_check_update) {
|
||||||
// 如果启用了自动检查更新但没有最后检查时间记录,则触发一次检查
|
// 如果启用了自动检查更新但没有记录,设置当前时间并延迟检查
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
localStorage.setItem("last_check_update", now.toString());
|
localStorage.setItem("last_check_update", now.toString());
|
||||||
setLastCheckUpdate(new Date(now).toLocaleString());
|
setSystemState(prev => ({
|
||||||
|
...prev,
|
||||||
// 延迟执行检查更新,避免在应用启动时立即执行
|
lastCheckUpdate: new Date(now).toLocaleString()
|
||||||
|
}));
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
checkUpdate().catch((e) => console.error("Error checking update:", e));
|
if (verge?.auto_check_update) {
|
||||||
|
checkUpdate().catch(console.error);
|
||||||
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
}, [verge?.auto_check_update]);
|
}, [verge?.auto_check_update]);
|
||||||
|
|
||||||
// 监听 checkUpdate 调用并更新时间
|
// 自动检查更新逻辑
|
||||||
useSWR(
|
useSWR(
|
||||||
"checkUpdate",
|
verge?.auto_check_update ? "checkUpdate" : null,
|
||||||
async () => {
|
async () => {
|
||||||
// 更新最后检查时间
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
localStorage.setItem("last_check_update", now.toString());
|
localStorage.setItem("last_check_update", now.toString());
|
||||||
setLastCheckUpdate(new Date(now).toLocaleString());
|
setSystemState(prev => ({
|
||||||
|
...prev,
|
||||||
// 实际执行检查更新
|
lastCheckUpdate: new Date(now).toLocaleString()
|
||||||
|
}));
|
||||||
return await checkUpdate();
|
return await checkUpdate();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
refreshInterval: 24 * 60 * 60 * 1000, // 每天检查一次更新
|
refreshInterval: 24 * 60 * 60 * 1000, // 每天检查一次
|
||||||
dedupingInterval: 60 * 60 * 1000, // 1小时内不重复检查,
|
dedupingInterval: 60 * 60 * 1000, // 1小时内不重复检查
|
||||||
isPaused: () => !(verge?.auto_check_update ?? true), // 根据 auto_check_update 设置决定是否启用
|
}
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 导航到设置页面
|
// 导航到设置页面
|
||||||
const goToSettings = () => {
|
const goToSettings = useCallback(() => {
|
||||||
navigate("/settings");
|
navigate("/settings");
|
||||||
};
|
}, [navigate]);
|
||||||
|
|
||||||
// 切换自启动状态
|
// 切换自启动状态
|
||||||
const toggleAutoLaunch = async () => {
|
const toggleAutoLaunch = useCallback(async () => {
|
||||||
|
if (!verge) return;
|
||||||
try {
|
try {
|
||||||
if (!verge) return;
|
|
||||||
// 将当前的启动状态取反
|
|
||||||
await patchVerge({ enable_auto_launch: !verge.enable_auto_launch });
|
await patchVerge({ enable_auto_launch: !verge.enable_auto_launch });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("切换开机自启动状态失败:", err);
|
console.error("切换开机自启动状态失败:", err);
|
||||||
}
|
}
|
||||||
};
|
}, [verge, patchVerge]);
|
||||||
|
|
||||||
// 安装系统服务
|
// 安装系统服务
|
||||||
const onInstallService = useLockFn(async () => {
|
const onInstallService = useLockFn(async () => {
|
||||||
@ -117,34 +115,48 @@ export const SystemInfoCard = () => {
|
|||||||
Notice.info(t("Installing Service..."), 1000);
|
Notice.info(t("Installing Service..."), 1000);
|
||||||
await installService();
|
await installService();
|
||||||
Notice.success(t("Service Installed Successfully"), 2000);
|
Notice.success(t("Service Installed Successfully"), 2000);
|
||||||
// 重新获取运行模式
|
|
||||||
await mutateRunningMode();
|
await mutateRunningMode();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Notice.error(err.message || err.toString(), 3000);
|
Notice.error(err.message || err.toString(), 3000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 点击运行模式
|
// 点击运行模式处理
|
||||||
const handleRunningModeClick = () => {
|
const handleRunningModeClick = useCallback(() => {
|
||||||
if (runningMode === "sidecar") {
|
if (runningMode === "sidecar") {
|
||||||
onInstallService();
|
onInstallService();
|
||||||
}
|
}
|
||||||
};
|
}, [runningMode, onInstallService]);
|
||||||
|
|
||||||
// 检查更新
|
// 检查更新
|
||||||
const onCheckUpdate = async () => {
|
const onCheckUpdate = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
const info = await checkUpdate();
|
const info = await checkUpdate();
|
||||||
if (!info?.available) {
|
if (!info?.available) {
|
||||||
Notice.success(t("Currently on the Latest Version"));
|
Notice.success(t("Currently on the Latest Version"));
|
||||||
} else {
|
} else {
|
||||||
Notice.info(t("Update Available"), 2000);
|
Notice.info(t("Update Available"), 2000);
|
||||||
goToSettings(); // 跳转到设置页面查看更新
|
goToSettings();
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Notice.error(err.message || err.toString());
|
Notice.error(err.message || err.toString());
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
|
// 是否启用自启动
|
||||||
|
const autoLaunchEnabled = useMemo(() => verge?.enable_auto_launch || false, [verge]);
|
||||||
|
|
||||||
|
// 运行模式样式
|
||||||
|
const runningModeStyle = useMemo(() => ({
|
||||||
|
cursor: runningMode === "sidecar" ? "pointer" : "default",
|
||||||
|
textDecoration: runningMode === "sidecar" ? "underline" : "none",
|
||||||
|
"&:hover": {
|
||||||
|
opacity: runningMode === "sidecar" ? 0.7 : 1,
|
||||||
|
},
|
||||||
|
}), [runningMode]);
|
||||||
|
|
||||||
|
// 只有当verge存在时才渲染内容
|
||||||
|
if (!verge) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
@ -157,84 +169,71 @@ export const SystemInfoCard = () => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{verge && (
|
<Stack spacing={1.5}>
|
||||||
<Stack spacing={1.5}>
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Typography variant="body2" color="text.secondary">
|
||||||
<Typography variant="body2" color="text.secondary">
|
{t("OS Info")}
|
||||||
{t("OS Info")}
|
</Typography>
|
||||||
</Typography>
|
<Typography variant="body2" fontWeight="medium">
|
||||||
<Typography variant="body2" fontWeight="medium">
|
{systemState.osInfo}
|
||||||
{osInfo}
|
</Typography>
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
<Divider />
|
|
||||||
<Stack direction="row" justifyContent="space-between">
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{t("Auto Launch")}
|
|
||||||
</Typography>
|
|
||||||
<Chip
|
|
||||||
size="small"
|
|
||||||
label={verge.enable_auto_launch ? t("Enabled") : t("Disabled")}
|
|
||||||
color={verge.enable_auto_launch ? "success" : "default"}
|
|
||||||
variant={verge.enable_auto_launch ? "filled" : "outlined"}
|
|
||||||
onClick={toggleAutoLaunch}
|
|
||||||
sx={{ cursor: "pointer" }}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<Divider />
|
|
||||||
<Stack direction="row" justifyContent="space-between">
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{t("Running Mode")}
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
fontWeight="medium"
|
|
||||||
onClick={handleRunningModeClick}
|
|
||||||
sx={{
|
|
||||||
cursor: runningMode === "sidecar" ? "pointer" : "default",
|
|
||||||
textDecoration:
|
|
||||||
runningMode === "sidecar" ? "underline" : "none",
|
|
||||||
"&:hover": {
|
|
||||||
opacity: runningMode === "sidecar" ? 0.7 : 1,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{runningMode === "service"
|
|
||||||
? t("Service Mode")
|
|
||||||
: t("Sidecar Mode")}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
<Divider />
|
|
||||||
<Stack direction="row" justifyContent="space-between">
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{t("Last Check Update")}
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
fontWeight="medium"
|
|
||||||
onClick={onCheckUpdate}
|
|
||||||
sx={{
|
|
||||||
cursor: "pointer",
|
|
||||||
textDecoration: "underline",
|
|
||||||
"&:hover": {
|
|
||||||
opacity: 0.7,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{lastCheckUpdate}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
<Divider />
|
|
||||||
<Stack direction="row" justifyContent="space-between">
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{t("Verge Version")}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" fontWeight="medium">
|
|
||||||
v{appVersion}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
<Divider />
|
||||||
|
<Stack direction="row" justifyContent="space-between">
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("Auto Launch")}
|
||||||
|
</Typography>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={autoLaunchEnabled ? t("Enabled") : t("Disabled")}
|
||||||
|
color={autoLaunchEnabled ? "success" : "default"}
|
||||||
|
variant={autoLaunchEnabled ? "filled" : "outlined"}
|
||||||
|
onClick={toggleAutoLaunch}
|
||||||
|
sx={{ cursor: "pointer" }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Divider />
|
||||||
|
<Stack direction="row" justifyContent="space-between">
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("Running Mode")}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
fontWeight="medium"
|
||||||
|
onClick={handleRunningModeClick}
|
||||||
|
sx={runningModeStyle}
|
||||||
|
>
|
||||||
|
{runningMode === "service" ? t("Service Mode") : t("Sidecar Mode")}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Divider />
|
||||||
|
<Stack direction="row" justifyContent="space-between">
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("Last Check Update")}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
fontWeight="medium"
|
||||||
|
onClick={onCheckUpdate}
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
textDecoration: "underline",
|
||||||
|
"&:hover": { opacity: 0.7 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{systemState.lastCheckUpdate}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Divider />
|
||||||
|
<Stack direction="row" justifyContent="space-between">
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("Verge Version")}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" fontWeight="medium">
|
||||||
|
v{appVersion}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
</EnhancedCard>
|
</EnhancedCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useMemo, useCallback } from "react";
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { Box, IconButton, Tooltip, alpha, styled } from "@mui/material";
|
import { Box, IconButton, Tooltip, alpha, styled } from "@mui/material";
|
||||||
import Grid from "@mui/material/Grid2";
|
import Grid from "@mui/material/Grid2";
|
||||||
@ -40,67 +40,79 @@ const ScrollBox = styled(Box)(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// 默认测试列表,移到组件外部避免重复创建
|
||||||
|
const DEFAULT_TEST_LIST = [
|
||||||
|
{
|
||||||
|
uid: nanoid(),
|
||||||
|
name: "Apple",
|
||||||
|
url: "https://www.apple.com",
|
||||||
|
icon: apple,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uid: nanoid(),
|
||||||
|
name: "GitHub",
|
||||||
|
url: "https://www.github.com",
|
||||||
|
icon: github,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uid: nanoid(),
|
||||||
|
name: "Google",
|
||||||
|
url: "https://www.google.com",
|
||||||
|
icon: google,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uid: nanoid(),
|
||||||
|
name: "Youtube",
|
||||||
|
url: "https://www.youtube.com",
|
||||||
|
icon: youtube,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const TestCard = () => {
|
export const TestCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const sensors = useSensors(useSensor(PointerSensor));
|
const sensors = useSensors(useSensor(PointerSensor));
|
||||||
const { verge, mutateVerge, patchVerge } = useVerge();
|
const { verge, mutateVerge, patchVerge } = useVerge();
|
||||||
|
const viewerRef = useRef<TestViewerRef>(null);
|
||||||
|
|
||||||
// test list
|
// 使用useMemo优化测试列表,避免每次渲染重新计算
|
||||||
const testList = verge?.test_list ?? [
|
const testList = useMemo(() => {
|
||||||
{
|
return verge?.test_list ?? DEFAULT_TEST_LIST;
|
||||||
uid: nanoid(),
|
}, [verge?.test_list]);
|
||||||
name: "Apple",
|
|
||||||
url: "https://www.apple.com",
|
|
||||||
icon: apple,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uid: nanoid(),
|
|
||||||
name: "GitHub",
|
|
||||||
url: "https://www.github.com",
|
|
||||||
icon: github,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uid: nanoid(),
|
|
||||||
name: "Google",
|
|
||||||
url: "https://www.google.com",
|
|
||||||
icon: google,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uid: nanoid(),
|
|
||||||
name: "Youtube",
|
|
||||||
url: "https://www.youtube.com",
|
|
||||||
icon: youtube,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const onTestListItemChange = (
|
// 使用useCallback优化函数引用,避免不必要的重新渲染
|
||||||
uid: string,
|
const onTestListItemChange = useCallback(
|
||||||
patch?: Partial<IVergeTestItem>,
|
(uid: string, patch?: Partial<IVergeTestItem>) => {
|
||||||
) => {
|
if (!patch) {
|
||||||
if (patch) {
|
mutateVerge();
|
||||||
const newList = testList.map((x) => {
|
return;
|
||||||
if (x.uid === uid) {
|
}
|
||||||
return { ...x, ...patch };
|
|
||||||
}
|
const newList = testList.map((x) =>
|
||||||
return x;
|
x.uid === uid ? { ...x, ...patch } : x
|
||||||
});
|
);
|
||||||
|
|
||||||
mutateVerge({ ...verge, test_list: newList }, false);
|
mutateVerge({ ...verge, test_list: newList }, false);
|
||||||
} else {
|
},
|
||||||
mutateVerge();
|
[testList, verge, mutateVerge]
|
||||||
}
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const onDeleteTestListItem = (uid: string) => {
|
const onDeleteTestListItem = useCallback(
|
||||||
const newList = testList.filter((x) => x.uid !== uid);
|
(uid: string) => {
|
||||||
patchVerge({ test_list: newList });
|
const newList = testList.filter((x) => x.uid !== uid);
|
||||||
mutateVerge({ ...verge, test_list: newList }, false);
|
patchVerge({ test_list: newList });
|
||||||
};
|
mutateVerge({ ...verge, test_list: newList }, false);
|
||||||
|
},
|
||||||
|
[testList, verge, patchVerge, mutateVerge]
|
||||||
|
);
|
||||||
|
|
||||||
const onDragEnd = async (event: DragEndEvent) => {
|
const onDragEnd = useCallback(
|
||||||
const { active, over } = event;
|
async (event: DragEndEvent) => {
|
||||||
if (over && active.id !== over.id) {
|
const { active, over } = event;
|
||||||
let old_index = testList.findIndex((x) => x.uid === active.id);
|
if (!over || active.id === over.id) return;
|
||||||
let new_index = testList.findIndex((x) => x.uid === over.id);
|
|
||||||
|
const old_index = testList.findIndex((x) => x.uid === active.id);
|
||||||
|
const new_index = testList.findIndex((x) => x.uid === over.id);
|
||||||
|
|
||||||
if (old_index >= 0 && new_index >= 0) {
|
if (old_index >= 0 && new_index >= 0) {
|
||||||
const newList = [...testList];
|
const newList = [...testList];
|
||||||
const [removed] = newList.splice(old_index, 1);
|
const [removed] = newList.splice(old_index, 1);
|
||||||
@ -109,17 +121,42 @@ export const TestCard = () => {
|
|||||||
await mutateVerge({ ...verge, test_list: newList }, false);
|
await mutateVerge({ ...verge, test_list: newList }, false);
|
||||||
await patchVerge({ test_list: newList });
|
await patchVerge({ test_list: newList });
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
[testList, verge, mutateVerge, patchVerge]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 仅在verge首次加载时初始化测试列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!verge) return;
|
if (verge && !verge.test_list) {
|
||||||
if (!verge?.test_list) {
|
patchVerge({ test_list: DEFAULT_TEST_LIST });
|
||||||
patchVerge({ test_list: testList });
|
|
||||||
}
|
}
|
||||||
}, [verge]);
|
}, [verge, patchVerge]);
|
||||||
|
|
||||||
const viewerRef = useRef<TestViewerRef>(null);
|
// 使用useMemo优化UI内容,减少渲染计算
|
||||||
|
const renderTestItems = useMemo(() => (
|
||||||
|
<Grid container spacing={1} columns={12}>
|
||||||
|
<SortableContext items={testList.map((x) => x.uid)}>
|
||||||
|
{testList.map((item) => (
|
||||||
|
<Grid key={item.uid} size={3}>
|
||||||
|
<TestItem
|
||||||
|
id={item.uid}
|
||||||
|
itemData={item}
|
||||||
|
onEdit={() => viewerRef.current?.edit(item)}
|
||||||
|
onDelete={onDeleteTestListItem}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</Grid>
|
||||||
|
), [testList, onDeleteTestListItem]);
|
||||||
|
|
||||||
|
const handleTestAll = useCallback(() => {
|
||||||
|
emit("verge://test-all");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCreateTest = useCallback(() => {
|
||||||
|
viewerRef.current?.create();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
@ -128,15 +165,12 @@ export const TestCard = () => {
|
|||||||
action={
|
action={
|
||||||
<Box sx={{ display: "flex", gap: 1 }}>
|
<Box sx={{ display: "flex", gap: 1 }}>
|
||||||
<Tooltip title={t("Test All")} arrow>
|
<Tooltip title={t("Test All")} arrow>
|
||||||
<IconButton size="small" onClick={() => emit("verge://test-all")}>
|
<IconButton size="small" onClick={handleTestAll}>
|
||||||
<NetworkCheck fontSize="small" />
|
<NetworkCheck fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t("Create Test")} arrow>
|
<Tooltip title={t("Create Test")} arrow>
|
||||||
<IconButton
|
<IconButton size="small" onClick={handleCreateTest}>
|
||||||
size="small"
|
|
||||||
onClick={() => viewerRef.current?.create()}
|
|
||||||
>
|
|
||||||
<Add fontSize="small" />
|
<Add fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -149,20 +183,7 @@ export const TestCard = () => {
|
|||||||
collisionDetection={closestCenter}
|
collisionDetection={closestCenter}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
>
|
>
|
||||||
<Grid container spacing={1} columns={12}>
|
{renderTestItems}
|
||||||
<SortableContext items={testList.map((x) => x.uid)}>
|
|
||||||
{testList.map((item) => (
|
|
||||||
<Grid key={item.uid} size={3}>
|
|
||||||
<TestItem
|
|
||||||
id={item.uid}
|
|
||||||
itemData={item}
|
|
||||||
onEdit={() => viewerRef.current?.edit(item)}
|
|
||||||
onDelete={onDeleteTestListItem}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</SortableContext>
|
|
||||||
</Grid>
|
|
||||||
</DndContext>
|
</DndContext>
|
||||||
</ScrollBox>
|
</ScrollBox>
|
||||||
|
|
||||||
|
@ -207,9 +207,9 @@
|
|||||||
"System Proxy Disabled": "System proxy is disabled, it is recommended for most users to turn on this option",
|
"System Proxy Disabled": "System proxy is disabled, it is recommended for most users to turn on this option",
|
||||||
"TUN Mode Service Required": "TUN mode requires service mode, please install the service first",
|
"TUN Mode Service Required": "TUN mode requires service mode, please install the service first",
|
||||||
"TUN Mode Intercept Info": "TUN mode can take over all application traffic, suitable for special applications",
|
"TUN Mode Intercept Info": "TUN mode can take over all application traffic, suitable for special applications",
|
||||||
"Rule Mode Description": "Routes traffic according to preset rules, provides flexible proxy strategies",
|
"rule Mode Description": "Routes traffic according to preset rules, provides flexible proxy strategies",
|
||||||
"Global Mode Description": "All traffic goes through proxy servers, suitable for scenarios requiring global internet access",
|
"global Mode Description": "All traffic goes through proxy servers, suitable for scenarios requiring global internet access",
|
||||||
"Direct Mode Description": "All traffic doesn't go through proxy nodes, but is forwarded by Clash kernel to target servers, suitable for specific scenarios requiring kernel traffic distribution",
|
"direct Mode Description": "All traffic doesn't go through proxy nodes, but is forwarded by Clash kernel to target servers, suitable for specific scenarios requiring kernel traffic distribution",
|
||||||
"Stack": "Tun Stack",
|
"Stack": "Tun Stack",
|
||||||
"System and Mixed Can Only be Used in Service Mode": "System and Mixed Can Only be Used in Service Mode",
|
"System and Mixed Can Only be Used in Service Mode": "System and Mixed Can Only be Used in Service Mode",
|
||||||
"Device": "Device Name",
|
"Device": "Device Name",
|
||||||
|
@ -207,9 +207,9 @@
|
|||||||
"System Proxy Disabled": "系统代理已关闭,建议大多数用户打开此选项",
|
"System Proxy Disabled": "系统代理已关闭,建议大多数用户打开此选项",
|
||||||
"TUN Mode Service Required": "TUN模式需要服务模式,请先安装服务",
|
"TUN Mode Service Required": "TUN模式需要服务模式,请先安装服务",
|
||||||
"TUN Mode Intercept Info": "TUN模式可以接管所有应用流量,适用于特殊应用",
|
"TUN Mode Intercept Info": "TUN模式可以接管所有应用流量,适用于特殊应用",
|
||||||
"Rule Mode Description": "基于预设规则智能判断流量走向,提供灵活的代理策略",
|
"rule Mode Description": "基于预设规则智能判断流量走向,提供灵活的代理策略",
|
||||||
"Global Mode Description": "所有流量均通过代理服务器,适用于需要全局科学上网的场景",
|
"global Mode Description": "所有流量均通过代理服务器,适用于需要全局科学上网的场景",
|
||||||
"Direct Mode Description": "所有流量不经过代理节点,但经过Clash内核转发连接目标服务器,适用于需要通过内核进行分流的特定场景",
|
"direct Mode Description": "所有流量不经过代理节点,但经过Clash内核转发连接目标服务器,适用于需要通过内核进行分流的特定场景",
|
||||||
"Stack": "TUN 模式堆栈",
|
"Stack": "TUN 模式堆栈",
|
||||||
"System and Mixed Can Only be Used in Service Mode": "System 和 Mixed 只能在服务模式下使用",
|
"System and Mixed Can Only be Used in Service Mode": "System 和 Mixed 只能在服务模式下使用",
|
||||||
"Device": "TUN 网卡名称",
|
"Device": "TUN 网卡名称",
|
||||||
|
@ -201,7 +201,7 @@ const HomeSettingsDialog = ({
|
|||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { current } = useProfiles();
|
const { current, mutateProfiles } = useProfiles();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@ -275,7 +275,10 @@ const HomePage = () => {
|
|||||||
{/* 订阅和当前节点部分 */}
|
{/* 订阅和当前节点部分 */}
|
||||||
{homeCards.profile && (
|
{homeCards.profile && (
|
||||||
<Grid size={6}>
|
<Grid size={6}>
|
||||||
<HomeProfileCard current={current} />
|
<HomeProfileCard
|
||||||
|
current={current}
|
||||||
|
onProfileUpdated={mutateProfiles}
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user