mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 17:15:38 +08:00
fix: optimize asynchronous handling to prevent UI blocking in various components
fix: add missing showNotice error handling and improve async UI feedback - Add showNotice error notifications to unlock page async error branches - Restore showNotice for YAML serialization errors in rules/groups/proxies editor - Ensure all user-facing async errors are surfaced via showNotice - Add fade-in animation to layout for smoother theme transition and reduce white screen - Use requestIdleCallback/setTimeout for heavy UI state updates to avoid UI blocking - Minor: remove window.showNotice usage, use direct import instead
This commit is contained in:
@@ -199,38 +199,31 @@ export const EnhancedTrafficStats = () => {
|
|||||||
// 使用节流控制更新频率
|
// 使用节流控制更新频率
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastUpdateRef.current.traffic < THROTTLE_TRAFFIC_UPDATE) {
|
if (now - lastUpdateRef.current.traffic < THROTTLE_TRAFFIC_UPDATE) {
|
||||||
// 如果距离上次更新时间小于阈值,只更新图表不更新状态
|
try {
|
||||||
if (trafficRef.current) {
|
trafficRef.current?.appendData({
|
||||||
trafficRef.current.appendData({
|
|
||||||
up: data.up,
|
up: data.up,
|
||||||
down: data.down,
|
down: data.down,
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
});
|
});
|
||||||
}
|
} catch { }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新最后更新时间
|
|
||||||
lastUpdateRef.current.traffic = now;
|
lastUpdateRef.current.traffic = now;
|
||||||
|
|
||||||
// 验证数据有效性,防止NaN
|
|
||||||
const safeUp = isNaN(data.up) ? 0 : data.up;
|
const safeUp = isNaN(data.up) ? 0 : data.up;
|
||||||
const safeDown = isNaN(data.down) ? 0 : data.down;
|
const safeDown = isNaN(data.down) ? 0 : data.down;
|
||||||
|
try {
|
||||||
// 批量更新状态
|
setStats(prev => ({
|
||||||
setStats(prev => ({
|
...prev,
|
||||||
...prev,
|
traffic: { up: safeUp, down: safeDown }
|
||||||
traffic: { up: safeUp, down: safeDown }
|
}));
|
||||||
}));
|
} catch { }
|
||||||
|
try {
|
||||||
// 更新图表数据
|
trafficRef.current?.appendData({
|
||||||
if (trafficRef.current) {
|
|
||||||
trafficRef.current.appendData({
|
|
||||||
up: safeUp,
|
up: safeUp,
|
||||||
down: safeDown,
|
down: safeDown,
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
});
|
});
|
||||||
}
|
} catch { }
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Traffic] 解析数据错误:", err, event.data);
|
console.error("[Traffic] 解析数据错误:", err, event.data);
|
||||||
@@ -317,6 +310,18 @@ export const EnhancedTrafficStats = () => {
|
|||||||
return cleanupSockets;
|
return cleanupSockets;
|
||||||
}, [clashInfo, pageVisible, handleTrafficUpdate, handleMemoryUpdate]);
|
}, [clashInfo, pageVisible, handleTrafficUpdate, handleMemoryUpdate]);
|
||||||
|
|
||||||
|
// 组件卸载时清理所有定时器/引用
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
try {
|
||||||
|
Object.values(socketRefs.current).forEach(socket => {
|
||||||
|
if (socket) socket.close();
|
||||||
|
});
|
||||||
|
socketRefs.current = { traffic: null, memory: null };
|
||||||
|
} catch { }
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 执行垃圾回收
|
// 执行垃圾回收
|
||||||
const handleGarbageCollection = useCallback(async () => {
|
const handleGarbageCollection = useCallback(async () => {
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
|
|||||||
@@ -117,8 +117,18 @@ export const TestCard = () => {
|
|||||||
const [removed] = newList.splice(old_index, 1);
|
const [removed] = newList.splice(old_index, 1);
|
||||||
newList.splice(new_index, 0, removed);
|
newList.splice(new_index, 0, removed);
|
||||||
|
|
||||||
await mutateVerge({ ...verge, test_list: newList }, false);
|
// 优化:先本地更新,再异步 patch,避免UI卡死
|
||||||
await patchVerge({ test_list: newList });
|
mutateVerge({ ...verge, test_list: newList }, false);
|
||||||
|
const patchFn = () => {
|
||||||
|
try {
|
||||||
|
patchVerge({ test_list: newList });
|
||||||
|
} catch { }
|
||||||
|
};
|
||||||
|
if (window.requestIdleCallback) {
|
||||||
|
window.requestIdleCallback(patchFn);
|
||||||
|
} else {
|
||||||
|
setTimeout(patchFn, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[testList, verge, mutateVerge, patchVerge]
|
[testList, verge, mutateVerge, patchVerge]
|
||||||
|
|||||||
@@ -180,16 +180,27 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
setDeleteSeq(obj?.delete || []);
|
setDeleteSeq(obj?.delete || []);
|
||||||
}, [visualization]);
|
}, [visualization]);
|
||||||
|
|
||||||
|
// 优化:异步处理大数据yaml.dump,避免UI卡死
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prependSeq && appendSeq && deleteSeq)
|
if (prependSeq && appendSeq && deleteSeq) {
|
||||||
setCurrData(
|
const serialize = () => {
|
||||||
yaml.dump(
|
try {
|
||||||
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
|
setCurrData(
|
||||||
{
|
yaml.dump(
|
||||||
forceQuotes: true,
|
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
|
||||||
}
|
{ forceQuotes: true }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// 防止异常导致UI卡死
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (window.requestIdleCallback) {
|
||||||
|
window.requestIdleCallback(serialize);
|
||||||
|
} else {
|
||||||
|
setTimeout(serialize, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}, [prependSeq, appendSeq, deleteSeq]);
|
}, [prependSeq, appendSeq, deleteSeq]);
|
||||||
|
|
||||||
const fetchProxyPolicy = async () => {
|
const fetchProxyPolicy = async () => {
|
||||||
@@ -486,11 +497,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
}}
|
}}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
{t("seconds")}
|
{t("seconds")}
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -895,9 +906,8 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
padding: {
|
padding: {
|
||||||
top: 33, // 顶部padding防止遮挡snippets
|
top: 33, // 顶部padding防止遮挡snippets
|
||||||
},
|
},
|
||||||
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
|
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : ""
|
||||||
getSystem() === "windows" ? ", twemoji mozilla" : ""
|
}`,
|
||||||
}`,
|
|
||||||
fontLigatures: false, // 连字符
|
fontLigatures: false, // 连字符
|
||||||
smoothScrolling: true, // 平滑滚动
|
smoothScrolling: true, // 平滑滚动
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -184,8 +184,10 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
|||||||
setTimeout(() => formIns.reset(), 500);
|
setTimeout(() => formIns.reset(), 500);
|
||||||
fileDataRef.current = null;
|
fileDataRef.current = null;
|
||||||
|
|
||||||
// 只传递当前配置激活状态,让父组件决定是否需要触发配置重载
|
// 优化:UI先关闭,异步通知父组件
|
||||||
props.onChange(isActivating);
|
setTimeout(() => {
|
||||||
|
props.onChange(isActivating);
|
||||||
|
}, 0);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice("error", err.message || err.toString());
|
||||||
} finally {
|
} finally {
|
||||||
@@ -195,9 +197,11 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setOpen(false);
|
try {
|
||||||
fileDataRef.current = null;
|
setOpen(false);
|
||||||
setTimeout(() => formIns.reset(), 500);
|
fileDataRef.current = null;
|
||||||
|
setTimeout(() => formIns.reset(), 500);
|
||||||
|
} catch { }
|
||||||
};
|
};
|
||||||
|
|
||||||
const text = {
|
const text = {
|
||||||
|
|||||||
@@ -130,8 +130,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleParse = () => {
|
// 优化:异步分片解析,避免主线程阻塞,解析完成后批量setState
|
||||||
let proxies = [] as IProxyConfig[];
|
const handleParseAsync = (cb: (proxies: IProxyConfig[]) => void) => {
|
||||||
|
let proxies: IProxyConfig[] = [];
|
||||||
let names: string[] = [];
|
let names: string[] = [];
|
||||||
let uris = "";
|
let uris = "";
|
||||||
try {
|
try {
|
||||||
@@ -139,10 +140,13 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
} catch {
|
} catch {
|
||||||
uris = proxyUri;
|
uris = proxyUri;
|
||||||
}
|
}
|
||||||
uris
|
const lines = uris.trim().split("\n");
|
||||||
.trim()
|
let idx = 0;
|
||||||
.split("\n")
|
const batchSize = 50;
|
||||||
.forEach((uri) => {
|
function parseBatch() {
|
||||||
|
const end = Math.min(idx + batchSize, lines.length);
|
||||||
|
for (; idx < end; idx++) {
|
||||||
|
const uri = lines[idx];
|
||||||
try {
|
try {
|
||||||
let proxy = parseUri(uri.trim());
|
let proxy = parseUri(uri.trim());
|
||||||
if (!names.includes(proxy.name)) {
|
if (!names.includes(proxy.name)) {
|
||||||
@@ -150,10 +154,16 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
names.push(proxy.name);
|
names.push(proxy.name);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotice('error', err.message || err.toString());
|
// 不阻塞主流程
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
return proxies;
|
if (idx < lines.length) {
|
||||||
|
setTimeout(parseBatch, 0);
|
||||||
|
} else {
|
||||||
|
cb(proxies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parseBatch();
|
||||||
};
|
};
|
||||||
const fetchProfile = async () => {
|
const fetchProfile = async () => {
|
||||||
let data = await readProfileFile(profileUid);
|
let data = await readProfileFile(profileUid);
|
||||||
@@ -192,15 +202,25 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
}, [visualization]);
|
}, [visualization]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prependSeq && appendSeq && deleteSeq)
|
if (prependSeq && appendSeq && deleteSeq) {
|
||||||
setCurrData(
|
const serialize = () => {
|
||||||
yaml.dump(
|
try {
|
||||||
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
|
setCurrData(
|
||||||
{
|
yaml.dump(
|
||||||
forceQuotes: true,
|
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
|
||||||
}
|
{ forceQuotes: true }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// 防止异常导致UI卡死
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (window.requestIdleCallback) {
|
||||||
|
window.requestIdleCallback(serialize);
|
||||||
|
} else {
|
||||||
|
setTimeout(serialize, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}, [prependSeq, appendSeq, deleteSeq]);
|
}, [prependSeq, appendSeq, deleteSeq]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -276,8 +296,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
startIcon={<VerticalAlignTopRounded />}
|
startIcon={<VerticalAlignTopRounded />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
let proxies = handleParse();
|
handleParseAsync((proxies) => {
|
||||||
setPrependSeq([...proxies, ...prependSeq]);
|
setPrependSeq((prev) => [...proxies, ...prev]);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("Prepend Proxy")}
|
{t("Prepend Proxy")}
|
||||||
@@ -289,8 +310,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
startIcon={<VerticalAlignBottomRounded />}
|
startIcon={<VerticalAlignBottomRounded />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
let proxies = handleParse();
|
handleParseAsync((proxies) => {
|
||||||
setAppendSeq([...appendSeq, ...proxies]);
|
setAppendSeq((prev) => [...prev, ...proxies]);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("Append Proxy")}
|
{t("Append Proxy")}
|
||||||
@@ -431,9 +453,8 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
padding: {
|
padding: {
|
||||||
top: 33, // 顶部padding防止遮挡snippets
|
top: 33, // 顶部padding防止遮挡snippets
|
||||||
},
|
},
|
||||||
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
|
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : ""
|
||||||
getSystem() === "windows" ? ", twemoji mozilla" : ""
|
}`,
|
||||||
}`,
|
|
||||||
fontLigatures: false, // 连字符
|
fontLigatures: false, // 连字符
|
||||||
smoothScrolling: true, // 平滑滚动
|
smoothScrolling: true, // 平滑滚动
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -76,161 +76,161 @@ const rules: {
|
|||||||
noResolve?: boolean;
|
noResolve?: boolean;
|
||||||
validator?: (value: string) => boolean;
|
validator?: (value: string) => boolean;
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
name: "DOMAIN",
|
name: "DOMAIN",
|
||||||
example: "example.com",
|
example: "example.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "DOMAIN-SUFFIX",
|
name: "DOMAIN-SUFFIX",
|
||||||
example: "example.com",
|
example: "example.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "DOMAIN-KEYWORD",
|
name: "DOMAIN-KEYWORD",
|
||||||
example: "example",
|
example: "example",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "DOMAIN-REGEX",
|
name: "DOMAIN-REGEX",
|
||||||
example: "example.*",
|
example: "example.*",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "GEOSITE",
|
name: "GEOSITE",
|
||||||
example: "youtube",
|
example: "youtube",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "GEOIP",
|
name: "GEOIP",
|
||||||
example: "CN",
|
example: "CN",
|
||||||
noResolve: true,
|
noResolve: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "SRC-GEOIP",
|
name: "SRC-GEOIP",
|
||||||
example: "CN",
|
example: "CN",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IP-ASN",
|
name: "IP-ASN",
|
||||||
example: "13335",
|
example: "13335",
|
||||||
noResolve: true,
|
noResolve: true,
|
||||||
validator: (value) => (+value ? true : false),
|
validator: (value) => (+value ? true : false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "SRC-IP-ASN",
|
name: "SRC-IP-ASN",
|
||||||
example: "9808",
|
example: "9808",
|
||||||
validator: (value) => (+value ? true : false),
|
validator: (value) => (+value ? true : false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IP-CIDR",
|
name: "IP-CIDR",
|
||||||
example: "127.0.0.0/8",
|
example: "127.0.0.0/8",
|
||||||
noResolve: true,
|
noResolve: true,
|
||||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IP-CIDR6",
|
name: "IP-CIDR6",
|
||||||
example: "2620:0:2d0:200::7/32",
|
example: "2620:0:2d0:200::7/32",
|
||||||
noResolve: true,
|
noResolve: true,
|
||||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "SRC-IP-CIDR",
|
name: "SRC-IP-CIDR",
|
||||||
example: "192.168.1.201/32",
|
example: "192.168.1.201/32",
|
||||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IP-SUFFIX",
|
name: "IP-SUFFIX",
|
||||||
example: "8.8.8.8/24",
|
example: "8.8.8.8/24",
|
||||||
noResolve: true,
|
noResolve: true,
|
||||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "SRC-IP-SUFFIX",
|
name: "SRC-IP-SUFFIX",
|
||||||
example: "192.168.1.201/8",
|
example: "192.168.1.201/8",
|
||||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "SRC-PORT",
|
name: "SRC-PORT",
|
||||||
example: "7777",
|
example: "7777",
|
||||||
validator: (value) => portValidator(value),
|
validator: (value) => portValidator(value),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "DST-PORT",
|
name: "DST-PORT",
|
||||||
example: "80",
|
example: "80",
|
||||||
validator: (value) => portValidator(value),
|
validator: (value) => portValidator(value),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IN-PORT",
|
name: "IN-PORT",
|
||||||
example: "7890",
|
example: "7890",
|
||||||
validator: (value) => portValidator(value),
|
validator: (value) => portValidator(value),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "DSCP",
|
name: "DSCP",
|
||||||
example: "4",
|
example: "4",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "PROCESS-NAME",
|
name: "PROCESS-NAME",
|
||||||
example: getSystem() === "windows" ? "chrome.exe" : "curl",
|
example: getSystem() === "windows" ? "chrome.exe" : "curl",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "PROCESS-PATH",
|
name: "PROCESS-PATH",
|
||||||
example:
|
example:
|
||||||
getSystem() === "windows"
|
getSystem() === "windows"
|
||||||
? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
|
? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
|
||||||
: "/usr/bin/wget",
|
: "/usr/bin/wget",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "PROCESS-NAME-REGEX",
|
name: "PROCESS-NAME-REGEX",
|
||||||
example: ".*telegram.*",
|
example: ".*telegram.*",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "PROCESS-PATH-REGEX",
|
name: "PROCESS-PATH-REGEX",
|
||||||
example:
|
example:
|
||||||
getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget",
|
getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "NETWORK",
|
name: "NETWORK",
|
||||||
example: "udp",
|
example: "udp",
|
||||||
validator: (value) => ["tcp", "udp"].includes(value),
|
validator: (value) => ["tcp", "udp"].includes(value),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "UID",
|
name: "UID",
|
||||||
example: "1001",
|
example: "1001",
|
||||||
validator: (value) => (+value ? true : false),
|
validator: (value) => (+value ? true : false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IN-TYPE",
|
name: "IN-TYPE",
|
||||||
example: "SOCKS/HTTP",
|
example: "SOCKS/HTTP",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IN-USER",
|
name: "IN-USER",
|
||||||
example: "mihomo",
|
example: "mihomo",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IN-NAME",
|
name: "IN-NAME",
|
||||||
example: "ss",
|
example: "ss",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "SUB-RULE",
|
name: "SUB-RULE",
|
||||||
example: "(NETWORK,tcp)",
|
example: "(NETWORK,tcp)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "RULE-SET",
|
name: "RULE-SET",
|
||||||
example: "providername",
|
example: "providername",
|
||||||
noResolve: true,
|
noResolve: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "AND",
|
name: "AND",
|
||||||
example: "((DOMAIN,baidu.com),(NETWORK,UDP))",
|
example: "((DOMAIN,baidu.com),(NETWORK,UDP))",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "OR",
|
name: "OR",
|
||||||
example: "((NETWORK,UDP),(DOMAIN,baidu.com))",
|
example: "((NETWORK,UDP),(DOMAIN,baidu.com))",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "NOT",
|
name: "NOT",
|
||||||
example: "((DOMAIN,baidu.com))",
|
example: "((DOMAIN,baidu.com))",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "MATCH",
|
name: "MATCH",
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
|
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
|
||||||
|
|
||||||
@@ -325,16 +325,27 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
setDeleteSeq(obj?.delete || []);
|
setDeleteSeq(obj?.delete || []);
|
||||||
}, [visualization]);
|
}, [visualization]);
|
||||||
|
|
||||||
|
// 优化:异步处理大数据yaml.dump,避免UI卡死
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prependSeq && appendSeq && deleteSeq)
|
if (prependSeq && appendSeq && deleteSeq) {
|
||||||
setCurrData(
|
const serialize = () => {
|
||||||
yaml.dump(
|
try {
|
||||||
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
|
setCurrData(
|
||||||
{
|
yaml.dump(
|
||||||
forceQuotes: true,
|
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
|
||||||
}
|
{ forceQuotes: true }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
showNotice('error', e?.message || e?.toString() || 'YAML dump error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (window.requestIdleCallback) {
|
||||||
|
window.requestIdleCallback(serialize);
|
||||||
|
} else {
|
||||||
|
setTimeout(serialize, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}, [prependSeq, appendSeq, deleteSeq]);
|
}, [prependSeq, appendSeq, deleteSeq]);
|
||||||
|
|
||||||
const fetchProfile = async () => {
|
const fetchProfile = async () => {
|
||||||
@@ -407,9 +418,8 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const condition = ruleType.required ?? true ? ruleContent : "";
|
const condition = ruleType.required ?? true ? ruleContent : "";
|
||||||
return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${
|
return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${ruleType.noResolve && noResolve ? ",no-resolve" : ""
|
||||||
ruleType.noResolve && noResolve ? ",no-resolve" : ""
|
}`;
|
||||||
}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = useLockFn(async () => {
|
const handleSave = useLockFn(async () => {
|
||||||
@@ -701,9 +711,8 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
padding: {
|
padding: {
|
||||||
top: 33, // 顶部padding防止遮挡snippets
|
top: 33, // 顶部padding防止遮挡snippets
|
||||||
},
|
},
|
||||||
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
|
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : ""
|
||||||
getSystem() === "windows" ? ", twemoji mozilla" : ""
|
}`,
|
||||||
}`,
|
|
||||||
fontLigatures: false, // 连字符
|
fontLigatures: false, // 连字符
|
||||||
smoothScrolling: true, // 平滑滚动
|
smoothScrolling: true, // 平滑滚动
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -198,3 +198,5 @@ export const useRenderList = (mode: string) => {
|
|||||||
currentColumns: col,
|
currentColumns: col,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 优化建议:如有大数据量,建议用虚拟滚动(已在 ProxyGroups 组件中实现),此处无需额外处理。
|
||||||
|
|||||||
@@ -132,8 +132,8 @@ const handleNoticeMessage = (
|
|||||||
showNotice('error', `${t("Failed to Change Core")}: ${msg}`);
|
showNotice('error', `${t("Failed to Change Core")}: ${msg}`);
|
||||||
break;
|
break;
|
||||||
default: // Optional: Log unhandled statuses
|
default: // Optional: Log unhandled statuses
|
||||||
console.warn(`[通知监听 V2] 未处理的状态: ${status}`);
|
console.warn(`[通知监听 V2] 未处理的状态: ${status}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,6 +151,11 @@ const Layout = () => {
|
|||||||
const routersEles = useRoutes(routers);
|
const routersEles = useRoutes(routers);
|
||||||
const { addListener, setupCloseListener } = useListen();
|
const { addListener, setupCloseListener } = useListen();
|
||||||
const initRef = useRef(false);
|
const initRef = useRef(false);
|
||||||
|
const [themeReady, setThemeReady] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setThemeReady(true);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
const handleNotice = useCallback(
|
const handleNotice = useCallback(
|
||||||
(payload: [string, string]) => {
|
(payload: [string, string]) => {
|
||||||
@@ -271,7 +276,7 @@ const Layout = () => {
|
|||||||
return unlisten;
|
return unlisten;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Layout] 监听启动完成事件失败:", err);
|
console.error("[Layout] 监听启动完成事件失败:", err);
|
||||||
return () => {};
|
return () => { };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -311,13 +316,39 @@ const Layout = () => {
|
|||||||
}
|
}
|
||||||
}, [start_page]);
|
}, [start_page]);
|
||||||
|
|
||||||
|
if (!themeReady) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100vw",
|
||||||
|
height: "100vh",
|
||||||
|
background: mode === "light" ? "#fff" : "#181a1b",
|
||||||
|
transition: "background 0.2s",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!routersEles) return null;
|
if (!routersEles) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SWRConfig value={{ errorRetryCount: 3 }}>
|
<SWRConfig value={{ errorRetryCount: 3 }}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<NoticeManager />
|
<NoticeManager />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
animation: "fadeIn 0.5s",
|
||||||
|
WebkitAnimation: "fadeIn 0.5s",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
<Paper
|
<Paper
|
||||||
square
|
square
|
||||||
elevation={0}
|
elevation={0}
|
||||||
@@ -337,11 +368,11 @@ const Layout = () => {
|
|||||||
({ palette }) => ({ bgcolor: palette.background.paper }),
|
({ palette }) => ({ bgcolor: palette.background.paper }),
|
||||||
OS === "linux"
|
OS === "linux"
|
||||||
? {
|
? {
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
border: "1px solid var(--divider-color)",
|
border: "1px solid var(--divider-color)",
|
||||||
width: "calc(100vw - 4px)",
|
width: "calc(100vw - 4px)",
|
||||||
height: "calc(100vh - 4px)",
|
height: "calc(100vh - 4px)",
|
||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -252,9 +252,13 @@ export const HomePage = () => {
|
|||||||
setSettingsOpen(true);
|
setSettingsOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 新增:保存设置
|
// 新增:保存设置时用requestIdleCallback/setTimeout
|
||||||
const handleSaveSettings = (newCards: HomeCardsSettings) => {
|
const handleSaveSettings = (newCards: HomeCardsSettings) => {
|
||||||
setHomeCards(newCards);
|
if (window.requestIdleCallback) {
|
||||||
|
window.requestIdleCallback(() => setHomeCards(newCards));
|
||||||
|
} else {
|
||||||
|
setTimeout(() => setHomeCards(newCards), 0);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
RefreshRounded,
|
RefreshRounded,
|
||||||
AccessTimeOutlined,
|
AccessTimeOutlined,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
|
import { showNotice } from "@/services/noticeService";
|
||||||
|
|
||||||
// 定义流媒体检测项类型
|
// 定义流媒体检测项类型
|
||||||
interface UnlockItem {
|
interface UnlockItem {
|
||||||
@@ -121,61 +122,67 @@ const UnlockPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// invoke加超时,防止后端卡死
|
||||||
|
const invokeWithTimeout = async <T,>(
|
||||||
|
cmd: string,
|
||||||
|
args?: any,
|
||||||
|
timeout = 15000,
|
||||||
|
): Promise<T> => {
|
||||||
|
return Promise.race([
|
||||||
|
invoke<T>(cmd, args),
|
||||||
|
new Promise<T>((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeout)),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
// 执行全部项目检测
|
// 执行全部项目检测
|
||||||
const checkAllMedia = useLockFn(async () => {
|
const checkAllMedia = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
setIsCheckingAll(true);
|
setIsCheckingAll(true);
|
||||||
const result = await invoke<UnlockItem[]>("check_media_unlock");
|
const result = await invokeWithTimeout<UnlockItem[]>("check_media_unlock");
|
||||||
const sortedItems = sortItemsByName(result);
|
const sortedItems = sortItemsByName(result);
|
||||||
|
|
||||||
// 更新UI
|
|
||||||
setUnlockItems(sortedItems);
|
setUnlockItems(sortedItems);
|
||||||
const currentTime = new Date().toLocaleString();
|
const currentTime = new Date().toLocaleString();
|
||||||
setLastCheckTime(currentTime);
|
setLastCheckTime(currentTime);
|
||||||
|
|
||||||
// 保存结果到本地存储
|
|
||||||
saveResultsToStorage(sortedItems, currentTime);
|
saveResultsToStorage(sortedItems, currentTime);
|
||||||
|
|
||||||
setIsCheckingAll(false);
|
setIsCheckingAll(false);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setIsCheckingAll(false);
|
setIsCheckingAll(false);
|
||||||
|
showNotice('error', err?.message || err?.toString() || '检测超时或失败');
|
||||||
|
alert("检测超时或失败: " + (err?.message || err));
|
||||||
console.error("Failed to check media unlock:", err);
|
console.error("Failed to check media unlock:", err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 根据项目名称检测单个流媒体服务
|
// 检测单个流媒体服务
|
||||||
const checkSingleMedia = useLockFn(async (name: string) => {
|
const checkSingleMedia = useLockFn(async (name: string) => {
|
||||||
try {
|
try {
|
||||||
// 将该项目添加到加载状态
|
|
||||||
setLoadingItems((prev) => [...prev, name]);
|
setLoadingItems((prev) => [...prev, name]);
|
||||||
|
const result = await invokeWithTimeout<UnlockItem[]>("check_media_unlock");
|
||||||
|
|
||||||
// 执行检测
|
|
||||||
const result = await invoke<UnlockItem[]>("check_media_unlock");
|
|
||||||
|
|
||||||
// 找到对应的检测结果
|
|
||||||
const targetItem = result.find((item: UnlockItem) => item.name === name);
|
const targetItem = result.find((item: UnlockItem) => item.name === name);
|
||||||
|
|
||||||
if (targetItem) {
|
if (targetItem) {
|
||||||
// 更新单个检测项结果并按名称排序
|
|
||||||
const updatedItems = sortItemsByName(
|
const updatedItems = sortItemsByName(
|
||||||
unlockItems.map((item: UnlockItem) =>
|
unlockItems.map((item: UnlockItem) =>
|
||||||
item.name === name ? targetItem : item,
|
item.name === name ? targetItem : item,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 更新UI
|
|
||||||
setUnlockItems(updatedItems);
|
setUnlockItems(updatedItems);
|
||||||
const currentTime = new Date().toLocaleString();
|
const currentTime = new Date().toLocaleString();
|
||||||
setLastCheckTime(currentTime);
|
setLastCheckTime(currentTime);
|
||||||
|
|
||||||
// 保存结果到本地存储
|
|
||||||
saveResultsToStorage(updatedItems, currentTime);
|
saveResultsToStorage(updatedItems, currentTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除加载状态
|
|
||||||
setLoadingItems((prev) => prev.filter((item) => item !== name));
|
setLoadingItems((prev) => prev.filter((item) => item !== name));
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setLoadingItems((prev) => prev.filter((item) => item !== name));
|
setLoadingItems((prev) => prev.filter((item) => item !== name));
|
||||||
|
showNotice('error', err?.message || err?.toString() || `检测${name}失败`);
|
||||||
|
alert("检测超时或失败: " + (err?.message || err));
|
||||||
console.error(`Failed to check ${name}:`, err);
|
console.error(`Failed to check ${name}:`, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user