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:
Tunglies
2025-05-30 17:34:38 +08:00
parent 756d303f6a
commit 1e3566ed7d
10 changed files with 378 additions and 275 deletions

View File

@@ -132,8 +132,8 @@ const handleNoticeMessage = (
showNotice('error', `${t("Failed to Change Core")}: ${msg}`);
break;
default: // Optional: Log unhandled statuses
console.warn(`[通知监听 V2] 未处理的状态: ${status}`);
break;
console.warn(`[通知监听 V2] 未处理的状态: ${status}`);
break;
}
};
@@ -151,6 +151,11 @@ const Layout = () => {
const routersEles = useRoutes(routers);
const { addListener, setupCloseListener } = useListen();
const initRef = useRef(false);
const [themeReady, setThemeReady] = useState(false);
useEffect(() => {
setThemeReady(true);
}, [theme]);
const handleNotice = useCallback(
(payload: [string, string]) => {
@@ -271,7 +276,7 @@ const Layout = () => {
return unlisten;
} catch (err) {
console.error("[Layout] 监听启动完成事件失败:", err);
return () => {};
return () => { };
}
};
@@ -288,7 +293,7 @@ const Layout = () => {
}, 100);
}, 100);
}, 100);
// 启动监听器
const unlistenPromise = listenStartupCompleted();
@@ -311,13 +316,39 @@ const Layout = () => {
}
}, [start_page]);
if (!themeReady) {
return (
<div
style={{
width: "100vw",
height: "100vh",
background: mode === "light" ? "#fff" : "#181a1b",
transition: "background 0.2s",
}}
/>
);
}
if (!routersEles) return null;
return (
<SWRConfig value={{ errorRetryCount: 3 }}>
<ThemeProvider theme={theme}>
<NoticeManager />
<div
style={{
animation: "fadeIn 0.5s",
WebkitAnimation: "fadeIn 0.5s",
}}
/>
<style>
{`
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
`}
</style>
<Paper
square
elevation={0}
@@ -337,11 +368,11 @@ const Layout = () => {
({ palette }) => ({ bgcolor: palette.background.paper }),
OS === "linux"
? {
borderRadius: "8px",
border: "1px solid var(--divider-color)",
width: "calc(100vw - 4px)",
height: "calc(100vh - 4px)",
}
borderRadius: "8px",
border: "1px solid var(--divider-color)",
width: "calc(100vw - 4px)",
height: "calc(100vh - 4px)",
}
: {},
]}
>

View File

@@ -252,9 +252,13 @@ export const HomePage = () => {
setSettingsOpen(true);
};
// 新增:保存设置
// 新增:保存设置时用requestIdleCallback/setTimeout
const handleSaveSettings = (newCards: HomeCardsSettings) => {
setHomeCards(newCards);
if (window.requestIdleCallback) {
window.requestIdleCallback(() => setHomeCards(newCards));
} else {
setTimeout(() => setHomeCards(newCards), 0);
}
};
return (

View File

@@ -24,6 +24,7 @@ import {
RefreshRounded,
AccessTimeOutlined,
} from "@mui/icons-material";
import { showNotice } from "@/services/noticeService";
// 定义流媒体检测项类型
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 () => {
try {
setIsCheckingAll(true);
const result = await invoke<UnlockItem[]>("check_media_unlock");
const result = await invokeWithTimeout<UnlockItem[]>("check_media_unlock");
const sortedItems = sortItemsByName(result);
// 更新UI
setUnlockItems(sortedItems);
const currentTime = new Date().toLocaleString();
setLastCheckTime(currentTime);
// 保存结果到本地存储
saveResultsToStorage(sortedItems, currentTime);
setIsCheckingAll(false);
} catch (err: any) {
setIsCheckingAll(false);
showNotice('error', err?.message || err?.toString() || '检测超时或失败');
alert("检测超时或失败: " + (err?.message || err));
console.error("Failed to check media unlock:", err);
}
});
// 根据项目名称检测单个流媒体服务
// 检测单个流媒体服务
const checkSingleMedia = useLockFn(async (name: string) => {
try {
// 将该项目添加到加载状态
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);
if (targetItem) {
// 更新单个检测项结果并按名称排序
const updatedItems = sortItemsByName(
unlockItems.map((item: UnlockItem) =>
item.name === name ? targetItem : item,
),
);
// 更新UI
setUnlockItems(updatedItems);
const currentTime = new Date().toLocaleString();
setLastCheckTime(currentTime);
// 保存结果到本地存储
saveResultsToStorage(updatedItems, currentTime);
}
// 移除加载状态
setLoadingItems((prev) => prev.filter((item) => item !== name));
} catch (err: any) {
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);
}
});