feat: 添加Notion导出自动分页功能 (#2098)
* fix: 长对话Notion导出失败(分页导出) * feat: 添加Notion导出自动分页设置
This commit is contained in:
parent
6b34aac263
commit
5c2d936688
@ -604,6 +604,20 @@
|
||||
"notion.page_name_key": "Page Title Field Name",
|
||||
"notion.page_name_key_placeholder": "Enter page title field name, default is Name",
|
||||
"notion.title": "Notion Configuration",
|
||||
"notion.help": "Notion Configuration Documentation",
|
||||
"notion.check": {
|
||||
"button": "Check",
|
||||
"fail": "Connection failed, please check network and Api_key and Database_id",
|
||||
"success": "Connection successful",
|
||||
"error": "Connection error, please check network configuration and Api_key and Database_id",
|
||||
"empty_api_key": "Api_key is not configured",
|
||||
"empty_database_id": "Database_id is not configured"
|
||||
},
|
||||
"notion.auto_split": "Auto split when exporting",
|
||||
"notion.auto_split_tip": "Automatically split pages when exporting long topics to Notion",
|
||||
"notion.split_size": "Split size",
|
||||
"notion.split_size_placeholder": "Enter block limit per page (default 90)",
|
||||
"notion.split_size_help": "Recommended: 90 for Free plan, 24990 for Plus plan, default is 90",
|
||||
"title": "Data Settings",
|
||||
"webdav": {
|
||||
"autoSync": "Auto Backup",
|
||||
|
||||
@ -604,6 +604,20 @@
|
||||
"notion.page_name_key": "ページタイトルフィールド名",
|
||||
"notion.page_name_key_placeholder": "ページタイトルフィールド名を入力してください。デフォルトは Name です",
|
||||
"notion.title": "Notion 設定",
|
||||
"notion.help": "Notion 設定ドキュメント",
|
||||
"notion.check": {
|
||||
"button": "確認",
|
||||
"fail": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください",
|
||||
"success": "接続に成功しました。",
|
||||
"error": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください",
|
||||
"empty_api_key": "Api_keyが設定されていません",
|
||||
"empty_database_id": "Database_idが設定されていません"
|
||||
},
|
||||
"notion.auto_split": "내보내기 시 자동 분할",
|
||||
"notion.auto_split_tip": "긴 주제를 Notion으로 내보낼 때 자동으로 페이지 분할",
|
||||
"notion.split_size": "분할 크기",
|
||||
"notion.split_size_placeholder": "페이지당 블록 제한 입력(기본값 90)",
|
||||
"notion.split_size_help": "권장: 무료 플랜 90, Plus 플랜 24990, 기본값 90",
|
||||
"title": "データ設定",
|
||||
"webdav": {
|
||||
"autoSync": "自動バックアップ",
|
||||
|
||||
@ -401,7 +401,8 @@
|
||||
"upgrade.success.button": "重启",
|
||||
"upgrade.success.content": "重启用以完成升级",
|
||||
"upgrade.success.title": "升级成功",
|
||||
"warn.notion.exporting": "正在导出到Notion, 请勿重复请求导出!"
|
||||
"warn.notion.exporting": "正在导出到Notion, 请勿重复请求导出!",
|
||||
"info.notion.block_reach_limit": "对话过长,正在分页导出到Notion"
|
||||
},
|
||||
"minapp": {
|
||||
"sidebar.add.title": "添加到侧边栏",
|
||||
@ -604,6 +605,20 @@
|
||||
"notion.page_name_key": "页面标题字段名",
|
||||
"notion.page_name_key_placeholder": "请输入页面标题字段名,默认为 Name",
|
||||
"notion.title": "Notion 配置",
|
||||
"notion.help": "Notion 配置文档",
|
||||
"notion.check": {
|
||||
"button": "检查",
|
||||
"fail": "连接失败,请检查网络及Api_key和Database_id是否正确",
|
||||
"success": "连接成功",
|
||||
"error": "连接异常,请检查网络及Api_key和Database_id是否正确",
|
||||
"empty_api_key": "未配置Api_key",
|
||||
"empty_database_id": "未配置Database_id"
|
||||
},
|
||||
"notion.auto_split_tip": "当要导出的话题过长时自动分页导出到Notion",
|
||||
"notion.auto_split": "导出对话时自动分页",
|
||||
"notion.split_size": "自动分页大小",
|
||||
"notion.split_size_placeholder": "请输入每页块数限制(默认90)",
|
||||
"notion.split_size_help": "Notion免费版用户建议设置为90,高级版用户建议设置为24990,默认值为90",
|
||||
"title": "数据设置",
|
||||
"webdav": {
|
||||
"autoSync": "自动备份",
|
||||
|
||||
@ -604,6 +604,20 @@
|
||||
"notion.page_name_key": "頁面標題欄位名稱",
|
||||
"notion.page_name_key_placeholder": "請輸入頁面標題欄位名稱,預設為 Name",
|
||||
"notion.title": "Notion 配置",
|
||||
"notion.help": "Notion 配置文檔",
|
||||
"notion.check": {
|
||||
"button": "檢查",
|
||||
"fail": "連接失敗,請檢查網絡及Api_key和Database_id是否正確",
|
||||
"success": "連線成功",
|
||||
"error": "連接異常,請檢查網絡及Api_key和Database_id是否正確",
|
||||
"empty_api_key": "未配置Api_key",
|
||||
"empty_database_id": "未配置Database_id"
|
||||
},
|
||||
"notion.auto_split": "導出對話時自動分頁",
|
||||
"notion.auto_split_tip": "當要導出的話題過長時自動分頁導出到Notion",
|
||||
"notion.split_size": "自動分頁大小",
|
||||
"notion.split_size_placeholder": "請輸入每頁塊數限制(默認90)",
|
||||
"notion.split_size_help": "Notion免費版用戶建議設置為90,高級版用戶建議設置為24990,默認值為90",
|
||||
"title": "數據設定",
|
||||
"webdav": {
|
||||
"autoSync": "自動備份",
|
||||
|
||||
@ -5,16 +5,30 @@ import MinApp from '@renderer/components/MinApp'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { backup, reset, restore } from '@renderer/services/BackupService'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
import { setNotionApiKey, setNotionDatabaseID, setNotionPageNameKey } from '@renderer/store/settings'
|
||||
import {
|
||||
setNotionApiKey,
|
||||
setNotionAutoSplit,
|
||||
setNotionDatabaseID,
|
||||
setNotionPageNameKey,
|
||||
setNotionSplitSize
|
||||
} from '@renderer/store/settings'
|
||||
import { AppInfo } from '@renderer/types'
|
||||
import { Button, Modal, Tooltip, Typography } from 'antd'
|
||||
import { Button, InputNumber, Modal, Switch, Tooltip, Typography } from 'antd'
|
||||
import Input from 'antd/es/input/Input'
|
||||
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, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import {
|
||||
SettingContainer,
|
||||
SettingDivider,
|
||||
SettingGroup,
|
||||
SettingHelpText,
|
||||
SettingRow,
|
||||
SettingRowTitle,
|
||||
SettingTitle
|
||||
} from '..'
|
||||
import WebDavSettings from './WebDavSettings'
|
||||
|
||||
// 新增的 NotionSettings 组件
|
||||
@ -26,6 +40,8 @@ const NotionSettings: FC = () => {
|
||||
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))
|
||||
@ -73,6 +89,16 @@ const NotionSettings: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
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 }}>
|
||||
@ -128,6 +154,37 @@ const NotionSettings: FC = () => {
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -71,6 +71,8 @@ export interface SettingsState {
|
||||
notionApiKey: string | null
|
||||
notionPageNameKey: string | null
|
||||
thoughtAutoCollapse: boolean
|
||||
notionAutoSplit: boolean
|
||||
notionSplitSize: number
|
||||
}
|
||||
|
||||
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
|
||||
@ -127,7 +129,9 @@ const initialState: SettingsState = {
|
||||
notionDatabaseID: '',
|
||||
notionApiKey: '',
|
||||
notionPageNameKey: 'Name',
|
||||
thoughtAutoCollapse: true
|
||||
thoughtAutoCollapse: true,
|
||||
notionAutoSplit: false,
|
||||
notionSplitSize: 90
|
||||
}
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
@ -293,6 +297,12 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setThoughtAutoCollapse: (state, action: PayloadAction<boolean>) => {
|
||||
state.thoughtAutoCollapse = action.payload
|
||||
},
|
||||
setNotionAutoSplit: (state, action: PayloadAction<boolean>) => {
|
||||
state.notionAutoSplit = action.payload
|
||||
},
|
||||
setNotionSplitSize: (state, action: PayloadAction<number>) => {
|
||||
state.notionSplitSize = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -348,7 +358,9 @@ export const {
|
||||
setNotionDatabaseID,
|
||||
setNotionApiKey,
|
||||
setNotionPageNameKey,
|
||||
setThoughtAutoCollapse
|
||||
setThoughtAutoCollapse,
|
||||
setNotionAutoSplit,
|
||||
setNotionSplitSize
|
||||
} = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
@ -41,6 +41,127 @@ export const exportMessageAsMarkdown = async (message: Message) => {
|
||||
window.api.file.save(fileName, markdown)
|
||||
}
|
||||
|
||||
// 修改 splitNotionBlocks 函数
|
||||
const splitNotionBlocks = (blocks: any[]) => {
|
||||
const { notionAutoSplit, notionSplitSize } = store.getState().settings
|
||||
|
||||
// 如果未开启自动分页,返回单页
|
||||
if (!notionAutoSplit) {
|
||||
return [blocks]
|
||||
}
|
||||
|
||||
const pages: any[][] = []
|
||||
let currentPage: any[] = []
|
||||
|
||||
blocks.forEach((block) => {
|
||||
if (currentPage.length >= notionSplitSize) {
|
||||
window.message.info({ content: i18n.t('message.info.notion.block_reach_limit'), key: 'notion-block-reach-limit' })
|
||||
pages.push(currentPage)
|
||||
currentPage = []
|
||||
}
|
||||
currentPage.push(block)
|
||||
})
|
||||
|
||||
if (currentPage.length > 0) {
|
||||
pages.push(currentPage)
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
// 创建页面标题块
|
||||
const createPageTitleBlocks = (title: string, pageNumber: number, totalPages: number) => {
|
||||
return [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'heading_1',
|
||||
heading_1: {
|
||||
rich_text: [{ type: 'text', text: { content: `${title} (${pageNumber}/${totalPages})` } }]
|
||||
}
|
||||
},
|
||||
{
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
paragraph: {
|
||||
rich_text: []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export const exportTopicToNotion = async (topic: Topic) => {
|
||||
const { isExporting } = store.getState().runtime.export
|
||||
if (isExporting) {
|
||||
window.message.warning({ content: i18n.t('message.warn.notion.exporting'), key: 'notion-exporting' })
|
||||
return
|
||||
}
|
||||
setExportState({
|
||||
isExporting: true
|
||||
})
|
||||
const { notionDatabaseID, notionApiKey } = store.getState().settings
|
||||
if (!notionApiKey || !notionDatabaseID) {
|
||||
window.message.error({ content: i18n.t('message.error.notion.no_api_key'), key: 'notion-no-apikey-error' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const notion = new Client({ auth: notionApiKey })
|
||||
const markdown = await topicToMarkdown(topic)
|
||||
const requestBody = JSON.stringify({ md: markdown })
|
||||
|
||||
const res = await fetch('https://md2notion.hilars.dev', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: requestBody
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
const allBlocks = data
|
||||
const blockPages = splitNotionBlocks(allBlocks)
|
||||
|
||||
if (blockPages.length === 0) {
|
||||
throw new Error('No content to export')
|
||||
}
|
||||
|
||||
// 创建主页面和子页面
|
||||
let mainPageResponse: any = null
|
||||
for (let i = 0; i < blockPages.length; i++) {
|
||||
const pageTitle = blockPages.length > 1 ? `${topic.name} (${i + 1}/${blockPages.length})` : topic.name
|
||||
const pageBlocks = blockPages[i]
|
||||
|
||||
const pageContent =
|
||||
i === 0 ? pageBlocks : [...createPageTitleBlocks(topic.name, i + 1, blockPages.length), ...pageBlocks]
|
||||
|
||||
const response = await notion.pages.create({
|
||||
parent: { database_id: notionDatabaseID },
|
||||
properties: {
|
||||
[store.getState().settings.notionPageNameKey || 'Name']: {
|
||||
title: [{ text: { content: pageTitle } }]
|
||||
}
|
||||
},
|
||||
children: pageContent
|
||||
})
|
||||
|
||||
// 保存主页面响应
|
||||
if (i === 0) {
|
||||
mainPageResponse = response
|
||||
}
|
||||
}
|
||||
|
||||
window.message.success({ content: i18n.t('message.success.notion.export'), key: 'notion-success' })
|
||||
return mainPageResponse
|
||||
} catch (error: any) {
|
||||
window.message.error({ content: i18n.t('message.error.notion.export'), key: 'notion-error' })
|
||||
return null
|
||||
} finally {
|
||||
setExportState({
|
||||
isExporting: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const exportMarkdownToNotion = async (title: string, content: string) => {
|
||||
const { isExporting } = store.getState().runtime.export
|
||||
|
||||
@ -93,4 +214,4 @@ export const exportMarkdownToNotion = async (title: string, content: string) =>
|
||||
isExporting: false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user