feat: enhance notes functionality with auto-save and file name synchronization

This commit is contained in:
自由的世界人 2025-07-18 13:13:23 +08:00
parent d4e2384f64
commit 9128a8019d
2 changed files with 253 additions and 113 deletions

View File

@ -4,10 +4,11 @@ import Scrollbar from '@renderer/components/Scrollbar'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useSettings } from '@renderer/hooks/useSettings'
import NotesNavbar from '@renderer/pages/notes/NotesNavbar'
import FileManager from '@renderer/services/FileManager'
import { ThemeMode } from '@renderer/types'
import { NotesTreeNode } from '@renderer/types/note'
import { Empty } from 'antd'
import { FC, useEffect, useRef, useState } from 'react'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import Vditor from 'vditor'
@ -25,6 +26,37 @@ const NotesPage: FC = () => {
const [activeNodeId, setActiveNodeId] = useState<string | undefined>(undefined)
const [isLoading, setIsLoading] = useState(false)
// 查找树节点 by ID
const findNodeById = useCallback((tree: NotesTreeNode[], nodeId: string): NotesTreeNode | null => {
for (const node of tree) {
if (node.id === nodeId) {
return node
}
if (node.children) {
const found = findNodeById(node.children, nodeId)
if (found) return found
}
}
return null
}, [])
// 保存当前笔记内容
const saveCurrentNote = useCallback(
async (content: string) => {
if (!activeNodeId) return
try {
const activeNode = findNodeById(notesTree, activeNodeId)
if (activeNode && activeNode.type === 'file') {
await NotesService.updateNote(activeNode, content)
}
} catch (error) {
console.error('Failed to save note:', error)
}
},
[activeNodeId, findNodeById, notesTree]
)
useEffect(() => {
const loadNotesTree = async () => {
try {
@ -38,63 +70,79 @@ const NotesPage: FC = () => {
loadNotesTree()
}, [])
// 初始化编辑器 - 只有在选择笔记后才初始化
useEffect(() => {
if (editorRef.current && !vditor && activeNodeId) {
const editor = new Vditor(editorRef.current, {
height: '100%',
mode: 'ir',
theme: theme === ThemeMode.dark ? 'dark' : 'classic',
toolbar: [
'headings',
'bold',
'italic',
'strike',
'link',
'|',
'list',
'ordered-list',
'check',
'outdent',
'indent',
'|',
'quote',
'line',
'code',
'inline-code',
'|',
'upload',
'table',
'|',
'undo',
'redo',
'|',
'fullscreen',
'preview'
],
placeholder: t('notes.content_placeholder'),
cache: {
enable: false
},
after: () => {
setVditor(editor)
},
input: (value) => {
// 自动保存当前笔记
if (activeNodeId) {
saveCurrentNote(value)
const initEditor = async () => {
if (editorRef.current && !vditor && activeNodeId) {
const editor = new Vditor(editorRef.current, {
height: '100%',
mode: 'ir',
theme: theme === ThemeMode.dark ? 'dark' : 'classic',
toolbar: [
'headings',
'bold',
'italic',
'strike',
'link',
'|',
'list',
'ordered-list',
'check',
'outdent',
'indent',
'|',
'quote',
'line',
'code',
'inline-code',
'|',
'upload',
'table',
'|',
'undo',
'redo',
'|',
'fullscreen',
'preview'
],
placeholder: t('notes.content_placeholder'),
cache: {
enable: false
},
after: async () => {
setVditor(editor)
// 编辑器初始化完成后,加载笔记内容
if (activeNodeId) {
try {
const activeNode = findNodeById(notesTree, activeNodeId)
if (activeNode && activeNode.type === 'file') {
const content = await NotesService.readNote(activeNode)
editor.setValue(content)
}
} catch (error) {
console.error('Failed to load note content after editor init:', error)
}
}
},
input: (value) => {
// 自动保存当前笔记
if (activeNodeId) {
saveCurrentNote(value)
}
}
}
})
})
}
}
initEditor()
return () => {
if (vditor) {
vditor.destroy()
setVditor(null)
}
}
}, [theme, activeNodeId, t])
}, [theme, activeNodeId, t, notesTree, vditor, findNodeById, saveCurrentNote])
// 监听主题变化,更新编辑器样式
useEffect(() => {
@ -107,34 +155,6 @@ const NotesPage: FC = () => {
}
}, [theme, vditor])
// 自动保存笔记内容
const saveCurrentNote = async (content: string) => {
if (!activeNodeId) return
try {
const activeNode = findNodeById(notesTree, activeNodeId)
if (activeNode && activeNode.type === 'file') {
await NotesService.updateNote(activeNode, content)
}
} catch (error) {
console.error('Failed to save note:', error)
}
}
// 在树中查找节点
const findNodeById = (tree: NotesTreeNode[], nodeId: string): NotesTreeNode | null => {
for (const node of tree) {
if (node.id === nodeId) {
return node
}
if (node.children) {
const found = findNodeById(node.children, nodeId)
if (found) return found
}
}
return null
}
// 创建文件夹
const handleCreateFolder = async (name: string, parentId?: string) => {
try {
@ -153,7 +173,13 @@ const NotesPage: FC = () => {
const handleCreateNote = async (name: string, parentId?: string) => {
try {
setIsLoading(true)
const newNote = await NotesService.createNote(name, '', parentId)
let noteName = name
if (!noteName.toLowerCase().endsWith('.md')) {
noteName += '.md'
}
const newNote = await NotesService.createNote(noteName, '', parentId)
const updatedTree = await NotesService.getNotesTree()
setNotesTree(updatedTree)
@ -173,10 +199,20 @@ const NotesPage: FC = () => {
setIsLoading(true)
setActiveNodeId(node.id)
// 读取笔记内容
const content = await NotesService.readNote(node)
if (node.fileId) {
const updatedFileMetadata = await FileManager.getFile(node.fileId)
if (updatedFileMetadata && updatedFileMetadata.origin_name !== node.name) {
// 如果数据库中的显示名称与树节点中的名称不同,更新树节点
const updatedTree = [...notesTree]
const updatedNode = findNodeById(updatedTree, node.id)
if (updatedNode) {
updatedNode.name = updatedFileMetadata.origin_name
setNotesTree(updatedTree)
}
}
}
// 如果编辑器已初始化,则更新内容
const content = await NotesService.readNote(node)
if (vditor) {
vditor.setValue(content)
}

View File

@ -1,9 +1,11 @@
import db from '@renderer/databases'
import FileManager from '@renderer/services/FileManager'
import { FileTypes } from '@renderer/types/file'
import { FileMetadata, FileTypes } from '@renderer/types'
import { NotesTreeNode } from '@renderer/types/note'
import { v4 as uuidv4 } from 'uuid'
const NOTES_FOLDER_PREFIX = 'notes'
const MARKDOWN_EXT = '.md'
export class NotesService {
private static readonly NOTES_STORAGE_KEY = 'notes-tree-structure'
@ -14,16 +16,79 @@ export class NotesService {
static async getNotesTree(): Promise<NotesTreeNode[]> {
try {
const storedTree = localStorage.getItem(this.NOTES_STORAGE_KEY)
if (storedTree) {
return JSON.parse(storedTree)
}
return []
const tree: NotesTreeNode[] = storedTree ? JSON.parse(storedTree) : []
await this.syncFileNames(tree)
return tree
} catch (error) {
console.error('Failed to get notes tree:', error)
return []
}
}
/**
*
*/
private static async syncFileNames(tree: NotesTreeNode[]): Promise<void> {
// 收集所有文件ID
const fileIds: string[] = []
this.collectFileIds(tree, fileIds)
if (fileIds.length === 0) return
try {
const filesMetadata = await Promise.all(fileIds.map((id) => FileManager.getFile(id)))
const metadataMap = new Map(filesMetadata.filter((file) => file !== null).map((file) => [file!.id, file]))
const hasChanges = this.updateFileNames(tree, metadataMap)
if (hasChanges) {
await this.saveNotesTree(tree)
}
} catch (error) {
console.error('Failed to sync file names:', error)
}
}
/**
* ID
*/
private static collectFileIds(tree: NotesTreeNode[], fileIds: string[]): void {
for (const node of tree) {
if (node.type === 'file' && node.fileId) {
fileIds.push(node.fileId)
}
if (node.children && node.children.length > 0) {
this.collectFileIds(node.children, fileIds)
}
}
}
/**
*
* @returns
*/
private static updateFileNames(tree: NotesTreeNode[], metadataMap: Map<string, any>): boolean {
let hasChanges = false
for (const node of tree) {
if (node.type === 'file' && node.fileId) {
const metadata = metadataMap.get(node.fileId)
if (metadata && metadata.origin_name !== node.name) {
node.name = metadata.origin_name
node.updatedAt = new Date().toISOString()
hasChanges = true
}
}
if (node.children && node.children.length > 0) {
const childChanges = this.updateFileNames(node.children, metadataMap)
hasChanges = hasChanges || childChanges
}
}
return hasChanges
}
/**
*
*/
@ -62,37 +127,43 @@ export class NotesService {
/**
*
* Markdown格式的文件noteId.md的格式存储
*/
static async createNote(name: string, content: string = '', parentId?: string): Promise<NotesTreeNode> {
const noteId = uuidv4()
const notePath = this.buildPath(name, parentId)
try {
// 创建临时文件并写入内容
const tempPath = await window.api.file.createTempFile(noteId)
await window.api.file.write(tempPath, content)
// 确保文件名是markdown格式
let displayName = name
if (!displayName.toLowerCase().endsWith(MARKDOWN_EXT)) {
displayName += MARKDOWN_EXT
}
// 通过FileManager上传文件
const fileMetadata = await FileManager.uploadFile({
try {
const fileMetadata: FileMetadata = {
id: noteId,
name,
origin_name: name,
path: tempPath,
name: noteId + MARKDOWN_EXT,
origin_name: displayName,
path: notePath,
size: content.length,
ext: '.md',
ext: MARKDOWN_EXT,
type: FileTypes.TEXT,
created_at: new Date().toISOString(),
count: 1
})
}
await window.api.file.writeWithId(fileMetadata.id + fileMetadata.ext, content)
await FileManager.addFile(fileMetadata)
// 创建树节点
const note: NotesTreeNode = {
id: noteId,
name,
name: displayName,
type: 'file',
path: notePath,
fileId: fileMetadata.id,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
fileId: noteId,
createdAt: fileMetadata.created_at,
updatedAt: fileMetadata.created_at
}
const tree = await this.getNotesTree()
@ -115,8 +186,12 @@ export class NotesService {
}
try {
// 直接使用文件ID读取
return await window.api.file.read(node.fileId)
const fileMetadata = await FileManager.getFile(node.fileId)
if (!fileMetadata) {
throw new Error('Note file not found in database')
}
return await window.api.file.read(fileMetadata.id + fileMetadata.ext)
} catch (error) {
console.error('Failed to read note:', error)
throw error
@ -132,9 +207,17 @@ export class NotesService {
}
try {
await window.api.file.writeWithId(node.fileId, content)
const fileMetadata = await FileManager.getFile(node.fileId)
if (!fileMetadata) {
throw new Error('Note file not found in database')
}
await window.api.file.writeWithId(fileMetadata.id + fileMetadata.ext, content)
await db.files.update(fileMetadata.id, {
size: content.length,
count: fileMetadata.count + 1
})
// 更新树结构中的修改时间
const tree = await this.getNotesTree()
const targetNode = this.findNodeInTree(tree, node.id)
if (targetNode) {
@ -159,10 +242,7 @@ export class NotesService {
}
try {
// 递归删除所有子节点的文件
await this.deleteNodeRecursively(node)
// 从树结构中移除节点
this.removeNodeFromTree(tree, nodeId)
await this.saveNotesTree(tree)
} catch (error) {
@ -182,9 +262,33 @@ export class NotesService {
throw new Error('Node not found')
}
node.name = newName
// 为文件类型自动添加.md后缀
let finalName = newName
if (node.type === 'file' && !finalName.toLowerCase().endsWith(MARKDOWN_EXT)) {
finalName += MARKDOWN_EXT
}
// 更新节点名称
node.name = finalName
node.updatedAt = new Date().toISOString()
// 如果是文件类型,还需要更新文件记录
if (node.type === 'file' && node.fileId) {
try {
// 获取文件元数据
const fileMetadata = await FileManager.getFile(node.fileId)
if (fileMetadata) {
// 更新文件的原始名称(显示名称)
await db.files.update(node.fileId, {
origin_name: finalName
})
}
} catch (error) {
console.error('Failed to update file metadata:', error)
throw error
}
}
await this.saveNotesTree(tree)
}
@ -225,10 +329,8 @@ export class NotesService {
throw new Error('Node not found')
}
// 从当前位置移除
this.removeNodeFromTree(tree, nodeId)
// 插入到新位置
this.insertNodeIntoTree(tree, node, newParentId)
await this.saveNotesTree(tree)
@ -309,10 +411,12 @@ export class NotesService {
*/
private static async deleteNodeRecursively(node: NotesTreeNode): Promise<void> {
if (node.type === 'file' && node.fileId) {
// 删除文件
await FileManager.deleteFile(node.fileId)
try {
await FileManager.deleteFile(node.fileId, true)
} catch (error) {
console.error(`Failed to delete file with id ${node.fileId}:`, error)
}
} else if (node.type === 'folder' && node.children) {
// 递归删除子节点
for (const child of node.children) {
await this.deleteNodeRecursively(child)
}