diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index 44b45d7ef..5af44837d 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -103,6 +103,20 @@ export const ProfileItem = (props: Props) => { const refreshTimeoutRef = useRef | undefined>( undefined, ) + const setLoading = useCallback( + (loading: boolean) => { + setLoadingCache((cache) => { + const next = new Set(cache) + if (loading) { + next.add(itemData.uid) + } else { + next.delete(itemData.uid) + } + return next + }) + }, + [itemData.uid, setLoadingCache], + ) const { uid, name = 'Profile', extra, updated = 0, option } = itemData @@ -251,7 +265,7 @@ export const ProfileItem = (props: Props) => { 100, ) - const loading = loadingCache[itemData.uid] ?? false + const loading = loadingCache.has(itemData.uid) // interval update fromNow field const [, forceRefresh] = useReducer((value: number) => value + 1, 0) @@ -380,7 +394,7 @@ export const ProfileItem = (props: Props) => { /// 2 至少使用一个代理,根据订阅,如果没订阅,默认使用系统代理 const onUpdate = useLockFn(async (type: 0 | 1 | 2): Promise => { setAnchorEl(null) - setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true })) + setLoading(true) // 根据类型设置初始更新选项 const option: Partial = {} @@ -408,7 +422,7 @@ export const ProfileItem = (props: Props) => { // 更新完全失败(包括后端的回退尝试) // 不需要做处理,后端会通过事件通知系统发送错误 } finally { - setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false })) + setLoading(false) } }) @@ -599,7 +613,7 @@ export const ProfileItem = (props: Props) => { Promise.allSettled([ listen<{ uid?: string }>('profile-update-started', ({ payload }) => { if (payload.uid === itemData.uid) { - setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true })) + setLoading(true) } }), listen<{ uid?: string }>('profile-update-completed', ({ payload }) => { @@ -607,7 +621,7 @@ export const ProfileItem = (props: Props) => { return } - setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false })) + setLoading(false) // 刷新 profile 数据以获取最新的 updated 时间戳 void mutateProfiles() // 更新完成后刷新显示 @@ -635,7 +649,7 @@ export const ProfileItem = (props: Props) => { disposed = true unlisteners.forEach((unlisten) => unlisten()) } - }, [fetchNextUpdateTime, itemData.uid, mutateProfiles, setLoadingCache]) + }, [fetchNextUpdateTime, itemData.uid, mutateProfiles, setLoading]) const handleSaveProfileDocument = useLockFn(async () => { const currentValue = profileDocument.value diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index b7dc18406..78c383006 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -66,9 +66,16 @@ import { } from '@/services/cmds' import { showNotice } from '@/services/notice-service' import { queryClient } from '@/services/query-client' -import { useSetLoadingCache, useThemeMode } from '@/services/states' +import { + useLoadingCache, + useSetLoadingCache, + useThemeMode, +} from '@/services/states' import { debugLog } from '@/utils/debug' +// 与 src-tauri/src/main.rs 的 worker_limit 上限(8)保持一致,避免前后端更新风暴不对齐 +const PROFILE_UPDATE_WORKER_LIMIT = 8 + // 记录profile切换状态 const debugProfileSwitch = (action: string, profile: string, extra?: any) => { const timestamp = new Date().toISOString().substring(11, 23) @@ -602,34 +609,68 @@ const ProfilePage = () => { }) // 更新所有订阅 + const loadingCache = useLoadingCache() const setLoadingCache = useSetLoadingCache() - const onUpdateAll = useLockFn(async () => { - const throttleMutate = throttle(mutateProfiles, 2000, { - trailing: true, - }) - const updateOne = async (uid: string) => { - try { - await updateProfile(uid) - throttleMutate() - } catch (err: any) { - console.error(`更新订阅 ${uid} 失败:`, err) - } finally { - setLoadingCache((cache) => ({ ...cache, [uid]: false })) - } - } - - return new Promise((resolve) => { + const setLoadingProfiles = useCallback( + (uids: string[], loading: boolean) => { setLoadingCache((cache) => { - // 获取没有正在更新的订阅 - const items = profileItems.filter( - (e) => e.type === 'remote' && !cache[e.uid], - ) - const change = Object.fromEntries(items.map((e) => [e.uid, true])) - - Promise.allSettled(items.map((e) => updateOne(e.uid))).then(resolve) - return { ...cache, ...change } + const next = new Set(cache) + for (const uid of uids) { + if (loading) { + next.add(uid) + } else { + next.delete(uid) + } + } + return next }) - }) + }, + [setLoadingCache], + ) + const runProfileUpdates = useCallback( + async (uids: string[]) => { + if (uids.length === 0) return + + const throttleMutate = throttle(mutateProfiles, 2000, { + trailing: true, + }) + let cursor = 0 + + const updateOne = async (uid: string) => { + try { + await updateProfile(uid) + throttleMutate() + } catch (err: any) { + console.error(`更新订阅 ${uid} 失败:`, err) + } + } + + const worker = async () => { + while (cursor < uids.length) { + const uid = uids[cursor++] + await updateOne(uid) + } + } + + try { + const active = Math.min(PROFILE_UPDATE_WORKER_LIMIT, uids.length) + await Promise.allSettled(Array.from({ length: active }, worker)) + } finally { + setLoadingProfiles(uids, false) + // 避免长时间批量更新后列表数据过晚刷新 + void mutateProfiles() + } + }, + [mutateProfiles, setLoadingProfiles], + ) + const onUpdateAll = useLockFn(async () => { + const items = profileItems.filter((e) => e.type === 'remote') + const target = items + .map((item) => item.uid) + .filter((uid) => !loadingCache.has(uid)) + + setLoadingProfiles(target, true) + await runProfileUpdates(target) }) const onCopyLink = async () => { diff --git a/src/services/states.ts b/src/services/states.ts index e6e30118a..3770c09aa 100644 --- a/src/services/states.ts +++ b/src/services/states.ts @@ -6,7 +6,7 @@ const [ThemeModeProvider, useThemeMode, useSetThemeMode] = createContextState< // save the state of each profile item loading const [LoadingCacheProvider, useLoadingCache, useSetLoadingCache] = - createContextState>({}) + createContextState>(new Set()) // save update state const [UpdateStateProvider, useUpdateState, useSetUpdateState] =