refactor(DataSettings): Modularize settings page with dynamic menu navigation
- Split DataSettings into separate components for Markdown Export, Notion, WebDAV, and Yuque settings - Implement dynamic menu navigation with ListItem component - Improve code organization and readability - Add state management for menu selection - Enhance settings page layout and user experience
This commit is contained in:
parent
7d7f9eaa35
commit
67bb1f19f0
@ -1,364 +1,38 @@
|
||||
import {
|
||||
DeleteOutlined,
|
||||
FileSearchOutlined,
|
||||
FolderOpenOutlined,
|
||||
InfoCircleOutlined,
|
||||
SaveOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { Client } from '@notionhq/client'
|
||||
import { FileSearchOutlined, FolderOpenOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import MinApp from '@renderer/components/MinApp'
|
||||
import ListItem from '@renderer/components/ListItem'
|
||||
import BackupPopup from '@renderer/components/Popups/BackupPopup'
|
||||
import RestorePopup from '@renderer/components/Popups/RestorePopup'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useKnowledgeFiles } from '@renderer/hooks/useKnowledgeFiles'
|
||||
import { reset } from '@renderer/services/BackupService'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
import {
|
||||
setmarkdownExportPath,
|
||||
setNotionApiKey,
|
||||
setNotionAutoSplit,
|
||||
setNotionDatabaseID,
|
||||
setNotionPageNameKey,
|
||||
setNotionSplitSize,
|
||||
setYuqueRepoId,
|
||||
setYuqueToken,
|
||||
setYuqueUrl
|
||||
} from '@renderer/store/settings'
|
||||
import { AppInfo } from '@renderer/types'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
import { Button, InputNumber, Modal, Switch, Tooltip, Typography } from 'antd'
|
||||
import Input from 'antd/es/input/Input'
|
||||
import { Button, Modal, Typography } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import {
|
||||
SettingContainer,
|
||||
SettingDivider,
|
||||
SettingGroup,
|
||||
SettingHelpText,
|
||||
SettingRow,
|
||||
SettingRowTitle,
|
||||
SettingTitle
|
||||
} from '..'
|
||||
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import MarkdownExportSettings from './MarkdownExportSettings'
|
||||
import NotionSettings from './NotionSettings'
|
||||
import WebDavSettings from './WebDavSettings'
|
||||
|
||||
// 新增的 MarkdownExportSettings 组件
|
||||
const MarkdownExportSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const markdownExportPath = useSelector((state: RootState) => state.settings.markdownExportPath)
|
||||
|
||||
const handleSelectFolder = async () => {
|
||||
const path = await window.api.file.selectFolder()
|
||||
if (path) {
|
||||
dispatch(setmarkdownExportPath(path))
|
||||
}
|
||||
}
|
||||
|
||||
const handleClearPath = () => {
|
||||
dispatch(setmarkdownExportPath(null))
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.markdown_export.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.markdown_export.path')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||
<Input
|
||||
type="text"
|
||||
value={markdownExportPath || ''}
|
||||
readOnly
|
||||
style={{ width: 250 }}
|
||||
placeholder={t('settings.data.markdown_export.path_placeholder')}
|
||||
suffix={
|
||||
markdownExportPath ? (
|
||||
<DeleteOutlined onClick={handleClearPath} style={{ color: 'var(--color-error)', cursor: 'pointer' }} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<Button onClick={handleSelectFolder} icon={<FolderOpenOutlined />}>
|
||||
{t('settings.data.markdown_export.select')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.markdown_export.help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
// 新增的 NotionSettings 组件
|
||||
const NotionSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const notionApiKey = useSelector((state: RootState) => state.settings.notionApiKey)
|
||||
const notionDatabaseID = useSelector((state: RootState) => state.settings.notionDatabaseID)
|
||||
const notionPageNameKey = useSelector((state: RootState) => state.settings.notionPageNameKey)
|
||||
const notionAutoSplit = useSelector((state: RootState) => state.settings.notionAutoSplit)
|
||||
const notionSplitSize = useSelector((state: RootState) => state.settings.notionSplitSize)
|
||||
|
||||
const handleNotionTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setNotionApiKey(e.target.value))
|
||||
}
|
||||
|
||||
const handleNotionDatabaseIdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setNotionDatabaseID(e.target.value))
|
||||
}
|
||||
|
||||
const handleNotionPageNameKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setNotionPageNameKey(e.target.value))
|
||||
}
|
||||
|
||||
const handleNotionConnectionCheck = () => {
|
||||
if (notionApiKey === null) {
|
||||
window.message.error(t('settings.data.notion.check.empty_api_key'))
|
||||
return
|
||||
}
|
||||
if (notionDatabaseID === null) {
|
||||
window.message.error(t('settings.data.notion.check.empty_database_id'))
|
||||
return
|
||||
}
|
||||
const notion = new Client({ auth: notionApiKey })
|
||||
notion.databases
|
||||
.retrieve({
|
||||
database_id: notionDatabaseID
|
||||
})
|
||||
.then((result) => {
|
||||
if (result) {
|
||||
window.message.success(t('settings.data.notion.check.success'))
|
||||
} else {
|
||||
window.message.error(t('settings.data.notion.check.fail'))
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
window.message.error(t('settings.data.notion.check.error'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleNotionTitleClick = () => {
|
||||
MinApp.start({
|
||||
id: 'notion-help',
|
||||
name: 'Notion Help',
|
||||
url: 'https://docs.cherry-ai.com/advanced-basic/notion'
|
||||
})
|
||||
}
|
||||
|
||||
const handleNotionAutoSplitChange = (checked: boolean) => {
|
||||
dispatch(setNotionAutoSplit(checked))
|
||||
}
|
||||
|
||||
const handleNotionSplitSizeChange = (value: number | null) => {
|
||||
if (value !== null) {
|
||||
dispatch(setNotionSplitSize(value))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle style={{ justifyContent: 'flex-start', gap: 10 }}>
|
||||
{t('settings.data.notion.title')}
|
||||
<Tooltip title={t('settings.data.notion.help')} placement="right">
|
||||
<InfoCircleOutlined
|
||||
style={{ color: 'var(--color-text-2)', cursor: 'pointer' }}
|
||||
onClick={handleNotionTitleClick}
|
||||
/>
|
||||
</Tooltip>
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.notion.database_id')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||
<Input
|
||||
type="text"
|
||||
value={notionDatabaseID || ''}
|
||||
onChange={handleNotionDatabaseIdChange}
|
||||
onBlur={handleNotionDatabaseIdChange}
|
||||
style={{ width: 315 }}
|
||||
placeholder={t('settings.data.notion.database_id_placeholder')}
|
||||
/>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.notion.page_name_key')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||
<Input
|
||||
type="text"
|
||||
value={notionPageNameKey || ''}
|
||||
onChange={handleNotionPageNameKeyChange}
|
||||
onBlur={handleNotionPageNameKeyChange}
|
||||
style={{ width: 315 }}
|
||||
placeholder={t('settings.data.notion.page_name_key_placeholder')}
|
||||
/>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.notion.api_key')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||
<Input
|
||||
type="password"
|
||||
value={notionApiKey || ''}
|
||||
onChange={handleNotionTokenChange}
|
||||
onBlur={handleNotionTokenChange}
|
||||
style={{ width: 250 }}
|
||||
placeholder={t('settings.data.notion.api_key_placeholder')}
|
||||
/>
|
||||
<Button onClick={handleNotionConnectionCheck}>{t('settings.data.notion.check.button')}</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider /> {/* 添加分割线 */}
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
<Tooltip title={t('settings.data.notion.auto_split_tip')} placement="right">
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
{t('settings.data.notion.auto_split')}
|
||||
<InfoCircleOutlined style={{ cursor: 'pointer' }} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<Switch checked={notionAutoSplit} onChange={handleNotionAutoSplitChange} />
|
||||
</SettingRow>
|
||||
{notionAutoSplit && (
|
||||
<>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.notion.split_size')}</SettingRowTitle>
|
||||
<InputNumber
|
||||
min={30}
|
||||
max={25000}
|
||||
value={notionSplitSize}
|
||||
onChange={handleNotionSplitSizeChange}
|
||||
keyboard={true}
|
||||
controls={true}
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText style={{ marginLeft: 10 }}>{t('settings.data.notion.split_size_help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
const YuqueSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const yuqueToken = useSelector((state: RootState) => state.settings.yuqueToken)
|
||||
const yuqueUrl = useSelector((state: RootState) => state.settings.yuqueUrl)
|
||||
|
||||
const handleYuqueTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setYuqueToken(e.target.value))
|
||||
}
|
||||
|
||||
const handleYuqueRepoUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setYuqueUrl(e.target.value))
|
||||
}
|
||||
|
||||
const handleYuqueConnectionCheck = async () => {
|
||||
if (!yuqueToken) {
|
||||
window.message.error(t('settings.data.yuque.check.empty_token'))
|
||||
return
|
||||
}
|
||||
if (!yuqueUrl) {
|
||||
window.message.error(t('settings.data.yuque.check.empty_url'))
|
||||
return
|
||||
}
|
||||
|
||||
const response = await fetch('https://www.yuque.com/api/v2/hello', {
|
||||
headers: {
|
||||
'X-Auth-Token': yuqueToken
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
window.message.error(t('settings.data.yuque.check.fail'))
|
||||
return
|
||||
}
|
||||
const yuqueSlug = yuqueUrl.replace('https://www.yuque.com/', '')
|
||||
const repoIDResponse = await fetch(`https://www.yuque.com/api/v2/repos/${yuqueSlug}`, {
|
||||
headers: {
|
||||
'X-Auth-Token': yuqueToken
|
||||
}
|
||||
})
|
||||
if (!repoIDResponse.ok) {
|
||||
window.message.error(t('settings.data.yuque.check.fail'))
|
||||
return
|
||||
}
|
||||
const data = await repoIDResponse.json()
|
||||
dispatch(setYuqueRepoId(data.data.id))
|
||||
window.message.success(t('settings.data.yuque.check.success'))
|
||||
}
|
||||
|
||||
const handleYuqueHelpClick = () => {
|
||||
MinApp.start({
|
||||
id: 'yuque-help',
|
||||
name: 'Yuque Help',
|
||||
url: 'https://www.yuque.com/settings/tokens'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.yuque.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.yuque.repo_url')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||
<Input
|
||||
type="text"
|
||||
value={yuqueUrl || ''}
|
||||
onChange={handleYuqueRepoUrlChange}
|
||||
style={{ width: 315 }}
|
||||
placeholder={t('settings.data.yuque.repo_url_placeholder')}
|
||||
/>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.data.yuque.token')}
|
||||
<Tooltip title={t('settings.data.yuque.help')} placement="left">
|
||||
<InfoCircleOutlined
|
||||
style={{ color: 'var(--color-text-2)', cursor: 'pointer', marginLeft: 4 }}
|
||||
onClick={handleYuqueHelpClick}
|
||||
/>
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||
<Input
|
||||
type="password"
|
||||
value={yuqueToken || ''}
|
||||
onChange={handleYuqueTokenChange}
|
||||
style={{ width: 250 }}
|
||||
placeholder={t('settings.data.yuque.token_placeholder')}
|
||||
/>
|
||||
<Button onClick={handleYuqueConnectionCheck}>{t('settings.data.yuque.check.button')}</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
import YuqueSettings from './YuqueSettings'
|
||||
|
||||
const DataSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [appInfo, setAppInfo] = useState<AppInfo>()
|
||||
const { size, removeAllFiles } = useKnowledgeFiles()
|
||||
const { theme } = useTheme()
|
||||
const [menu, setMenu] = useState<string>('data')
|
||||
|
||||
const menuItems = [
|
||||
{ key: 'data', title: 'settings.data.data.title' },
|
||||
{ key: 'webdav', title: 'settings.data.webdav.title' },
|
||||
{ key: 'markdown_export', title: 'settings.data.markdown_export.title' },
|
||||
{ key: 'notion', title: 'settings.data.notion.title' },
|
||||
{ key: 'yuque', title: 'settings.data.yuque.title' }
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
window.api.getAppInfo().then(setAppInfo)
|
||||
@ -411,78 +85,91 @@ const DataSettings: FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingContainer theme={theme}>
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||
<HStack gap="5px" justifyContent="space-between">
|
||||
<Button onClick={BackupPopup.show} icon={<SaveOutlined />}>
|
||||
{t('settings.general.backup.button')}
|
||||
</Button>
|
||||
<Button onClick={RestorePopup.show} icon={<FolderOpenOutlined />}>
|
||||
{t('settings.general.restore.button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.reset.title')}</SettingRowTitle>
|
||||
<HStack gap="5px">
|
||||
<Button onClick={reset} danger>
|
||||
{t('settings.general.reset.button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
<SettingGroup theme={theme}>
|
||||
<WebDavSettings />
|
||||
</SettingGroup>
|
||||
<MarkdownExportSettings />
|
||||
<NotionSettings />
|
||||
<YuqueSettings />
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.data.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.app_data')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px">
|
||||
<Typography.Text style={{ color: 'var(--color-text-3)' }}>{appInfo?.appDataPath}</Typography.Text>
|
||||
<StyledIcon onClick={() => handleOpenPath(appInfo?.appDataPath)} />
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.app_logs')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px">
|
||||
<Typography.Text style={{ color: 'var(--color-text-3)' }}>{appInfo?.logsPath}</Typography.Text>
|
||||
<StyledIcon onClick={() => handleOpenPath(appInfo?.logsPath)} />
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.app_knowledge')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px">
|
||||
<Button onClick={handleRemoveAllFiles} danger>
|
||||
{t('settings.data.app_knowledge.button.delete')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.clear_cache.title')}</SettingRowTitle>
|
||||
<HStack gap="5px">
|
||||
<Button onClick={handleClearCache} danger>
|
||||
{t('settings.data.clear_cache.button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
</SettingContainer>
|
||||
<Container>
|
||||
<MenuList>
|
||||
{menuItems.map((item) => (
|
||||
<ListItem key={item.key} title={t(item.title)} active={menu === item.key} onClick={() => setMenu(item.key)} />
|
||||
))}
|
||||
</MenuList>
|
||||
<SettingContainer theme={theme} style={{ display: 'flex', flex: 1 }}>
|
||||
{menu === 'data' && (
|
||||
<>
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||
<HStack gap="5px" justifyContent="space-between">
|
||||
<Button onClick={BackupPopup.show} icon={<SaveOutlined />}>
|
||||
{t('settings.general.backup.button')}
|
||||
</Button>
|
||||
<Button onClick={RestorePopup.show} icon={<FolderOpenOutlined />}>
|
||||
{t('settings.general.restore.button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.reset.title')}</SettingRowTitle>
|
||||
<HStack gap="5px">
|
||||
<Button onClick={reset} danger>
|
||||
{t('settings.general.reset.button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.data.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.app_data')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px">
|
||||
<Typography.Text style={{ color: 'var(--color-text-3)' }}>{appInfo?.appDataPath}</Typography.Text>
|
||||
<StyledIcon onClick={() => handleOpenPath(appInfo?.appDataPath)} />
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.app_logs')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px">
|
||||
<Typography.Text style={{ color: 'var(--color-text-3)' }}>{appInfo?.logsPath}</Typography.Text>
|
||||
<StyledIcon onClick={() => handleOpenPath(appInfo?.logsPath)} />
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.app_knowledge')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px">
|
||||
<Button onClick={handleRemoveAllFiles} danger>
|
||||
{t('settings.data.app_knowledge.button.delete')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.clear_cache.title')}</SettingRowTitle>
|
||||
<HStack gap="5px">
|
||||
<Button onClick={handleClearCache} danger>
|
||||
{t('settings.data.clear_cache.button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
</>
|
||||
)}
|
||||
{menu === 'webdav' && <WebDavSettings />}
|
||||
{menu === 'markdown_export' && <MarkdownExportSettings />}
|
||||
{menu === 'notion' && <NotionSettings />}
|
||||
{menu === 'yuque' && <YuqueSettings />}
|
||||
</SettingContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled(HStack)`
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
const StyledIcon = styled(FileSearchOutlined)`
|
||||
color: var(--color-text-2);
|
||||
cursor: pointer;
|
||||
@ -493,4 +180,14 @@ const StyledIcon = styled(FileSearchOutlined)`
|
||||
}
|
||||
`
|
||||
|
||||
const MenuList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: var(--settings-width);
|
||||
padding: 12px;
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
export default DataSettings
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
import { DeleteOutlined, FolderOpenOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
import { setmarkdownExportPath } from '@renderer/store/settings'
|
||||
import { Button } from 'antd'
|
||||
import Input from 'antd/es/input/Input'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const MarkdownExportSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const markdownExportPath = useSelector((state: RootState) => state.settings.markdownExportPath)
|
||||
|
||||
const handleSelectFolder = async () => {
|
||||
const path = await window.api.file.selectFolder()
|
||||
if (path) {
|
||||
dispatch(setmarkdownExportPath(path))
|
||||
}
|
||||
}
|
||||
|
||||
const handleClearPath = () => {
|
||||
dispatch(setmarkdownExportPath(null))
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.markdown_export.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.markdown_export.path')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||
<Input
|
||||
type="text"
|
||||
value={markdownExportPath || ''}
|
||||
readOnly
|
||||
style={{ width: 250 }}
|
||||
placeholder={t('settings.data.markdown_export.path_placeholder')}
|
||||
suffix={
|
||||
markdownExportPath ? (
|
||||
<DeleteOutlined onClick={handleClearPath} style={{ color: 'var(--color-error)', cursor: 'pointer' }} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<Button onClick={handleSelectFolder} icon={<FolderOpenOutlined />}>
|
||||
{t('settings.data.markdown_export.select')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.markdown_export.help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default MarkdownExportSettings
|
||||
178
src/renderer/src/pages/settings/DataSettings/NotionSettings.tsx
Normal file
178
src/renderer/src/pages/settings/DataSettings/NotionSettings.tsx
Normal file
@ -0,0 +1,178 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { Client } from '@notionhq/client'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import MinApp from '@renderer/components/MinApp'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
import {
|
||||
setNotionApiKey,
|
||||
setNotionAutoSplit,
|
||||
setNotionDatabaseID,
|
||||
setNotionPageNameKey,
|
||||
setNotionSplitSize
|
||||
} from '@renderer/store/settings'
|
||||
import { Button, InputNumber, Switch, Tooltip } from 'antd'
|
||||
import Input from 'antd/es/input/Input'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
const NotionSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const notionApiKey = useSelector((state: RootState) => state.settings.notionApiKey)
|
||||
const notionDatabaseID = useSelector((state: RootState) => state.settings.notionDatabaseID)
|
||||
const notionPageNameKey = useSelector((state: RootState) => state.settings.notionPageNameKey)
|
||||
const notionAutoSplit = useSelector((state: RootState) => state.settings.notionAutoSplit)
|
||||
const notionSplitSize = useSelector((state: RootState) => state.settings.notionSplitSize)
|
||||
|
||||
const handleNotionTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setNotionApiKey(e.target.value))
|
||||
}
|
||||
|
||||
const handleNotionDatabaseIdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setNotionDatabaseID(e.target.value))
|
||||
}
|
||||
|
||||
const handleNotionPageNameKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setNotionPageNameKey(e.target.value))
|
||||
}
|
||||
|
||||
const handleNotionConnectionCheck = () => {
|
||||
if (notionApiKey === null) {
|
||||
window.message.error(t('settings.data.notion.check.empty_api_key'))
|
||||
return
|
||||
}
|
||||
if (notionDatabaseID === null) {
|
||||
window.message.error(t('settings.data.notion.check.empty_database_id'))
|
||||
return
|
||||
}
|
||||
const notion = new Client({ auth: notionApiKey })
|
||||
notion.databases
|
||||
.retrieve({
|
||||
database_id: notionDatabaseID
|
||||
})
|
||||
.then((result) => {
|
||||
if (result) {
|
||||
window.message.success(t('settings.data.notion.check.success'))
|
||||
} else {
|
||||
window.message.error(t('settings.data.notion.check.fail'))
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
window.message.error(t('settings.data.notion.check.error'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleNotionTitleClick = () => {
|
||||
MinApp.start({
|
||||
id: 'notion-help',
|
||||
name: 'Notion Help',
|
||||
url: 'https://docs.cherry-ai.com/advanced-basic/notion'
|
||||
})
|
||||
}
|
||||
|
||||
const handleNotionAutoSplitChange = (checked: boolean) => {
|
||||
dispatch(setNotionAutoSplit(checked))
|
||||
}
|
||||
|
||||
const handleNotionSplitSizeChange = (value: number | null) => {
|
||||
if (value !== null) {
|
||||
dispatch(setNotionSplitSize(value))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle style={{ justifyContent: 'flex-start', gap: 10 }}>
|
||||
{t('settings.data.notion.title')}
|
||||
<Tooltip title={t('settings.data.notion.help')} placement="right">
|
||||
<InfoCircleOutlined
|
||||
style={{ color: 'var(--color-text-2)', cursor: 'pointer' }}
|
||||
onClick={handleNotionTitleClick}
|
||||
/>
|
||||
</Tooltip>
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.notion.database_id')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||
<Input
|
||||
type="text"
|
||||
value={notionDatabaseID || ''}
|
||||
onChange={handleNotionDatabaseIdChange}
|
||||
onBlur={handleNotionDatabaseIdChange}
|
||||
style={{ width: 315 }}
|
||||
placeholder={t('settings.data.notion.database_id_placeholder')}
|
||||
/>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.notion.page_name_key')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||
<Input
|
||||
type="text"
|
||||
value={notionPageNameKey || ''}
|
||||
onChange={handleNotionPageNameKeyChange}
|
||||
onBlur={handleNotionPageNameKeyChange}
|
||||
style={{ width: 315 }}
|
||||
placeholder={t('settings.data.notion.page_name_key_placeholder')}
|
||||
/>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.notion.api_key')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||
<Input
|
||||
type="password"
|
||||
value={notionApiKey || ''}
|
||||
onChange={handleNotionTokenChange}
|
||||
onBlur={handleNotionTokenChange}
|
||||
style={{ width: 250 }}
|
||||
placeholder={t('settings.data.notion.api_key_placeholder')}
|
||||
/>
|
||||
<Button onClick={handleNotionConnectionCheck}>{t('settings.data.notion.check.button')}</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider /> {/* 添加分割线 */}
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
<Tooltip title={t('settings.data.notion.auto_split_tip')} placement="right">
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
{t('settings.data.notion.auto_split')}
|
||||
<InfoCircleOutlined style={{ cursor: 'pointer' }} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<Switch checked={notionAutoSplit} onChange={handleNotionAutoSplitChange} />
|
||||
</SettingRow>
|
||||
{notionAutoSplit && (
|
||||
<>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.notion.split_size')}</SettingRowTitle>
|
||||
<InputNumber
|
||||
min={30}
|
||||
max={25000}
|
||||
value={notionSplitSize}
|
||||
onChange={handleNotionSplitSizeChange}
|
||||
keyboard={true}
|
||||
controls={true}
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText style={{ marginLeft: 10 }}>{t('settings.data.notion.split_size_help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotionSettings
|
||||
@ -1,5 +1,6 @@
|
||||
import { FolderOpenOutlined, SaveOutlined, SyncOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { backupToWebdav, restoreFromWebdav, startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
|
||||
@ -17,7 +18,7 @@ import dayjs from 'dayjs'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const WebDavSettings: FC = () => {
|
||||
const {
|
||||
@ -39,6 +40,7 @@ const WebDavSettings: FC = () => {
|
||||
const [restoring, setRestoring] = useState(false)
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const { theme } = useTheme()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
@ -112,7 +114,7 @@ const WebDavSettings: FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.webdav.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
@ -196,7 +198,7 @@ const WebDavSettings: FC = () => {
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
116
src/renderer/src/pages/settings/DataSettings/YuqueSettings.tsx
Normal file
116
src/renderer/src/pages/settings/DataSettings/YuqueSettings.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import MinApp from '@renderer/components/MinApp'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
import { setYuqueRepoId, setYuqueToken, setYuqueUrl } from '@renderer/store/settings'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import Input from 'antd/es/input/Input'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const YuqueSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const yuqueToken = useSelector((state: RootState) => state.settings.yuqueToken)
|
||||
const yuqueUrl = useSelector((state: RootState) => state.settings.yuqueUrl)
|
||||
|
||||
const handleYuqueTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setYuqueToken(e.target.value))
|
||||
}
|
||||
|
||||
const handleYuqueRepoUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setYuqueUrl(e.target.value))
|
||||
}
|
||||
|
||||
const handleYuqueConnectionCheck = async () => {
|
||||
if (!yuqueToken) {
|
||||
window.message.error(t('settings.data.yuque.check.empty_token'))
|
||||
return
|
||||
}
|
||||
if (!yuqueUrl) {
|
||||
window.message.error(t('settings.data.yuque.check.empty_url'))
|
||||
return
|
||||
}
|
||||
|
||||
const response = await fetch('https://www.yuque.com/api/v2/hello', {
|
||||
headers: {
|
||||
'X-Auth-Token': yuqueToken
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
window.message.error(t('settings.data.yuque.check.fail'))
|
||||
return
|
||||
}
|
||||
const yuqueSlug = yuqueUrl.replace('https://www.yuque.com/', '')
|
||||
const repoIDResponse = await fetch(`https://www.yuque.com/api/v2/repos/${yuqueSlug}`, {
|
||||
headers: {
|
||||
'X-Auth-Token': yuqueToken
|
||||
}
|
||||
})
|
||||
if (!repoIDResponse.ok) {
|
||||
window.message.error(t('settings.data.yuque.check.fail'))
|
||||
return
|
||||
}
|
||||
const data = await repoIDResponse.json()
|
||||
dispatch(setYuqueRepoId(data.data.id))
|
||||
window.message.success(t('settings.data.yuque.check.success'))
|
||||
}
|
||||
|
||||
const handleYuqueHelpClick = () => {
|
||||
MinApp.start({
|
||||
id: 'yuque-help',
|
||||
name: 'Yuque Help',
|
||||
url: 'https://www.yuque.com/settings/tokens'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.yuque.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.yuque.repo_url')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||
<Input
|
||||
type="text"
|
||||
value={yuqueUrl || ''}
|
||||
onChange={handleYuqueRepoUrlChange}
|
||||
style={{ width: 315 }}
|
||||
placeholder={t('settings.data.yuque.repo_url_placeholder')}
|
||||
/>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.data.yuque.token')}
|
||||
<Tooltip title={t('settings.data.yuque.help')} placement="left">
|
||||
<InfoCircleOutlined
|
||||
style={{ color: 'var(--color-text-2)', cursor: 'pointer', marginLeft: 4 }}
|
||||
onClick={handleYuqueHelpClick}
|
||||
/>
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||
<Input
|
||||
type="password"
|
||||
value={yuqueToken || ''}
|
||||
onChange={handleYuqueTokenChange}
|
||||
style={{ width: 250 }}
|
||||
placeholder={t('settings.data.yuque.token_placeholder')}
|
||||
/>
|
||||
<Button onClick={handleYuqueConnectionCheck}>{t('settings.data.yuque.check.button')}</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default YuqueSettings
|
||||
Loading…
x
Reference in New Issue
Block a user