refactor: proxy components

This commit is contained in:
Slinetrac
2025-10-15 09:00:03 +08:00
parent e6b7d512fb
commit ef9ccafe61
8 changed files with 177 additions and 165 deletions

View File

@@ -31,7 +31,7 @@ import {
Typography, Typography,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useSWR from "swr"; import useSWR from "swr";
import { import {
@@ -196,9 +196,10 @@ export const ProxyChain = ({
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const { proxies } = useAppData(); const { proxies } = useAppData();
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [isConnecting, setIsConnecting] = useState(false); const [isConnecting, setIsConnecting] = useState(false);
const [isConnected, setIsConnected] = useState(false); const markUnsavedChanges = useCallback(() => {
onMarkUnsavedChanges?.();
}, [onMarkUnsavedChanges]);
// 获取当前代理信息以检查连接状态 // 获取当前代理信息以检查连接状态
const { data: currentProxies, mutate: mutateProxies } = useSWR( const { data: currentProxies, mutate: mutateProxies } = useSWR(
@@ -211,52 +212,26 @@ export const ProxyChain = ({
}, },
); );
// 检查连接状态 const isConnected = useMemo(() => {
useEffect(() => {
if (!currentProxies || proxyChain.length < 2) { if (!currentProxies || proxyChain.length < 2) {
setIsConnected(false); return false;
return;
} }
// 获取用户配置的最后一个节点
const lastNode = proxyChain[proxyChain.length - 1]; const lastNode = proxyChain[proxyChain.length - 1];
// 根据模式确定要检查的代理组和当前选中的代理
if (mode === "global") { if (mode === "global") {
// 全局模式:检查 global 对象 return currentProxies.global?.now === lastNode.name;
if (!currentProxies.global || !currentProxies.global.now) {
setIsConnected(false);
return;
}
// 检查当前选中的代理是否是配置的最后一个节点
if (currentProxies.global.now === lastNode.name) {
setIsConnected(true);
} else {
setIsConnected(false);
}
} else {
// 规则模式:检查指定的代理组
if (!selectedGroup) {
setIsConnected(false);
return;
}
const proxyChainGroup = currentProxies.groups.find(
(group) => group.name === selectedGroup,
);
if (!proxyChainGroup || !proxyChainGroup.now) {
setIsConnected(false);
return;
}
// 检查当前选中的代理是否是配置的最后一个节点
if (proxyChainGroup.now === lastNode.name) {
setIsConnected(true);
} else {
setIsConnected(false);
}
} }
if (!selectedGroup || !Array.isArray(currentProxies.groups)) {
return false;
}
const proxyChainGroup = currentProxies.groups.find(
(group) => group.name === selectedGroup,
);
return proxyChainGroup?.now === lastNode.name;
}, [currentProxies, proxyChain, mode, selectedGroup]); }, [currentProxies, proxyChain, mode, selectedGroup]);
// 监听链的变化,但排除从配置加载的情况 // 监听链的变化,但排除从配置加载的情况
@@ -267,10 +242,10 @@ export const ProxyChain = ({
chainLengthRef.current !== proxyChain.length && chainLengthRef.current !== proxyChain.length &&
chainLengthRef.current !== 0 chainLengthRef.current !== 0
) { ) {
setHasUnsavedChanges(true); markUnsavedChanges();
} }
chainLengthRef.current = proxyChain.length; chainLengthRef.current = proxyChain.length;
}, [proxyChain.length]); }, [proxyChain.length, markUnsavedChanges]);
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
@@ -288,26 +263,21 @@ export const ProxyChain = ({
const newIndex = proxyChain.findIndex((item) => item.id === over?.id); const newIndex = proxyChain.findIndex((item) => item.id === over?.id);
onUpdateChain(arrayMove(proxyChain, oldIndex, newIndex)); onUpdateChain(arrayMove(proxyChain, oldIndex, newIndex));
setHasUnsavedChanges(true); markUnsavedChanges();
} }
}, },
[proxyChain, onUpdateChain], [proxyChain, onUpdateChain, markUnsavedChanges],
); );
const handleRemoveProxy = useCallback( const handleRemoveProxy = useCallback(
(id: string) => { (id: string) => {
const newChain = proxyChain.filter((item) => item.id !== id); const newChain = proxyChain.filter((item) => item.id !== id);
onUpdateChain(newChain); onUpdateChain(newChain);
setHasUnsavedChanges(true); markUnsavedChanges();
}, },
[proxyChain, onUpdateChain], [proxyChain, onUpdateChain, markUnsavedChanges],
); );
const handleClearAll = useCallback(() => {
onUpdateChain([]);
setHasUnsavedChanges(true);
}, [onUpdateChain]);
const handleConnect = useCallback(async () => { const handleConnect = useCallback(async () => {
if (isConnected) { if (isConnected) {
// 如果已连接,则断开连接 // 如果已连接,则断开连接
@@ -327,10 +297,6 @@ export const ProxyChain = ({
// 清空链式代理配置UI // 清空链式代理配置UI
// onUpdateChain([]); // onUpdateChain([]);
// setHasUnsavedChanges(false);
// 强制更新连接状态
setIsConnected(false);
} catch (error) { } catch (error) {
console.error("Failed to disconnect from proxy chain:", error); console.error("Failed to disconnect from proxy chain:", error);
alert(t("Failed to disconnect from proxy chain") || "断开链式代理失败"); alert(t("Failed to disconnect from proxy chain") || "断开链式代理失败");
@@ -372,9 +338,6 @@ export const ProxyChain = ({
// 刷新代理信息以更新连接状态 // 刷新代理信息以更新连接状态
mutateProxies(); mutateProxies();
// 清除未保存标记
setHasUnsavedChanges(false);
console.log("Successfully connected to proxy chain"); console.log("Successfully connected to proxy chain");
} catch (error) { } catch (error) {
console.error("Failed to connect to proxy chain:", error); console.error("Failed to connect to proxy chain:", error);
@@ -411,7 +374,6 @@ export const ProxyChain = ({
delay: undefined, delay: undefined,
})) || []; })) || [];
onUpdateChain(chainItems); onUpdateChain(chainItems);
setHasUnsavedChanges(false);
} catch (parseError) { } catch (parseError) {
console.error("Failed to parse YAML:", parseError); console.error("Failed to parse YAML:", parseError);
onUpdateChain([]); onUpdateChain([]);
@@ -435,7 +397,6 @@ export const ProxyChain = ({
delay: undefined, delay: undefined,
})) || []; })) || [];
onUpdateChain(chainItems); onUpdateChain(chainItems);
setHasUnsavedChanges(false);
} catch (jsonError) { } catch (jsonError) {
console.error("Failed to parse as JSON either:", jsonError); console.error("Failed to parse as JSON either:", jsonError);
onUpdateChain([]); onUpdateChain([]);
@@ -448,7 +409,6 @@ export const ProxyChain = ({
} else if (chainConfigData === "") { } else if (chainConfigData === "") {
// Empty string means no proxies available, show empty state // Empty string means no proxies available, show empty state
onUpdateChain([]); onUpdateChain([]);
setHasUnsavedChanges(false);
} }
}, [chainConfigData, onUpdateChain]); }, [chainConfigData, onUpdateChain]);
@@ -519,7 +479,6 @@ export const ProxyChain = ({
onClick={() => { onClick={() => {
updateProxyChainConfigInRuntime(null); updateProxyChainConfigInRuntime(null);
onUpdateChain([]); onUpdateChain([]);
setHasUnsavedChanges(false);
}} }}
sx={{ sx={{
color: theme.palette.error.main, color: theme.palette.error.main,

View File

@@ -46,6 +46,8 @@ interface ProxyChainItem {
delay?: number; delay?: number;
} }
const VirtuosoFooter = () => <div style={{ height: "8px" }} />;
export const ProxyGroups = (props: Props) => { export const ProxyGroups = (props: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { mode, isChainMode = false, chainConfigData } = props; const { mode, isChainMode = false, chainConfigData } = props;
@@ -61,23 +63,25 @@ export const ProxyGroups = (props: Props) => {
const { verge } = useVerge(); const { verge } = useVerge();
const { proxies: proxiesData } = useAppData(); const { proxies: proxiesData } = useAppData();
const groups = proxiesData?.groups;
const availableGroups = useMemo(() => groups ?? [], [groups]);
// 当链式代理模式且规则模式下,如果没有选择代理组,默认选择第一个 const defaultRuleGroup = useMemo(() => {
useEffect(() => { if (isChainMode && mode === "rule" && availableGroups.length > 0) {
if ( return availableGroups[0].name;
isChainMode &&
mode === "rule" &&
!selectedGroup &&
proxiesData?.groups?.length > 0
) {
setSelectedGroup(proxiesData.groups[0].name);
} }
}, [isChainMode, mode, selectedGroup, proxiesData]); return null;
}, [availableGroups, isChainMode, mode]);
const activeSelectedGroup = useMemo(
() => selectedGroup ?? defaultRuleGroup,
[selectedGroup, defaultRuleGroup],
);
const { renderList, onProxies, onHeadState } = useRenderList( const { renderList, onProxies, onHeadState } = useRenderList(
mode, mode,
isChainMode, isChainMode,
selectedGroup, activeSelectedGroup,
); );
const getGroupHeadState = useCallback( const getGroupHeadState = useCallback(
@@ -112,6 +116,8 @@ export const ProxyGroups = (props: Props) => {
useEffect(() => { useEffect(() => {
if (renderList.length === 0) return; if (renderList.length === 0) return;
let restoreTimer: ReturnType<typeof setTimeout> | null = null;
try { try {
const savedPositions = localStorage.getItem("proxy-scroll-positions"); const savedPositions = localStorage.getItem("proxy-scroll-positions");
if (savedPositions) { if (savedPositions) {
@@ -120,7 +126,7 @@ export const ProxyGroups = (props: Props) => {
const savedPosition = positions[mode]; const savedPosition = positions[mode];
if (savedPosition !== undefined) { if (savedPosition !== undefined) {
setTimeout(() => { restoreTimer = setTimeout(() => {
virtuosoRef.current?.scrollTo({ virtuosoRef.current?.scrollTo({
top: savedPosition, top: savedPosition,
behavior: "auto", behavior: "auto",
@@ -131,6 +137,12 @@ export const ProxyGroups = (props: Props) => {
} catch (e) { } catch (e) {
console.error("Error restoring scroll position:", e); console.error("Error restoring scroll position:", e);
} }
return () => {
if (restoreTimer) {
clearTimeout(restoreTimer);
}
};
}, [mode, renderList.length]); }, [mode, renderList.length]);
// 改为使用节流函数保存滚动位置 // 改为使用节流函数保存滚动位置
@@ -150,25 +162,30 @@ export const ProxyGroups = (props: Props) => {
); );
// 使用改进的滚动处理 // 使用改进的滚动处理
const handleScroll = useCallback( const handleScroll = useMemo(
throttle((e: any) => { () =>
const scrollTop = e.target.scrollTop; throttle((event: Event) => {
setShowScrollTop(scrollTop > 100); const target = event.target as HTMLElement | null;
// 使用稳定的节流来保存位置而不是setTimeout const scrollTop = target?.scrollTop ?? 0;
saveScrollPosition(scrollTop); setShowScrollTop(scrollTop > 100);
}, 500), // 增加到500ms以确保平滑滚动 // 使用稳定的节流来保存位置而不是setTimeout
saveScrollPosition(scrollTop);
}, 500), // 增加到500ms以确保平滑滚动
[saveScrollPosition], [saveScrollPosition],
); );
// 添加和清理滚动事件监听器 // 添加和清理滚动事件监听器
useEffect(() => { useEffect(() => {
if (!scrollerRef.current) return; const node = scrollerRef.current;
scrollerRef.current.addEventListener("scroll", handleScroll, { if (!node) return;
passive: true,
}); const listener = handleScroll as EventListener;
const options: AddEventListenerOptions = { passive: true };
node.addEventListener("scroll", listener, options);
return () => { return () => {
scrollerRef.current?.removeEventListener("scroll", handleScroll); node.removeEventListener("scroll", listener, options);
}; };
}, [handleScroll]); }, [handleScroll]);
@@ -186,18 +203,14 @@ export const ProxyGroups = (props: Props) => {
setDuplicateWarning({ open: false, message: "" }); setDuplicateWarning({ open: false, message: "" });
}, []); }, []);
// 获取当前选中的代理组信息 const currentGroup = useMemo(() => {
const getCurrentGroup = useCallback(() => { if (!activeSelectedGroup) return null;
if (!selectedGroup || !proxiesData?.groups) return null; return (
return proxiesData.groups.find( availableGroups.find(
(group: any) => group.name === selectedGroup, (group: any) => group.name === activeSelectedGroup,
) ?? null
); );
}, [selectedGroup, proxiesData]); }, [activeSelectedGroup, availableGroups]);
// 获取可用的代理组列表
const getAvailableGroups = useCallback(() => {
return proxiesData?.groups || [];
}, [proxiesData]);
// 处理代理组选择菜单 // 处理代理组选择菜单
const handleGroupMenuOpen = (event: React.MouseEvent<HTMLElement>) => { const handleGroupMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
@@ -220,9 +233,6 @@ export const ProxyGroups = (props: Props) => {
} }
}; };
const currentGroup = getCurrentGroup();
const availableGroups = getAvailableGroups();
const handleChangeProxy = useCallback( const handleChangeProxy = useCallback(
(group: IProxyGroupItem, proxy: IProxyItem) => { (group: IProxyGroupItem, proxy: IProxyItem) => {
if (isChainMode) { if (isChainMode) {
@@ -472,7 +482,7 @@ export const ProxyGroups = (props: Props) => {
scrollerRef.current = ref as Element; scrollerRef.current = ref as Element;
}} }}
components={{ components={{
Footer: () => <div style={{ height: "8px" }} />, Footer: VirtuosoFooter,
}} }}
initialScrollTop={scrollPositionRef.current[mode]} initialScrollTop={scrollPositionRef.current[mode]}
computeItemKey={(index) => renderList[index].key} computeItemKey={(index) => renderList[index].key}
@@ -498,7 +508,7 @@ export const ProxyGroups = (props: Props) => {
onUpdateChain={setProxyChain} onUpdateChain={setProxyChain}
chainConfigData={chainConfigData} chainConfigData={chainConfigData}
mode={mode} mode={mode}
selectedGroup={selectedGroup} selectedGroup={activeSelectedGroup}
/> />
</Box> </Box>
</Box> </Box>
@@ -530,11 +540,11 @@ export const ProxyGroups = (props: Props) => {
}, },
}} }}
> >
{availableGroups.map((group: any, _index: number) => ( {availableGroups.map((group: any) => (
<MenuItem <MenuItem
key={group.name} key={group.name}
onClick={() => handleGroupSelect(group.name)} onClick={() => handleGroupSelect(group.name)}
selected={selectedGroup === group.name} selected={activeSelectedGroup === group.name}
sx={{ sx={{
fontSize: "14px", fontSize: "14px",
py: 1, py: 1,
@@ -591,7 +601,7 @@ export const ProxyGroups = (props: Props) => {
scrollerRef.current = ref as Element; scrollerRef.current = ref as Element;
}} }}
components={{ components={{
Footer: () => <div style={{ height: "8px" }} />, Footer: VirtuosoFooter,
}} }}
// 添加平滑滚动设置 // 添加平滑滚动设置
initialScrollTop={scrollPositionRef.current[mode]} initialScrollTop={scrollPositionRef.current[mode]}

View File

@@ -31,8 +31,10 @@ interface Props {
onHeadState: (val: Partial<HeadState>) => void; onHeadState: (val: Partial<HeadState>) => void;
} }
const defaultSx: SxProps = {};
export const ProxyHead = ({ export const ProxyHead = ({
sx = {}, sx = defaultSx,
url, url,
groupName, groupName,
headState, headState,

View File

@@ -1,7 +1,7 @@
import { CheckCircleOutlineRounded } from "@mui/icons-material"; import { CheckCircleOutlineRounded } from "@mui/icons-material";
import { alpha, Box, ListItemButton, styled, Typography } from "@mui/material"; import { alpha, Box, ListItemButton, styled, Typography } from "@mui/material";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useEffect, useState } from "react"; import { useCallback, useEffect, useReducer } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { BaseLoading } from "@/components/base"; import { BaseLoading } from "@/components/base";
@@ -26,7 +26,7 @@ export const ProxyItemMini = (props: Props) => {
const isPreset = presetList.includes(proxy.name); const isPreset = presetList.includes(proxy.name);
// -1/<=0 为 不显示 // -1/<=0 为 不显示
// -2 为 loading // -2 为 loading
const [delay, setDelay] = useState(-1); const [delay, setDelay] = useReducer((_: number, value: number) => value, -1);
const { verge } = useVerge(); const { verge } = useVerge();
const timeout = verge?.default_latency_timeout || 10000; const timeout = verge?.default_latency_timeout || 10000;
@@ -39,11 +39,15 @@ export const ProxyItemMini = (props: Props) => {
}; };
}, [isPreset, proxy.name, group.name]); }, [isPreset, proxy.name, group.name]);
useEffect(() => { const updateDelay = useCallback(() => {
if (!proxy) return; if (!proxy) return;
setDelay(delayManager.getDelayFix(proxy, group.name)); setDelay(delayManager.getDelayFix(proxy, group.name));
}, [proxy, group.name]); }, [proxy, group.name]);
useEffect(() => {
updateDelay();
}, [updateDelay]);
const onDelay = useLockFn(async () => { const onDelay = useLockFn(async () => {
setDelay(-2); setDelay(-2);
setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout)); setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout));

View File

@@ -11,7 +11,7 @@ import {
Theme, Theme,
} from "@mui/material"; } from "@mui/material";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useEffect, useState } from "react"; import { useCallback, useEffect, useReducer } from "react";
import { BaseLoading } from "@/components/base"; import { BaseLoading } from "@/components/base";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
@@ -51,7 +51,7 @@ export const ProxyItem = (props: Props) => {
const isPreset = presetList.includes(proxy.name); const isPreset = presetList.includes(proxy.name);
// -1/<=0 为 不显示 // -1/<=0 为 不显示
// -2 为 loading // -2 为 loading
const [delay, setDelay] = useState(-1); const [delay, setDelay] = useReducer((_: number, value: number) => value, -1);
const { verge } = useVerge(); const { verge } = useVerge();
const timeout = verge?.default_latency_timeout || 10000; const timeout = verge?.default_latency_timeout || 10000;
useEffect(() => { useEffect(() => {
@@ -63,10 +63,14 @@ export const ProxyItem = (props: Props) => {
}; };
}, [proxy.name, group.name, isPreset]); }, [proxy.name, group.name, isPreset]);
useEffect(() => { const updateDelay = useCallback(() => {
if (!proxy) return; if (!proxy) return;
setDelay(delayManager.getDelayFix(proxy, group.name)); setDelay(delayManager.getDelayFix(proxy, group.name));
}, [group.name, proxy]); }, [proxy, group.name]);
useEffect(() => {
updateDelay();
}, [updateDelay]);
const onDelay = useLockFn(async () => { const onDelay = useLockFn(async () => {
setDelay(-2); setDelay(-2);

View File

@@ -14,7 +14,7 @@ import {
Tooltip, Tooltip,
} from "@mui/material"; } from "@mui/material";
import { convertFileSrc } from "@tauri-apps/api/core"; import { convertFileSrc } from "@tauri-apps/api/core";
import { useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
@@ -49,7 +49,7 @@ export const ProxyRender = (props: RenderProps) => {
onCheckAll, onCheckAll,
onHeadState, onHeadState,
onChangeProxy, onChangeProxy,
isChainMode = false, isChainMode: _ = false,
} = props; } = props;
const { type, group, headState, proxy, proxyCol } = item; const { type, group, headState, proxy, proxyCol } = item;
const { verge } = useVerge(); const { verge } = useVerge();
@@ -59,23 +59,42 @@ export const ProxyRender = (props: RenderProps) => {
const itembackgroundcolor = isDark ? "#282A36" : "#ffffff"; const itembackgroundcolor = isDark ? "#282A36" : "#ffffff";
const [iconCachePath, setIconCachePath] = useState(""); const [iconCachePath, setIconCachePath] = useState("");
useEffect(() => { const initIconCachePath = useCallback(async () => {
initIconCachePath();
}, [group]);
async function initIconCachePath() {
if (group.icon && group.icon.trim().startsWith("http")) { if (group.icon && group.icon.trim().startsWith("http")) {
const fileName = const fileName =
group.name.replaceAll(" ", "") + "-" + getFileName(group.icon); group.name.replaceAll(" ", "") + "-" + getFileName(group.icon);
const iconPath = await downloadIconCache(group.icon, fileName); const iconPath = await downloadIconCache(group.icon, fileName);
setIconCachePath(convertFileSrc(iconPath)); setIconCachePath(convertFileSrc(iconPath));
} else {
setIconCachePath("");
} }
} }, [group.icon, group.name]);
useEffect(() => {
initIconCachePath();
}, [initIconCachePath]);
function getFileName(url: string) { function getFileName(url: string) {
return url.substring(url.lastIndexOf("/") + 1); return url.substring(url.lastIndexOf("/") + 1);
} }
const proxyColItemsMemo = useMemo(() => {
if (type !== 4 || !proxyCol) {
return null;
}
return proxyCol.map((proxyItem) => (
<ProxyItemMini
key={`${item.key}-${proxyItem?.name ?? "unknown"}`}
group={group}
proxy={proxyItem!}
selected={group.now === proxyItem?.name}
showType={headState?.showType}
onClick={() => onChangeProxy(group, proxyItem!)}
/>
));
}, [type, proxyCol, item.key, group, headState, onChangeProxy]);
if (type === 0) { if (type === 0) {
return ( return (
<ListItemButton <ListItemButton
@@ -205,18 +224,6 @@ export const ProxyRender = (props: RenderProps) => {
} }
if (type === 4) { if (type === 4) {
const proxyColItemsMemo = useMemo(() => {
return proxyCol?.map((proxy) => (
<ProxyItemMini
key={item.key + proxy.name}
group={group}
proxy={proxy!}
selected={group.now === proxy.name}
showType={headState?.showType}
onClick={() => onChangeProxy(group, proxy!)}
/>
));
}, [proxyCol, group, headState]);
return ( return (
<Box <Box
sx={{ sx={{

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useReducer } from "react";
import delayManager from "@/services/delay"; import delayManager from "@/services/delay";
@@ -11,7 +11,7 @@ export default function useFilterSort(
filterText: string, filterText: string,
sortType: ProxySortType, sortType: ProxySortType,
) { ) {
const [, setRefresh] = useState({}); const [_, bumpRefresh] = useReducer((count: number) => count + 1, 0);
useEffect(() => { useEffect(() => {
let last = 0; let last = 0;
@@ -21,7 +21,7 @@ export default function useFilterSort(
const now = Date.now(); const now = Date.now();
if (now - last > 666) { if (now - last > 666) {
last = now; last = now;
setRefresh({}); bumpRefresh();
} }
}); });

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useReducer } from "react";
import { useProfiles } from "@/hooks/use-profiles"; import { useProfiles } from "@/hooks/use-profiles";
@@ -25,15 +25,38 @@ export const DEFAULT_STATE: HeadState = {
testUrl: "", testUrl: "",
}; };
type HeadStateAction =
| { type: "reset" }
| { type: "replace"; payload: Record<string, HeadState> }
| { type: "update"; groupName: string; patch: Partial<HeadState> };
function headStateReducer(
state: Record<string, HeadState>,
action: HeadStateAction,
): Record<string, HeadState> {
switch (action.type) {
case "reset":
return {};
case "replace":
return action.payload;
case "update": {
const prev = state[action.groupName] || DEFAULT_STATE;
return { ...state, [action.groupName]: { ...prev, ...action.patch } };
}
default:
return state;
}
}
export function useHeadStateNew() { export function useHeadStateNew() {
const { profiles } = useProfiles(); const { profiles } = useProfiles();
const current = profiles?.current || ""; const current = profiles?.current || "";
const [state, setState] = useState<Record<string, HeadState>>({}); const [state, dispatch] = useReducer(headStateReducer, {});
useEffect(() => { useEffect(() => {
if (!current) { if (!current) {
setState({}); dispatch({ type: "reset" });
return; return;
} }
@@ -45,36 +68,39 @@ export function useHeadStateNew() {
const value = data[current] || {}; const value = data[current] || {};
if (value && typeof value === "object") { if (value && typeof value === "object") {
setState(value); dispatch({ type: "replace", payload: value });
} else { } else {
setState({}); dispatch({ type: "reset" });
} }
} catch {} } catch {
dispatch({ type: "reset" });
}
}, [current]); }, [current]);
useEffect(() => {
if (!current) return;
const timer = setTimeout(() => {
try {
const item = localStorage.getItem(HEAD_STATE_KEY);
let data = (item ? JSON.parse(item) : {}) as HeadStateStorage;
if (!data || typeof data !== "object") data = {};
data[current] = state;
localStorage.setItem(HEAD_STATE_KEY, JSON.stringify(data));
} catch {}
});
return () => clearTimeout(timer);
}, [state, current]);
const setHeadState = useCallback( const setHeadState = useCallback(
(groupName: string, obj: Partial<HeadState>) => { (groupName: string, obj: Partial<HeadState>) => {
setState((old) => { if (!current) return;
const state = old[groupName] || DEFAULT_STATE; dispatch({ type: "update", groupName, patch: obj });
const ret = { ...old, [groupName]: { ...state, ...obj } };
// 保存到存储中
setTimeout(() => {
try {
const item = localStorage.getItem(HEAD_STATE_KEY);
let data = (item ? JSON.parse(item) : {}) as HeadStateStorage;
if (!data || typeof data !== "object") data = {};
data[current] = ret;
localStorage.setItem(HEAD_STATE_KEY, JSON.stringify(data));
} catch {}
});
return ret;
});
}, },
[current], [current],
); );