From ce4cad67a684dc8b2d3183fe64eaf0a238db084e Mon Sep 17 00:00:00 2001 From: SuYao Date: Sun, 31 Aug 2025 21:53:53 +0800 Subject: [PATCH] Fix/newNode (#9727) * feat: enhance note saving functionality with immediate cache invalidation * fix: improve file name handling and localization updates * feat: implement multi-level note sorting and enhance state management - Introduced sorting options for notes by name, update time, and creation time, allowing users to sort notes in ascending and descending order. - Updated NotesPage and NotesSidebar components to handle sorting functionality. - Enhanced Redux store to manage the sorting type, improving state management for note organization. - Refactored related services to support recursive sorting logic, ensuring a consistent user experience. * feat(i18n): add new file upload messages for multiple languages * fix(styles): adjust padding in richtext.scss to accommodate scrollbar * style(NotesSidebar): add border-top-left-radius to enhance sidebar aesthetics * feat(RichEditPopup): add isFullWidth prop to enhance popup layout * feat(RichEditPopup): disable keyboard interaction for improved user experience * feat(NotesPage): integrate sorting after node deletion and movement - Added sorting functionality to be triggered after deleting or moving nodes, ensuring notes are organized immediately. - Updated dependencies in useCallback hooks to include sortType for consistent behavior across operations. * feat(NotesService): update initWorkSpace and sortAllLevels to accept sortType - Modified initWorkSpace to include sortType for improved note organization during initialization. - Enhanced sortAllLevels to optionally accept a tree parameter, allowing for more flexible sorting operations. - Updated NotesPage to utilize the new parameters, ensuring consistent sorting behavior across various actions. * feat(NotesSidebar): implement in-place editing for note renaming - Introduced a new hook, useInPlaceEdit, to manage in-place editing of note names, enhancing user experience during renaming. - Updated the NotesSidebar component to utilize this hook, streamlining the editing process and improving state management. - Removed redundant state variables related to editing, simplifying the component's logic. * refactor(NotesPage): remove commented code for clarity - Removed a comment regarding folder selection behavior to streamline the code and improve readability. - This change does not affect functionality but enhances the overall code quality. * feat(NotesSettings): update initWorkSpace to include default sort type - Modified initWorkSpace calls in NotesSettings to accept a default sort type of 'sort_a2z', ensuring consistent note organization during path updates and resets. - This change enhances the initialization process by applying a predefined sorting method. --- src/main/utils/file.ts | 3 +- src/renderer/src/assets/styles/richtext.scss | 3 +- .../src/components/Popups/RichEditPopup.tsx | 2 + src/renderer/src/i18n/locales/en-us.json | 3 + src/renderer/src/i18n/locales/ja-jp.json | 3 + src/renderer/src/i18n/locales/ru-ru.json | 3 + src/renderer/src/i18n/locales/zh-cn.json | 3 + src/renderer/src/i18n/locales/zh-tw.json | 3 + src/renderer/src/pages/notes/NotesPage.tsx | 96 +++++++++---- src/renderer/src/pages/notes/NotesSidebar.tsx | 134 ++++++++++++------ .../src/pages/settings/NotesSettings.tsx | 4 +- src/renderer/src/services/NotesService.ts | 10 +- src/renderer/src/store/note.ts | 11 +- 13 files changed, 198 insertions(+), 80 deletions(-) diff --git a/src/main/utils/file.ts b/src/main/utils/file.ts index e683f4faea..2f622d3544 100644 --- a/src/main/utils/file.ts +++ b/src/main/utils/file.ts @@ -311,8 +311,7 @@ export async function scanDir(dirPath: string, depth = 0, basePath?: string): Pr */ export function getName(baseDir: string, fileName: string, isFile: boolean): string { // 首先清理文件名 - const sanitizedName = sanitizeFilename(fileName) - const baseName = sanitizedName.replace(/\d+$/, '') + const baseName = sanitizeFilename(fileName) let candidate = isFile ? baseName + '.md' : baseName let counter = 1 diff --git a/src/renderer/src/assets/styles/richtext.scss b/src/renderer/src/assets/styles/richtext.scss index 91ebf0940d..3890f013ec 100644 --- a/src/renderer/src/assets/styles/richtext.scss +++ b/src/renderer/src/assets/styles/richtext.scss @@ -1,5 +1,6 @@ .tiptap { - padding: 12px 60px; + // 预留5px给scrollbar + padding: 12px 55px 12px 60px; outline: none; min-height: 120px; overflow-wrap: break-word; diff --git a/src/renderer/src/components/Popups/RichEditPopup.tsx b/src/renderer/src/components/Popups/RichEditPopup.tsx index 1c7b32e188..09ddbe1191 100644 --- a/src/renderer/src/components/Popups/RichEditPopup.tsx +++ b/src/renderer/src/components/Popups/RichEditPopup.tsx @@ -97,6 +97,7 @@ const PopupContainer: React.FC = ({ afterClose={onClose} afterOpenChange={handleAfterOpenChange} maskClosable={false} + keyboard={false} centered> = ({ onCommandsReady={handleCommandsReady} minHeight={300} maxHeight={500} + isFullWidth={true} className="rich-edit-popup-editor" /> diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 28b56831c9..571674567d 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1615,9 +1615,12 @@ "new_folder": "New Folder", "new_note": "Create a new note", "no_content_to_copy": "No content to copy", + "no_file_selected": "Please select the file to upload", "only_markdown": "Only Markdown files are supported", + "only_one_file_allowed": "Only one file can be uploaded", "open_folder": "Open an external folder", "rename": "Rename", + "rename_changed": "Due to security policies, the filename has been changed from {{original}} to {{final}}", "save": "Save to Notes", "settings": { "data": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 7380d0c68e..cd4762ee83 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1615,9 +1615,12 @@ "new_folder": "新しいフォルダーを作成する", "new_note": "新規ノート作成", "no_content_to_copy": "コピーするコンテンツはありません", + "no_file_selected": "アップロードするファイルを選択してください", "only_markdown": "Markdown ファイルのみをアップロードできます", + "only_one_file_allowed": "アップロードできるファイルは1つだけです", "open_folder": "外部フォルダーを開きます", "rename": "名前の変更", + "rename_changed": "セキュリティポリシーにより、ファイル名は{{original}}から{{final}}に変更されました", "save": "メモに保存する", "settings": { "data": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 0505c0f19f..17036fd103 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1615,9 +1615,12 @@ "new_folder": "Новая папка", "new_note": "Создать заметку", "no_content_to_copy": "Нет контента для копирования", + "no_file_selected": "Пожалуйста, выберите файл для загрузки", "only_markdown": "Только Markdown", + "only_one_file_allowed": "Можно загрузить только один файл", "open_folder": "Откройте внешнюю папку", "rename": "переименовать", + "rename_changed": "В связи с политикой безопасности имя файла было изменено с {{Original}} на {{final}}", "save": "Сохранить в заметки", "settings": { "data": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 219e1ed411..678cf39df3 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1615,9 +1615,12 @@ "new_folder": "新建文件夹", "new_note": "新建笔记", "no_content_to_copy": "没有内容可复制", + "no_file_selected": "请选择要上传的文件", "only_markdown": "仅支持 Markdown 格式", + "only_one_file_allowed": "只能上传一个文件", "open_folder": "打开外部文件夹", "rename": "重命名", + "rename_changed": "由于安全策略,文件名已从 {{original}} 更改为 {{final}}", "save": "保存到笔记", "settings": { "data": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 31c4b9a0cc..c13cb0c370 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1615,9 +1615,12 @@ "new_folder": "新建文件夾", "new_note": "新建筆記", "no_content_to_copy": "沒有內容可複制", + "no_file_selected": "請選擇要上傳的文件", "only_markdown": "僅支援 Markdown 格式", + "only_one_file_allowed": "只能上傳一個文件", "open_folder": "打開外部文件夾", "rename": "重命名", + "rename_changed": "由於安全策略,文件名已從 {{original}} 更改為 {{final}}", "save": "儲存到筆記", "settings": { "data": { diff --git a/src/renderer/src/pages/notes/NotesPage.tsx b/src/renderer/src/pages/notes/NotesPage.tsx index edb5b29986..eb0e627323 100644 --- a/src/renderer/src/pages/notes/NotesPage.tsx +++ b/src/renderer/src/pages/notes/NotesPage.tsx @@ -16,7 +16,7 @@ import { } from '@renderer/services/NotesService' import { getNotesTree, isParentNode, updateNodeInTree } from '@renderer/services/NotesTreeService' import { useAppDispatch, useAppSelector } from '@renderer/store' -import { selectActiveFilePath, setActiveFilePath } from '@renderer/store/note' +import { selectActiveFilePath, selectSortType, setActiveFilePath, setSortType } from '@renderer/store/note' import { NotesSortType, NotesTreeNode } from '@renderer/types/note' import { FileChangeEvent } from '@shared/config/types' import { useLiveQuery } from 'dexie-react-hooks' @@ -37,6 +37,7 @@ const NotesPage: FC = () => { const { showWorkspace } = useSettings() const dispatch = useAppDispatch() const activeFilePath = useAppSelector(selectActiveFilePath) + const sortType = useAppSelector(selectSortType) const { settings, notesPath, updateNotesPath } = useNotesSettings() // 混合策略:useLiveQuery用于笔记树,React Query用于文件内容 @@ -53,6 +54,8 @@ const NotesPage: FC = () => { const isEditorInitialized = useRef(false) const lastContentRef = useRef('') const isInitialSortApplied = useRef(false) + const isRenamingRef = useRef(false) + const isCreatingNoteRef = useRef(false) useEffect(() => { const updateCharCount = () => { @@ -131,7 +134,7 @@ const NotesPage: FC = () => { async function applyInitialSort() { if (notesTree.length > 0 && !isInitialSortApplied.current) { try { - await sortAllLevels('sort_a2z') + await sortAllLevels(sortType) isInitialSortApplied.current = true } catch (error) { logger.error('Failed to apply initial sorting:', error as Error) @@ -140,14 +143,21 @@ const NotesPage: FC = () => { } applyInitialSort() - }, [notesTree.length]) + }, [notesTree.length, sortType]) // 处理树同步时的状态管理 useEffect(() => { if (notesTree.length === 0) return // 如果有activeFilePath但找不到对应节点,清空选择 - if (activeFilePath && !activeNode) { + // 但要排除正在同步树结构、重命名或创建笔记的情况,避免在这些操作中误清空 + if ( + activeFilePath && + !activeNode && + !isSyncingTreeRef.current && + !isRenamingRef.current && + !isCreatingNoteRef.current + ) { dispatch(setActiveFilePath(undefined)) } }, [notesTree, activeFilePath, activeNode, dispatch]) @@ -191,7 +201,7 @@ const NotesPage: FC = () => { invalidateFileContent(filePath) } } else { - await initWorkSpace(notesPath) + await initWorkSpace(notesPath, sortType) } break } @@ -210,7 +220,7 @@ const NotesPage: FC = () => { // 重新同步数据库,useLiveQuery会自动响应数据库变化 try { - await initWorkSpace(notesPath) + await initWorkSpace(notesPath, sortType) } catch (error) { logger.error('Failed to sync database:', error as Error) } finally { @@ -264,7 +274,8 @@ const NotesPage: FC = () => { dispatch, currentContent, debouncedSave, - saveCurrentNote + saveCurrentNote, + sortType ]) useEffect(() => { @@ -273,7 +284,7 @@ const NotesPage: FC = () => { // 标记编辑器已初始化 isEditorInitialized.current = true } - }, [currentContent]) + }, [currentContent, activeFilePath]) // 切换文件时重置编辑器初始化状态并兜底保存 useEffect(() => { @@ -319,17 +330,27 @@ const NotesPage: FC = () => { const handleCreateNote = useCallback( async (name: string) => { try { + isCreatingNoteRef.current = true + const targetPath = getTargetFolderPath() if (!targetPath) { throw new Error('No folder path selected') } const newNote = await createNote(name, '', targetPath) dispatch(setActiveFilePath(newNote.externalPath)) + setSelectedFolderId(null) + + await sortAllLevels(sortType) } catch (error) { logger.error('Failed to create note:', error as Error) + } finally { + // 延迟重置标志,给数据库同步一些时间 + setTimeout(() => { + isCreatingNoteRef.current = false + }, 500) } }, - [dispatch, getTargetFolderPath] + [dispatch, getTargetFolderPath, sortType] ) // 切换展开状态 @@ -410,10 +431,7 @@ const NotesPage: FC = () => { logger.error('Failed to load note:', error as Error) } } else if (node.type === 'folder') { - // 设置选中的文件夹,同时清除活动文件 setSelectedFolderId(node.id) - // 清除活动文件状态,这样文件的高亮会被清除 - dispatch(setActiveFilePath(undefined)) await handleToggleExpanded(node.id) } }, @@ -432,6 +450,7 @@ const NotesPage: FC = () => { (nodeToDelete.externalPath === activeFilePath || isParentNode(notesTree, nodeId, activeNode?.id || '')) await deleteNode(nodeId) + await sortAllLevels(sortType) // 如果删除的是当前活动节点或其父节点,清空编辑器 if (isActiveNodeOrParent) { @@ -444,24 +463,47 @@ const NotesPage: FC = () => { logger.error('Failed to delete node:', error as Error) } }, - [activeFilePath, activeNode, notesTree, dispatch, findNodeById] + [findNodeById, notesTree, activeFilePath, activeNode?.id, sortType, dispatch] ) // 重命名节点 const handleRenameNode = useCallback( async (nodeId: string, newName: string) => { try { + isRenamingRef.current = true + const tree = await getNotesTree() const node = findNodeById(tree, nodeId) if (node && node.name !== newName) { - await renameNode(nodeId, newName) + const oldExternalPath = node.externalPath + const renamedNode = await renameNode(nodeId, newName) + + if (renamedNode.type === 'file' && activeFilePath === oldExternalPath) { + dispatch(setActiveFilePath(renamedNode.externalPath)) + } else if ( + renamedNode.type === 'folder' && + activeFilePath && + activeFilePath.startsWith(oldExternalPath + '/') + ) { + const relativePath = activeFilePath.substring(oldExternalPath.length) + const newFilePath = renamedNode.externalPath + relativePath + dispatch(setActiveFilePath(newFilePath)) + } + await sortAllLevels(sortType) + if (renamedNode.name !== newName) { + window.message.info(t('notes.rename_changed', { original: newName, final: renamedNode.name })) + } } } catch (error) { logger.error('Failed to rename node:', error as Error) + } finally { + setTimeout(() => { + isRenamingRef.current = false + }, 500) } }, - [findNodeById] + [activeFilePath, dispatch, findNodeById, sortType, t] ) // 处理文件上传 @@ -507,22 +549,28 @@ const NotesPage: FC = () => { async (sourceNodeId: string, targetNodeId: string, position: 'before' | 'after' | 'inside') => { try { await moveNode(sourceNodeId, targetNodeId, position) + await sortAllLevels(sortType) } catch (error) { logger.error('Failed to move nodes:', error as Error) } }, - [] + [sortType] ) // 处理节点排序 - const handleSortNodes = useCallback(async (sortType: NotesSortType) => { - try { - await sortAllLevels(sortType) - } catch (error) { - logger.error('Failed to sort notes:', error as Error) - throw error - } - }, []) + const handleSortNodes = useCallback( + async (newSortType: NotesSortType) => { + try { + // 更新Redux中的排序类型 + dispatch(setSortType(newSortType)) + await sortAllLevels(newSortType) + } catch (error) { + logger.error('Failed to sort notes:', error as Error) + throw error + } + }, + [dispatch] + ) const getCurrentNoteContent = useCallback(() => { if (settings.defaultEditMode === 'source') { diff --git a/src/renderer/src/pages/notes/NotesSidebar.tsx b/src/renderer/src/pages/notes/NotesSidebar.tsx index 8c79de18f7..9400ad37cf 100644 --- a/src/renderer/src/pages/notes/NotesSidebar.tsx +++ b/src/renderer/src/pages/notes/NotesSidebar.tsx @@ -2,11 +2,14 @@ import { loggerService } from '@logger' import { DeleteIcon } from '@renderer/components/Icons' import SaveToKnowledgePopup from '@renderer/components/Popups/SaveToKnowledgePopup' import Scrollbar from '@renderer/components/Scrollbar' +import { useInPlaceEdit } from '@renderer/hooks/useInPlaceEdit' import { useKnowledgeBases } from '@renderer/hooks/useKnowledge' import { useActiveNode } from '@renderer/hooks/useNotesQuery' import NotesSidebarHeader from '@renderer/pages/notes/NotesSidebarHeader' +import { useAppSelector } from '@renderer/store' +import { selectSortType } from '@renderer/store/note' import { NotesSortType, NotesTreeNode } from '@renderer/types/note' -import { Dropdown, Input, MenuProps } from 'antd' +import { Dropdown, Input, InputRef, MenuProps } from 'antd' import { ChevronDown, ChevronRight, @@ -19,7 +22,7 @@ import { Star, StarOff } from 'lucide-react' -import { FC, useCallback, useMemo, useRef, useState } from 'react' +import { FC, Ref, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -57,8 +60,8 @@ const NotesSidebar: FC = ({ const { t } = useTranslation() const { bases } = useKnowledgeBases() const { activeNode } = useActiveNode(notesTree) + const sortType = useAppSelector(selectSortType) const [editingNodeId, setEditingNodeId] = useState(null) - const [editingName, setEditingName] = useState('') const [draggedNodeId, setDraggedNodeId] = useState(null) const [dragOverNodeId, setDragOverNodeId] = useState(null) const [dragPosition, setDragPosition] = useState<'before' | 'inside' | 'after'>('inside') @@ -66,8 +69,56 @@ const NotesSidebar: FC = ({ const [isShowSearch, setIsShowSearch] = useState(false) const [searchKeyword, setSearchKeyword] = useState('') const [isDragOverSidebar, setIsDragOverSidebar] = useState(false) - const [sortType, setSortType] = useState('sort_a2z') const dragNodeRef = useRef(null) + const scrollbarRef = useRef(null) + + const inPlaceEdit = useInPlaceEdit({ + onSave: (newName: string) => { + if (editingNodeId && newName) { + onRenameNode(editingNodeId, newName) + logger.debug(`Renamed node ${editingNodeId} to "${newName}"`) + } + setEditingNodeId(null) + }, + onCancel: () => { + setEditingNodeId(null) + } + }) + + // 滚动到活动节点 + useEffect(() => { + if (activeNode?.id && !isShowStarred && !isShowSearch && scrollbarRef.current) { + // 延迟一下确保DOM已更新 + setTimeout(() => { + const scrollContainer = scrollbarRef.current as HTMLElement + if (scrollContainer) { + const activeElement = scrollContainer.querySelector(`[data-node-id="${activeNode.id}"]`) as HTMLElement + if (activeElement) { + // 获取元素相对于滚动容器的位置 + const containerHeight = scrollContainer.clientHeight + const elementOffsetTop = activeElement.offsetTop + const elementHeight = activeElement.offsetHeight + const currentScrollTop = scrollContainer.scrollTop + + // 检查元素是否在可视区域内 + const elementTop = elementOffsetTop + const elementBottom = elementOffsetTop + elementHeight + const viewTop = currentScrollTop + const viewBottom = currentScrollTop + containerHeight + + // 如果元素不在可视区域内,滚动到中心位置 + if (elementTop < viewTop || elementBottom > viewBottom) { + const targetScrollTop = elementOffsetTop - (containerHeight - elementHeight) / 2 + scrollContainer.scrollTo({ + top: Math.max(0, targetScrollTop), + behavior: 'smooth' + }) + } + } + } + }, 200) + } + }, [activeNode?.id, isShowStarred, isShowSearch]) const handleCreateFolder = useCallback(() => { onCreateFolder(t('notes.untitled_folder')) @@ -79,30 +130,18 @@ const NotesSidebar: FC = ({ const handleSelectSortType = useCallback( (selectedSortType: NotesSortType) => { - setSortType(selectedSortType) onSortNodes(selectedSortType) }, [onSortNodes] ) - const handleStartEdit = useCallback((node: NotesTreeNode) => { - setEditingNodeId(node.id) - setEditingName(node.name) - }, []) - - const handleFinishEdit = useCallback(() => { - if (editingNodeId && editingName.trim()) { - onRenameNode(editingNodeId, editingName.trim()) - } - setEditingNodeId(null) - setEditingName('') - logger.debug(`Renamed node ${editingNodeId} to "${editingName.trim()}"`) - }, [editingNodeId, editingName, onRenameNode]) - - const handleCancelEdit = useCallback(() => { - setEditingNodeId(null) - setEditingName('') - }, []) + const handleStartEdit = useCallback( + (node: NotesTreeNode) => { + setEditingNodeId(node.id) + inPlaceEdit.startEdit(node.name) + }, + [inPlaceEdit] + ) const handleDeleteNode = useCallback( (node: NotesTreeNode) => { @@ -306,8 +345,10 @@ const NotesSidebar: FC = ({ const renderTreeNode = useCallback( (node: NotesTreeNode, depth: number = 0) => { - const isActive = node.id === activeNode?.id || (node.type === 'folder' && node.id === selectedFolderId) - const isEditing = editingNodeId === node.id + const isActive = selectedFolderId + ? node.type === 'folder' && node.id === selectedFolderId + : node.id === activeNode?.id + const isEditing = editingNodeId === node.id && inPlaceEdit.isEditing const hasChildren = node.children && node.children.length > 0 const isDragging = draggedNodeId === node.id const isDragOver = dragOverNodeId === node.id @@ -328,6 +369,7 @@ const NotesSidebar: FC = ({ isDragInside={isDragInside} isDragAfter={isDragAfter} draggable={!isEditing} + data-node-id={node.id} onDragStart={(e) => handleDragStart(e, node)} onDragOver={(e) => handleDragOver(e, node)} onDragLeave={handleDragLeave} @@ -361,15 +403,13 @@ const NotesSidebar: FC = ({ {isEditing ? ( setEditingName(e.target.value)} - onPressEnter={handleFinishEdit} - onBlur={handleFinishEdit} - onKeyDown={(e) => { - if (e.key === 'Escape') { - handleCancelEdit() - } - }} + ref={inPlaceEdit.inputRef as Ref} + value={inPlaceEdit.editValue} + onChange={inPlaceEdit.handleInputChange} + onPressEnter={inPlaceEdit.saveEdit} + onBlur={inPlaceEdit.saveEdit} + onKeyDown={inPlaceEdit.handleKeyDown} + onClick={(e) => e.stopPropagation()} autoFocus size="small" /> @@ -388,24 +428,27 @@ const NotesSidebar: FC = ({ ) }, [ - activeNode, selectedFolderId, + activeNode?.id, editingNodeId, - editingName, + inPlaceEdit.isEditing, + inPlaceEdit.inputRef, + inPlaceEdit.editValue, + inPlaceEdit.handleInputChange, + inPlaceEdit.saveEdit, + inPlaceEdit.handleKeyDown, draggedNodeId, dragOverNodeId, dragPosition, - onSelectNode, - onToggleExpanded, - handleFinishEdit, - handleCancelEdit, + getMenuItems, + handleDragLeave, + handleDragEnd, + t, handleDragStart, handleDragOver, - handleDragLeave, handleDrop, - handleDragEnd, - getMenuItems, - t + onSelectNode, + onToggleExpanded ] ) @@ -451,7 +494,7 @@ const NotesSidebar: FC = ({ /> - + {filteredTree.map((node) => renderTreeNode(node))} {!isShowStarred && !isShowSearch && ( @@ -480,6 +523,7 @@ const SidebarContainer = styled.div` height: 100vh; background-color: var(--color-background); border-right: 1px solid var(--color-border); + border-top-left-radius: 10px; display: flex; flex-direction: column; position: relative; diff --git a/src/renderer/src/pages/settings/NotesSettings.tsx b/src/renderer/src/pages/settings/NotesSettings.tsx index 0955c93edd..65256f263b 100644 --- a/src/renderer/src/pages/settings/NotesSettings.tsx +++ b/src/renderer/src/pages/settings/NotesSettings.tsx @@ -70,7 +70,7 @@ const NotesSettings: FC = () => { } updateNotesPath(tempPath) - initWorkSpace(tempPath) + initWorkSpace(tempPath, 'sort_a2z') window.message.success(t('notes.settings.data.path_updated')) } catch (error) { logger.error('Failed to apply notes path:', error as Error) @@ -83,7 +83,7 @@ const NotesSettings: FC = () => { const info = await window.api.getAppInfo() setTempPath(info.notesPath) updateNotesPath(info.notesPath) - initWorkSpace(info.notesPath) + initWorkSpace(info.notesPath, 'sort_a2z') window.message.success(t('notes.settings.data.reset_to_default')) } catch (error) { logger.error('Failed to reset to default:', error as Error) diff --git a/src/renderer/src/services/NotesService.ts b/src/renderer/src/services/NotesService.ts index 0b150a25a5..fa3607270e 100644 --- a/src/renderer/src/services/NotesService.ts +++ b/src/renderer/src/services/NotesService.ts @@ -22,9 +22,9 @@ const logger = loggerService.withContext('NotesService') /** * 初始化/同步笔记树结构 */ -export async function initWorkSpace(folderPath: string): Promise { +export async function initWorkSpace(folderPath: string, sortType: NotesSortType): Promise { const tree = await window.api.file.getDirectoryStructure(folderPath) - await db.notes_tree.put({ id: NOTES_TREE_ID, tree }) + await sortAllLevels(sortType, tree) } /** @@ -326,9 +326,11 @@ function getSortFunction(sortType: NotesSortType): (a: NotesTreeNode, b: NotesTr /** * 递归排序笔记树中的所有层级 */ -export async function sortAllLevels(sortType: NotesSortType): Promise { +export async function sortAllLevels(sortType: NotesSortType, tree?: NotesTreeNode[]): Promise { try { - const tree = await getNotesTree() + if (!tree) { + tree = await getNotesTree() + } sortNodesArray(tree, sortType) recursiveSortNodes(tree, sortType) await db.notes_tree.put({ id: NOTES_TREE_ID, tree }) diff --git a/src/renderer/src/store/note.ts b/src/renderer/src/store/note.ts index 4d481ed391..cd3e8d63cc 100644 --- a/src/renderer/src/store/note.ts +++ b/src/renderer/src/store/note.ts @@ -1,6 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { RootState } from '@renderer/store/index' import { EditorView } from '@renderer/types' +import { NotesSortType } from '@renderer/types/note' export interface NotesSettings { isFullWidth: boolean @@ -15,6 +16,7 @@ export interface NoteState { activeFilePath: string | undefined // 使用文件路径而不是nodeId settings: NotesSettings notesPath: string + sortType: NotesSortType } export const initialState: NoteState = { @@ -27,7 +29,8 @@ export const initialState: NoteState = { defaultEditMode: 'preview', showTabStatus: true }, - notesPath: '' + notesPath: '', + sortType: 'sort_a2z' } const noteSlice = createSlice({ @@ -45,15 +48,19 @@ const noteSlice = createSlice({ }, setNotesPath: (state, action: PayloadAction) => { state.notesPath = action.payload + }, + setSortType: (state, action: PayloadAction) => { + state.sortType = action.payload } } }) -export const { setActiveNodeId, setActiveFilePath, updateNotesSettings, setNotesPath } = noteSlice.actions +export const { setActiveNodeId, setActiveFilePath, updateNotesSettings, setNotesPath, setSortType } = noteSlice.actions export const selectActiveNodeId = (state: RootState) => state.note.activeNodeId export const selectActiveFilePath = (state: RootState) => state.note.activeFilePath export const selectNotesSettings = (state: RootState) => state.note.settings export const selectNotesPath = (state: RootState) => state.note.notesPath +export const selectSortType = (state: RootState) => state.note.sortType export default noteSlice.reducer