diff --git a/src/components/home/clash-info-card.tsx b/src/components/home/clash-info-card.tsx index 02c94e6d..f8ef7acb 100644 --- a/src/components/home/clash-info-card.tsx +++ b/src/components/home/clash-info-card.tsx @@ -7,47 +7,94 @@ import { EnhancedCard } from "./enhanced-card"; import useSWR from "swr"; import { getRules } from "@/services/api"; 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 = () => { const { t } = useTranslation(); const { clashInfo } = useClashInfo(); const { version: clashVersion } = useClash(); - // 计算运行时间 - const [uptime, setUptime] = useState("0:00:00"); - - // 使用SWR定期获取应用运行时间 - useSWR( + // 使用SWR获取应用运行时间,降低更新频率 + const { data: uptimeMs = 0 } = useSWR( "appUptime", - async () => { - 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; - }, + getAppUptime, { - refreshInterval: 1000, // 每秒更新一次 + refreshInterval: 1000, revalidateOnFocus: false, - dedupingInterval: 500, + dedupingInterval: 1000, }, ); - // 获取规则数 - const { data: rulesData } = useSWR("getRules", getRules, { - fallbackData: [], - suspense: false, + // 使用useMemo缓存格式化后的uptime,避免频繁计算 + const uptime = useMemo(() => formatUptime(uptimeMs), [uptimeMs]); + + // 获取规则数据,只在组件加载时获取一次 + const { data: rules = [] } = useSWR("getRules", getRules, { revalidateOnFocus: false, errorRetryCount: 2, }); - // 获取规则数据 - const rules = rulesData || []; + // 使用备忘录组件内容,减少重新渲染 + const cardContent = useMemo(() => { + if (!clashInfo) return null; + + return ( + + + + {t("Core Version")} + + + {clashVersion || "-"} + + + + + + {t("System Proxy Address")} + + + {clashInfo.server || "-"} + + + + + + {t("Mixed Port")} + + + {clashInfo.mixed_port || "-"} + + + + + + {t("Uptime")} + + + {uptime} + + + + + + {t("Rules Count")} + + + {rules.length} + + + + ); + }, [clashInfo, clashVersion, t, uptime, rules.length]); return ( { iconColor="warning" action={null} > - {clashInfo && ( - - - - {t("Core Version")} - - - {clashVersion || "-"} - - - - - - {t("System Proxy Address")} - - - {clashInfo.server || "-"} - - - - - - {t("Mixed Port")} - - - {clashInfo.mixed_port || "-"} - - - - - - {t("Uptime")} - - - {uptime} - - - - - - {t("Rules Count")} - - - {rules.length} - - - - )} + {cardContent} ); }; diff --git a/src/components/home/clash-mode-card.tsx b/src/components/home/clash-mode-card.tsx index 7274de75..ea50039e 100644 --- a/src/components/home/clash-mode-card.tsx +++ b/src/components/home/clash-mode-card.tsx @@ -10,7 +10,7 @@ import { MultipleStopRounded, DirectionsRounded, } from "@mui/icons-material"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; export const ClashModeCard = () => { const { t } = useTranslation(); @@ -20,15 +20,13 @@ export const ClashModeCard = () => { const { data: clashConfig, mutate: mutateClash } = useSWR( "getClashConfig", getClashConfig, - { - revalidateOnFocus: false, - }, + { revalidateOnFocus: false } ); - // 支持的模式列表 - 添加直连模式 - const modeList = ["rule", "global", "direct"]; + // 支持的模式列表 + const modeList = useMemo(() => ["rule", "global", "direct"] as const, []); - // 本地状态记录当前模式,提供更快的UI响应 + // 本地状态记录当前模式 const [localMode, setLocalMode] = useState("rule"); // 当从API获取到当前模式时更新本地状态 @@ -38,25 +36,27 @@ export const ClashModeCard = () => { } }, [clashConfig]); + // 模式图标映射 + const modeIcons = useMemo(() => ({ + rule: , + global: , + direct: + }), []); + // 切换模式的处理函数 const onChangeMode = useLockFn(async (mode: string) => { - // 如果已经是当前模式,不做任何操作 if (mode === localMode) return; - - // 立即更新本地UI状态 + setLocalMode(mode); - - // 断开连接(如果启用了设置) + if (verge?.auto_close_connection) { closeAllConnections(); } try { await patchClashMode(mode); - // 成功后刷新数据 mutateClash(); } catch (error) { - // 如果操作失败,恢复之前的状态 console.error("Failed to change mode:", error); if (clashConfig?.mode) { setLocalMode(clashConfig.mode.toLowerCase()); @@ -64,32 +64,55 @@ export const ClashModeCard = () => { } }); - // 获取模式对应的图标 - const getModeIcon = (mode: string) => { - switch (mode) { - case "rule": - return ; - case "global": - return ; - case "direct": - return ; - default: - return null; - } - }; + // 按钮样式 + const buttonStyles = (mode: string) => ({ + 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%)", + } + : {}, + }); - // 获取模式说明文字 - const getModeDescription = (mode: string) => { - switch (mode) { - case "rule": - return t("Rule Mode Description"); - case "global": - return t("Global Mode Description"); - case "direct": - return t("Direct Mode Description"); - default: - return ""; - } + // 描述样式 + const descriptionStyles = { + 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", }; return ( @@ -111,44 +134,9 @@ export const ClashModeCard = () => { key={mode} elevation={mode === localMode ? 2 : 0} onClick={() => onChangeMode(mode)} - sx={{ - 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%)", - } - : {}, - }} + sx={buttonStyles(mode)} > - {getModeIcon(mode)} + {modeIcons[mode]} { overflow: "visible", }} > - {localMode === "rule" && ( - - - {getModeDescription("rule")} - - - )} - - {localMode === "global" && ( - - - {getModeDescription("global")} - - - )} - - {localMode === "direct" && ( - - - {getModeDescription("direct")} - - - )} + + + {t(`${localMode} Mode Description`)} + + ); diff --git a/src/components/home/current-proxy-card.tsx b/src/components/home/current-proxy-card.tsx index 7f7b4930..c6528252 100644 --- a/src/components/home/current-proxy-card.tsx +++ b/src/components/home/current-proxy-card.tsx @@ -13,7 +13,7 @@ import { SelectChangeEvent, Tooltip, } from "@mui/material"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo, useCallback, useRef } from "react"; import { SignalWifi4Bar as SignalStrong, SignalWifi3Bar as SignalGood, @@ -45,17 +45,7 @@ interface ProxyOption { } // 将delayManager返回的颜色格式转换为MUI Chip组件需要的格式 -function convertDelayColor( - delayValue: number, -): - | "default" - | "success" - | "warning" - | "error" - | "primary" - | "secondary" - | "info" - | undefined { +function convertDelayColor(delayValue: number) { const colorStr = delayManager.formatDelayColor(delayValue); if (!colorStr) return "default"; @@ -63,445 +53,365 @@ function convertDelayColor( const mainColor = colorStr.split(".")[0]; switch (mainColor) { - case "success": - return "success"; - case "warning": - return "warning"; - case "error": - return "error"; - case "primary": - return "primary"; - default: - return "default"; + case "success": return "success"; + case "warning": return "warning"; + case "error": return "error"; + case "primary": return "primary"; + default: return "default"; } } // 根据延迟值获取合适的WiFi信号图标 -function getSignalIcon(delay: number): { - icon: JSX.Element; - text: string; - color: string; -} { +function getSignalIcon(delay: number) { if (delay < 0) - return { - icon: , - text: "未测试", - color: "text.secondary", - }; + return { icon: , text: "未测试", color: "text.secondary" }; if (delay >= 10000) - return { - icon: , - text: "超时", - color: "error.main", - }; + return { icon: , text: "超时", color: "error.main" }; if (delay >= 500) - return { - icon: , - text: "延迟较高", - color: "error.main", - }; + return { icon: , text: "延迟较高", color: "error.main" }; if (delay >= 300) - return { - icon: , - text: "延迟中等", - color: "warning.main", - }; + return { icon: , text: "延迟中等", color: "warning.main" }; if (delay >= 200) - return { - icon: , - text: "延迟良好", - color: "info.main", - }; - return { - icon: , - text: "延迟极佳", - color: "success.main", + return { icon: , text: "延迟良好", color: "info.main" }; + return { icon: , text: "延迟极佳", color: "success.main" }; +} + +// 简单的防抖函数 +function debounce(fn: Function, ms = 100) { + let timeoutId: ReturnType; + return function(this: any, ...args: any[]) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn.apply(this, args), ms); }; } export const CurrentProxyCard = () => { const { t } = useTranslation(); - const { currentProxy, primaryGroupName, mode, refreshProxy } = - useCurrentProxy(); + const { currentProxy, primaryGroupName, mode, refreshProxy } = useCurrentProxy(); const navigate = useNavigate(); const theme = useTheme(); const { verge } = useVerge(); // 判断模式 const isGlobalMode = mode === "global"; - const isDirectMode = mode === "direct"; // 添加直连模式判断 + const isDirectMode = mode === "direct"; - // 从本地存储获取初始值,如果是特殊模式或没有存储值则使用默认值 - const getSavedGroup = () => { - // 全局模式使用 GLOBAL 组 - if (isGlobalMode) { - return "GLOBAL"; - } - // 直连模式使用 DIRECT - if (isDirectMode) { - return "DIRECT"; - } - const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP); - return savedGroup || primaryGroupName || "GLOBAL"; + // 使用 useRef 存储最后一次刷新时间和是否正在刷新 + const lastRefreshRef = useRef(0); + const isRefreshingRef = useRef(false); + const pendingRefreshRef = useRef(false); + + // 定义状态类型 + type ProxyState = { + proxyData: { + groups: { name: string; now: string; all: string[] }[]; + records: Record; + globalProxy: string; + directProxy: any; + }; + selection: { + group: string; + proxy: string; + }; + displayProxy: any; }; - // 状态管理 - const [groups, setGroups] = useState< - { name: string; now: string; all: string[] }[] - >([]); - const [selectedGroup, setSelectedGroup] = useState(getSavedGroup()); - const [proxyOptions, setProxyOptions] = useState([]); - const [selectedProxy, setSelectedProxy] = useState(""); - const [displayProxy, setDisplayProxy] = useState(null); - const [records, setRecords] = useState>({}); - const [globalProxy, setGlobalProxy] = useState(""); // 存储全局代理 - const [directProxy, setDirectProxy] = useState(null); // 存储直连代理信息 + // 合并状态,减少状态更新次数 + const [state, setState] = useState({ + proxyData: { + groups: [], + records: {}, + globalProxy: "", + directProxy: null, + }, + selection: { + group: "", + 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(() => { + // 根据模式确定初始组 if (isGlobalMode) { - setSelectedGroup("GLOBAL"); + setState(prev => ({ + ...prev, + selection: { + ...prev.selection, + group: "GLOBAL" + } + })); } else if (isDirectMode) { - setSelectedGroup("DIRECT"); - } else if (primaryGroupName) { + setState(prev => ({ + ...prev, + selection: { + ...prev.selection, + group: "DIRECT" + } + })); + } else { const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP); - setSelectedGroup(savedGroup || primaryGroupName); + setState(prev => ({ + ...prev, + selection: { + ...prev.selection, + group: savedGroup || primaryGroupName || "" + } + })); } }, [isGlobalMode, isDirectMode, primaryGroupName]); - // 获取所有代理组和代理信息 - useEffect(() => { - const fetchProxies = async () => { - try { - const data = await getProxies(); - // 保存所有节点记录信息,用于显示详细节点信息 - 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]); - } + // 带锁的代理数据获取函数,防止并发请求 + const fetchProxyData = useCallback(async (force = false) => { + // 防止重复请求 + if (isRefreshingRef.current) { + pendingRefreshRef.current = true; + return; } - }, [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 { const data = await getProxies(); - // 更新所有代理记录 - setRecords(data.records); - - // 更新代理组信息 + + // 过滤和格式化组 const filteredGroups = data.groups - .filter((g) => g.name !== "DIRECT" && g.name !== "REJECT") - .map((g) => ({ + .filter(g => g.name !== "DIRECT" && g.name !== "REJECT") + .map(g => ({ name: g.name, 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) { - const globalNow = data.global.now || ""; - setSelectedProxy(globalNow); - - if (globalNow && data.records[globalNow]) { - setDisplayProxy(data.records[globalNow]); - } - - // 更新全局组的代理选项 - const options = data.global.all.map((proxy) => ({ - name: proxy.name, - })); - - setProxyOptions(options); - } - // 更新直连代理信息 - else if (isDirectMode && data.records["DIRECT"]) { - setDirectProxy(data.records["DIRECT"]); - setDisplayProxy(data.records["DIRECT"]); - } - // 更新普通模式下当前选中组的信息 - else { - const currentGroup = filteredGroups.find( - (g) => g.name === selectedGroup, - ); - if (currentGroup) { - // 如果当前选中的代理节点与组中的now不一致,则需要更新 - if (currentGroup.now !== selectedProxy) { - setSelectedProxy(currentGroup.now); - - if (data.records[currentGroup.now]) { - setDisplayProxy(data.records[currentGroup.now]); + // 根据模式确定新代理 + if (isDirectMode) { + newGroup = "DIRECT"; + newProxy = "DIRECT"; + newDisplayProxy = data.records?.DIRECT || null; + } else if (isGlobalMode && data.global) { + newGroup = "GLOBAL"; + newProxy = data.global.now || ""; + newDisplayProxy = data.records?.[newProxy] || null; + } else { + // 普通模式 - 检查当前选择的组是否存在 + const currentGroup = filteredGroups.find(g => g.name === prev.selection.group); + + // 如果当前组不存在或为空,自动选择第一个组 + if (!currentGroup && filteredGroups.length > 0) { + newGroup = filteredGroups[0].name; + const firstGroup = filteredGroups[0]; + newProxy = firstGroup.now; + newDisplayProxy = data.records?.[newProxy] || null; + + // 保存到本地存储 + if (!isGlobalMode && !isDirectMode) { + localStorage.setItem(STORAGE_KEY_GROUP, newGroup); + if (newProxy) { + localStorage.setItem(STORAGE_KEY_PROXY, newProxy); + } } + } else if (currentGroup) { + // 使用当前组的代理 + newProxy = currentGroup.now; + newDisplayProxy = data.records?.[newProxy] || null; } - - // 更新代理选项 - 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) { - console.error("刷新代理信息失败", error); + console.error("获取代理信息失败", error); + } finally { + isRefreshingRef.current = false; + + // 处理待处理的刷新请求 + if (pendingRefreshRef.current) { + pendingRefreshRef.current = false; + setTimeout(() => fetchProxyData(), 100); + } } - }; + }, [isGlobalMode, isDirectMode]); - // 每隔一段时间刷新代理信息 - 修改为在所有模式下都刷新 + // 响应 currentProxy 变化 useEffect(() => { - // 初始刷新一次 - refreshProxyData(); + if (currentProxy && (!state.displayProxy || currentProxy.name !== state.displayProxy.name)) { + fetchProxyData(true); + } + }, [currentProxy, fetchProxyData, state.displayProxy]); - // 定期刷新所有模式下的代理信息 - const refreshInterval = setInterval(refreshProxyData, 2000); - return () => clearInterval(refreshInterval); - }, [isGlobalMode, isDirectMode, selectedGroup]); // 依赖项添加selectedGroup以便在切换组时重新设置定时器 + // 平滑的定期刷新,使用固定间隔 + useEffect(() => { + fetchProxyData(); + + 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; - + 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; - + const newProxy = event.target.value; - const previousProxy = selectedProxy; // 保存变更前的代理节点名称 - - setSelectedProxy(newProxy); - - // 更新显示的代理节点信息 - if (records[newProxy]) { - setDisplayProxy(records[newProxy]); + const currentGroup = state.selection.group; + const previousProxy = state.selection.proxy; + + // 立即更新UI,优化体验 + debouncedSetState((prev: ProxyState) => ({ + ...prev, + selection: { + ...prev.selection, + proxy: newProxy + }, + displayProxy: prev.proxyData.records[newProxy] || null + })); + + // 非特殊模式下保存到本地存储 + if (!isGlobalMode && !isDirectMode) { + localStorage.setItem(STORAGE_KEY_PROXY, newProxy); } - + try { // 更新代理设置 - await updateProxy(selectedGroup, newProxy); - - // 添加断开连接逻辑 - 与proxy-groups.tsx中的逻辑相同 + await updateProxy(currentGroup, newProxy); + + // 自动关闭连接设置 if (verge?.auto_close_connection && previousProxy) { getConnections().then(({ connections }) => { - connections.forEach((conn) => { + connections.forEach(conn => { if (conn.chains.includes(previousProxy)) { deleteConnection(conn.id); } }); }); } - + + // 刷新代理信息,使用较短的延迟 setTimeout(() => { refreshProxy(); - if (isGlobalMode || isDirectMode) { - refreshProxyData(); // 特殊模式下额外刷新数据 - } - }, 300); + fetchProxyData(true); + }, 200); } catch (error) { console.error("更新代理失败", error); } - }; + }, [isDirectMode, isGlobalMode, state.proxyData.records, state.selection, verge?.auto_close_connection, refreshProxy, fetchProxyData, debouncedSetState]); // 导航到代理页面 - const goToProxies = () => { - // 修正路由路径,根据_routers.tsx配置,代理页面的路径是"/" + const goToProxies = useCallback(() => { navigate("/"); - }; + }, [navigate]); // 获取要显示的代理节点 - const proxyToDisplay = displayProxy || currentProxy; - + const proxyToDisplay = state.displayProxy || currentProxy; + // 获取当前节点的延迟 const currentDelay = proxyToDisplay - ? delayManager.getDelayFix(proxyToDisplay, selectedGroup) + ? delayManager.getDelayFix(proxyToDisplay, state.selection.group) : -1; - + // 获取信号图标 const signalInfo = getSignalIcon(currentDelay); // 自定义渲染选择框中的值 - const renderProxyValue = (selected: string) => { - if (!selected || !records[selected]) return selected; + const renderProxyValue = useCallback((selected: string) => { + if (!selected || !state.proxyData.records[selected]) return selected; const delayValue = delayManager.getDelayFix( - records[selected], - selectedGroup, + state.proxyData.records[selected], + state.selection.group ); return ( - + {selected} { /> ); - }; + }, [state.proxyData.records, state.selection.group]); return ( { {proxyToDisplay.name} - - + + {proxyToDisplay.type} {isGlobalMode && ( - + )} {isDirectMode && ( - + )} {/* 节点特性 */} - {proxyToDisplay.udp && ( - - )} - {proxyToDisplay.tfo && ( - - )} - {proxyToDisplay.xudp && ( - - )} - {proxyToDisplay.mptcp && ( - - )} - {proxyToDisplay.smux && ( - - )} + {proxyToDisplay.udp && } + {proxyToDisplay.tfo && } + {proxyToDisplay.xudp && } + {proxyToDisplay.mptcp && } + {proxyToDisplay.smux && } @@ -610,31 +494,22 @@ export const CurrentProxyCard = () => { {proxyToDisplay && !isDirectMode && ( )} {/* 代理组选择器 */} - + {t("Group")} { > {proxyOptions.map((proxy) => { const delayValue = delayManager.getDelayFix( - records[proxy.name], - selectedGroup, + state.proxyData.records[proxy.name], + state.selection.group ); return ( ( }); }, []); - // 初始化空数据缓冲区 + // 初始化数据缓冲区 useEffect(() => { // 生成10分钟的初始数据点 const now = Date.now(); @@ -217,8 +216,9 @@ export const EnhancedTrafficGraph = memo(forwardRef( timestamp: timestamp, }; - // 直接更新ref,不触发重渲染 - dataBufferRef.current = [...dataBufferRef.current.slice(1), newPoint]; + // 更新ref,但保持原数组大小 + const newBuffer = [...dataBufferRef.current.slice(1), newPoint]; + dataBufferRef.current = newBuffer; // 使用节流更新显示数据 throttledUpdateData(); @@ -264,161 +264,23 @@ export const EnhancedTrafficGraph = memo(forwardRef( return t("{{time}} Minutes", { time: timeRange }); }, [timeRange, t]); - // 渲染图表内的标签 - const renderInnerLabels = useCallback(() => ( - <> - {/* 上传标签 - 右上角 */} - - {t("Upload")} - - - {/* 下载标签 - 右上角下方 */} - - {t("Download")} - - - ), [colors.up, colors.down, t]); - // 共享图表配置 - const commonProps = useMemo(() => ({ + const chartConfig = useMemo(() => ({ data: displayData, margin: { top: 10, right: 20, left: 0, bottom: 0 }, }), [displayData]); - // 曲线类型 - 使用平滑曲线 - const curveType = "basis"; + // 共享的线条/区域配置 + const commonLineProps = useMemo(() => ({ + dot: false, + strokeWidth: 2, + connectNulls: false, + activeDot: { r: 4, strokeWidth: 1 }, + isAnimationActive: false, // 禁用动画以减少CPU使用 + }), []); - // 共享图表子组件 - const commonChildren = useMemo(() => ( - <> - - - - `${t("Time")}: ${label}`} - contentStyle={{ - backgroundColor: colors.tooltip, - borderColor: colors.grid, - borderRadius: 4, - }} - itemStyle={{ color: colors.text }} - isAnimationActive={false} - /> - - {/* 可点击的时间范围标签 */} - - - {getTimeRangeText()} - - - - ), [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" ? ( - - {commonChildren} - - - {renderInnerLabels()} - - ) : ( - - {commonChildren} - - - {renderInnerLabels()} - - ); - }, [chartStyle, commonProps, commonChildren, renderInnerLabels, colors, t]); + // 曲线类型 - 使用线性曲线避免错位 + const curveType = "monotone"; return ( ( onClick={toggleStyle} > - {renderChart()} + {chartStyle === "line" ? ( + + + + + `${t("Time")}: ${label}`} + contentStyle={{ + backgroundColor: colors.tooltip, + borderColor: colors.grid, + borderRadius: 4, + }} + itemStyle={{ color: colors.text }} + isAnimationActive={false} + /> + + + + {/* 可点击的时间范围标签 */} + + {getTimeRangeText()} + + + {/* 上传标签 - 右上角 */} + + {t("Upload")} + + + {/* 下载标签 - 右上角下方 */} + + {t("Download")} + + + ) : ( + + + + + `${t("Time")}: ${label}`} + contentStyle={{ + backgroundColor: colors.tooltip, + borderColor: colors.grid, + borderRadius: 4, + }} + itemStyle={{ color: colors.text }} + isAnimationActive={false} + /> + + + + {/* 可点击的时间范围标签 */} + + {getTimeRangeText()} + + + {/* 上传标签 - 右上角 */} + + {t("Upload")} + + + {/* 下载标签 - 右上角下方 */} + + {t("Download")} + + + )} ); diff --git a/src/components/home/enhanced-traffic-stats.tsx b/src/components/home/enhanced-traffic-stats.tsx index ed961884..56ebebdb 100644 --- a/src/components/home/enhanced-traffic-stats.tsx +++ b/src/components/home/enhanced-traffic-stats.tsx @@ -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 { Typography, @@ -62,6 +62,7 @@ declare global { // 控制更新频率 const CONNECTIONS_UPDATE_INTERVAL = 5000; // 5秒更新一次连接数据 +const THROTTLE_TRAFFIC_UPDATE = 500; // 500ms节流流量数据更新 // 统计卡片组件 - 使用memo优化 const CompactStatCard = memo(({ @@ -74,18 +75,18 @@ const CompactStatCard = memo(({ }: StatCardProps) => { const theme = useTheme(); - // 获取调色板颜色 - const getColorFromPalette = (colorName: string) => { + // 获取调色板颜色 - 使用useMemo避免重复计算 + const colorValue = useMemo(() => { const palette = theme.palette; if ( - colorName in palette && - palette[colorName as keyof typeof palette] && - "main" in (palette[colorName as keyof typeof palette] as PaletteColor) + color in palette && + palette[color as keyof typeof palette] && + "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; - }; + }, [theme.palette, color]); return ( {icon} @@ -156,24 +157,24 @@ export const EnhancedTrafficStats = () => { const pageVisible = useVisibility(); const [isDebug, setIsDebug] = useState(false); - // 为流量数据和内存数据准备状态 - const [trafficData, setTrafficData] = useState({ - up: 0, - down: 0, - }); - const [memoryData, setMemoryData] = useState({ inuse: 0 }); - const [trafficStats, setTrafficStats] = useState({ - uploadTotal: 0, - downloadTotal: 0, - activeConnections: 0, + // 使用单一状态对象减少状态更新次数 + const [stats, setStats] = useState({ + traffic: { up: 0, down: 0 }, + memory: { inuse: 0, oslimit: undefined as number | undefined }, + connections: { uploadTotal: 0, downloadTotal: 0, activeConnections: 0 }, }); + // 创建一个标记来追踪最后更新时间,用于节流 + const lastUpdateRef = useRef({ traffic: 0 }); + // 是否显示流量图表 const trafficGraph = verge?.traffic_graph ?? true; // WebSocket引用 - const trafficSocketRef = useRef | null>(null); - const memorySocketRef = useRef | null>(null); + const socketRefs = useRef({ + traffic: null as ReturnType | null, + memory: null as ReturnType | null, + }); // 获取连接数据 const fetchConnections = useCallback(async () => { @@ -191,11 +192,14 @@ export const EnhancedTrafficStats = () => { 0, ); - setTrafficStats({ - uploadTotal, - downloadTotal, - activeConnections: connections.connections.length, - }); + setStats(prev => ({ + ...prev, + connections: { + uploadTotal, + downloadTotal, + activeConnections: connections.connections.length, + } + })); } } catch (err) { console.error("Failed to fetch connections:", err); @@ -204,11 +208,11 @@ export const EnhancedTrafficStats = () => { // 定期更新连接数据 useEffect(() => { - if (pageVisible) { - fetchConnections(); - const intervalId = setInterval(fetchConnections, CONNECTIONS_UPDATE_INTERVAL); - return () => clearInterval(intervalId); - } + if (!pageVisible) return; + + fetchConnections(); + const intervalId = setInterval(fetchConnections, CONNECTIONS_UPDATE_INTERVAL); + return () => clearInterval(intervalId); }, [pageVisible, fetchConnections]); // 检查是否支持调试 @@ -216,7 +220,7 @@ export const EnhancedTrafficStats = () => { isDebugEnabled().then((flag) => setIsDebug(flag)); }, []); - // 处理流量数据更新 + // 处理流量数据更新 - 使用节流控制更新频率 const handleTrafficUpdate = useCallback((event: MessageEvent) => { try { const data = JSON.parse(event.data) as ITrafficItem; @@ -225,28 +229,40 @@ export const EnhancedTrafficStats = () => { typeof data.up === "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 const safeUp = isNaN(data.up) ? 0 : data.up; const safeDown = isNaN(data.down) ? 0 : data.down; - setTrafficData({ - up: safeUp, - down: safeDown, - }); + // 批量更新状态 + setStats(prev => ({ + ...prev, + traffic: { up: safeUp, down: safeDown } + })); // 更新图表数据 if (trafficRef.current) { trafficRef.current.appendData({ up: safeUp, down: safeDown, - timestamp: Date.now(), + timestamp: now, }); - - // 清除之前可能存在的动画帧 - if (window.animationFrameId) { - cancelAnimationFrame(window.animationFrameId); - window.animationFrameId = undefined; - } } } } catch (err) { @@ -259,74 +275,50 @@ export const EnhancedTrafficStats = () => { try { const data = JSON.parse(event.data) as MemoryUsage; if (data && typeof data.inuse === "number") { - setMemoryData({ - inuse: isNaN(data.inuse) ? 0 : data.inuse, - oslimit: data.oslimit, - }); + setStats(prev => ({ + ...prev, + memory: { + inuse: isNaN(data.inuse) ? 0 : data.inuse, + oslimit: data.oslimit, + } + })); } } catch (err) { console.error("[Memory] 解析数据错误:", err); } }, []); - // 使用 WebSocket 连接获取流量数据 + // 使用 WebSocket 连接获取数据 - 合并流量和内存连接逻辑 useEffect(() => { if (!clashInfo || !pageVisible) return; const { server, secret = "" } = clashInfo; if (!server) return; + // 清理现有连接的函数 + const cleanupSockets = () => { + Object.values(socketRefs.current).forEach(socket => { + if (socket) { + socket.close(); + } + }); + socketRefs.current = { traffic: null, memory: null }; + }; + // 关闭现有连接 - if (trafficSocketRef.current) { - trafficSocketRef.current.close(); - } + cleanupSockets(); // 创建新连接 - trafficSocketRef.current = createAuthSockette(`${server}/traffic`, secret, { + socketRefs.current.traffic = createAuthSockette(`${server}/traffic`, secret, { onmessage: handleTrafficUpdate, }); - - return () => { - 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, { + + socketRefs.current.memory = createAuthSockette(`${server}/memory`, secret, { onmessage: handleMemoryUpdate, }); - return () => { - if (memorySocketRef.current) { - 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, - ); + return cleanupSockets; + }, [clashInfo, pageVisible, handleTrafficUpdate, handleMemoryUpdate]); // 执行垃圾回收 const handleGarbageCollection = useCallback(async () => { @@ -340,8 +332,22 @@ export const EnhancedTrafficStats = () => { } }, [isDebug]); - // 渲染流量图表 - const renderTrafficGraph = useCallback(() => { + // 使用useMemo计算解析后的流量数据 + 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; return ( @@ -382,58 +388,58 @@ export const EnhancedTrafficStats = () => { ); }, [trafficGraph, pageVisible, theme.palette.divider, isDebug]); - // 统计卡片配置 - const statCards = [ + // 使用useMemo计算统计卡片配置 + const statCards = useMemo(() => [ { icon: , title: t("Upload Speed"), - value: up, - unit: `${upUnit}/s`, + value: parsedData.up, + unit: `${parsedData.upUnit}/s`, color: "secondary" as const, }, { icon: , title: t("Download Speed"), - value: down, - unit: `${downUnit}/s`, + value: parsedData.down, + unit: `${parsedData.downUnit}/s`, color: "primary" as const, }, { icon: , title: t("Active Connections"), - value: trafficStats.activeConnections, + value: stats.connections.activeConnections, unit: "", color: "success" as const, }, { icon: , title: t("Uploaded"), - value: uploadTotal, - unit: uploadTotalUnit, + value: parsedData.uploadTotal, + unit: parsedData.uploadTotalUnit, color: "secondary" as const, }, { icon: , title: t("Downloaded"), - value: downloadTotal, - unit: downloadTotalUnit, + value: parsedData.downloadTotal, + unit: parsedData.downloadTotalUnit, color: "primary" as const, }, { icon: , title: t("Memory Usage"), - value: inuse, - unit: inuseUnit, + value: parsedData.inuse, + unit: parsedData.inuseUnit, color: "error" as const, onClick: isDebug ? handleGarbageCollection : undefined, }, - ]; + ], [t, parsedData, stats.connections.activeConnections, isDebug, handleGarbageCollection]); return ( {/* 流量图表区域 */} - {renderTrafficGraph()} + {trafficGraphComponent} {/* 统计卡片区域 */} {statCards.map((card, index) => ( diff --git a/src/components/home/home-profile-card.tsx b/src/components/home/home-profile-card.tsx index 0d0468a4..20f776fa 100644 --- a/src/components/home/home-profile-card.tsx +++ b/src/components/home/home-profile-card.tsx @@ -22,7 +22,7 @@ import { } from "@mui/icons-material"; import dayjs from "dayjs"; import parseTraffic from "@/utils/parse-traffic"; -import { useState } from "react"; +import { useMemo, useCallback, useState } from "react"; import { openWebUrl, updateProfile } from "@/services/cmds"; import { useLockFn } from "ahooks"; import { Notice } from "@/components/base"; @@ -35,16 +35,16 @@ const round = keyframes` `; // 辅助函数解析URL和过期时间 -function parseUrl(url?: string) { +const parseUrl = (url?: string) => { if (!url) return "-"; if (url.startsWith("http")) return new URL(url).host; return "local"; -} +}; -function parseExpire(expire?: number) { +const parseExpire = (expire?: number) => { if (!expire) return "-"; return dayjs(expire * 1000).format("YYYY-MM-DD"); -} +}; // 使用类型定义,而不是导入 interface ProfileExtra { @@ -64,20 +64,178 @@ export interface ProfileItem { updated?: number; extra?: ProfileExtra; home?: string; - option?: any; // 添加option以兼容原始类型 + option?: any; } export interface HomeProfileCardProps { 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 navigate = useNavigate(); 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 ( + + + {current.url && ( + + + + {t("From")}:{" "} + {current.home ? ( + current.home && openWebUrl(current.home)} + sx={{ display: "inline-flex", alignItems: "center" }} + > + {parseUrl(current.url)} + + + ) : ( + + {parseUrl(current.url)} + + )} + + + )} + + {current.updated && ( + + + + {t("Update Time")}:{" "} + + {dayjs(current.updated * 1000).format("YYYY-MM-DD HH:mm")} + + + + )} + + {current.extra && ( + <> + + + + {t("Used / Total")}:{" "} + + {parseTraffic(usedTraffic)} / {parseTraffic(current.extra.total)} + + + + + {current.extra.expire > 0 && ( + + + + {t("Expire Time")}:{" "} + + {parseExpire(current.extra.expire)} + + + + )} + + + + {trafficPercentage}% + + + + + )} + + + ); +}; + +// 提取空配置组件 +const EmptyProfile = ({ onClick }: { onClick: () => void }) => { + const { t } = useTranslation(); + + return ( + + + + {t("Import")} {t("Profiles")} + + + {t("Click to import subscription")} + + + ); +}; + +export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardProps) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + // 更新当前订阅 const [updating, setUpdating] = useState(false); + const onUpdateProfile = useLockFn(async () => { if (!current?.uid) return; @@ -85,6 +243,7 @@ export const HomeProfileCard = ({ current }: HomeProfileCardProps) => { try { await updateProfile(current.uid); Notice.success(t("Update subscription successfully")); + onProfileUpdated?.(); } catch (err: any) { Notice.error(err?.message || err.toString()); } finally { @@ -93,204 +252,71 @@ export const HomeProfileCard = ({ current }: HomeProfileCardProps) => { }); // 导航到订阅页面 - const goToProfiles = () => { + const goToProfiles = useCallback(() => { navigate("/profile"); - }; + }, [navigate]); + + // 卡片标题 + const cardTitle = useMemo(() => { + if (!current) return t("Profiles"); + + if (!current.home) return current.name; + + return ( + current.home && openWebUrl(current.home)} + sx={{ + display: "inline-flex", + alignItems: "center", + color: "inherit", + textDecoration: "none", + }} + > + {current.name} + + + ); + }, [current, t]); + + // 卡片操作按钮 + const cardAction = useMemo(() => { + if (!current) return null; + + return ( + + ); + }, [current, goToProfiles, t]); return ( current.home && openWebUrl(current.home)} - sx={{ - display: "inline-flex", - alignItems: "center", - color: "inherit", - textDecoration: "none", - }} - > - {current.name} - - - ) : ( - current.name - ) - ) : ( - t("Profiles") - ) - } + title={cardTitle} icon={} iconColor="info" - action={ - current && ( - - ) - } + action={cardAction} > {current ? ( - // 已导入订阅,显示详情 - - - {current.url && ( - - - - {t("From")}:{" "} - {current.home ? ( - current.home && openWebUrl(current.home)} - sx={{ display: "inline-flex", alignItems: "center" }} - > - {parseUrl(current.url)} - - - ) : ( - - {parseUrl(current.url)} - - )} - - - )} - - {current.updated && ( - - - - {t("Update Time")}:{" "} - - {dayjs(current.updated * 1000).format("YYYY-MM-DD HH:mm")} - - - - )} - - {current.extra && ( - <> - - - - {t("Used / Total")}:{" "} - - {parseTraffic( - current.extra.upload + current.extra.download, - )}{" "} - / {parseTraffic(current.extra.total)} - - - - - {current.extra.expire > 0 && ( - - - - {t("Expire Time")}:{" "} - - {parseExpire(current.extra.expire)} - - - - )} - - - - {Math.min( - Math.round( - ((current.extra.download + current.extra.upload) * - 100) / - (current.extra.total + 0.01), - ) + 1, - 100, - )} - % - - - - - )} - - + ) : ( - // 未导入订阅,显示导入按钮 - - - - {t("Import")} {t("Profiles")} - - - {t("Click to import subscription")} - - + )} ); diff --git a/src/components/home/ip-info-card.tsx b/src/components/home/ip-info-card.tsx index 926dc586..dcfd22c1 100644 --- a/src/components/home/ip-info-card.tsx +++ b/src/components/home/ip-info-card.tsx @@ -15,15 +15,50 @@ import { } from "@mui/icons-material"; import { EnhancedCard } from "./enhanced-card"; import { getIpInfo } from "@/services/api"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, memo } from "react"; // 定义刷新时间(秒) const IP_REFRESH_SECONDS = 300; +// 提取InfoItem子组件并使用memo优化 +const InfoItem = memo(({ label, value }: { label: string; value: string }) => ( + + + {label}: + + + {value || "Unknown"} + + +)); + +// 获取国旗表情 +const getCountryFlag = (countryCode: string) => { + if (!countryCode) return ""; + const codePoints = countryCode + .toUpperCase() + .split("") + .map((char) => 127397 + char.charCodeAt(0)); + return String.fromCodePoint(...codePoints); +}; + // IP信息卡片组件 export const IpInfoCard = () => { const { t } = useTranslation(); - const theme = useTheme(); const [ipInfo, setIpInfo] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); @@ -48,250 +83,241 @@ export const IpInfoCard = () => { // 组件加载时获取IP信息 useEffect(() => { fetchIpInfo(); - }, [fetchIpInfo]); - - // 倒计时自动刷新 - useEffect(() => { - const timer = setInterval(() => { - setCountdown((prev) => { - if (prev <= 1) { + + // 倒计时实现优化,减少不必要的重渲染 + let timer: number | null = null; + let currentCount = IP_REFRESH_SECONDS; + + // 只在必要时更新状态,减少重渲染次数 + const startCountdown = () => { + timer = window.setInterval(() => { + currentCount -= 1; + + if (currentCount <= 0) { fetchIpInfo(); - return IP_REFRESH_SECONDS; + currentCount = IP_REFRESH_SECONDS; } - return prev - 1; - }); - }, 1000); - - return () => clearInterval(timer); + + // 每5秒或倒计时结束时才更新UI + if (currentCount % 5 === 0 || currentCount <= 0) { + setCountdown(currentCount); + } + }, 1000); + }; + + startCountdown(); + return () => { + if (timer) clearInterval(timer); + }; }, [fetchIpInfo]); - // 刷新按钮点击处理 - const handleRefresh = () => { - fetchIpInfo(); - }; + const toggleShowIp = useCallback(() => { + setShowIp(prev => !prev); + }, []); - // 切换显示/隐藏IP - const toggleShowIp = () => { - setShowIp(!showIp); - }; - - // 获取国旗表情 - const getCountryFlag = (countryCode: string) => { - if (!countryCode) return ""; - const codePoints = countryCode - .toUpperCase() - .split("") - .map((char) => 127397 + char.charCodeAt(0)); - return String.fromCodePoint(...codePoints); - }; - - // 信息项组件 - 默认不换行,但在需要时可以换行 - const InfoItem = ({ label, value }: { label: string; value: string }) => ( - - } + iconColor="info" + action={ + + + + } > - {label}: - - - {value || t("Unknown")} - - - ); + + + + + + + + ); + } + // 渲染错误状态 + if (error) { + return ( + } + iconColor="info" + action={ + + + + } + > + + + {error} + + + + + ); + } + + // 渲染正常数据 return ( } iconColor="info" action={ - + } > - {loading ? ( - - - - - - - ) : error ? ( - - - {error} - - - - ) : ( - <> + + {/* 左侧:国家和IP地址 */} + - {/* 左侧:国家和IP地址 */} - - - - {getCountryFlag(ipInfo?.country_code)} - - - {ipInfo?.country || t("Unknown")} - - - - - - {t("IP")}: - - - - {showIp ? ipInfo?.ip : "••••••••••"} - - - {showIp ? ( - - ) : ( - - )} - - - - - - - - {/* 右侧:组织、ISP和位置信息 */} - - - - - - - - - - - - - - {t("Auto refresh")}: {countdown}s - - - {ipInfo?.country_code}, {ipInfo?.longitude?.toFixed(2)},{" "} - {ipInfo?.latitude?.toFixed(2)} + {getCountryFlag(ipInfo?.country_code)} + + + {ipInfo?.country || t("Unknown")} - - )} + + + + {t("IP")}: + + + + {showIp ? ipInfo?.ip : "••••••••••"} + + + {showIp ? ( + + ) : ( + + )} + + + + + + + + {/* 右侧:组织、ISP和位置信息 */} + + + + + + + + + + + {t("Auto refresh")}: {countdown}s + + + {ipInfo?.country_code}, {ipInfo?.longitude?.toFixed(2)},{" "} + {ipInfo?.latitude?.toFixed(2)} + + ); diff --git a/src/components/home/proxy-tun-card.tsx b/src/components/home/proxy-tun-card.tsx index cfe14319..ae915369 100644 --- a/src/components/home/proxy-tun-card.tsx +++ b/src/components/home/proxy-tun-card.tsx @@ -7,17 +7,16 @@ import { Tooltip, alpha, useTheme, - Button, Fade, } from "@mui/material"; -import { useState, useEffect } from "react"; +import { useState, useMemo, memo, FC } from "react"; import ProxyControlSwitches from "@/components/shared/ProxyControlSwitches"; import { Notice } from "@/components/base"; import { - LanguageRounded, ComputerRounded, TroubleshootRounded, HelpOutlineRounded, + SvgIconComponent, } from "@mui/icons-material"; import useSWR from "swr"; import { @@ -26,15 +25,131 @@ import { getRunningMode, } 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 = memo(({ + isActive, + onClick, + icon: Icon, + label, + hasIndicator = false +}) => ( + + + + {label} + + {hasIndicator && ( + + )} + +)); + +interface TabDescriptionProps { + description: string; + tooltipTitle: string; +} + +// 抽取描述文本组件 +const TabDescription: FC = memo(({ description, tooltipTitle }) => ( + + + {description} + + + + + +)); + +export const ProxyTunCard: FC = () => { const { t } = useTranslation(); const theme = useTheme(); - const [error, setError] = useState(null); - const [activeTab, setActiveTab] = useState("system"); + const [activeTab, setActiveTab] = useState(() => + localStorage.getItem(LOCAL_STORAGE_TAB_KEY) || "system" + ); // 获取代理状态信息 const { data: sysproxy } = useSWR("getSystemProxy", getSystemProxy); - const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy); const { data: runningMode } = useSWR("getRunningMode", getRunningMode); // 是否以sidecar模式运行 @@ -42,26 +157,34 @@ export const ProxyTunCard = () => { // 处理错误 const handleError = (err: Error) => { - setError(err.message); Notice.error(err.message || err.toString(), 3000); }; - // 用户提示文本 - const getTabDescription = (tab: string) => { - switch (tab) { - case "system": - 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 ""; - } + // 处理标签切换并保存到localStorage + const handleTabChange = (tab: string) => { + setActiveTab(tab); + localStorage.setItem(LOCAL_STORAGE_TAB_KEY, tab); }; + // 用户提示文本 - 使用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 ( {/* 选项卡 */} @@ -75,112 +198,19 @@ export const ProxyTunCard = () => { zIndex: 2, }} > - setActiveTab("system")} - sx={{ - cursor: "pointer", - px: 2, - py: 1, - display: "flex", - alignItems: "center", - justifyContent: "center", - gap: 1, - bgcolor: - 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%)", - } - : {}, - }} - > - - - {t("System Proxy")} - - {sysproxy?.enable && ( - - )} - - 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%)", - } - : {}, - }} - > - - - {t("Tun Mode")} - - + handleTabChange("system")} + icon={ComputerRounded} + label={t("System Proxy")} + hasIndicator={sysproxy?.enable} + /> + handleTabChange("tun")} + icon={TroubleshootRounded} + label={t("Tun Mode")} + /> {/* 说明文本区域 */} @@ -194,71 +224,10 @@ export const ProxyTunCard = () => { overflow: "visible", }} > - {activeTab === "system" && ( - - - {getTabDescription("system")} - - - - - - )} - - {activeTab === "tun" && ( - - - {getTabDescription("tun")} - - - - - - )} + {/* 控制开关部分 */} diff --git a/src/components/home/system-info-card.tsx b/src/components/home/system-info-card.tsx index bc4b7b29..82d7874c 100644 --- a/src/components/home/system-info-card.tsx +++ b/src/components/home/system-info-card.tsx @@ -7,7 +7,7 @@ import useSWR from "swr"; import { getRunningMode, getSystemInfo, installService } from "@/services/cmds"; import { useNavigate } from "react-router-dom"; 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 { useLockFn } from "ahooks"; import { Notice } from "@/components/base"; @@ -17,99 +17,97 @@ export const SystemInfoCard = () => { const { verge, patchVerge } = useVerge(); const navigate = useNavigate(); + // 系统信息状态 + const [systemState, setSystemState] = useState({ + osInfo: "", + lastCheckUpdate: "-", + }); + // 获取运行模式 const { data: runningMode = "sidecar", mutate: mutateRunningMode } = useSWR( "getRunningMode", getRunningMode, + { suspense: false, revalidateOnFocus: false } ); - // 获取系统信息 - const [osInfo, setOsInfo] = useState(""); + // 初始化系统信息 useEffect(() => { + // 获取系统信息 getSystemInfo() .then((info) => { const lines = info.split("\n"); if (lines.length > 0) { - // 提取系统名称和版本信息 - const sysNameLine = lines[0]; // System Name: xxx - const sysVersionLine = lines[1]; // System Version: xxx - - const sysName = sysNameLine.split(": ")[1] || ""; - const sysVersion = sysVersionLine.split(": ")[1] || ""; - - setOsInfo(`${sysName} ${sysVersion}`); + const sysName = lines[0].split(": ")[1] || ""; + const sysVersion = lines[1].split(": ")[1] || ""; + setSystemState(prev => ({ ...prev, osInfo: `${sysName} ${sysVersion}` })); } }) - .catch((err) => { - console.error("Error getting system info:", err); - }); - }, []); + .catch(console.error); - // 获取最后检查更新时间 - const [lastCheckUpdate, setLastCheckUpdate] = useState("-"); - - // 在组件挂载时检查本地存储中的最后更新时间 - useEffect(() => { // 获取最后检查更新时间 const lastCheck = localStorage.getItem("last_check_update"); if (lastCheck) { try { const timestamp = parseInt(lastCheck, 10); if (!isNaN(timestamp)) { - const date = new Date(timestamp); - setLastCheckUpdate(date.toLocaleString()); + setSystemState(prev => ({ + ...prev, + lastCheckUpdate: new Date(timestamp).toLocaleString() + })); } } catch (e) { console.error("Error parsing last check update time", e); } } else if (verge?.auto_check_update) { - // 如果启用了自动检查更新但没有最后检查时间记录,则触发一次检查 + // 如果启用了自动检查更新但没有记录,设置当前时间并延迟检查 const now = Date.now(); localStorage.setItem("last_check_update", now.toString()); - setLastCheckUpdate(new Date(now).toLocaleString()); - - // 延迟执行检查更新,避免在应用启动时立即执行 + setSystemState(prev => ({ + ...prev, + lastCheckUpdate: new Date(now).toLocaleString() + })); + setTimeout(() => { - checkUpdate().catch((e) => console.error("Error checking update:", e)); + if (verge?.auto_check_update) { + checkUpdate().catch(console.error); + } }, 5000); } }, [verge?.auto_check_update]); - // 监听 checkUpdate 调用并更新时间 + // 自动检查更新逻辑 useSWR( - "checkUpdate", + verge?.auto_check_update ? "checkUpdate" : null, async () => { - // 更新最后检查时间 const now = Date.now(); localStorage.setItem("last_check_update", now.toString()); - setLastCheckUpdate(new Date(now).toLocaleString()); - - // 实际执行检查更新 + setSystemState(prev => ({ + ...prev, + lastCheckUpdate: new Date(now).toLocaleString() + })); return await checkUpdate(); }, { revalidateOnFocus: false, - refreshInterval: 24 * 60 * 60 * 1000, // 每天检查一次更新 - dedupingInterval: 60 * 60 * 1000, // 1小时内不重复检查, - isPaused: () => !(verge?.auto_check_update ?? true), // 根据 auto_check_update 设置决定是否启用 - }, + refreshInterval: 24 * 60 * 60 * 1000, // 每天检查一次 + dedupingInterval: 60 * 60 * 1000, // 1小时内不重复检查 + } ); // 导航到设置页面 - const goToSettings = () => { + const goToSettings = useCallback(() => { navigate("/settings"); - }; + }, [navigate]); // 切换自启动状态 - const toggleAutoLaunch = async () => { + const toggleAutoLaunch = useCallback(async () => { + if (!verge) return; try { - if (!verge) return; - // 将当前的启动状态取反 await patchVerge({ enable_auto_launch: !verge.enable_auto_launch }); } catch (err) { console.error("切换开机自启动状态失败:", err); } - }; + }, [verge, patchVerge]); // 安装系统服务 const onInstallService = useLockFn(async () => { @@ -117,34 +115,48 @@ export const SystemInfoCard = () => { Notice.info(t("Installing Service..."), 1000); await installService(); Notice.success(t("Service Installed Successfully"), 2000); - // 重新获取运行模式 await mutateRunningMode(); } catch (err: any) { Notice.error(err.message || err.toString(), 3000); } }); - // 点击运行模式 - const handleRunningModeClick = () => { + // 点击运行模式处理 + const handleRunningModeClick = useCallback(() => { if (runningMode === "sidecar") { onInstallService(); } - }; + }, [runningMode, onInstallService]); // 检查更新 - const onCheckUpdate = async () => { + const onCheckUpdate = useLockFn(async () => { try { const info = await checkUpdate(); if (!info?.available) { Notice.success(t("Currently on the Latest Version")); } else { Notice.info(t("Update Available"), 2000); - goToSettings(); // 跳转到设置页面查看更新 + goToSettings(); } } catch (err: any) { 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 ( { } > - {verge && ( - - - - {t("OS Info")} - - - {osInfo} - - - - - - {t("Auto Launch")} - - - - - - - {t("Running Mode")} - - - {runningMode === "service" - ? t("Service Mode") - : t("Sidecar Mode")} - - - - - - {t("Last Check Update")} - - - {lastCheckUpdate} - - - - - - {t("Verge Version")} - - - v{appVersion} - - + + + + {t("OS Info")} + + + {systemState.osInfo} + - )} + + + + {t("Auto Launch")} + + + + + + + {t("Running Mode")} + + + {runningMode === "service" ? t("Service Mode") : t("Sidecar Mode")} + + + + + + {t("Last Check Update")} + + + {systemState.lastCheckUpdate} + + + + + + {t("Verge Version")} + + + v{appVersion} + + + ); }; diff --git a/src/components/home/test-card.tsx b/src/components/home/test-card.tsx index cab3641b..83e8a9d6 100644 --- a/src/components/home/test-card.tsx +++ b/src/components/home/test-card.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useMemo, useCallback } from "react"; import { useVerge } from "@/hooks/use-verge"; import { Box, IconButton, Tooltip, alpha, styled } from "@mui/material"; 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 = () => { const { t } = useTranslation(); const sensors = useSensors(useSensor(PointerSensor)); const { verge, mutateVerge, patchVerge } = useVerge(); + const viewerRef = useRef(null); - // test list - const testList = verge?.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, - }, - ]; + // 使用useMemo优化测试列表,避免每次渲染重新计算 + const testList = useMemo(() => { + return verge?.test_list ?? DEFAULT_TEST_LIST; + }, [verge?.test_list]); - const onTestListItemChange = ( - uid: string, - patch?: Partial, - ) => { - if (patch) { - const newList = testList.map((x) => { - if (x.uid === uid) { - return { ...x, ...patch }; - } - return x; - }); + // 使用useCallback优化函数引用,避免不必要的重新渲染 + const onTestListItemChange = useCallback( + (uid: string, patch?: Partial) => { + if (!patch) { + mutateVerge(); + return; + } + + const newList = testList.map((x) => + x.uid === uid ? { ...x, ...patch } : x + ); + mutateVerge({ ...verge, test_list: newList }, false); - } else { - mutateVerge(); - } - }; + }, + [testList, verge, mutateVerge] + ); - const onDeleteTestListItem = (uid: string) => { - const newList = testList.filter((x) => x.uid !== uid); - patchVerge({ test_list: newList }); - mutateVerge({ ...verge, test_list: newList }, false); - }; + const onDeleteTestListItem = useCallback( + (uid: string) => { + const newList = testList.filter((x) => x.uid !== uid); + patchVerge({ test_list: newList }); + mutateVerge({ ...verge, test_list: newList }, false); + }, + [testList, verge, patchVerge, mutateVerge] + ); - const onDragEnd = async (event: DragEndEvent) => { - const { active, over } = event; - if (over && active.id !== over.id) { - let old_index = testList.findIndex((x) => x.uid === active.id); - let new_index = testList.findIndex((x) => x.uid === over.id); + const onDragEnd = useCallback( + async (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + + 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) { const newList = [...testList]; const [removed] = newList.splice(old_index, 1); @@ -109,17 +121,42 @@ export const TestCard = () => { await mutateVerge({ ...verge, test_list: newList }, false); await patchVerge({ test_list: newList }); } - } - }; + }, + [testList, verge, mutateVerge, patchVerge] + ); + // 仅在verge首次加载时初始化测试列表 useEffect(() => { - if (!verge) return; - if (!verge?.test_list) { - patchVerge({ test_list: testList }); + if (verge && !verge.test_list) { + patchVerge({ test_list: DEFAULT_TEST_LIST }); } - }, [verge]); + }, [verge, patchVerge]); - const viewerRef = useRef(null); + // 使用useMemo优化UI内容,减少渲染计算 + const renderTestItems = useMemo(() => ( + + x.uid)}> + {testList.map((item) => ( + + viewerRef.current?.edit(item)} + onDelete={onDeleteTestListItem} + /> + + ))} + + + ), [testList, onDeleteTestListItem]); + + const handleTestAll = useCallback(() => { + emit("verge://test-all"); + }, []); + + const handleCreateTest = useCallback(() => { + viewerRef.current?.create(); + }, []); return ( { action={ - emit("verge://test-all")}> + - viewerRef.current?.create()} - > + @@ -149,20 +183,7 @@ export const TestCard = () => { collisionDetection={closestCenter} onDragEnd={onDragEnd} > - - x.uid)}> - {testList.map((item) => ( - - viewerRef.current?.edit(item)} - onDelete={onDeleteTestListItem} - /> - - ))} - - + {renderTestItems} diff --git a/src/locales/en.json b/src/locales/en.json index 17ff8601..73517834 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -207,9 +207,9 @@ "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 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", - "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", + "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", + "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", "System and Mixed Can Only be Used in Service Mode": "System and Mixed Can Only be Used in Service Mode", "Device": "Device Name", diff --git a/src/locales/zh.json b/src/locales/zh.json index 97b765ea..69897c52 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -207,9 +207,9 @@ "System Proxy Disabled": "系统代理已关闭,建议大多数用户打开此选项", "TUN Mode Service Required": "TUN模式需要服务模式,请先安装服务", "TUN Mode Intercept Info": "TUN模式可以接管所有应用流量,适用于特殊应用", - "Rule Mode Description": "基于预设规则智能判断流量走向,提供灵活的代理策略", - "Global Mode Description": "所有流量均通过代理服务器,适用于需要全局科学上网的场景", - "Direct Mode Description": "所有流量不经过代理节点,但经过Clash内核转发连接目标服务器,适用于需要通过内核进行分流的特定场景", + "rule Mode Description": "基于预设规则智能判断流量走向,提供灵活的代理策略", + "global Mode Description": "所有流量均通过代理服务器,适用于需要全局科学上网的场景", + "direct Mode Description": "所有流量不经过代理节点,但经过Clash内核转发连接目标服务器,适用于需要通过内核进行分流的特定场景", "Stack": "TUN 模式堆栈", "System and Mixed Can Only be Used in Service Mode": "System 和 Mixed 只能在服务模式下使用", "Device": "TUN 网卡名称", diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 56f242e7..a5a8eaf7 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -201,7 +201,7 @@ const HomeSettingsDialog = ({ const HomePage = () => { const { t } = useTranslation(); const { verge } = useVerge(); - const { current } = useProfiles(); + const { current, mutateProfiles } = useProfiles(); const navigate = useNavigate(); const theme = useTheme(); @@ -275,7 +275,10 @@ const HomePage = () => { {/* 订阅和当前节点部分 */} {homeCards.profile && ( - + )}