mirror of
https://github.com/clash-verge-rev/clash-verge-rev
synced 2025-05-05 10:23:44 +08:00
266 lines
7.0 KiB
TypeScript
266 lines
7.0 KiB
TypeScript
import useSWR from "swr";
|
||
import { useEffect, useMemo, useRef, useState, useCallback } from "react";
|
||
import { getProxies } from "@/services/api";
|
||
import { useVerge } from "@/hooks/use-verge";
|
||
import { filterSort } from "./use-filter-sort";
|
||
import { useWindowWidth } from "./use-window-width";
|
||
import {
|
||
useHeadStateNew,
|
||
DEFAULT_STATE,
|
||
type HeadState,
|
||
} from "./use-head-state";
|
||
|
||
export interface IRenderItem {
|
||
// 组 | head | item | empty | item col
|
||
type: 0 | 1 | 2 | 3 | 4;
|
||
key: string;
|
||
group: IProxyGroupItem;
|
||
proxy?: IProxyItem;
|
||
col?: number;
|
||
proxyCol?: IProxyItem[];
|
||
headState?: HeadState;
|
||
}
|
||
|
||
interface ProxiesData {
|
||
groups: IProxyGroupItem[];
|
||
global?: IProxyGroupItem;
|
||
proxies: any[];
|
||
}
|
||
|
||
// 缓存计算结果
|
||
const groupCache = new WeakMap<ProxiesData, Map<string, IProxyGroupItem>>();
|
||
// 用于追踪缓存的key
|
||
const cacheKeys = new Set<ProxiesData>();
|
||
|
||
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(
|
||
"getProxies",
|
||
fetchProxies,
|
||
{
|
||
refreshInterval: isUserInteracting.current ? 0 : 2000,
|
||
dedupingInterval: 1000,
|
||
revalidateOnFocus: false,
|
||
keepPreviousData: 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);
|
||
return !oldGroup || oldGroup.now !== group.now;
|
||
});
|
||
|
||
if (!needUpdate) return;
|
||
} catch (e) {
|
||
console.error("Data comparison error:", e);
|
||
return;
|
||
}
|
||
}
|
||
},
|
||
},
|
||
);
|
||
|
||
// 优化mutateProxies包装函数
|
||
const wrappedMutateProxies = useCallback(async () => {
|
||
refreshLock.current = true;
|
||
isUserInteracting.current = true;
|
||
|
||
if (interactionTimer.current) {
|
||
clearTimeout(interactionTimer.current);
|
||
}
|
||
|
||
try {
|
||
if (!lastValidData && proxiesData) {
|
||
setLastValidData(proxiesData);
|
||
}
|
||
return await mutateProxies();
|
||
} finally {
|
||
interactionTimer.current = window.setTimeout(() => {
|
||
isUserInteracting.current = false;
|
||
refreshLock.current = false;
|
||
interactionTimer.current = null;
|
||
}, 2000);
|
||
}
|
||
}, [proxiesData, lastValidData, mutateProxies]);
|
||
|
||
// 确保初始数据加载后更新lastValidData
|
||
useEffect(() => {
|
||
if (proxiesData && !lastValidData) {
|
||
setLastValidData(proxiesData);
|
||
}
|
||
}, [proxiesData]);
|
||
|
||
const { verge } = useVerge();
|
||
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();
|
||
|
||
// 优化初始数据加载
|
||
useEffect(() => {
|
||
if (!proxiesData) return;
|
||
const { groups, proxies } = proxiesData;
|
||
|
||
if (
|
||
(mode === "rule" && !groups.length) ||
|
||
(mode === "global" && proxies.length < 2)
|
||
) {
|
||
const timer = setTimeout(() => mutateProxies(), 500);
|
||
return () => clearTimeout(timer);
|
||
}
|
||
}, [proxiesData, mode, mutateProxies]);
|
||
|
||
// 优化渲染列表计算
|
||
const renderList = useMemo(() => {
|
||
const currentData = proxiesData || lastValidData;
|
||
if (!currentData) return lastRenderList.current;
|
||
|
||
const useRule = mode === "rule" || mode === "script";
|
||
const renderGroups =
|
||
(useRule && currentData.groups.length
|
||
? currentData.groups
|
||
: [currentData.global!]) || [];
|
||
|
||
const newList = renderGroups.flatMap((group: IProxyGroupItem) => {
|
||
const headState = headStates[group.name] || DEFAULT_STATE;
|
||
const ret: IRenderItem[] = [
|
||
{ type: 0, key: group.name, group, headState },
|
||
];
|
||
|
||
if (headState?.open || !useRule) {
|
||
const proxies = filterSort(
|
||
group.all,
|
||
group.name,
|
||
headState.filterText,
|
||
headState.sortType,
|
||
);
|
||
|
||
ret.push({ type: 1, key: `head-${group.name}`, group, headState });
|
||
|
||
if (!proxies.length) {
|
||
ret.push({ type: 3, key: `empty-${group.name}`, group, headState });
|
||
return ret;
|
||
}
|
||
|
||
if (col > 1) {
|
||
return ret.concat(
|
||
groupList(proxies, col).map((proxyCol) => ({
|
||
type: 4,
|
||
key: `col-${group.name}-${proxyCol[0].name}`,
|
||
group,
|
||
headState,
|
||
col,
|
||
proxyCol,
|
||
})),
|
||
);
|
||
}
|
||
|
||
return ret.concat(
|
||
proxies.map((proxy) => ({
|
||
type: 2,
|
||
key: `${group.name}-${proxy!.name}`,
|
||
group,
|
||
proxy,
|
||
headState,
|
||
})),
|
||
);
|
||
}
|
||
return ret;
|
||
});
|
||
|
||
const filteredList = !useRule
|
||
? newList.slice(1)
|
||
: newList.filter((item) => !item.group.hidden);
|
||
|
||
lastRenderList.current = filteredList;
|
||
return filteredList;
|
||
}, [headStates, proxiesData, lastValidData, mode, col]);
|
||
|
||
return {
|
||
renderList,
|
||
onProxies: wrappedMutateProxies,
|
||
onHeadState: setHeadState,
|
||
};
|
||
};
|
||
|
||
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[][]);
|
||
}
|