mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
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.
This commit is contained in:
parent
4f7a14b044
commit
bcbd5ed717
@ -23,7 +23,7 @@ const WebviewContainer = ({ url, isActive }: { url: string; isActive: boolean })
|
|||||||
export const AppShell = () => {
|
export const AppShell = () => {
|
||||||
const { tabs, activeTabId, setActiveTab, closeTab, updateTab, addTab } = useTabs()
|
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) => {
|
const handleUrlChange = (tabId: string, url: string) => {
|
||||||
updateTab(tabId, { url, title: getDefaultRouteTitle(url) })
|
updateTab(tabId, { url, title: getDefaultRouteTitle(url) })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,20 +24,20 @@ export const TabRouter = ({ tab, isActive, onUrlChange }: TabRouterProps) => {
|
|||||||
return createRouter({ routeTree, history })
|
return createRouter({ routeTree, history })
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [tab.id])
|
}, [tab.id])
|
||||||
|
|
||||||
// Sync internal navigation back to tab state
|
// Sync internal navigation back to tab state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return router.subscribe('onResolved', ({ toLocation }) => {
|
return router.subscribe('onResolved', ({ toLocation }) => {
|
||||||
if (toLocation.pathname !== tab.url) {
|
const nextHref = toLocation.href
|
||||||
onUrlChange(toLocation.pathname)
|
if (nextHref !== tab.url) {
|
||||||
|
onUrlChange(nextHref)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [router, tab.url, onUrlChange])
|
}, [router, tab.url, onUrlChange])
|
||||||
|
|
||||||
// Navigate when tab.url changes externally (e.g., from Sidebar)
|
// Navigate when tab.url changes externally (e.g., from Sidebar)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentPath = router.state.location.pathname
|
const currentHref = router.state.location.href
|
||||||
if (tab.url !== currentPath) {
|
if (tab.url !== currentHref) {
|
||||||
router.navigate({ to: tab.url })
|
router.navigate({ to: tab.url })
|
||||||
}
|
}
|
||||||
}, [router, tab.url])
|
}, [router, tab.url])
|
||||||
|
|||||||
@ -179,7 +179,7 @@ export function useTabs() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加 LRU 字段
|
// 添加 LRU 字段,保留完整 URL(含 search/hash)
|
||||||
const newTab: Tab = {
|
const newTab: Tab = {
|
||||||
...tab,
|
...tab,
|
||||||
lastAccessTime: Date.now(),
|
lastAccessTime: Date.now(),
|
||||||
@ -255,7 +255,7 @@ export function useTabs() {
|
|||||||
const newTab: Tab = {
|
const newTab: Tab = {
|
||||||
id: id || uuid(),
|
id: id || uuid(),
|
||||||
type,
|
type,
|
||||||
url,
|
url, // full URL including search/hash
|
||||||
title: title || getDefaultRouteTitle(url),
|
title: title || getDefaultRouteTitle(url),
|
||||||
lastAccessTime: Date.now(),
|
lastAccessTime: Date.now(),
|
||||||
isDormant: false
|
isDormant: false
|
||||||
|
|||||||
@ -94,8 +94,8 @@ const ProviderList: FC = () => {
|
|||||||
// 清除 id 参数
|
// 清除 id 参数
|
||||||
navigate({
|
navigate({
|
||||||
to: location.pathname,
|
to: location.pathname,
|
||||||
search: (prev) => {
|
search: ({ id, ...rest }) => {
|
||||||
const { id: _, ...rest } = prev as Record<string, unknown>
|
void id
|
||||||
return rest
|
return rest
|
||||||
},
|
},
|
||||||
replace: true
|
replace: true
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
|
import NavigationHandler from '@renderer/handler/NavigationHandler'
|
||||||
import { createRootRoute, Outlet } from '@tanstack/react-router'
|
import { createRootRoute, Outlet } from '@tanstack/react-router'
|
||||||
|
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
component: () => <Outlet />
|
component: () => (
|
||||||
|
<>
|
||||||
|
<NavigationHandler />
|
||||||
|
<Outlet />
|
||||||
|
</>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import i18n from '@renderer/i18n'
|
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
|
* Route to i18n key mapping for default tab titles
|
||||||
*/
|
*/
|
||||||
@ -29,30 +32,35 @@ const routeTitleKeys: Record<string, string> = {
|
|||||||
* getDefaultRouteTitle('/unknown') // 'unknown'
|
* getDefaultRouteTitle('/unknown') // 'unknown'
|
||||||
*/
|
*/
|
||||||
export function getDefaultRouteTitle(url: string): string {
|
export function getDefaultRouteTitle(url: string): string {
|
||||||
|
const sanitizedUrl = new URL(url, BASE_URL).pathname
|
||||||
|
|
||||||
// Try exact match first
|
// Try exact match first
|
||||||
const exactKey = routeTitleKeys[url]
|
const exactKey = routeTitleKeys[sanitizedUrl]
|
||||||
if (exactKey) {
|
if (exactKey) {
|
||||||
return i18n.t(exactKey)
|
return i18n.t(exactKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try matching base path (e.g., '/chat/123' -> '/chat')
|
// 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]
|
const baseKey = routeTitleKeys[basePath]
|
||||||
if (baseKey) {
|
if (baseKey) {
|
||||||
return i18n.t(baseKey)
|
return i18n.t(baseKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to URL path
|
// Fallback to last segment of pathname
|
||||||
return url.split('/').pop() || url
|
const segments = sanitizedUrl.split('/').filter(Boolean)
|
||||||
|
return segments.pop() || sanitizedUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the i18n key for a route (without translating)
|
* Get the i18n key for a route (without translating)
|
||||||
*/
|
*/
|
||||||
export function getRouteTitleKey(url: string): string | undefined {
|
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
|
if (exactKey) return exactKey
|
||||||
|
|
||||||
const basePath = '/' + url.split('/').filter(Boolean)[0]
|
const basePath = '/' + sanitizedUrl.split('/').filter(Boolean)[0]
|
||||||
return routeTitleKeys[basePath]
|
return routeTitleKeys[basePath]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user