Compare commits

...

2 Commits

Author SHA1 Message Date
Teo
8c66f0e41a fix: 修复选中问题 2025-01-27 12:30:35 +08:00
Teo
fd1629e004 refactor(settings): 重构设置页面,改为弹框 2025-01-27 12:30:35 +08:00
7 changed files with 255 additions and 162 deletions

View File

@@ -16,7 +16,6 @@ import FilesPage from './pages/files/FilesPage'
import HomePage from './pages/home/HomePage'
import KnowledgePage from './pages/knowledge/KnowledgePage'
import PaintingsPage from './pages/paintings/PaintingsPage'
import SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage'
function App(): JSX.Element {
@@ -37,7 +36,6 @@ function App(): JSX.Element {
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>
</HashRouter>
</TopViewContainer>

View File

@@ -0,0 +1,66 @@
import SettingsPage, { SettingsTab } from '@renderer/pages/settings/SettingsPage'
import { Modal } from 'antd'
import { FC, useState } from 'react'
import styled, { createGlobalStyle } from 'styled-components'
interface Props {
actionButton?: React.ReactNode
activeTab?: SettingsTab
}
const SettingsPopup: FC<Props> = (props) => {
const [open, setOpen] = useState(false)
const [activeTab, setActiveTab] = useState<SettingsTab | undefined>(props.activeTab)
const onOpen = () => {
if (props.activeTab) {
setActiveTab(props.activeTab)
}
setOpen(true)
}
const onCancel = () => {
setOpen(false)
}
return (
<>
<div onClick={onOpen}>{props.actionButton}</div>
<GlobalStyle />
<StyledModal
transitionName="ant-move-down"
width="80vw"
title={null}
open={open}
onCancel={onCancel}
footer={null}>
<SettingsPage activeTab={activeTab} onTabChange={setActiveTab} />
</StyledModal>
</>
)
}
const GlobalStyle = createGlobalStyle`
.ant-modal-mask {
backdrop-filter: blur(10px);
background-color: transparent !important;
}
`
const StyledModal = styled(Modal)`
min-width: 900px;
max-width: 1300px;
padding-bottom: 0;
.ant-modal-content {
padding: 0;
overflow: hidden;
border-radius: 12px;
}
.ant-modal-close {
top: 4px;
right: 4px;
}
`
export default SettingsPopup

View File

@@ -1,10 +1,10 @@
import { FileSearchOutlined, FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons'
import { isMac } from '@renderer/config/constant'
import { isLocalAi, UserAvatar } from '@renderer/config/env'
import { UserAvatar } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import type { MenuProps } from 'antd'
import { Tooltip } from 'antd'
@@ -18,14 +18,13 @@ import styled from 'styled-components'
import DragableList from '../DragableList'
import MinAppIcon from '../Icons/MinAppIcon'
import MinApp from '../MinApp'
import SettingsPopup from '../Popups/SettingsPopup'
import UserPopup from '../Popups/UserPopup'
const Sidebar: FC = () => {
const { pathname } = useLocation()
const avatar = useAvatar()
const { minappShow } = useRuntime()
const { t } = useTranslation()
const navigate = useNavigate()
const { windowStyle, sidebarIcons } = useSettings()
const { theme, toggleTheme } = useTheme()
const { pinned } = useMinapps()
@@ -37,11 +36,6 @@ const Sidebar: FC = () => {
const showPinnedApps = pinned.length > 0 && sidebarIcons.visible.includes('minapp')
const to = async (path: string) => {
await modelGenerating()
navigate(path)
}
return (
<Container
id="app-sidebar"
@@ -73,13 +67,15 @@ const Sidebar: FC = () => {
)}
</Icon>
</Tooltip>
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}>
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}>
<i className="iconfont icon-setting" />
</Icon>
</StyledLink>
</Tooltip>
<SettingsPopup
actionButton={
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
<Icon>
<i className="iconfont icon-setting" />
</Icon>
</Tooltip>
}
/>
</Menus>
</Container>
)

View File

@@ -164,7 +164,7 @@ const ProviderListContainer = styled.div`
display: flex;
flex-direction: column;
min-width: calc(var(--settings-width) + 10px);
height: calc(100vh - var(--navbar-height));
height: calc(75vh - var(--navbar-height));
border-right: 0.5px solid var(--color-border);
`
@@ -180,19 +180,18 @@ const ProviderListItem = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding: 5px 10px;
padding: 8px 8px;
width: 100%;
cursor: grab;
border-radius: var(--list-item-border-radius);
border-radius: 8px;
font-size: 14px;
transition: all 0.2s ease-in-out;
border: 0.5px solid transparent;
&:hover {
background: var(--color-background-soft);
background: var(--color-primary-mute);
}
&.active {
background: var(--color-background-soft);
border: 0.5px solid var(--color-border);
background: var(--color-primary-mute);
color: var(--color-primary);
font-weight: bold !important;
}
`

View File

@@ -7,11 +7,10 @@ import {
SaveOutlined,
SettingOutlined
} from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { isLocalAi } from '@renderer/config/env'
import { FC } from 'react'
import { Breadcrumb, Button, Menu } from 'antd'
import { FC, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Link, Route, Routes, useLocation } from 'react-router-dom'
import styled from 'styled-components'
import AboutSettings from './AboutSettings'
@@ -23,94 +22,145 @@ import ProvidersList from './ProviderSettings'
import QuickAssistantSettings from './QuickAssistantSettings'
import ShortcutSettings from './ShortcutSettings'
const SettingsPage: FC = () => {
const { pathname } = useLocation()
const { t } = useTranslation()
export type SettingsTab =
| 'provider'
| 'model'
| 'general'
| 'display'
| 'data'
| 'quickAssistant'
| 'shortcut'
| 'about'
const isRoute = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
return (
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('settings.title')}</NavbarCenter>
</Navbar>
<ContentContainer id="content-container">
<SettingMenus>
{!isLocalAi && (
<>
<MenuItemLink to="/settings/provider">
<MenuItem className={isRoute('/settings/provider')}>
<CloudOutlined />
{t('settings.provider.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/model">
<MenuItem className={isRoute('/settings/model')}>
<i className="iconfont icon-ai-model" />
{t('settings.model')}
</MenuItem>
</MenuItemLink>
</>
)}
<MenuItemLink to="/settings/general">
<MenuItem className={isRoute('/settings/general')}>
<SettingOutlined />
{t('settings.general')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/display">
<MenuItem className={isRoute('/settings/display')}>
<LayoutOutlined />
{t('settings.display.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/shortcut">
<MenuItem className={isRoute('/settings/shortcut')}>
<MacCommandOutlined />
{t('settings.shortcuts.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/quickAssistant">
<MenuItem className={isRoute('/settings/quickAssistant')}>
<RocketOutlined />
{t('settings.quickAssistant.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/data">
<MenuItem className={isRoute('/settings/data')}>
<SaveOutlined />
{t('settings.data.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/about">
<MenuItem className={isRoute('/settings/about')}>
<InfoCircleOutlined />
{t('settings.about')}
</MenuItem>
</MenuItemLink>
</SettingMenus>
<SettingContent>
<Routes>
<Route path="provider" element={<ProvidersList />} />
<Route path="model" element={<ModelSettings />} />
<Route path="general/*" element={<GeneralSettings />} />
<Route path="display" element={<DisplaySettings />} />
<Route path="data/*" element={<DataSettings />} />
<Route path="quickAssistant" element={<QuickAssistantSettings />} />
<Route path="shortcut" element={<ShortcutSettings />} />
<Route path="about" element={<AboutSettings />} />
</Routes>
</SettingContent>
</ContentContainer>
</Container>
)
interface Props {
activeTab?: SettingsTab
onTabChange?: (tab: SettingsTab) => void
}
interface MenuItem {
label: string
icon: React.ReactNode
key: string
enabled: boolean
}
const Container = styled.div`
display: flex;
flex-direction: column;
flex: 1;
`
const SettingsPage: FC<Props> = (props) => {
const { t } = useTranslation()
const [collapsed, setCollapsed] = useState(false)
const activeTab = props.activeTab || 'provider'
const settingMenus = useMemo<MenuItem[]>(
() => [
{
label: t('settings.provider.title'),
icon: <CloudOutlined />,
key: 'provider',
enabled: !isLocalAi
},
{
label: t('settings.model'),
icon: <i className="iconfont icon-ai-model" />,
key: 'model',
enabled: !isLocalAi
},
{
label: t('settings.general'),
icon: <SettingOutlined />,
key: 'general',
enabled: true
},
{
label: t('settings.display.title'),
icon: <LayoutOutlined />,
key: 'display',
enabled: true
},
{
label: t('settings.shortcuts.title'),
icon: <MacCommandOutlined />,
key: 'shortcut',
enabled: true
},
{
label: t('settings.quickAssistant.title'),
icon: <RocketOutlined />,
key: 'quickAssistant',
enabled: true
},
{
label: t('settings.data.title'),
icon: <SaveOutlined />,
key: 'data',
enabled: true
},
{
label: t('settings.about'),
icon: <InfoCircleOutlined />,
key: 'about',
enabled: true
}
],
[t]
)
const breadcrumbItems = useMemo(() => {
return [
{
title: t('settings.title')
},
{
title: settingMenus.find((item) => item.key === activeTab)?.label
}
]
}, [t, activeTab, settingMenus])
const renderContent = () => {
switch (activeTab) {
case 'provider':
return <ProvidersList />
case 'model':
return <ModelSettings />
case 'general':
return <GeneralSettings />
case 'display':
return <DisplaySettings />
case 'data':
return <DataSettings />
case 'quickAssistant':
return <QuickAssistantSettings />
case 'shortcut':
return <ShortcutSettings />
case 'about':
return <AboutSettings />
default:
return <GeneralSettings />
}
}
return (
<ContentContainer>
<MenuContainer $isCollapsed={collapsed}>
<Title>{t('settings.title')}</Title>
<Menu
mode="inline"
onClick={(e) => props.onTabChange?.(e.key as SettingsTab)}
selectedKeys={[activeTab]}
items={settingMenus.filter((item) => item.enabled)}
inlineCollapsed={collapsed}
/>
</MenuContainer>
<SettingContent>
<SettingHeader>
<CollapseButton shape="circle" type="text" onClick={() => setCollapsed(!collapsed)} $isCollapsed={collapsed}>
<i className="iconfont icon-hide-sidebar" />
</CollapseButton>
<Breadcrumb items={breadcrumbItems} />
</SettingHeader>
{renderContent()}
</SettingContent>
</ContentContainer>
)
}
const ContentContainer = styled.div`
display: flex;
@@ -118,57 +168,40 @@ const ContentContainer = styled.div`
flex-direction: row;
`
const SettingMenus = styled.ul`
display: flex;
flex-direction: column;
min-width: var(--settings-width);
border-right: 0.5px solid var(--color-border);
padding: 10px;
user-select: none;
`
const MenuItemLink = styled(Link)`
text-decoration: none;
color: var(--color-text-1);
margin-bottom: 5px;
`
const MenuItem = styled.li`
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
padding: 6px 10px;
width: 100%;
cursor: pointer;
border-radius: var(--list-item-border-radius);
font-weight: 500;
transition: all 0.2s ease-in-out;
border: 0.5px solid transparent;
.anticon {
font-size: 16px;
opacity: 0.8;
const MenuContainer = styled.div<{ $isCollapsed: boolean }>`
width: ${({ $isCollapsed }) => ($isCollapsed ? '80px' : '160px')};
background-color: var(--color-background-mute);
transition: width 0.3s ease-in-out;
position: relative;
.ant-menu-light {
background-color: var(--color-background-mute);
}
`
const CollapseButton = styled(Button)<{ $isCollapsed: boolean }>`
color: var(--color-icon);
.iconfont {
font-size: 18px;
line-height: 18px;
opacity: 0.7;
margin-left: -1px;
}
&:hover {
background: var(--color-background-soft);
}
&.active {
background: var(--color-background-soft);
border: 0.5px solid var(--color-border);
transform: rotate(${({ $isCollapsed }) => ($isCollapsed ? '180deg' : '0deg')});
}
`
const Title = styled.div`
font-size: 16px;
font-weight: 600;
padding: 16px 24px;
`
const SettingContent = styled.div`
display: flex;
height: 100%;
flex: 1;
border-right: 0.5px solid var(--color-border);
`
const SettingHeader = styled.div`
padding: 4px 8px;
border-bottom: 0.5px solid var(--color-border);
display: flex;
align-items: center;
gap: 8px;
`
export default SettingsPage

View File

@@ -7,12 +7,11 @@ export const SettingContainer = styled.div<{ theme?: ThemeMode }>`
display: flex;
flex-direction: column;
flex: 1;
height: calc(100vh - var(--navbar-height));
padding: 20px;
height: calc(75vh - var(--navbar-height));
padding: 16px;
padding-top: 15px;
overflow-y: scroll;
font-family: Ubuntu;
background: ${(props) => (props.theme === 'dark' ? 'transparent' : 'var(--color-background-soft)')};
&::-webkit-scrollbar {
display: none;

View File

@@ -1,6 +1,7 @@
import { CheckOutlined, SendOutlined, SettingOutlined, SwapOutlined, WarningOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import CopyIcon from '@renderer/components/Icons/CopyIcon'
import SettingsPopup from '@renderer/components/Popups/SettingsPopup'
import { isLocalAi } from '@renderer/config/env'
import { TranslateLanguageOptions } from '@renderer/config/translate'
import db from '@renderer/databases'
@@ -90,9 +91,10 @@ const TranslatePage: FC = () => {
if (translateModel) {
return (
<Link to="/settings/model" style={{ color: 'var(--color-text-2)' }}>
<SettingOutlined />
</Link>
<SettingsPopup
activeTab="model"
actionButton={<Button type="text" shape="circle" icon={<SettingOutlined />} />}
/>
)
}