mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 04:31:27 +08:00
refactor: finalize Multi MemoryRouter architecture and add developer docs
- Fix TanStack Router path config using resolve() for absolute paths - Add openTab API to useTabs hook for Tab-level navigation - Remove AppRouter.tsx (merged into AppShell) - Add developer documentation (README.md, README.zh-CN.md) - Add settings route placeholder - Update App.tsx with TODO comments for migration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
37766d5685
commit
255be7d4d1
@ -84,8 +84,8 @@ export default defineConfig({
|
||||
tanstackRouter({
|
||||
target: 'react',
|
||||
autoCodeSplitting: true,
|
||||
routesDirectory: './src/renderer/src/routes',
|
||||
generatedRouteTree: './src/renderer/src/routeTree.gen.ts'
|
||||
routesDirectory: resolve('src/renderer/src/routes'),
|
||||
generatedRouteTree: resolve('src/renderer/src/routeTree.gen.ts')
|
||||
}),
|
||||
(async () => (await import('@tailwindcss/vite')).default())(),
|
||||
react({
|
||||
|
||||
@ -7,6 +7,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { Provider } from 'react-redux'
|
||||
import { PersistGate } from 'redux-persist/integration/react'
|
||||
|
||||
// TODO: 新路由系统入口,迁移完成后启用
|
||||
// import { AppShell } from './components/layout/AppShell'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import AntdProvider from './context/AntdProvider'
|
||||
import { CodeStyleProvider } from './context/CodeStyleProvider'
|
||||
@ -42,6 +44,7 @@ function App(): React.ReactElement {
|
||||
<CodeStyleProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
{/* TODO: 迁移完成后切换到 <AppShell /> */}
|
||||
<Router />
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
import { createRouter, RouterProvider } from '@tanstack/react-router'
|
||||
|
||||
import { routeTree } from './routeTree.gen'
|
||||
|
||||
// Create a new router instance
|
||||
const router = createRouter({ routeTree })
|
||||
|
||||
// Register the router instance for type safety
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router
|
||||
}
|
||||
}
|
||||
|
||||
export const AppRouter = () => {
|
||||
return <RouterProvider router={router} />
|
||||
}
|
||||
@ -1,10 +1,11 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
|
||||
import { usePersistCache } from '../data/hooks/useCache'
|
||||
import { uuid } from '../utils'
|
||||
|
||||
// Re-export types from shared schema
|
||||
export type { Tab, TabsState, TabType } from '@shared/data/cache/cacheSchemas'
|
||||
import type { Tab } from '@shared/data/cache/cacheSchemas'
|
||||
import type { Tab, TabType } from '@shared/data/cache/cacheSchemas'
|
||||
|
||||
const DEFAULT_TAB: Tab = {
|
||||
id: 'home',
|
||||
@ -13,6 +14,20 @@ const DEFAULT_TAB: Tab = {
|
||||
title: 'Home'
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for opening a tab
|
||||
*/
|
||||
export interface OpenTabOptions {
|
||||
/** Force open a new tab even if one with the same URL exists */
|
||||
forceNew?: boolean
|
||||
/** Tab title (defaults to URL path) */
|
||||
title?: string
|
||||
/** Tab type (defaults to 'route') */
|
||||
type?: TabType
|
||||
/** Custom tab ID (auto-generated if not provided) */
|
||||
id?: string
|
||||
}
|
||||
|
||||
export function useTabs() {
|
||||
const [tabsState, setTabsState] = usePersistCache('ui.tab.state')
|
||||
|
||||
@ -80,14 +95,72 @@ export function useTabs() {
|
||||
[tabs, tabsState, setTabsState]
|
||||
)
|
||||
|
||||
/**
|
||||
* Open a Tab - reuses existing tab or creates new one
|
||||
*
|
||||
* @example
|
||||
* // Basic usage - reuses existing tab if URL matches
|
||||
* openTab('/settings')
|
||||
*
|
||||
* @example
|
||||
* // With custom title
|
||||
* openTab('/chat/123', { title: 'Chat with Alice' })
|
||||
*
|
||||
* @example
|
||||
* // Force open new tab (e.g., Cmd+Click)
|
||||
* openTab('/settings', { forceNew: true })
|
||||
*
|
||||
* @example
|
||||
* // Open webview tab
|
||||
* openTab('https://example.com', { type: 'webview', title: 'Example' })
|
||||
*/
|
||||
const openTab = useCallback(
|
||||
(url: string, options: OpenTabOptions = {}) => {
|
||||
const { forceNew = false, title, type = 'route', id } = options
|
||||
|
||||
// Try to find existing tab with same URL (unless forceNew)
|
||||
if (!forceNew) {
|
||||
const existingTab = tabs.find((t) => t.type === type && t.url === url)
|
||||
if (existingTab) {
|
||||
setActiveTab(existingTab.id)
|
||||
return existingTab.id
|
||||
}
|
||||
}
|
||||
|
||||
// Create new tab
|
||||
const newTab: Tab = {
|
||||
id: id || uuid(),
|
||||
type,
|
||||
url,
|
||||
title: title || url.split('/').pop() || url
|
||||
}
|
||||
|
||||
addTab(newTab)
|
||||
return newTab.id
|
||||
},
|
||||
[tabs, setActiveTab, addTab]
|
||||
)
|
||||
|
||||
/**
|
||||
* Get the currently active tab
|
||||
*/
|
||||
const activeTab = useMemo(() => tabs.find((t) => t.id === activeTabId), [tabs, activeTabId])
|
||||
|
||||
return {
|
||||
// State
|
||||
tabs,
|
||||
activeTabId,
|
||||
activeTab,
|
||||
isLoading: false,
|
||||
|
||||
// Basic operations
|
||||
addTab,
|
||||
closeTab,
|
||||
setActiveTab,
|
||||
updateTab,
|
||||
setTabs
|
||||
setTabs,
|
||||
|
||||
// High-level Tab operations
|
||||
openTab
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,8 +9,14 @@
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as SettingsRouteImport } from './routes/settings'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
|
||||
const SettingsRoute = SettingsRouteImport.update({
|
||||
id: '/settings',
|
||||
path: '/settings',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
@ -19,28 +25,39 @@ const IndexRoute = IndexRouteImport.update({
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/settings': typeof SettingsRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/settings': typeof SettingsRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/settings': typeof SettingsRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/'
|
||||
fullPaths: '/' | '/settings'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/'
|
||||
id: '__root__' | '/'
|
||||
to: '/' | '/settings'
|
||||
id: '__root__' | '/' | '/settings'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
SettingsRoute: typeof SettingsRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/settings': {
|
||||
id: '/settings'
|
||||
path: '/settings'
|
||||
fullPath: '/settings'
|
||||
preLoaderRoute: typeof SettingsRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
@ -53,6 +70,7 @@ declare module '@tanstack/react-router' {
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
SettingsRoute: SettingsRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
194
src/renderer/src/routes/README.md
Normal file
194
src/renderer/src/routes/README.md
Normal file
@ -0,0 +1,194 @@
|
||||
# Routing System Developer Guide
|
||||
|
||||
This project uses **TanStack Router + Multi MemoryRouter** architecture, where each Tab has its own independent router instance, enabling native KeepAlive behavior.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Adding a New Page
|
||||
|
||||
Create a file in the `src/renderer/src/routes/` directory:
|
||||
|
||||
```typescript
|
||||
// routes/knowledge.tsx
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/knowledge')({
|
||||
component: KnowledgePage
|
||||
})
|
||||
|
||||
function KnowledgePage() {
|
||||
return <div>Knowledge Page</div>
|
||||
}
|
||||
```
|
||||
|
||||
After running `yarn dev`, TanStack Router will automatically update `routeTree.gen.ts`.
|
||||
|
||||
### 2. Routes with Parameters
|
||||
|
||||
```typescript
|
||||
// routes/chat/$topicId.tsx
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/chat/$topicId')({
|
||||
component: ChatPage
|
||||
})
|
||||
|
||||
function ChatPage() {
|
||||
const { topicId } = Route.useParams()
|
||||
return <div>Chat: {topicId}</div>
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Nested Routes
|
||||
|
||||
```text
|
||||
routes/
|
||||
├── settings.tsx # /settings (layout)
|
||||
├── settings/
|
||||
│ ├── general.tsx # /settings/general
|
||||
│ └── provider.tsx # /settings/provider
|
||||
```
|
||||
|
||||
```typescript
|
||||
// routes/settings.tsx
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/settings')({
|
||||
component: SettingsLayout
|
||||
})
|
||||
|
||||
function SettingsLayout() {
|
||||
return (
|
||||
<div className="flex">
|
||||
<aside>Settings Menu</aside>
|
||||
<main><Outlet /></main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Navigation API
|
||||
|
||||
This project provides two navigation methods:
|
||||
|
||||
### 1. Tab-Level Navigation - `openTab`
|
||||
|
||||
Open a new Tab or switch to an existing Tab using the `useTabs` hook:
|
||||
|
||||
```typescript
|
||||
import { useTabs } from '@renderer/hooks/useTabs'
|
||||
|
||||
function MyComponent() {
|
||||
const { openTab, closeTab } = useTabs()
|
||||
|
||||
// Basic usage - reuse existing Tab or create new one
|
||||
openTab('/settings')
|
||||
|
||||
// With title
|
||||
openTab('/chat/123', { title: 'Chat with Alice' })
|
||||
|
||||
// Force new Tab (even if same URL exists)
|
||||
openTab('/settings', { forceNew: true })
|
||||
|
||||
// Open Webview Tab
|
||||
openTab('https://example.com', {
|
||||
type: 'webview',
|
||||
title: 'Example Site'
|
||||
})
|
||||
|
||||
// Close Tab
|
||||
closeTab(tabId)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. In-Tab Navigation - `useNavigate`
|
||||
|
||||
Navigate within the same Tab (won't create a new Tab) using TanStack Router's `useNavigate`:
|
||||
|
||||
```typescript
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
|
||||
function SettingsPage() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
// Navigate to sub-page within current Tab
|
||||
navigate({ to: '/settings/provider' })
|
||||
|
||||
// Navigate with parameters
|
||||
navigate({ to: '/chat/$topicId', params: { topicId: '123' } })
|
||||
}
|
||||
```
|
||||
|
||||
### Comparison
|
||||
|
||||
| Scenario | Method | Result |
|
||||
|----------|--------|--------|
|
||||
| Open new feature module | `openTab('/knowledge')` | Creates new Tab |
|
||||
| Switch sub-page in settings | `navigate({ to: '/settings/provider' })` | Navigates within current Tab |
|
||||
| Open detail from list | `openTab('/chat/123', { title: '...' })` | Creates new Tab |
|
||||
| Go back to previous page | `navigate({ to: '..' })` | Goes back within current Tab |
|
||||
|
||||
### API Reference
|
||||
|
||||
#### `useTabs()` Return Value
|
||||
|
||||
| Property/Method | Type | Description |
|
||||
|-----------------|------|-------------|
|
||||
| `tabs` | `Tab[]` | List of all Tabs |
|
||||
| `activeTabId` | `string` | Currently active Tab ID |
|
||||
| `activeTab` | `Tab \| undefined` | Currently active Tab object |
|
||||
| `openTab(url, options?)` | `(url: string, options?: OpenTabOptions) => string` | Open Tab, returns Tab ID |
|
||||
| `closeTab(id)` | `(id: string) => void` | Close specified Tab |
|
||||
| `setActiveTab(id)` | `(id: string) => void` | Switch to specified Tab |
|
||||
| `updateTab(id, updates)` | `(id: string, updates: Partial<Tab>) => void` | Update Tab properties |
|
||||
|
||||
#### `OpenTabOptions`
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `forceNew` | `boolean` | `false` | Force create new Tab |
|
||||
| `title` | `string` | URL path | Tab title |
|
||||
| `type` | `'route' \| 'webview'` | `'route'` | Tab type |
|
||||
| `id` | `string` | Auto-generated | Custom Tab ID |
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```text
|
||||
AppShell
|
||||
├── Sidebar
|
||||
├── TabBar
|
||||
└── Content Area
|
||||
├── TabRouter #1 (Home)
|
||||
│ └── Activity(visible) → MemoryRouter → RouterProvider
|
||||
├── TabRouter #2 (Settings)
|
||||
│ └── Activity(hidden) → MemoryRouter → RouterProvider
|
||||
└── WebviewContainer (for webview tabs)
|
||||
```
|
||||
|
||||
- Each Tab has its own independent `MemoryRouter` instance
|
||||
- Uses React 19 `<Activity>` component to control visibility
|
||||
- Components are not unmounted on Tab switch, state is fully preserved (KeepAlive)
|
||||
|
||||
## File Structure
|
||||
|
||||
```text
|
||||
src/renderer/src/
|
||||
├── routes/ # Route pages (TanStack Router file-based routing)
|
||||
│ ├── __root.tsx # Root route (renders Outlet)
|
||||
│ ├── index.tsx # / Home page
|
||||
│ ├── settings.tsx # /settings
|
||||
│ └── README.md # This document
|
||||
├── components/layout/
|
||||
│ ├── AppShell.tsx # Main layout (Sidebar + TabBar + Content)
|
||||
│ └── TabRouter.tsx # Tab router container (MemoryRouter + Activity)
|
||||
├── hooks/
|
||||
│ └── useTabs.ts # Tab state management hook
|
||||
└── routeTree.gen.ts # Auto-generated route tree (do not edit manually)
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Do not manually edit `routeTree.gen.ts`** - It is automatically generated by TanStack Router
|
||||
2. **File name determines route path** - `routes/settings.tsx` → `/settings`
|
||||
3. **Dynamic parameters use `$`** - `routes/chat/$topicId.tsx` → `/chat/:topicId`
|
||||
4. **Page state is automatically preserved** - Tab switching won't lose `useState`, scroll position, etc.
|
||||
194
src/renderer/src/routes/README.zh-CN.md
Normal file
194
src/renderer/src/routes/README.zh-CN.md
Normal file
@ -0,0 +1,194 @@
|
||||
# 路由系统开发指南
|
||||
|
||||
本项目使用 **TanStack Router + Multi MemoryRouter** 架构,每个 Tab 拥有独立的路由实例,实现原生 KeepAlive。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 添加新页面
|
||||
|
||||
在 `src/renderer/src/routes/` 目录下创建文件:
|
||||
|
||||
```typescript
|
||||
// routes/knowledge.tsx
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/knowledge')({
|
||||
component: KnowledgePage
|
||||
})
|
||||
|
||||
function KnowledgePage() {
|
||||
return <div>Knowledge Page</div>
|
||||
}
|
||||
```
|
||||
|
||||
运行 `yarn dev` 后,TanStack Router 会自动更新 `routeTree.gen.ts`。
|
||||
|
||||
### 2. 带参数的路由
|
||||
|
||||
```typescript
|
||||
// routes/chat/$topicId.tsx
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/chat/$topicId')({
|
||||
component: ChatPage
|
||||
})
|
||||
|
||||
function ChatPage() {
|
||||
const { topicId } = Route.useParams()
|
||||
return <div>Chat: {topicId}</div>
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 嵌套路由
|
||||
|
||||
```text
|
||||
routes/
|
||||
├── settings.tsx # /settings (布局)
|
||||
├── settings/
|
||||
│ ├── general.tsx # /settings/general
|
||||
│ └── provider.tsx # /settings/provider
|
||||
```
|
||||
|
||||
```typescript
|
||||
// routes/settings.tsx
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/settings')({
|
||||
component: SettingsLayout
|
||||
})
|
||||
|
||||
function SettingsLayout() {
|
||||
return (
|
||||
<div className="flex">
|
||||
<aside>Settings Menu</aside>
|
||||
<main><Outlet /></main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 导航 API
|
||||
|
||||
本项目有两种导航方式:
|
||||
|
||||
### 1. Tab 级别导航 - `openTab`
|
||||
|
||||
打开新 Tab 或切换到已有 Tab,使用 `useTabs` hook:
|
||||
|
||||
```typescript
|
||||
import { useTabs } from '@renderer/hooks/useTabs'
|
||||
|
||||
function MyComponent() {
|
||||
const { openTab, closeTab } = useTabs()
|
||||
|
||||
// 基础用法 - 复用已有 Tab 或新建
|
||||
openTab('/settings')
|
||||
|
||||
// 带标题
|
||||
openTab('/chat/123', { title: 'Chat with Alice' })
|
||||
|
||||
// 强制新开 Tab(即使已有相同 URL)
|
||||
openTab('/settings', { forceNew: true })
|
||||
|
||||
// 打开 Webview Tab
|
||||
openTab('https://example.com', {
|
||||
type: 'webview',
|
||||
title: 'Example Site'
|
||||
})
|
||||
|
||||
// 关闭 Tab
|
||||
closeTab(tabId)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Tab 内部导航 - `useNavigate`
|
||||
|
||||
在同一个 Tab 内跳转路由(不会新开 Tab),使用 TanStack Router 的 `useNavigate`:
|
||||
|
||||
```typescript
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
|
||||
function SettingsPage() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
// 在当前 Tab 内跳转到子页面
|
||||
navigate({ to: '/settings/provider' })
|
||||
|
||||
// 带参数跳转
|
||||
navigate({ to: '/chat/$topicId', params: { topicId: '123' } })
|
||||
}
|
||||
```
|
||||
|
||||
### 两者区别
|
||||
|
||||
| 场景 | 使用 | 效果 |
|
||||
|-----|------|------|
|
||||
| 打开新功能模块 | `openTab('/knowledge')` | 新建 Tab |
|
||||
| 设置页内切换子页 | `navigate({ to: '/settings/provider' })` | 当前 Tab 内跳转 |
|
||||
| 从列表打开详情 | `openTab('/chat/123', { title: '...' })` | 新建 Tab |
|
||||
| 返回上一页 | `navigate({ to: '..' })` | 当前 Tab 内返回 |
|
||||
|
||||
### API 参考
|
||||
|
||||
#### `useTabs()` 返回值
|
||||
|
||||
| 属性/方法 | 类型 | 说明 |
|
||||
|----------|------|------|
|
||||
| `tabs` | `Tab[]` | 所有 Tab 列表 |
|
||||
| `activeTabId` | `string` | 当前激活的 Tab ID |
|
||||
| `activeTab` | `Tab \| undefined` | 当前激活的 Tab 对象 |
|
||||
| `openTab(url, options?)` | `(url: string, options?: OpenTabOptions) => string` | 打开 Tab,返回 Tab ID |
|
||||
| `closeTab(id)` | `(id: string) => void` | 关闭指定 Tab |
|
||||
| `setActiveTab(id)` | `(id: string) => void` | 切换到指定 Tab |
|
||||
| `updateTab(id, updates)` | `(id: string, updates: Partial<Tab>) => void` | 更新 Tab 属性 |
|
||||
|
||||
#### `OpenTabOptions`
|
||||
|
||||
| 选项 | 类型 | 默认值 | 说明 |
|
||||
|-----|------|-------|------|
|
||||
| `forceNew` | `boolean` | `false` | 强制新开 Tab |
|
||||
| `title` | `string` | URL 路径 | Tab 标题 |
|
||||
| `type` | `'route' \| 'webview'` | `'route'` | Tab 类型 |
|
||||
| `id` | `string` | 自动生成 | 自定义 Tab ID |
|
||||
|
||||
## 架构说明
|
||||
|
||||
```text
|
||||
AppShell
|
||||
├── Sidebar
|
||||
├── TabBar
|
||||
└── Content Area
|
||||
├── TabRouter #1 (Home)
|
||||
│ └── Activity(visible) → MemoryRouter → RouterProvider
|
||||
├── TabRouter #2 (Settings)
|
||||
│ └── Activity(hidden) → MemoryRouter → RouterProvider
|
||||
└── WebviewContainer (for webview tabs)
|
||||
```
|
||||
|
||||
- 每个 Tab 拥有独立的 `MemoryRouter` 实例
|
||||
- 使用 React 19 `<Activity>` 组件控制可见性
|
||||
- Tab 切换时组件不卸载,状态完全保持(KeepAlive)
|
||||
|
||||
## 文件结构
|
||||
|
||||
```text
|
||||
src/renderer/src/
|
||||
├── routes/ # 路由页面(TanStack Router 文件路由)
|
||||
│ ├── __root.tsx # 根路由(渲染 Outlet)
|
||||
│ ├── index.tsx # / 首页
|
||||
│ ├── settings.tsx # /settings
|
||||
│ └── README.md # 本文档
|
||||
├── components/layout/
|
||||
│ ├── AppShell.tsx # 主布局(Sidebar + TabBar + Content)
|
||||
│ └── TabRouter.tsx # Tab 路由容器(MemoryRouter + Activity)
|
||||
├── hooks/
|
||||
│ └── useTabs.ts # Tab 状态管理 Hook
|
||||
└── routeTree.gen.ts # 自动生成的路由树(勿手动编辑)
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **不要手动编辑 `routeTree.gen.ts`** - 它由 TanStack Router 自动生成
|
||||
2. **路由文件命名即路径** - `routes/settings.tsx` → `/settings`
|
||||
3. **动态参数使用 `$`** - `routes/chat/$topicId.tsx` → `/chat/:topicId`
|
||||
4. **页面状态自动保持** - Tab 切换不会丢失 `useState`、滚动位置等
|
||||
@ -1,7 +1,5 @@
|
||||
import { createRootRoute } from '@tanstack/react-router'
|
||||
|
||||
import { AppShell } from '../components/layout/AppShell'
|
||||
import { createRootRoute, Outlet } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: () => <AppShell />
|
||||
component: () => <Outlet />
|
||||
})
|
||||
|
||||
@ -6,9 +6,9 @@ export const Route = createFileRoute('/')({
|
||||
|
||||
function Index() {
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<h3>Welcome to Cherry Studio!</h3>
|
||||
<p>Select a tab to start.</p>
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 p-8">
|
||||
<h3 className="font-semibold text-xl">Home</h3>
|
||||
<p className="text-muted-foreground text-sm">TODO: Migrate HomePage</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
16
src/renderer/src/routes/settings.tsx
Normal file
16
src/renderer/src/routes/settings.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { Settings } from 'lucide-react'
|
||||
|
||||
export const Route = createFileRoute('/settings')({
|
||||
component: SettingsPage
|
||||
})
|
||||
|
||||
function SettingsPage() {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-4 bg-background p-8">
|
||||
<Settings className="size-12 text-muted-foreground" />
|
||||
<h2 className="font-semibold text-xl">Settings</h2>
|
||||
<p className="text-muted-foreground text-sm">TODO: Migrate SettingsPage</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user