From 4317f4b672bad9202b6cc787b615afb4c78f4a0d Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Sun, 15 Jun 2025 14:10:36 +0800 Subject: [PATCH] feat: use tabs wip wip wip wip --- src/renderer/src/App.tsx | 5 +- src/renderer/src/Routes.tsx | 108 ++------ src/renderer/src/assets/styles/color.scss | 26 -- src/renderer/src/assets/styles/index.scss | 1 + src/renderer/src/assets/styles/variables.scss | 18 ++ .../src/components/Tabs/TabsContainer.tsx | 237 ++++++++++++++++++ src/renderer/src/components/app/Navbar.tsx | 37 +-- src/renderer/src/context/ThemeProvider.tsx | 1 - .../src/handler/NavigationHandler.tsx | 19 +- src/renderer/src/hooks/useAppInit.ts | 13 - src/renderer/src/hooks/useTabs.ts | 64 +++++ src/renderer/src/i18n/locales/en-us.json | 12 + src/renderer/src/i18n/locales/ja-jp.json | 12 + src/renderer/src/i18n/locales/ru-ru.json | 12 + src/renderer/src/i18n/locales/zh-cn.json | 12 + src/renderer/src/i18n/locales/zh-tw.json | 12 + src/renderer/src/pages/apps/App.tsx | 8 - src/renderer/src/pages/home/Chat.tsx | 2 +- src/renderer/src/pages/home/ChatNavbar.tsx | 81 ++---- .../pages/home/MainSidebar/MainSidebar.tsx | 3 +- .../home/MainSidebar/MainSidebarStyles.tsx | 5 +- .../src/pages/launchpad/LaunchpadPage.tsx | 118 +++++++++ .../src/pages/settings/SettingsPage.tsx | 4 - src/renderer/src/store/index.ts | 4 +- src/renderer/src/store/tabs.ts | 57 +++++ 25 files changed, 619 insertions(+), 252 deletions(-) create mode 100644 src/renderer/src/assets/styles/variables.scss create mode 100644 src/renderer/src/components/Tabs/TabsContainer.tsx create mode 100644 src/renderer/src/hooks/useTabs.ts create mode 100644 src/renderer/src/pages/launchpad/LaunchpadPage.tsx create mode 100644 src/renderer/src/store/tabs.ts diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 94d3a564be..85171e4edb 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -5,6 +5,7 @@ import { Provider } from 'react-redux' import { HashRouter } from 'react-router-dom' import { PersistGate } from 'redux-persist/integration/react' +import TabsContainer from './components/Tabs/TabsContainer' import TopViewContainer from './components/TopView' import AntdProvider from './context/AntdProvider' import { CodeStyleProvider } from './context/CodeStyleProvider' @@ -26,7 +27,9 @@ function App(): React.ReactElement { - + + + diff --git a/src/renderer/src/Routes.tsx b/src/renderer/src/Routes.tsx index 7990d4b7c8..489ac8c98f 100644 --- a/src/renderer/src/Routes.tsx +++ b/src/renderer/src/Routes.tsx @@ -1,111 +1,31 @@ -import { IpcChannel } from '@shared/IpcChannel' -import { AnimatePresence, easeInOut, motion } from 'framer-motion' -import { useEffect } from 'react' -import { Route, Routes, useLocation, useNavigate } from 'react-router-dom' -import styled from 'styled-components' +import { Route, Routes } from 'react-router-dom' import AgentsPage from './pages/agents/AgentsPage' import AppsPage from './pages/apps/AppsPage' import FilesPage from './pages/files/FilesPage' import HomePage from './pages/home/HomePage' import KnowledgePage from './pages/knowledge/KnowledgePage' +import LaunchpadPage from './pages/launchpad/LaunchpadPage' import McpServersPage from './pages/mcp-servers' import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage' import SettingsPage from './pages/settings/SettingsPage' import TranslatePage from './pages/translate/TranslatePage' -const WILDCARD_ROUTES = ['/settings', '/paintings', '/mcp-servers'] - const RouteContainer = () => { - const navigate = useNavigate() - const location = useLocation() - const isHomePage = location.pathname === '/' - - // 获取当前路径的主路由部分 - const mainPath = WILDCARD_ROUTES.find((route) => location.pathname.startsWith(route)) - - // 使用主路由作为 key,这样同一主路由下的切换不会触发动画 - const animationKey = mainPath || location.pathname - - useEffect(() => { - window.api.navigation.url(location.pathname) - }, [location.pathname]) - - useEffect(() => { - window.electron.ipcRenderer.on(IpcChannel.Navigation_Close, () => navigate('/')) - }, [navigate]) - return ( - - - - {!isHomePage && ( - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - )} - - + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + ) } -const Container = styled.div` - position: relative; - width: 100%; - height: 100%; - min-width: 0; - overflow: hidden; -` - -const HomePageWrapper = styled(HomePage)` - display: flex; - width: 100%; - height: 100%; -` - -const PageContainer = styled(motion.div)` - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: var(--color-base); - display: flex; - width: 100%; - height: 100%; - z-index: 10; - will-change: transform; - backface-visibility: hidden; - transform-style: preserve-3d; - perspective: 1000px; - -webkit-font-smoothing: subpixel-antialiased; -` - -const pageTransition = { - type: 'tween' as const, - duration: 0.25, - ease: easeInOut -} - -const pageVariants = { - initial: { translateY: '100%' }, - animate: { translateY: '0%' }, - exit: { translateY: '100%' } -} - export default RouteContainer diff --git a/src/renderer/src/assets/styles/color.scss b/src/renderer/src/assets/styles/color.scss index 2e90b5a1aa..e4bb90fea3 100644 --- a/src/renderer/src/assets/styles/color.scss +++ b/src/renderer/src/assets/styles/color.scss @@ -57,23 +57,10 @@ --navbar-background-win: rgba(20, 20, 20, 0.75); --navbar-background: #1f1f1f; - --navbar-height: 42px; - --sidebar-width: 50px; - --status-bar-height: 40px; - --input-bar-height: 100px; - - --assistants-width: 275px; - --topic-list-width: 275px; - --settings-width: 250px; - --scrollbar-width: 5px; - --chat-background: transparent; --chat-background-user: rgba(255, 255, 255, 0.08); --chat-background-assistant: transparent; --chat-text-user: var(--color-black); - - --list-item-border-radius: 8px; - --border-width: 0.5px; } [theme-mode='light'] { @@ -139,17 +126,4 @@ --chat-background-user: rgba(0, 0, 0, 0.045); --chat-background-assistant: transparent; --chat-text-user: var(--color-text); - - --border-width: 0.5px; -} - -[transparent-window='true'] { - &[theme-mode='light'] { - --color-list-item: rgba(255, 255, 255, 0.8); - --color-list-item-hover: rgba(255, 255, 255, 0.4); - } - &[theme-mode='dark'] { - --color-list-item: rgba(255, 255, 255, 0.1); - --color-list-item-hover: rgba(255, 255, 255, 0.05); - } } diff --git a/src/renderer/src/assets/styles/index.scss b/src/renderer/src/assets/styles/index.scss index bd02d9dba2..ce7d70e9cb 100644 --- a/src/renderer/src/assets/styles/index.scss +++ b/src/renderer/src/assets/styles/index.scss @@ -1,3 +1,4 @@ +@use './variables.scss'; @use './color.scss'; @use './font.scss'; @use './markdown.scss'; diff --git a/src/renderer/src/assets/styles/variables.scss b/src/renderer/src/assets/styles/variables.scss new file mode 100644 index 0000000000..371590ff8a --- /dev/null +++ b/src/renderer/src/assets/styles/variables.scss @@ -0,0 +1,18 @@ +:root { + --navbar-height: 42px; + --sidebar-width: 50px; + --status-bar-height: 40px; + --input-bar-height: 100px; + + --assistants-width: 275px; + --topic-list-width: 275px; + --settings-width: 250px; + --scrollbar-width: 5px; + + --list-item-border-radius: 15px; + --border-width: 0.5px; + + --main-height: calc(100vh - var(--navbar-height) - 6px); + + --border-width: 0.5px; +} diff --git a/src/renderer/src/components/Tabs/TabsContainer.tsx b/src/renderer/src/components/Tabs/TabsContainer.tsx new file mode 100644 index 0000000000..6f25e340d8 --- /dev/null +++ b/src/renderer/src/components/Tabs/TabsContainer.tsx @@ -0,0 +1,237 @@ +import { PlusOutlined } from '@ant-design/icons' +import { isMac } from '@renderer/config/constant' +import { useFullscreen } from '@renderer/hooks/useFullscreen' +import { useAppDispatch, useAppSelector } from '@renderer/store' +import type { Tab } from '@renderer/store/tabs' +import { addTab, removeTab, setActiveTab } from '@renderer/store/tabs' +import { + FileSearch, + Folder, + Home, + Languages, + LayoutGrid, + Palette, + Settings, + Sparkle, + SquareTerminal, + X +} from 'lucide-react' +import { useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { useLocation, useNavigate } from 'react-router-dom' +import styled from 'styled-components' + +interface TabsContainerProps { + children: React.ReactNode +} + +const getTabIcon = (tabId: string): React.ReactNode | undefined => { + switch (tabId) { + case 'home': + return + case 'agents': + return + case 'translate': + return + case 'paintings': + return + case 'apps': + return + case 'knowledge': + return + case 'mcp': + return + case 'files': + return + case 'settings': + return + default: + return null + } +} + +const TabsContainer: React.FC = ({ children }) => { + const { t } = useTranslation() + const location = useLocation() + const navigate = useNavigate() + const dispatch = useAppDispatch() + const tabs = useAppSelector((state) => state.tabs.tabs) + const activeTabId = useAppSelector((state) => state.tabs.activeTabId) + const isFullscreen = useFullscreen() + + const getTabId = (path: string): string => { + if (path === '/') return 'home' + const segments = path.split('/') + return segments[1] // 获取第一个路径段作为 id + } + + const shouldCreateTab = (path: string) => { + if (path === '/') return false + return !tabs.some((tab) => tab.id === getTabId(path)) + } + + useEffect(() => { + const tabId = getTabId(location.pathname) + const currentTab = tabs.find((tab) => tab.id === tabId) + + if (!currentTab && shouldCreateTab(location.pathname)) { + dispatch( + addTab({ + id: tabId, + path: location.pathname + }) + ) + } else if (currentTab) { + dispatch(setActiveTab(currentTab.id)) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dispatch, location.pathname]) + + const closeTab = (tabId: string) => { + const tabToClose = tabs.find((tab) => tab.id === tabId) + if (!tabToClose) return + + if (tabs.length === 1) return + + if (tabId === activeTabId) { + const remainingTabs = tabs.filter((tab) => tab.id !== tabId) + const lastTab = remainingTabs[remainingTabs.length - 1] + navigate(lastTab.path) + } + + dispatch(removeTab(tabId)) + } + + const handleAddTab = () => { + navigate('/launchpad') + } + + return ( + + + {tabs.map((tab) => ( + navigate(tab.path)}> + + {tab.id && {getTabIcon(tab.id)}} + {t(`title.${tab.id}`)} + + {tab.id !== 'home' && ( + { + e.stopPropagation() + closeTab(tab.id) + }}> + + + )} + + ))} + + + + + {children} + + ) +} + +const Container = styled.div` + display: flex; + flex-direction: column; + height: 100%; +` + +const TabsBar = styled.div<{ $isFullscreen: boolean }>` + display: flex; + flex-direction: row; + align-items: center; + gap: 5px; + padding-left: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? '80px' : '8px')}; + -webkit-app-region: drag; + height: var(--navbar-height); +` + +const Tab = styled.div<{ active?: boolean }>` + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 10px; + background: ${(props) => (props.active ? 'var(--color-background)' : 'transparent')}; + border-radius: 8px; + cursor: pointer; + user-select: none; + -webkit-app-region: none; + min-width: 100px; + transition: background 0.2s; + .close-button { + opacity: 0; + transition: opacity 0.2s; + margin-right: -2px; + } + + &:hover { + background: ${(props) => (props.active ? 'var(--color-background)' : 'var(--color-background-soft)')}; + .close-button { + opacity: 1; + } + } +` + +const TabHeader = styled.div` + display: flex; + align-items: center; + gap: 6px; +` + +const TabIcon = styled.span` + display: flex; + align-items: center; + margin-right: 6px; + color: var(--color-text-2); +` + +const TabTitle = styled.span` + color: var(--color-text); + font-size: 13px; + margin-right: 8px; + display: flex; + align-items: center; +` + +const CloseButton = styled.span` + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; +` + +const AddTabButton = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + cursor: pointer; + color: var(--color-text); + -webkit-app-region: none; + + &:hover { + background: var(--color-background-soft); + border-radius: 8px; + } +` + +const TabContent = styled.div` + display: flex; + flex: 1; + overflow: hidden; + width: calc(100vw - 12px); + margin: 6px; + margin-top: 0; + border-radius: 8px; + overflow: hidden; +` + +export default TabsContainer diff --git a/src/renderer/src/components/app/Navbar.tsx b/src/renderer/src/components/app/Navbar.tsx index 74af3a2b3a..bf223d6146 100644 --- a/src/renderer/src/components/app/Navbar.tsx +++ b/src/renderer/src/components/app/Navbar.tsx @@ -1,10 +1,8 @@ -import { isLinux, isMac, isWindows } from '@renderer/config/constant' +import { isLinux, isWindows } from '@renderer/config/constant' import { useFullscreen } from '@renderer/hooks/useFullscreen' import { Button } from 'antd' -import { CircleArrowLeft, X } from 'lucide-react' import type { FC, PropsWithChildren } from 'react' import type { HTMLAttributes } from 'react' -import { useNavigate } from 'react-router-dom' import styled, { keyframes } from 'styled-components' type Props = PropsWithChildren & HTMLAttributes @@ -31,41 +29,11 @@ export const NavbarMain: FC = ({ children, ...props }) => { return ( - {children} - ) } -const MacCloseIcon = () => { - const navigate = useNavigate() - - if (!isMac) { - return null - } - - return } onClick={() => navigate('/')} className="nodrag" /> -} - -const CloseIcon = () => { - const navigate = useNavigate() - - if (isMac) { - return null - } - - return ( -