mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-28 05:11:24 +08:00
feat(discover): enhance Discover page with Tailwind CSS integration and routing improvements
- Added Tailwind CSS import to the entry point for styling. - Updated the ThemeProvider to dynamically apply Tailwind themes based on user selection. - Refactored Discover page to utilize new ROUTERS structure for better routing management. - Simplified category handling in useDiscoverCategories hook by leveraging ROUTERS_ENTRIES. - Introduced InternalCategory interface for better type management in Discover components. - Cleaned up unused code and comments for improved readability.
This commit is contained in:
parent
0d6156cc1b
commit
833ea86e82
@ -49,6 +49,7 @@ body {
|
|||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
transition: background-color 0.3s linear;
|
transition: background-color 0.3s linear;
|
||||||
|
background-color: unset;
|
||||||
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|||||||
@ -23,6 +23,12 @@ interface ThemeProviderProps extends PropsWithChildren {
|
|||||||
defaultTheme?: ThemeMode
|
defaultTheme?: ThemeMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tailwindThemeChange = (theme: ThemeMode) => {
|
||||||
|
const root = window.document.documentElement
|
||||||
|
root.classList.remove('light', 'dark')
|
||||||
|
root.classList.add(theme)
|
||||||
|
}
|
||||||
|
|
||||||
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||||
// 用户设置的主题
|
// 用户设置的主题
|
||||||
const { theme: settedTheme, setTheme: setSettedTheme } = useSettings()
|
const { theme: settedTheme, setTheme: setSettedTheme } = useSettings()
|
||||||
@ -64,7 +70,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.api.setTheme(settedTheme)
|
window.api.setTheme(settedTheme)
|
||||||
// tailwindThemeChange(settedTheme)
|
tailwindThemeChange(settedTheme)
|
||||||
}, [settedTheme])
|
}, [settedTheme])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import './assets/styles/tailwind.css'
|
|
||||||
import './assets/styles/index.scss'
|
import './assets/styles/index.scss'
|
||||||
|
import './assets/styles/tailwind.css'
|
||||||
import '@ant-design/v5-patch-for-react-19'
|
import '@ant-design/v5-patch-for-react-19'
|
||||||
|
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
|
|||||||
@ -1,29 +1,29 @@
|
|||||||
import React, { Suspense } from 'react'
|
import React, { Suspense } from 'react'
|
||||||
import { Navigate, Route, Routes, useLocation } from 'react-router-dom'
|
import { Navigate, Route, Routes, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import { discoverRouters, InternalCategory } from '../routers'
|
import { ROUTERS } from '../routers'
|
||||||
|
import { InternalCategory } from '../type'
|
||||||
|
|
||||||
export interface DiscoverContentProps {
|
export interface DiscoverContentProps {
|
||||||
activeTabId: string // This should be one of the CherryStoreType values, e.g., "Assistant"
|
activeTabId: string
|
||||||
// selectedSubcategoryId: string
|
|
||||||
currentCategory: InternalCategory | undefined
|
currentCategory: InternalCategory | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const DiscoverContent: React.FC<DiscoverContentProps> = ({ activeTabId, currentCategory }) => {
|
const DiscoverContent: React.FC<DiscoverContentProps> = ({ activeTabId, currentCategory }) => {
|
||||||
const location = useLocation() // To see the current path for debugging or more complex logic
|
const location = useLocation()
|
||||||
|
|
||||||
if (!currentCategory || !activeTabId) {
|
if (!currentCategory || !activeTabId) {
|
||||||
return <div className="p-4 text-center">Loading: Category or Tab ID missing...</div>
|
return <div className="p-4 text-center">Loading: Category or Tab ID missing...</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!activeTabId && !location.pathname.startsWith('/discover/')) {
|
if (!activeTabId && !location.pathname.startsWith('/discover/')) {
|
||||||
return <Navigate to="/discover/assistant?subcategory=all" replace /> // Fallback redirect, adjust as needed
|
return <Navigate to="/discover/assistant?subcategory=all" replace />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
<Suspense fallback={null}>
|
||||||
<Routes>
|
<Routes>
|
||||||
{discoverRouters.map((_Route) => {
|
{ROUTERS.map((_Route) => {
|
||||||
if (!_Route.component) return null
|
if (!_Route.component) return null
|
||||||
return <Route key={_Route.path} path={`/${_Route.path}`} element={<_Route.component />} />
|
return <Route key={_Route.path} path={`/${_Route.path}`} element={<_Route.component />} />
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
SidebarProvider
|
SidebarProvider
|
||||||
} from '@renderer/ui/sidebar'
|
} from '@renderer/ui/sidebar'
|
||||||
|
|
||||||
import { InternalCategory } from '../hooks/useDiscoverCategories'
|
import { InternalCategory } from '../type'
|
||||||
|
|
||||||
interface DiscoverSidebarProps {
|
interface DiscoverSidebarProps {
|
||||||
activeCategory: InternalCategory | undefined
|
activeCategory: InternalCategory | undefined
|
||||||
|
|||||||
@ -1,18 +1,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
import { CATEGORY_REGISTRY, InternalCategory } from '../routers'
|
import { ROUTERS, ROUTERS_ENTRIES } from '../routers'
|
||||||
|
|
||||||
// 导出接口供其他文件使用
|
|
||||||
export type { InternalCategory }
|
|
||||||
|
|
||||||
// Helper to find category by path
|
|
||||||
const findCategoryByPath = (path: string | undefined): InternalCategory | undefined =>
|
|
||||||
CATEGORY_REGISTRY.find((cat) => cat.path === path)
|
|
||||||
|
|
||||||
// Helper to find category by id (activeTab)
|
|
||||||
const findCategoryById = (id: string | undefined): InternalCategory | undefined =>
|
|
||||||
CATEGORY_REGISTRY.find((cat) => cat.id === id)
|
|
||||||
|
|
||||||
export function useDiscoverCategories() {
|
export function useDiscoverCategories() {
|
||||||
const [activeTab, setActiveTab] = useState<string>('')
|
const [activeTab, setActiveTab] = useState<string>('')
|
||||||
@ -34,8 +23,8 @@ export function useDiscoverCategories() {
|
|||||||
|
|
||||||
// 处理基础路径重定向
|
// 处理基础路径重定向
|
||||||
if (location.pathname === '/discover' || location.pathname === '/discover/') {
|
if (location.pathname === '/discover' || location.pathname === '/discover/') {
|
||||||
if (CATEGORY_REGISTRY.length > 0) {
|
if (ROUTERS.length > 0) {
|
||||||
const firstCategory = CATEGORY_REGISTRY[0]
|
const firstCategory = ROUTERS[0]
|
||||||
navigate(`/discover/${firstCategory.path}?category=${firstCategory.id}&subcategory=all`, { replace: true })
|
navigate(`/discover/${firstCategory.path}?category=${firstCategory.id}&subcategory=all`, { replace: true })
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -46,14 +35,14 @@ export function useDiscoverCategories() {
|
|||||||
|
|
||||||
// 如果没有 category 参数,尝试从路径推断
|
// 如果没有 category 参数,尝试从路径推断
|
||||||
if (!targetCategoryId && currentCategoryPath) {
|
if (!targetCategoryId && currentCategoryPath) {
|
||||||
const categoryFromPath = findCategoryByPath(currentCategoryPath)
|
const categoryFromPath = ROUTERS_ENTRIES[currentCategoryPath]
|
||||||
targetCategoryId = categoryFromPath?.id || null
|
targetCategoryId = categoryFromPath?.id || null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理无效分类重定向
|
// 处理无效分类重定向
|
||||||
if (!targetCategoryId || !findCategoryById(targetCategoryId)) {
|
if (!targetCategoryId || !ROUTERS_ENTRIES[targetCategoryId]) {
|
||||||
if (CATEGORY_REGISTRY.length > 0) {
|
if (ROUTERS.length > 0) {
|
||||||
const firstCategory = CATEGORY_REGISTRY[0]
|
const firstCategory = ROUTERS[0]
|
||||||
navigate(`/discover/${firstCategory.path}?category=${firstCategory.id}&subcategory=all`, { replace: true })
|
navigate(`/discover/${firstCategory.path}?category=${firstCategory.id}&subcategory=all`, { replace: true })
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -74,7 +63,7 @@ export function useDiscoverCategories() {
|
|||||||
}, [location.pathname, location.search, navigate]) // 故意不包含 activeTab 和 selectedSubcategory 以避免重复渲染
|
}, [location.pathname, location.search, navigate]) // 故意不包含 activeTab 和 selectedSubcategory 以避免重复渲染
|
||||||
|
|
||||||
const currentCategory = useMemo(() => {
|
const currentCategory = useMemo(() => {
|
||||||
return findCategoryById(activeTab)
|
return ROUTERS_ENTRIES[activeTab]
|
||||||
}, [activeTab])
|
}, [activeTab])
|
||||||
|
|
||||||
// 优化的 Tab 选择处理,使用 useCallback 避免重复渲染
|
// 优化的 Tab 选择处理,使用 useCallback 避免重复渲染
|
||||||
@ -83,7 +72,7 @@ export function useDiscoverCategories() {
|
|||||||
(tabId: string) => {
|
(tabId: string) => {
|
||||||
if (activeTab === tabId) return // 如果已经是当前 tab,直接返回
|
if (activeTab === tabId) return // 如果已经是当前 tab,直接返回
|
||||||
|
|
||||||
const categoryToSelect = findCategoryById(tabId)
|
const categoryToSelect = ROUTERS_ENTRIES[tabId]
|
||||||
if (categoryToSelect?.path) {
|
if (categoryToSelect?.path) {
|
||||||
isUserNavigationRef.current = true
|
isUserNavigationRef.current = true
|
||||||
navigate(`/discover/${categoryToSelect.path}?category=${tabId}&subcategory=all`)
|
navigate(`/discover/${categoryToSelect.path}?category=${tabId}&subcategory=all`)
|
||||||
@ -97,7 +86,7 @@ export function useDiscoverCategories() {
|
|||||||
(subcategoryId: string) => {
|
(subcategoryId: string) => {
|
||||||
if (selectedSubcategory === subcategoryId) return // 如果已经是当前子分类,直接返回
|
if (selectedSubcategory === subcategoryId) return // 如果已经是当前子分类,直接返回
|
||||||
|
|
||||||
const currentCatDetails = findCategoryById(activeTab)
|
const currentCatDetails = ROUTERS_ENTRIES[activeTab]
|
||||||
if (currentCatDetails?.path) {
|
if (currentCatDetails?.path) {
|
||||||
isUserNavigationRef.current = true
|
isUserNavigationRef.current = true
|
||||||
navigate(`/discover/${currentCatDetails.path}?category=${activeTab}&subcategory=${subcategoryId}`)
|
navigate(`/discover/${currentCatDetails.path}?category=${activeTab}&subcategory=${subcategoryId}`)
|
||||||
@ -107,7 +96,6 @@ export function useDiscoverCategories() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categories: CATEGORY_REGISTRY, // 直接返回静态注册表
|
|
||||||
activeTab,
|
activeTab,
|
||||||
selectedSubcategory,
|
selectedSubcategory,
|
||||||
currentCategory,
|
currentCategory,
|
||||||
|
|||||||
@ -6,19 +6,14 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import DiscoverMain from './components/DiscoverMain'
|
import DiscoverMain from './components/DiscoverMain'
|
||||||
import DiscoverSidebar from './components/DiscoverSidebar'
|
import DiscoverSidebar from './components/DiscoverSidebar'
|
||||||
import { useDiscoverCategories } from './hooks/useDiscoverCategories'
|
import { useDiscoverCategories } from './hooks/useDiscoverCategories'
|
||||||
|
import { ROUTERS } from './routers'
|
||||||
|
|
||||||
export default function DiscoverPage() {
|
export default function DiscoverPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { categories, activeTab, selectedSubcategory, currentCategory, handleSelectTab, handleSelectSubcategory } =
|
const { activeTab, selectedSubcategory, currentCategory, handleSelectTab, handleSelectSubcategory } =
|
||||||
useDiscoverCategories()
|
useDiscoverCategories()
|
||||||
|
|
||||||
// 使用 useMemo 优化 tabs 数据,避免每次渲染都创建新数组
|
const tabs = useMemo(() => ROUTERS.map((router) => ({ id: router.id, label: router.title })), [])
|
||||||
const vercelTabsData = useMemo(() => {
|
|
||||||
return categories.map((category) => ({
|
|
||||||
id: category.id,
|
|
||||||
label: category.title
|
|
||||||
}))
|
|
||||||
}, [categories])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
@ -29,9 +24,9 @@ export default function DiscoverPage() {
|
|||||||
</NavbarCenter>
|
</NavbarCenter>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|
||||||
{categories.length > 0 && (
|
{ROUTERS.length > 0 && (
|
||||||
<div className="px-4 py-2">
|
<div className="p-2 pl-0">
|
||||||
<Tabs tabs={vercelTabsData} onTabChange={handleSelectTab} />
|
<Tabs tabs={tabs} activeTab={activeTab} onTabChange={handleSelectTab} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -45,16 +40,9 @@ export default function DiscoverPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* {!currentCategory && categories.length > 0 && (
|
|
||||||
<div className="w-64 flex-shrink-0 border-r p-4 text-muted-foreground">Select a category...</div>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
<main className="w-full overflow-hidden">
|
<main className="w-full overflow-hidden">
|
||||||
<DiscoverMain
|
<DiscoverMain activeTabId={activeTab} currentCategory={currentCategory} />
|
||||||
activeTabId={activeTab}
|
|
||||||
// selectedSubcategoryId={selectedSubcategory}
|
|
||||||
currentCategory={currentCategory}
|
|
||||||
/>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -185,7 +185,7 @@ const AgentsPage: FC = () => {
|
|||||||
{/* <Navbar> */}
|
{/* <Navbar> */}
|
||||||
{/* <NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}> */}
|
{/* <NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}> */}
|
||||||
{/* {t('agents.title')} */}
|
{/* {t('agents.title')} */}
|
||||||
<div className="p-4">
|
{/* <div className="flex justify-center p-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('common.search')}
|
placeholder={t('common.search')}
|
||||||
className="nodrag"
|
className="nodrag"
|
||||||
@ -201,8 +201,7 @@ const AgentsPage: FC = () => {
|
|||||||
onPressEnter={handleSearch}
|
onPressEnter={handleSearch}
|
||||||
onBlur={handleSearchInputBlur}
|
onBlur={handleSearchInputBlur}
|
||||||
/>
|
/>
|
||||||
<div style={{ width: 80 }} />
|
</div> */}
|
||||||
</div>
|
|
||||||
{/* </NavbarCenter> */}
|
{/* </NavbarCenter> */}
|
||||||
{/* </Navbar> */}
|
{/* </Navbar> */}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import { Navbar, NavbarMain } from '@renderer/components/app/Navbar'
|
|
||||||
import App from '@renderer/components/MinApp/MinApp'
|
import App from '@renderer/components/MinApp/MinApp'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||||
import { useNavbarPosition } from '@renderer/hooks/useSettings'
|
|
||||||
import { Button, Input } from 'antd'
|
import { Button, Input } from 'antd'
|
||||||
import { Search, SettingsIcon } from 'lucide-react'
|
import { Search, SettingsIcon } from 'lucide-react'
|
||||||
import React, { FC, useState } from 'react'
|
import React, { FC, useState } from 'react'
|
||||||
@ -16,7 +14,7 @@ const AppsPage: FC = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const { minapps } = useMinapps()
|
const { minapps } = useMinapps()
|
||||||
const { isTopNavbar } = useNavbarPosition()
|
// const { isTopNavbar } = useNavbarPosition()
|
||||||
|
|
||||||
const filteredApps = search
|
const filteredApps = search
|
||||||
? minapps.filter(
|
? minapps.filter(
|
||||||
@ -37,10 +35,10 @@ const AppsPage: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container onContextMenu={handleContextMenu}>
|
<Container onContextMenu={handleContextMenu}>
|
||||||
<Navbar>
|
{/* <Navbar> */}
|
||||||
<NavbarMain>
|
{/* <NavbarMain> */}
|
||||||
{/* {t('minapp.title')} */}
|
{/* {t('minapp.title')} */}
|
||||||
{/* <div className="p-2"> */}
|
<div className="p-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('common.search')}
|
placeholder={t('common.search')}
|
||||||
className="nodrag"
|
className="nodrag"
|
||||||
@ -64,13 +62,13 @@ const AppsPage: FC = () => {
|
|||||||
icon={<SettingsIcon size={18} color="var(--color-text-2)" />}
|
icon={<SettingsIcon size={18} color="var(--color-text-2)" />}
|
||||||
onClick={MinappSettingsPopup.show}
|
onClick={MinappSettingsPopup.show}
|
||||||
/>
|
/>
|
||||||
{/* </div> */}
|
</div>
|
||||||
</NavbarMain>
|
{/* </NavbarMain> */}
|
||||||
</Navbar>
|
{/* </Navbar> */}
|
||||||
<ContentContainer id="content-container">
|
<ContentContainer id="content-container">
|
||||||
<MainContainer>
|
<MainContainer>
|
||||||
<RightContainer>
|
<RightContainer>
|
||||||
{isTopNavbar && (
|
{/* {isTopNavbar && (
|
||||||
<HeaderContainer>
|
<HeaderContainer>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('common.search')}
|
placeholder={t('common.search')}
|
||||||
@ -88,7 +86,7 @@ const AppsPage: FC = () => {
|
|||||||
onClick={() => MinappSettingsPopup.show()}
|
onClick={() => MinappSettingsPopup.show()}
|
||||||
/>
|
/>
|
||||||
</HeaderContainer>
|
</HeaderContainer>
|
||||||
)}
|
)} */}
|
||||||
<AppsContainerWrapper>
|
<AppsContainerWrapper>
|
||||||
<AppsContainer style={{ height: containerHeight }}>
|
<AppsContainer style={{ height: containerHeight }}>
|
||||||
{filteredApps.map((app) => (
|
{filteredApps.map((app) => (
|
||||||
@ -119,15 +117,15 @@ const ContentContainer = styled.div`
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
`
|
`
|
||||||
|
|
||||||
const HeaderContainer = styled.div`
|
// const HeaderContainer = styled.div`
|
||||||
display: flex;
|
// display: flex;
|
||||||
flex-direction: row;
|
// flex-direction: row;
|
||||||
justify-content: center;
|
// justify-content: center;
|
||||||
align-items: center;
|
// align-items: center;
|
||||||
height: 60px;
|
// height: 60px;
|
||||||
width: 100%;
|
// width: 100%;
|
||||||
gap: 10px;
|
// gap: 10px;
|
||||||
`
|
// `
|
||||||
|
|
||||||
const MainContainer = styled.div`
|
const MainContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -2,18 +2,22 @@ import i18n from '@renderer/i18n'
|
|||||||
import { CherryStoreType } from '@renderer/types/cherryStore'
|
import { CherryStoreType } from '@renderer/types/cherryStore'
|
||||||
import { lazy } from 'react'
|
import { lazy } from 'react'
|
||||||
|
|
||||||
export const discoverRouters = [
|
export const ROUTERS = [
|
||||||
{
|
{
|
||||||
id: CherryStoreType.ASSISTANT,
|
id: CherryStoreType.ASSISTANT,
|
||||||
title: i18n.t('assistants.title'),
|
title: i18n.t('assistants.title'),
|
||||||
path: 'assistant',
|
path: 'assistant',
|
||||||
component: lazy(() => import('./pages/agents/AgentsPage'))
|
component: lazy(() => import('./pages/agents/AgentsPage')),
|
||||||
|
hasSidebar: false, // 目前都没有侧边栏
|
||||||
|
items: [{ id: 'all', name: `All ${i18n.t('assistants.title')}` }] // 预设 "All" 子分类
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: CherryStoreType.MINI_APP,
|
id: CherryStoreType.MINI_APP,
|
||||||
title: i18n.t('minapp.title'),
|
title: i18n.t('minapp.title'),
|
||||||
path: 'mini-app',
|
path: 'mini-app',
|
||||||
component: lazy(() => import('./pages/minapps/MinAppsPage'))
|
component: lazy(() => import('./pages/minapps/MinAppsPage')),
|
||||||
|
hasSidebar: false, // 目前都没有侧边栏
|
||||||
|
items: [{ id: 'all', name: `All ${i18n.t('minapp.title')}` }] // 预设 "All" 子分类
|
||||||
}
|
}
|
||||||
// {
|
// {
|
||||||
// id: CherryStoreType.TRANSLATE,
|
// id: CherryStoreType.TRANSLATE,
|
||||||
@ -43,20 +47,10 @@ export const discoverRouters = [
|
|||||||
// }
|
// }
|
||||||
]
|
]
|
||||||
|
|
||||||
// 静态注册表 - 避免每次渲染都重新生成
|
export const ROUTERS_ENTRIES = ROUTERS.reduce(
|
||||||
export interface InternalCategory {
|
(acc, { id, ...rest }) => {
|
||||||
id: string
|
acc[id] = rest
|
||||||
title: string
|
return acc
|
||||||
path: string
|
},
|
||||||
hasSidebar?: boolean
|
{} as Record<(typeof ROUTERS)[number]['id'], Omit<(typeof ROUTERS)[number], 'id'>>
|
||||||
items: Array<{ id: string; name: string; count?: number }>
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// 预生成的分类注册表
|
|
||||||
export const CATEGORY_REGISTRY: InternalCategory[] = discoverRouters.map((router) => ({
|
|
||||||
id: router.id,
|
|
||||||
title: router.title,
|
|
||||||
path: router.path,
|
|
||||||
hasSidebar: false, // 目前都没有侧边栏
|
|
||||||
items: [{ id: 'all', name: `All ${router.title}` }] // 预设 "All" 子分类
|
|
||||||
}))
|
|
||||||
|
|||||||
7
src/renderer/src/pages/discover/type.ts
Normal file
7
src/renderer/src/pages/discover/type.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface InternalCategory {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
path: string
|
||||||
|
hasSidebar?: boolean
|
||||||
|
items: Array<{ id: string; name: string; count?: number }>
|
||||||
|
}
|
||||||
@ -1,13 +1,13 @@
|
|||||||
export enum CherryStoreType {
|
export enum CherryStoreType {
|
||||||
ASSISTANT = 'Assistant',
|
ASSISTANT = 'Assistant',
|
||||||
MINI_APP = 'Mini-App',
|
MINI_APP = 'Mini-App'
|
||||||
KNOWLEDGE = 'Knowledge',
|
// KNOWLEDGE = 'Knowledge',
|
||||||
MCP_SERVER = 'MCP-Server',
|
// MCP_SERVER = 'MCP-Server',
|
||||||
MODEL_PROVIDER = 'Model-Provider',
|
// MODEL_PROVIDER = 'Model-Provider',
|
||||||
AGENT = 'Agent',
|
// AGENT = 'Agent',
|
||||||
TRANSLATE = 'Translate',
|
// TRANSLATE = 'Translate',
|
||||||
PAINTINGS = 'Paintings',
|
// PAINTINGS = 'Paintings',
|
||||||
FILES = 'Files'
|
// FILES = 'Files'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubCategoryItem {
|
export interface SubCategoryItem {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { cn } from '@renderer/utils'
|
import { cn } from '@renderer/utils'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
interface Tab {
|
interface Tab {
|
||||||
id: string
|
id: string
|
||||||
@ -13,27 +13,37 @@ interface TabsProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
onTabChange?: (tabId: string) => void
|
onTabChange?: (tabId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tabs = ({
|
// 提取常用的性能优化类
|
||||||
|
const PERFORMANCE_CLASSES = 'will-change-transform [backface-visibility:hidden] [transform-style:preserve-3d]'
|
||||||
|
|
||||||
|
const TabsComponent = ({
|
||||||
ref,
|
ref,
|
||||||
className,
|
className,
|
||||||
tabs,
|
tabs,
|
||||||
activeTab: _,
|
activeTab,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
...props
|
...props
|
||||||
}: TabsProps & { ref?: React.RefObject<HTMLDivElement | null> }) => {
|
}: TabsProps & { ref?: React.RefObject<HTMLDivElement | null> }) => {
|
||||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
|
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
|
||||||
const [activeIndex, setActiveIndex] = useState(0)
|
const [hoverStyle, setHoverStyle] = useState({ transform: 'translate3d(0px, 0px, 0px)', width: '0px' })
|
||||||
const [hoverStyle, setHoverStyle] = useState({})
|
const [activeStyle, setActiveStyle] = useState({ transform: 'translate3d(0px, 0px, 0px)', width: '0px' })
|
||||||
const [activeStyle, setActiveStyle] = useState({ left: '0px', width: '0px' })
|
|
||||||
const tabRefs = useRef<(HTMLDivElement | null)[]>([])
|
const tabRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||||
|
|
||||||
|
const activeIndex = useMemo(() => {
|
||||||
|
if (activeTab) {
|
||||||
|
const index = tabs.findIndex((tab) => tab.id === activeTab)
|
||||||
|
return index !== -1 ? index : 0
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}, [activeTab, tabs])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hoveredIndex !== null) {
|
if (hoveredIndex !== null) {
|
||||||
const hoveredElement = tabRefs.current[hoveredIndex]
|
const hoveredElement = tabRefs.current[hoveredIndex]
|
||||||
if (hoveredElement) {
|
if (hoveredElement) {
|
||||||
const { offsetLeft, offsetWidth } = hoveredElement
|
const { offsetLeft, offsetWidth } = hoveredElement
|
||||||
setHoverStyle({
|
setHoverStyle({
|
||||||
left: `${offsetLeft}px`,
|
transform: `translate3d(${offsetLeft}px, 0px, 0px)`,
|
||||||
width: `${offsetWidth}px`
|
width: `${offsetWidth}px`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -41,45 +51,48 @@ const Tabs = ({
|
|||||||
}, [hoveredIndex])
|
}, [hoveredIndex])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
const activeElement = tabRefs.current[activeIndex]
|
const activeElement = tabRefs.current[activeIndex]
|
||||||
if (activeElement) {
|
if (activeElement) {
|
||||||
const { offsetLeft, offsetWidth } = activeElement
|
const { offsetLeft, offsetWidth } = activeElement
|
||||||
setActiveStyle({
|
setActiveStyle({
|
||||||
left: `${offsetLeft}px`,
|
transform: `translate3d(${offsetLeft}px, 0px, 0px)`,
|
||||||
width: `${offsetWidth}px`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [activeIndex])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const firstElement = tabRefs.current[0]
|
|
||||||
if (firstElement) {
|
|
||||||
const { offsetLeft, offsetWidth } = firstElement
|
|
||||||
setActiveStyle({
|
|
||||||
left: `${offsetLeft}px`,
|
|
||||||
width: `${offsetWidth}px`
|
width: `${offsetWidth}px`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [])
|
}, [activeIndex]) // 使用 translate3d 强制启用硬件加速
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={cn('relative', className)} {...props}>
|
<div ref={ref} className={cn('relative', className)} {...props}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* Hover Highlight */}
|
{/* Hover Highlight */}
|
||||||
<div
|
<div
|
||||||
className="absolute flex h-[30px] items-center rounded-[6px] bg-[#0e0f1114] transition-all duration-300 ease-out dark:bg-[#ffffff1a]"
|
className={cn(
|
||||||
|
'absolute flex h-[30px] items-center rounded-[6px]',
|
||||||
|
'bg-[#0e0f1114] dark:bg-[#ffffff1a]',
|
||||||
|
'transition-all duration-300 ease-out',
|
||||||
|
PERFORMANCE_CLASSES,
|
||||||
|
hoveredIndex !== null ? 'opacity-100' : 'opacity-0'
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
...hoverStyle,
|
transform: hoverStyle.transform,
|
||||||
opacity: hoveredIndex !== null ? 1 : 0
|
width: hoverStyle.width
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Active Indicator */}
|
{/* Active Indicator */}
|
||||||
<div
|
<div
|
||||||
className="absolute bottom-[-6px] h-[2px] bg-[#0e0f11] transition-all duration-300 ease-out dark:bg-white"
|
className={cn(
|
||||||
style={activeStyle}
|
'absolute bottom-[-6px] h-[2px]',
|
||||||
|
'bg-[#0e0f11] dark:bg-white',
|
||||||
|
'transition-all duration-300 ease-out',
|
||||||
|
PERFORMANCE_CLASSES
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
transform: activeStyle.transform,
|
||||||
|
width: activeStyle.width
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
@ -97,7 +110,6 @@ const Tabs = ({
|
|||||||
onMouseEnter={() => setHoveredIndex(index)}
|
onMouseEnter={() => setHoveredIndex(index)}
|
||||||
onMouseLeave={() => setHoveredIndex(null)}
|
onMouseLeave={() => setHoveredIndex(null)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveIndex(index)
|
|
||||||
onTabChange?.(tab.id)
|
onTabChange?.(tab.id)
|
||||||
}}>
|
}}>
|
||||||
<div className="flex h-full items-center justify-center text-sm leading-5 font-medium whitespace-nowrap">
|
<div className="flex h-full items-center justify-center text-sm leading-5 font-medium whitespace-nowrap">
|
||||||
@ -110,6 +122,9 @@ const Tabs = ({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Tabs = React.memo(TabsComponent)
|
||||||
|
|
||||||
Tabs.displayName = 'Tabs'
|
Tabs.displayName = 'Tabs'
|
||||||
|
|
||||||
export { Tabs }
|
export { Tabs }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user