diff --git a/src/providers/RouterProvider.tsx b/src/providers/RouterProvider.tsx index 523068b..22239af 100644 --- a/src/providers/RouterProvider.tsx +++ b/src/providers/RouterProvider.tsx @@ -28,10 +28,11 @@ const isValidPage = (page: unknown): page is PageType => { }; /** - * 校验页面列表是否合法 + * 从页面列表中过滤非法项(如已移除的 dashboard),保留合法 PageType */ -const isValidPageList = (pages: unknown): pages is PageType[] => { - return Array.isArray(pages) && pages.every(isValidPage); +const sanitizePageList = (pages: unknown): PageType[] | null => { + if (!Array.isArray(pages)) return null; + return pages.filter(isValidPage); }; /** @@ -47,6 +48,15 @@ const mergeWithDefaults = (saved: PageType[], defaults: PageType[]): PageType[] return [...preserved, ...newItems]; }; +/** + * 解析并合并页面列表:过滤非法项后与默认列表合并 + */ +const resolvePageList = (pages: unknown, defaults: PageType[]): PageType[] => { + const sanitized = sanitizePageList(pages); + if (sanitized === null) return defaults; + return mergeWithDefaults(sanitized, defaults); +}; + interface RouterContextType { currentPage: PageType; visiblePages: PageType[]; @@ -81,26 +91,19 @@ export function RouterProvider({ getSyncSnapshot(syncKey as string, defaultRoute, isValidPage), ); - const [visiblePages, setVisiblePages] = useState(() => { - const snapshot = getSyncSnapshot( - visiblePagesKey as string, + const [visiblePages, setVisiblePages] = useState(() => + resolvePageList( + getSyncSnapshot(visiblePagesKey as string, null), getDefaultVisibleFeatureKeys(), - isValidPageList, - ); - return mergeWithDefaults(snapshot, getDefaultVisibleFeatureKeys()); - }); + ), + ); - const [pageOrder, setPageOrder] = useState(() => { - const snapshot = getSyncSnapshot( - pageOrderKey as string, - getDefaultPageOrder(), - isValidPageList, - ); - return mergeWithDefaults(snapshot, getDefaultPageOrder()); - }); + const [pageOrder, setPageOrder] = useState(() => + resolvePageList(getSyncSnapshot(pageOrderKey as string, null), getDefaultPageOrder()), + ); - const [recentlyUsedTools, setRecentlyUsedTools] = useState(() => - getSyncSnapshot('app/recentlyUsedTools', [], isValidPageList), + const [recentlyUsedTools, setRecentlyUsedTools] = useState( + () => sanitizePageList(getSyncSnapshot('app/recentlyUsedTools', null)) ?? [], ); const [isLoaded, setIsLoaded] = useState(false); @@ -126,14 +129,15 @@ export function RouterProvider({ if (isValidPage(savedRoute) && syncRoute && !hasUserNavigatedRef.current) { setCurrentPage(savedRoute); } - if (isValidPageList(savedVisiblePages)) { - setVisiblePages(mergeWithDefaults(savedVisiblePages, getDefaultVisibleFeatureKeys())); + if (sanitizePageList(savedVisiblePages) !== null) { + setVisiblePages(resolvePageList(savedVisiblePages, getDefaultVisibleFeatureKeys())); } - if (isValidPageList(savedPageOrder) && savedPageOrder.length > 0) { - setPageOrder(mergeWithDefaults(savedPageOrder, getDefaultPageOrder())); + if (sanitizePageList(savedPageOrder) !== null) { + setPageOrder(resolvePageList(savedPageOrder, getDefaultPageOrder())); } - if (isValidPageList(savedRecentTools)) { - setRecentlyUsedTools(savedRecentTools); + const sanitizedRecentTools = sanitizePageList(savedRecentTools); + if (sanitizedRecentTools !== null) { + setRecentlyUsedTools(sanitizedRecentTools); } canPersistRef.current = true; @@ -249,20 +253,21 @@ export function RouterProvider({ } if (changes[visiblePagesKey as string]) { const newPages = changes[visiblePagesKey as string].newValue; - if (isValidPageList(newPages)) { - setVisiblePages(mergeWithDefaults(newPages, getDefaultVisibleFeatureKeys())); + if (sanitizePageList(newPages) !== null) { + setVisiblePages(resolvePageList(newPages, getDefaultVisibleFeatureKeys())); } } if (changes[pageOrderKey as string]) { const newOrder = changes[pageOrderKey as string].newValue; - if (isValidPageList(newOrder)) { - setPageOrder(mergeWithDefaults(newOrder, getDefaultPageOrder())); + if (sanitizePageList(newOrder) !== null) { + setPageOrder(resolvePageList(newOrder, getDefaultPageOrder())); } } if (changes['app/recentlyUsedTools']) { const newRecent = changes['app/recentlyUsedTools'].newValue; - if (isValidPageList(newRecent)) { - setRecentlyUsedTools(newRecent); + const sanitizedRecent = sanitizePageList(newRecent); + if (sanitizedRecent !== null) { + setRecentlyUsedTools(sanitizedRecent); } } if (changes['contextMenu/pendingData']) { diff --git a/src/providers/__tests__/RouterProvider.test.tsx b/src/providers/__tests__/RouterProvider.test.tsx index 2585a6c..efa4432 100644 --- a/src/providers/__tests__/RouterProvider.test.tsx +++ b/src/providers/__tests__/RouterProvider.test.tsx @@ -193,6 +193,57 @@ describe('RouterProvider', () => { }); }); + it('storage 列表含 dashboard 时应过滤并保留其余自定义配置', async () => { + mockStorageBatch({ + 'app/popupVisiblePages': ['dashboard', 'jwt', 'timestamp'], + 'app/popupPageOrder': ['jwt', 'dashboard', 'timestamp'], + 'app/recentlyUsedTools': ['dashboard', 'jwt'], + }); + + render( + + + , + ); + + await waitFor(() => { + const visiblePages = screen.getByTestId('visible-pages').textContent!; + const pageOrder = screen.getByTestId('page-order').textContent!; + + expect(visiblePages.startsWith('jwt,timestamp')).toBe(true); + expect(pageOrder.startsWith('jwt,timestamp')).toBe(true); + expect(visiblePages).not.toContain('dashboard'); + expect(pageOrder).not.toContain('dashboard'); + }); + }); + + it('localStorage 快照含 dashboard 时应过滤并保留其余配置', async () => { + localStorage.setItem( + 'snapshot/app/popupVisiblePages', + JSON.stringify(['dashboard', 'jwt', 'timestamp']), + ); + localStorage.setItem( + 'snapshot/app/popupPageOrder', + JSON.stringify(['jwt', 'dashboard', 'timestamp']), + ); + + mockStorageBatch(); + + render( + + + , + ); + + const visiblePages = screen.getByTestId('visible-pages').textContent!; + const pageOrder = screen.getByTestId('page-order').textContent!; + + expect(visiblePages.startsWith('jwt,timestamp')).toBe(true); + expect(pageOrder.startsWith('jwt,timestamp')).toBe(true); + expect(visiblePages).not.toContain('dashboard'); + expect(pageOrder).not.toContain('dashboard'); + }); + it('应该从 localStorage 快照合并缺失的新功能', async () => { const oldVisiblePages = [ 'timestamp',