From bcbd5ed71748bb6803f3e9c7fc2c941a74820578 Mon Sep 17 00:00:00 2001 From: MyPrototypeWhat Date: Fri, 12 Dec 2025 15:04:01 +0800 Subject: [PATCH] refactor(tabs): enhance URL handling and default title logic - Updated `AppShell` and `TabRouter` components to improve URL synchronization with tab states, ensuring full URLs (including search/hash) are correctly managed. - Enhanced `getDefaultRouteTitle` utility to handle full URLs for better title mapping, improving internationalization support. - Refactored `useTabs` to retain complete URLs in tab management, optimizing tab state tracking. These changes enhance the application's navigation and tab management, providing a more consistent user experience. --- .../src/components/layout/AppShell.tsx | 2 +- .../src/components/layout/TabRouter.tsx | 10 +++++----- src/renderer/src/hooks/useTabs.ts | 4 ++-- .../ProviderSettings/ProviderList.tsx | 4 ++-- src/renderer/src/routes/__root.tsx | 8 +++++++- src/renderer/src/utils/routeTitle.ts | 20 +++++++++++++------ 6 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/renderer/src/components/layout/AppShell.tsx b/src/renderer/src/components/layout/AppShell.tsx index dcecbe61b3..37cd987f4a 100644 --- a/src/renderer/src/components/layout/AppShell.tsx +++ b/src/renderer/src/components/layout/AppShell.tsx @@ -23,7 +23,7 @@ const WebviewContainer = ({ url, isActive }: { url: string; isActive: boolean }) export const AppShell = () => { const { tabs, activeTabId, setActiveTab, closeTab, updateTab, addTab } = useTabs() - // Sync internal navigation back to tab state with default title + // Sync internal navigation back to tab state with default title (url may include search/hash) const handleUrlChange = (tabId: string, url: string) => { updateTab(tabId, { url, title: getDefaultRouteTitle(url) }) } diff --git a/src/renderer/src/components/layout/TabRouter.tsx b/src/renderer/src/components/layout/TabRouter.tsx index 8e2e372dbd..9c2f5df258 100644 --- a/src/renderer/src/components/layout/TabRouter.tsx +++ b/src/renderer/src/components/layout/TabRouter.tsx @@ -24,20 +24,20 @@ export const TabRouter = ({ tab, isActive, onUrlChange }: TabRouterProps) => { return createRouter({ routeTree, history }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [tab.id]) - // Sync internal navigation back to tab state useEffect(() => { return router.subscribe('onResolved', ({ toLocation }) => { - if (toLocation.pathname !== tab.url) { - onUrlChange(toLocation.pathname) + const nextHref = toLocation.href + if (nextHref !== tab.url) { + onUrlChange(nextHref) } }) }, [router, tab.url, onUrlChange]) // Navigate when tab.url changes externally (e.g., from Sidebar) useEffect(() => { - const currentPath = router.state.location.pathname - if (tab.url !== currentPath) { + const currentHref = router.state.location.href + if (tab.url !== currentHref) { router.navigate({ to: tab.url }) } }, [router, tab.url]) diff --git a/src/renderer/src/hooks/useTabs.ts b/src/renderer/src/hooks/useTabs.ts index faba51aed3..cebe77c0c3 100644 --- a/src/renderer/src/hooks/useTabs.ts +++ b/src/renderer/src/hooks/useTabs.ts @@ -179,7 +179,7 @@ export function useTabs() { return } - // 添加 LRU 字段 + // 添加 LRU 字段,保留完整 URL(含 search/hash) const newTab: Tab = { ...tab, lastAccessTime: Date.now(), @@ -255,7 +255,7 @@ export function useTabs() { const newTab: Tab = { id: id || uuid(), type, - url, + url, // full URL including search/hash title: title || getDefaultRouteTitle(url), lastAccessTime: Date.now(), isDormant: false diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx index 6bb0a9f93d..220613f703 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx @@ -94,8 +94,8 @@ const ProviderList: FC = () => { // 清除 id 参数 navigate({ to: location.pathname, - search: (prev) => { - const { id: _, ...rest } = prev as Record + search: ({ id, ...rest }) => { + void id return rest }, replace: true diff --git a/src/renderer/src/routes/__root.tsx b/src/renderer/src/routes/__root.tsx index d1a273acb7..07dcf9524c 100644 --- a/src/renderer/src/routes/__root.tsx +++ b/src/renderer/src/routes/__root.tsx @@ -1,5 +1,11 @@ +import NavigationHandler from '@renderer/handler/NavigationHandler' import { createRootRoute, Outlet } from '@tanstack/react-router' export const Route = createRootRoute({ - component: () => + component: () => ( + <> + + + + ) }) diff --git a/src/renderer/src/utils/routeTitle.ts b/src/renderer/src/utils/routeTitle.ts index b880683c01..5873f2713e 100644 --- a/src/renderer/src/utils/routeTitle.ts +++ b/src/renderer/src/utils/routeTitle.ts @@ -1,5 +1,8 @@ import i18n from '@renderer/i18n' +/** Base URL for parsing relative route paths */ +const BASE_URL = 'https://www.cherry-ai.com/' + /** * Route to i18n key mapping for default tab titles */ @@ -29,30 +32,35 @@ const routeTitleKeys: Record = { * getDefaultRouteTitle('/unknown') // 'unknown' */ export function getDefaultRouteTitle(url: string): string { + const sanitizedUrl = new URL(url, BASE_URL).pathname + // Try exact match first - const exactKey = routeTitleKeys[url] + const exactKey = routeTitleKeys[sanitizedUrl] if (exactKey) { return i18n.t(exactKey) } // Try matching base path (e.g., '/chat/123' -> '/chat') - const basePath = '/' + url.split('/').filter(Boolean)[0] + const basePath = '/' + sanitizedUrl.split('/').filter(Boolean)[0] const baseKey = routeTitleKeys[basePath] if (baseKey) { return i18n.t(baseKey) } - // Fallback to URL path - return url.split('/').pop() || url + // Fallback to last segment of pathname + const segments = sanitizedUrl.split('/').filter(Boolean) + return segments.pop() || sanitizedUrl } /** * Get the i18n key for a route (without translating) */ export function getRouteTitleKey(url: string): string | undefined { - const exactKey = routeTitleKeys[url] + const sanitizedUrl = new URL(url, BASE_URL).pathname + + const exactKey = routeTitleKeys[sanitizedUrl] if (exactKey) return exactKey - const basePath = '/' + url.split('/').filter(Boolean)[0] + const basePath = '/' + sanitizedUrl.split('/').filter(Boolean)[0] return routeTitleKeys[basePath] }