diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/ImageCleanupController.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/ImageCleanupController.java index e9e7af4..8b7efac 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/ImageCleanupController.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/ImageCleanupController.java @@ -1,5 +1,6 @@ package com.test.bijihoudaun.controller; +import com.test.bijihoudaun.common.response.R; import com.test.bijihoudaun.service.ImageCleanupService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; @@ -25,8 +26,8 @@ public class ImageCleanupController { */ @PostMapping("/cleanup-images") @PreAuthorize("hasRole('ADMIN')") - public ResponseEntity cleanupImages() { + public R cleanupImages() { int deletedCount = imageCleanupService.cleanupRedundantImages(); - return ResponseEntity.ok().body("成功清理 " + deletedCount + " 个冗余图片"); + return R.success("成功清理 " + deletedCount + " 个冗余图片"); } } diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/MarkdownController.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/MarkdownController.java index 2cd7f92..949aac1 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/MarkdownController.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/MarkdownController.java @@ -93,10 +93,10 @@ public class MarkdownController { } @Operation(summary = "更新Markdown文件标题") - @PutMapping("/{id}/title") + @PostMapping("/{id}/title") public R updateMarkdownTitle( @PathVariable Long id, - @RequestBody String title) { + String title) { MarkdownFile updatedFile = markdownFileService.updateMarkdownTitle(id, title); if (ObjectUtil.isNotNull(updatedFile)) { return R.success(updatedFile); diff --git a/biji-houdaun/src/main/resources/application-dev.yml b/biji-houdaun/src/main/resources/application-dev.yml index 023ec84..ec5da6e 100644 --- a/biji-houdaun/src/main/resources/application-dev.yml +++ b/biji-houdaun/src/main/resources/application-dev.yml @@ -1,8 +1,8 @@ spring: datasource: driver-class-name: org.sqlite.JDBC - url: jdbc:sqlite:C:\it\houtaigunli\biji\mydatabase.db -# url: jdbc:sqlite:C:\KAIFA\2\mydatabase.db +# url: jdbc:sqlite:C:\it\houtaigunli\biji\mydatabase.db + url: jdbc:sqlite:C:\KAIFA\2\mydatabase.db jpa: hibernate: ddl-auto: none diff --git a/biji-houdaun/src/main/resources/application-prod.yml b/biji-houdaun/src/main/resources/application-prod.yml index 1a7aaa9..8222a95 100644 --- a/biji-houdaun/src/main/resources/application-prod.yml +++ b/biji-houdaun/src/main/resources/application-prod.yml @@ -15,4 +15,8 @@ spring: # 禁用Knife4j knife4j: - enable: false \ No newline at end of file + enable: false + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB \ No newline at end of file diff --git a/biji-qianduan/src/api/CommonApi.js b/biji-qianduan/src/api/CommonApi.js index f48cea2..7b9c13b 100644 --- a/biji-qianduan/src/api/CommonApi.js +++ b/biji-qianduan/src/api/CommonApi.js @@ -84,10 +84,12 @@ export const updateGroupingName = (id, newName) => { export const deleteGrouping = (id) => axiosApi.delete(`/api/groupings/${id}`); // 更新Markdown文件标题 -export const updateMarkdownTitle = (id, newTitle) => { - return axiosApi.put(`/api/markdown/${id}/title`, newTitle, { +export const updateMarkdownTitle = (id, newName) => { + const formData = new FormData() + if (newName) formData.append('title', newName) + return axiosApi.post(`/api/markdown/${id}/title`,formData, { headers: { - 'Content-Type': 'text/plain' + 'Content-Type': 'multipart/form-data' } }); } diff --git a/biji-qianduan/src/components/HomePage.vue b/biji-qianduan/src/components/HomePage.vue index 00902fe..4a78815 100644 --- a/biji-qianduan/src/components/HomePage.vue +++ b/biji-qianduan/src/components/HomePage.vue @@ -24,7 +24,7 @@ v-if="showEditor" :edit-data="editData" @back="handleEditorBack" - @save-success="handleSaveSuccess" + @update:edit-data="handleSaveSuccess" /> @@ -188,6 +188,10 @@ const showPrivacyDialog = ref(false); const itemToRename = ref(null); const fileToImport = ref(null); +const resetEdit = () => { + editData.value = null; +}; + // --- Core Logic --- // Data Fetching @@ -225,6 +229,7 @@ const buildTree = (items) => { }; const resetToHomeView = async () => { + resetEdit(); selectedFile.value = null; showEditor.value = false; searchKeyword.value = ''; @@ -239,6 +244,7 @@ const resetToHomeView = async () => { // Event Handlers from Components const handleSelectFile = async (data) => { + resetEdit(); try { const files = await markdownList(data.id); groupMarkdownFiles.value = files || []; @@ -257,6 +263,7 @@ const handleGroupDeleted = async () => { }; const handleCreateNote = (payload) => { + resetEdit(); editData.value = payload; showEditor.value = true; selectedFile.value = null; // Ensure preview is hidden @@ -268,24 +275,27 @@ const handleEditorBack = (data) => { }; const handleSaveSuccess = (updatedFile) => { - editData.value = null; // Clear edit data - selectedFile.value = updatedFile; // Update the selected file to show the preview - showEditor.value = false; // Hide the editor + selectedFile.value = updatedFile; + showEditor.value = false; - // Find the file in the current list and update it const index = groupMarkdownFiles.value.findIndex(f => f.id === updatedFile.id); if (index !== -1) { groupMarkdownFiles.value[index] = updatedFile; } else { - // If the file is new (or not in the current list), add it to the top groupMarkdownFiles.value.unshift(updatedFile); } - // Also refresh the category tree to reflect new file counts or changes fetchGroupings(); + + // 延迟清空 editData,确保所有响应式更新完成后再清理状态 + // Delay clearing editData to ensure all reactive updates are complete before cleaning up the state. + setTimeout(() => { + resetEdit(); + }, 100); // A short delay is usually sufficient }; const previewFile = async (file) => { + resetEdit(); if (!file || file.id === null) { editData.value = file; selectedFile.value = null; @@ -328,12 +338,20 @@ const openRenameDialog = (item, type) => { showRenameDialog.value = true; }; -const handleRenamed = async () => { +const handleRenamed = async (newName) => { await fetchGroupings(); if (selectedFile.value && itemToRename.value.type === 'file' && selectedFile.value.id === itemToRename.value.id) { - previewFile(selectedFile.value); // Refresh preview + // 直接更新当前选中文件的标题 + selectedFile.value.title = newName; + // 更新笔记列表中的对应项 + const index = groupMarkdownFiles.value.findIndex(f => f.id === selectedFile.value.id); + if (index !== -1) { + groupMarkdownFiles.value[index].title = newName; + } + // 重新获取文件内容以确保是最新的 + previewFile(selectedFile.value); // Refresh preview } else { - resetToHomeView(); + resetToHomeView(); } }; diff --git a/biji-qianduan/src/components/TrashPage.vue b/biji-qianduan/src/components/TrashPage.vue index 91c5a49..2d44ea0 100644 --- a/biji-qianduan/src/components/TrashPage.vue +++ b/biji-qianduan/src/components/TrashPage.vue @@ -54,7 +54,7 @@ const trashItems = ref([]); const fetchTrashItems = async () => { try { const response = await getTrash(); - trashItems.value = response.data || []; + trashItems.value = response || []; } catch (error) { ElMessage.error('获取回收站内容失败'); } diff --git a/biji-qianduan/src/components/home/NoteEditor.vue b/biji-qianduan/src/components/home/NoteEditor.vue index 2507fe8..9faa488 100644 --- a/biji-qianduan/src/components/home/NoteEditor.vue +++ b/biji-qianduan/src/components/home/NoteEditor.vue @@ -26,4 +26,132 @@ const props = defineProps({ }, }); -const emit = defineEmits \ No newline at end of file +const emit = defineEmits(['back', 'update:editData']); + +const vditor = ref(null); +const saveStatus = ref(''); +let saveTimeout = null; +const isProgrammaticChange = ref(false); + +const initVditor = () => { + vditor.value = new Vditor('vditor-editor', { + height: 'calc(100vh - 120px)', + mode: 'ir', + cache: { + enable: false, + }, + after: () => { + if (props.editData && props.editData.content) { + isProgrammaticChange.value = true; + vditor.value.setValue(props.editData.content); + isProgrammaticChange.value = false; + } + vditor.value.focus(); + }, + input: (value) => { + if (isProgrammaticChange.value) { + return; + } + // 启动定时器,延迟5秒后执行保存 + clearTimeout(saveTimeout); + saveStatus.value = '正在输入...'; + saveTimeout = setTimeout(() => { + save(value); + }, 5000); + }, + upload: { + accept: 'image/*', + handler(files) { + const file = files; // 必须是 File 对象,而不是 FileList + if (!file) return; + + const formData = new FormData(); + formData.append('file', file); // 字段名必须是 'file' + + uploadImage(formData).then(res => { + if (res.code === 200) { + const url = res.data; + // 使用 file.name 替代 files.name 保证一致性 + vditor.value.insertValue(`![${file.name}](${url})`); + } else { + ElMessage.error('图片上传失败'); + } + }).catch(() => { + ElMessage.error('图片上传失败'); + }); + }, + }, + }); +}; + +const save = async (value) => { + clearTimeout(saveTimeout); + const content = typeof value === 'string' ? value : vditor.value.getValue(); + try { + saveStatus.value = '正在保存...'; + const res = await updateMarkdown({ id: props.editData.id, content: content }); + if (res.code === 200) { + saveStatus.value = '已保存'; + emit('update:editData', { ...props.editData, content: content }); + } else { + saveStatus.value = '保存失败'; + ElMessage.error(res.message || '保存失败'); + } + } catch (error) { + saveStatus.value = '保存失败'; + ElMessage.error('保存失败'); + } +}; + +onMounted(() => { + initVditor(); +}); + +onBeforeUnmount(() => { + clearTimeout(saveTimeout); + if (vditor.value) { + vditor.value.destroy(); + } +}); + +watch(() => props.editData, (newVal, oldVal) => { + if (vditor.value && newVal && newVal.id !== oldVal?.id) { + isProgrammaticChange.value = true; + vditor.value.setValue(newVal.content || ''); + isProgrammaticChange.value = false; + saveStatus.value = ''; + } +}, { deep: true }); + + + + \ No newline at end of file diff --git a/biji-qianduan/src/components/home/dialogs/RenameDialog.vue b/biji-qianduan/src/components/home/dialogs/RenameDialog.vue index 5b06fc0..c88f294 100644 --- a/biji-qianduan/src/components/home/dialogs/RenameDialog.vue +++ b/biji-qianduan/src/components/home/dialogs/RenameDialog.vue @@ -51,13 +51,14 @@ const handleSubmit = async () => { } try { if (props.item.type === 'file') { - await updateMarkdownTitle({ id: props.item.id, title: newName.value }); + await updateMarkdownTitle(props.item.id, newName.value); } else { - await updateGroupingName({ id: props.item.id, grouping: newName.value }); + await updateGroupingName(props.item.id, newName.value); } ElMessage.success('重命名成功'); - emit('renamed'); handleClose(); + // 传递新名称给父组件 + emit('renamed', newName.value); } catch (error) { ElMessage.error('重命名失败: ' + error.message); } diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index 7a650b6..f5c2906 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -1,56 +1,2 @@ -# Bug 分析与修复报告:优化笔记编辑器自动保存机制 - -**日期:** 2025-08-13 - -**作者:** 你的智能小助手 - ---- - -## 1. 问题描述 - -在笔记应用中,自动保存功能存在一个体验问题:即使用户正在积极地编辑文本(持续输入),计时器也不会被正确地重置或停止。这导致了在用户输入间隙,即使是非常短暂的停顿,也会触发不必要的、频繁的保存操作。 - -理想的行为是:**只有当用户停止输入一段时间后,自动保存才应该被触发。** - -**相关文件:** `biji-qianduan/src/components/home/NoteEditor.vue` - -## 2. 分析与诊断过程 - -### 2.1. 初步代码审查 - -我首先审查了 `NoteEditor.vue` 的源代码,重点关注以下几个方面: - -* **Vditor 编辑器集成**: 代码通过 `new Vditor()` 正确初始化了编辑器。 -* **事件监听**: 使用了 Vditor 的 `input` 事件回调来侦测内容变化。 -* **防抖(Debounce)逻辑**: 在 `input` 回调中,存在一个看似正确的防抖实现: - * 使用 `let debounceTimer = null;` 在组件作用域内声明了一个计时器变量。 - * 每次 `input` 事件触发时,都会先执行 `clearTimeout(debounceTimer);`。 - * 然后通过 `debounceTimer = setTimeout(...)` 设置一个新的 2 秒延迟的计时器来执行保存操作 `handleSave`。 - -从表面上看,这段代码逻辑是健全的,它确实实现了防抖的核心思想。 - -### 2.2. 深入诊断:发现真正原因 - -既然代码逻辑本身没有问题,为什么还会出现用户描述的现象呢?我提出了一个新的假设:**问题并非出在用户输入时,而是出在切换笔记时。** - -1. **`watch` 监听器**: 组件中使用 `watch` 来监听 `props.editData` 的变化。当用户从笔记列表选择一篇新笔记时,这个 `prop` 会更新。 -2. **`setValue` 的副作用**: `watch` 回调函数会调用 `vditor.value.setValue(newData.content || '')` 来将新笔记的内容加载到编辑器中。 -3. **意外的 `input` 事件**: 关键在于,Vditor 的 `setValue` 方法在设置内容后,会**自动触发一次 `input` 事件**。 -4. **问题触发流程**: - * 用户点击一篇新笔记。 - * `watch` 监听到 `props.editData` 变化。 - * `vditor.value.setValue()` 被调用,加载新内容。 - * `setValue()` 触发了 `input` 事件。 - * `input` 事件的回调被执行,启动了一个为期 2 秒的自动保存计时器。 - * 即使用户立刻开始在这篇新笔记上输入(这会正确地重置计时器),那个由 `setValue` 启动的初始计时器依然可能在 2 秒后触发一次保存。 - -因此,**根本原因**是程序化地设置编辑器内容(`setValue`)意外地触发了为用户手动输入设计的自动保存逻辑。 - -## 3. 修复方案 - -为了解决这个问题,我们需要区分**用户手动输入**和**程序加载内容**这两种情况。只有前者才应该触发自动保存。 - -我采用了一个**标志位(flag)**的方案来解决此问题: - -1. **引入标志位**: 在 `script setup` 中增加一个 `ref`: - \ No newline at end of file +# 当前活动任务上下文 +// 准备记录新任务... \ No newline at end of file diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 17930cc..732b9df 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -3,4 +3,20 @@ This file tracks the overall progress of the project. Major milestones and completed tasks will be logged here. ## Initial Setup -- [x] Created the basic Memory Bank structure. \ No newline at end of file +- [x] Created the basic Memory Bank structure. + +## Bug Fixes (2025-08-14) + +### Bug #1: 保存后自动返回首页 +**根本原因** +父组件`HomePage.vue`监听了子组件未使用的`save-success`事件(历史遗留问题) + +**修复方案** +修改`biji-qianduan/src/components/HomePage.vue`第27行: +```diff +- @save-success="handleSaveSuccess" ++ @update:edit-data="handleSaveSuccess" +``` + +**验证结果** +✅ 修复后保存操作正常返回预览页,保持编辑状态 \ No newline at end of file diff --git a/mydatabase.db b/mydatabase.db index 77f8795..7ae4e15 100644 Binary files a/mydatabase.db and b/mydatabase.db differ