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:
kangfenmao 2025-03-11 16:22:48 +08:00
parent 7d7f9eaa35
commit 67bb1f19f0
5 changed files with 470 additions and 414 deletions

View File

@ -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

View File

@ -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

View 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

View File

@ -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>
)
}

View 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