mirror of
https://github.com/clash-verge-rev/clash-verge-rev
synced 2025-05-05 03:33:45 +08:00
refactor: Optimize proxy rendering and layout calculation
This commit is contained in:
parent
23d1d210c7
commit
2b534e0d51
@ -473,7 +473,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
<div style={{ position: "relative", height: "100%" }}>
|
<div style={{ position: "relative", height: "100%" }}>
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
style={{ height: "calc(100% - 16px)" }}
|
style={{ height: "calc(100% - 14px)" }}
|
||||||
totalCount={renderList.length}
|
totalCount={renderList.length}
|
||||||
increaseViewportBy={{ top: 256, bottom: 256 }}
|
increaseViewportBy={{ top: 256, bottom: 256 }}
|
||||||
overscan={150}
|
overscan={150}
|
||||||
@ -482,7 +482,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
scrollerRef.current = ref as Element;
|
scrollerRef.current = ref as Element;
|
||||||
}}
|
}}
|
||||||
components={{
|
components={{
|
||||||
Footer: () => <div style={{ height: "16px" }} />,
|
Footer: () => <div style={{ height: "8px" }} />,
|
||||||
}}
|
}}
|
||||||
itemContent={(index) => (
|
itemContent={(index) => (
|
||||||
<ProxyRender
|
<ProxyRender
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { useEffect, useMemo, useRef, useState, useCallback } from "react";
|
import { useEffect, useMemo, useCallback } from "react";
|
||||||
import { getProxies } from "@/services/api";
|
import { getProxies } from "@/services/api";
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { filterSort } from "./use-filter-sort";
|
import { filterSort } from "./use-filter-sort";
|
||||||
@ -11,7 +11,7 @@ import {
|
|||||||
} from "./use-head-state";
|
} from "./use-head-state";
|
||||||
|
|
||||||
export interface IRenderItem {
|
export interface IRenderItem {
|
||||||
// 组 | head | item | empty | item col
|
// 组 | head | item | empty | item col
|
||||||
type: 0 | 1 | 2 | 3 | 4;
|
type: 0 | 1 | 2 | 3 | 4;
|
||||||
key: string;
|
key: string;
|
||||||
group: IProxyGroupItem;
|
group: IProxyGroupItem;
|
||||||
@ -19,180 +19,59 @@ export interface IRenderItem {
|
|||||||
col?: number;
|
col?: number;
|
||||||
proxyCol?: IProxyItem[];
|
proxyCol?: IProxyItem[];
|
||||||
headState?: HeadState;
|
headState?: HeadState;
|
||||||
|
// 新增支持图标和其他元数据
|
||||||
|
icon?: string;
|
||||||
|
provider?: string;
|
||||||
|
testUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProxiesData {
|
// 优化列布局计算
|
||||||
groups: IProxyGroupItem[];
|
const calculateColumns = (width: number, configCol: number): number => {
|
||||||
global?: IProxyGroupItem;
|
if (configCol > 0 && configCol < 6) return configCol;
|
||||||
proxies: any[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓存计算结果
|
if (width > 1920) return 5;
|
||||||
const groupCache = new WeakMap<ProxiesData, Map<string, IProxyGroupItem>>();
|
if (width > 1450) return 4;
|
||||||
// 用于追踪缓存的key
|
if (width > 1024) return 3;
|
||||||
const cacheKeys = new Set<ProxiesData>();
|
if (width > 900) return 2;
|
||||||
|
if (width >= 600) return 2;
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 优化分组逻辑
|
||||||
|
const groupProxies = <T = any>(list: T[], size: number): T[][] => {
|
||||||
|
return list.reduce((acc, item) => {
|
||||||
|
const lastGroup = acc[acc.length - 1];
|
||||||
|
if (!lastGroup || lastGroup.length >= size) {
|
||||||
|
acc.push([item]);
|
||||||
|
} else {
|
||||||
|
lastGroup.push(item);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [] as T[][]);
|
||||||
|
};
|
||||||
|
|
||||||
export const useRenderList = (mode: string) => {
|
export const useRenderList = (mode: string) => {
|
||||||
// 添加用户交互标记
|
|
||||||
const isUserInteracting = useRef(false);
|
|
||||||
const interactionTimer = useRef<number | null>(null);
|
|
||||||
// 添加上一次有效的数据缓存
|
|
||||||
const [lastValidData, setLastValidData] = useState<ProxiesData | null>(null);
|
|
||||||
// 添加刷新锁
|
|
||||||
const refreshLock = useRef(false);
|
|
||||||
const lastRenderList = useRef<IRenderItem[]>([]);
|
|
||||||
|
|
||||||
// 组件卸载时清理
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (interactionTimer.current) {
|
|
||||||
clearTimeout(interactionTimer.current);
|
|
||||||
}
|
|
||||||
refreshLock.current = false;
|
|
||||||
isUserInteracting.current = false;
|
|
||||||
// 清理 WeakMap 缓存
|
|
||||||
cacheKeys.forEach((key) => {
|
|
||||||
groupCache.delete(key);
|
|
||||||
});
|
|
||||||
cacheKeys.clear();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 优化数据获取函数
|
|
||||||
const fetchProxies = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
if (isUserInteracting.current || refreshLock.current) {
|
|
||||||
return lastValidData;
|
|
||||||
}
|
|
||||||
const data = await getProxies();
|
|
||||||
|
|
||||||
// 预处理和缓存组数据
|
|
||||||
if (data && !groupCache.has(data)) {
|
|
||||||
const groupMap = new Map();
|
|
||||||
data.groups.forEach((group) => {
|
|
||||||
groupMap.set(group.name, group);
|
|
||||||
});
|
|
||||||
groupCache.set(data, groupMap);
|
|
||||||
cacheKeys.add(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLastValidData(data);
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
if (lastValidData) return lastValidData;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}, [lastValidData]);
|
|
||||||
|
|
||||||
const { data: proxiesData, mutate: mutateProxies } = useSWR(
|
const { data: proxiesData, mutate: mutateProxies } = useSWR(
|
||||||
"getProxies",
|
"getProxies",
|
||||||
fetchProxies,
|
getProxies,
|
||||||
{
|
{
|
||||||
refreshInterval: 2000,
|
refreshInterval: 2000,
|
||||||
dedupingInterval: 1000,
|
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
keepPreviousData: true,
|
revalidateOnReconnect: true,
|
||||||
onSuccess: (data) => {
|
|
||||||
if (!data || isUserInteracting.current) return;
|
|
||||||
|
|
||||||
if (proxiesData) {
|
|
||||||
try {
|
|
||||||
const groupMap = groupCache.get(proxiesData);
|
|
||||||
if (!groupMap) return;
|
|
||||||
|
|
||||||
const needUpdate = data.groups.some((group: IProxyGroupItem) => {
|
|
||||||
const oldGroup = groupMap.get(group.name);
|
|
||||||
if (!oldGroup) return true;
|
|
||||||
|
|
||||||
return (
|
|
||||||
oldGroup.now !== group.now ||
|
|
||||||
oldGroup.type !== group.type ||
|
|
||||||
JSON.stringify(oldGroup.all) !== JSON.stringify(group.all)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!needUpdate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Data comparison error:", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 优化mutateProxies包装函数
|
|
||||||
const wrappedMutateProxies = useCallback(async () => {
|
|
||||||
if (interactionTimer.current) {
|
|
||||||
clearTimeout(interactionTimer.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 立即更新本地状态以响应UI
|
|
||||||
if (proxiesData) {
|
|
||||||
const currentGroup = proxiesData.groups.find(
|
|
||||||
(g) => g.now !== undefined,
|
|
||||||
);
|
|
||||||
if (currentGroup) {
|
|
||||||
const optimisticData = { ...proxiesData };
|
|
||||||
setLastValidData(optimisticData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行实际的更新并等待结果
|
|
||||||
const result = await mutateProxies();
|
|
||||||
|
|
||||||
// 更新最新数据
|
|
||||||
if (result) {
|
|
||||||
setLastValidData(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to update proxies:", error);
|
|
||||||
// 发生错误时恢复到之前的状态
|
|
||||||
if (proxiesData) {
|
|
||||||
setLastValidData(proxiesData);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
// 重置状态
|
|
||||||
isUserInteracting.current = false;
|
|
||||||
refreshLock.current = false;
|
|
||||||
if (interactionTimer.current) {
|
|
||||||
clearTimeout(interactionTimer.current);
|
|
||||||
interactionTimer.current = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [proxiesData, mutateProxies]);
|
|
||||||
|
|
||||||
// 确保初始数据加载后更新lastValidData
|
|
||||||
useEffect(() => {
|
|
||||||
if (proxiesData && !lastValidData) {
|
|
||||||
setLastValidData(proxiesData);
|
|
||||||
}
|
|
||||||
}, [proxiesData]);
|
|
||||||
|
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { width } = useWindowWidth();
|
const { width } = useWindowWidth();
|
||||||
|
|
||||||
const col = useMemo(() => {
|
|
||||||
const baseCol = Math.floor(verge?.proxy_layout_column || 6);
|
|
||||||
if (baseCol >= 6 || baseCol <= 0) {
|
|
||||||
if (width > 1450) return 4;
|
|
||||||
if (width > 1024) return 3;
|
|
||||||
if (width > 900) return 2;
|
|
||||||
if (width >= 600) return 2;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return baseCol;
|
|
||||||
}, [verge?.proxy_layout_column, width]);
|
|
||||||
|
|
||||||
const [headStates, setHeadState] = useHeadStateNew();
|
const [headStates, setHeadState] = useHeadStateNew();
|
||||||
|
|
||||||
// 优化初始数据加载
|
// 计算列数
|
||||||
|
const col = useMemo(
|
||||||
|
() => calculateColumns(width, verge?.proxy_layout_column || 6),
|
||||||
|
[width, verge?.proxy_layout_column],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 确保代理数据加载
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!proxiesData) return;
|
if (!proxiesData) return;
|
||||||
const { groups, proxies } = proxiesData;
|
const { groups, proxies } = proxiesData;
|
||||||
@ -201,26 +80,31 @@ export const useRenderList = (mode: string) => {
|
|||||||
(mode === "rule" && !groups.length) ||
|
(mode === "rule" && !groups.length) ||
|
||||||
(mode === "global" && proxies.length < 2)
|
(mode === "global" && proxies.length < 2)
|
||||||
) {
|
) {
|
||||||
const timer = setTimeout(() => mutateProxies(), 500);
|
setTimeout(() => mutateProxies(), 500);
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
}
|
||||||
}, [proxiesData, mode, mutateProxies]);
|
}, [proxiesData, mode, mutateProxies]);
|
||||||
|
|
||||||
// 优化渲染列表计算
|
// 处理渲染列表
|
||||||
const renderList = useMemo(() => {
|
const renderList: IRenderItem[] = useMemo(() => {
|
||||||
const currentData = proxiesData || lastValidData;
|
if (!proxiesData) return [];
|
||||||
if (!currentData) return lastRenderList.current;
|
|
||||||
|
|
||||||
const useRule = mode === "rule" || mode === "script";
|
const useRule = mode === "rule" || mode === "script";
|
||||||
const renderGroups =
|
const renderGroups =
|
||||||
(useRule && currentData.groups.length
|
useRule && proxiesData.groups.length
|
||||||
? currentData.groups
|
? proxiesData.groups
|
||||||
: [currentData.global!]) || [];
|
: [proxiesData.global!];
|
||||||
|
|
||||||
const newList = renderGroups.flatMap((group: IProxyGroupItem) => {
|
const retList = renderGroups.flatMap((group) => {
|
||||||
const headState = headStates[group.name] || DEFAULT_STATE;
|
const headState = headStates[group.name] || DEFAULT_STATE;
|
||||||
const ret: IRenderItem[] = [
|
const ret: IRenderItem[] = [
|
||||||
{ type: 0, key: group.name, group, headState },
|
{
|
||||||
|
type: 0,
|
||||||
|
key: group.name,
|
||||||
|
group,
|
||||||
|
headState,
|
||||||
|
icon: group.icon,
|
||||||
|
testUrl: group.testUrl,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (headState?.open || !useRule) {
|
if (headState?.open || !useRule) {
|
||||||
@ -231,94 +115,56 @@ export const useRenderList = (mode: string) => {
|
|||||||
headState.sortType,
|
headState.sortType,
|
||||||
);
|
);
|
||||||
|
|
||||||
ret.push({ type: 1, key: `head-${group.name}`, group, headState });
|
ret.push({
|
||||||
|
type: 1,
|
||||||
|
key: `head-${group.name}`,
|
||||||
|
group,
|
||||||
|
headState,
|
||||||
|
});
|
||||||
|
|
||||||
if (!proxies.length) {
|
if (!proxies.length) {
|
||||||
ret.push({ type: 3, key: `empty-${group.name}`, group, headState });
|
ret.push({
|
||||||
return ret;
|
type: 3,
|
||||||
}
|
key: `empty-${group.name}`,
|
||||||
|
group,
|
||||||
if (col > 1) {
|
headState,
|
||||||
|
});
|
||||||
|
} else if (col > 1) {
|
||||||
return ret.concat(
|
return ret.concat(
|
||||||
groupList(proxies, col).map((proxyCol) => ({
|
groupProxies(proxies, col).map((proxyCol) => ({
|
||||||
type: 4,
|
type: 4,
|
||||||
key: `col-${group.name}-${proxyCol[0].name}`,
|
key: `col-${group.name}-${proxyCol[0].name}`,
|
||||||
group,
|
group,
|
||||||
headState,
|
headState,
|
||||||
col,
|
col,
|
||||||
proxyCol,
|
proxyCol,
|
||||||
|
provider: proxyCol[0].provider,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return ret.concat(
|
||||||
|
proxies.map((proxy) => ({
|
||||||
|
type: 2,
|
||||||
|
key: `${group.name}-${proxy!.name}`,
|
||||||
|
group,
|
||||||
|
proxy,
|
||||||
|
headState,
|
||||||
|
provider: proxy.provider,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret.concat(
|
|
||||||
proxies.map((proxy) => ({
|
|
||||||
type: 2,
|
|
||||||
key: `${group.name}-${proxy!.name}`,
|
|
||||||
group,
|
|
||||||
proxy,
|
|
||||||
headState,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredList = !useRule
|
if (!useRule) return retList.slice(1);
|
||||||
? newList.slice(1)
|
return retList.filter((item) => !item.group.hidden);
|
||||||
: newList.filter((item) => !item.group.hidden);
|
}, [headStates, proxiesData, mode, col]);
|
||||||
|
|
||||||
lastRenderList.current = filteredList;
|
|
||||||
return filteredList;
|
|
||||||
}, [headStates, proxiesData, lastValidData, mode, col]);
|
|
||||||
|
|
||||||
// 添加滚动处理
|
|
||||||
useEffect(() => {
|
|
||||||
const handleScroll = () => {
|
|
||||||
if (!isUserInteracting.current) {
|
|
||||||
isUserInteracting.current = true;
|
|
||||||
|
|
||||||
// 清除之前的定时器
|
|
||||||
if (interactionTimer.current) {
|
|
||||||
clearTimeout(interactionTimer.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置新的定时器,在滚动停止后恢复刷新
|
|
||||||
interactionTimer.current = window.setTimeout(() => {
|
|
||||||
isUserInteracting.current = false;
|
|
||||||
// 手动触发一次更新
|
|
||||||
wrappedMutateProxies();
|
|
||||||
}, 1000) as unknown as number;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("scroll", handleScroll);
|
|
||||||
if (interactionTimer.current) {
|
|
||||||
clearTimeout(interactionTimer.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [wrappedMutateProxies]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
renderList,
|
renderList,
|
||||||
onProxies: wrappedMutateProxies,
|
onProxies: mutateProxies,
|
||||||
onHeadState: setHeadState,
|
onHeadState: setHeadState,
|
||||||
|
currentColumns: col,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function groupList<T = any>(list: T[], size: number): T[][] {
|
|
||||||
return list.reduce((p, n) => {
|
|
||||||
if (!p.length) return [[n]];
|
|
||||||
|
|
||||||
const i = p.length - 1;
|
|
||||||
if (p[i].length < size) {
|
|
||||||
p[i].push(n);
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.push([n]);
|
|
||||||
return p;
|
|
||||||
}, [] as T[][]);
|
|
||||||
}
|
|
||||||
|
@ -377,7 +377,7 @@ const ProfilePage = () => {
|
|||||||
sx={{
|
sx={{
|
||||||
pl: "10px",
|
pl: "10px",
|
||||||
pr: "10px",
|
pr: "10px",
|
||||||
height: "94%",
|
height: "calc(100% - 48px)",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -416,7 +416,7 @@ const ProfilePage = () => {
|
|||||||
flexItem
|
flexItem
|
||||||
sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
|
sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
|
||||||
></Divider>
|
></Divider>
|
||||||
<Box sx={{ mt: 1.5 }}>
|
<Box sx={{ mt: 1.5, mb: "10px" }}>
|
||||||
<Grid2 container spacing={{ xs: 1, lg: 1 }}>
|
<Grid2 container spacing={{ xs: 1, lg: 1 }}>
|
||||||
<Grid2 size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
|
<Grid2 size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
|
||||||
<ProfileMore
|
<ProfileMore
|
||||||
|
Loading…
x
Reference in New Issue
Block a user