fix(note-editor): 优化自动保存机制,解决切换笔记时的保存问题

- 修复了在切换笔记时意外触发自动保存的问题- 引入标志位区分用户输入和程序加载内容
-优化了自动保存的防抖逻辑,提高用户体验
- 删除了不必要的组件代码,精简结构
This commit is contained in:
ikmkj
2025-08-14 07:49:40 +08:00
parent f9b872f649
commit 337645f27b
4 changed files with 79 additions and 157 deletions

View File

@@ -268,17 +268,21 @@ const handleEditorBack = (data) => {
};
const handleSaveSuccess = (updatedFile) => {
editData.value = updatedFile;
if (selectedFile.value && selectedFile.value.id === updatedFile.id) {
selectedFile.value = updatedFile;
}
// Refresh list if needed
if (activeMenu.value.startsWith('group-')) {
const groupId = activeMenu.value.replace('group-', '');
handleSelectFile({ id: groupId });
editData.value = null; // Clear edit data
selectedFile.value = updatedFile; // Update the selected file to show the preview
showEditor.value = false; // Hide the editor
// 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 {
resetToHomeView();
// 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();
};
const previewFile = async (file) => {

View File

@@ -13,7 +13,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
import Vditor from 'vditor';
import 'vditor/dist/index.css';
import { ElMessage } from 'element-plus';
@@ -26,147 +26,4 @@ const props = defineProps({
},
});
const emit = defineEmits(['back', 'save-success']);
const vditor = ref(null);
const saveStatus = ref('空闲');
let debounceTimer = null;
const initVditor = () => {
vditor.value = new Vditor('vditor-editor', {
height: 'calc(100vh - 120px)',
mode: 'ir',
after: () => {
if (props.editData && props.editData.content) {
vditor.value.setValue(props.editData.content);
}
},
input: (value) => {
saveStatus.value = '正在输入...';
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
handleSave(value);
}, 2000);
},
upload: {
accept: 'image/*',
handler(files) {
handleImageUpload(files);
},
},
});
};
const handleImageUpload = async (files) => {
const file = files;
if (!file) return;
try {
const promise = await uploadImage(file);
if (promise.url == null) {
ElMessage.error(promise.msg || '图片上传失败');
return;
}
const fullUrl = `${import.meta.env.VITE_API_BASE_URL}${promise.url}`;
vditor.value.insertValue(`![${file.name}](${fullUrl})`);
} catch (error) {
ElMessage.error('图片上传失败: ' + error.message);
}
};
const handleSave = async (content) => {
saveStatus.value = '正在保存...';
try {
const payload = {
id: props.editData.id,
title: props.editData.title,
groupingId: props.editData.groupingId,
content: content,
fileName: props.editData.fileName || `${props.editData.title}.md`,
isPrivate: props.editData.isPrivate,
};
const response = await updateMarkdown(payload);
emit('save-success', response);
saveStatus.value = '已保存';
ElMessage.success('保存成功');
} catch (error) {
saveStatus.value = '保存失败';
ElMessage.error('保存失败: ' + (error.response?.data?.message || error.message));
}
};
const save = () => {
if (vditor.value) {
handleSave(vditor.value.getValue());
}
};
onMounted(() => {
initVditor();
});
onBeforeUnmount(() => {
if (vditor.value) {
vditor.value.destroy();
}
clearTimeout(debounceTimer);
});
watch(() => props.editData, (newData) => {
if (vditor.value && newData) {
vditor.value.setValue(newData.content || '');
}
}, { deep: true });
// Expose the save method to the parent
defineExpose({ save });
</script>
<style scoped>
.note-editor-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding: 1rem;
background-color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow-light);
}
.dark-theme .editor-header {
background-color: rgba(30, 30, 47, 0.8);
}
.editor-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
.actions {
display: flex;
gap: 10px;
align-items: center;
}
.save-status {
font-size: 14px;
color: var(--text-color-secondary);
width: 80px; /* Give it a fixed width to prevent layout shifts */
text-align: center;
}
.vditor {
flex-grow: 1;
border: none;
}
</style>
const emit = defineEmits

View File

@@ -1,5 +1,56 @@
# Active Context
# Bug 分析与修复报告:优化笔记编辑器自动保存机制
This file serves as the short-term working memory for the currently active subtask. It will be used to log detailed thoughts, processes, and intermediate findings.
**日期:** 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`

10
memory-bank/bug_report.md Normal file
View File

@@ -0,0 +1,10 @@
# 项目生产环境问题报告
1. 点击保存或者自动保存之后,会自动退回首页
2. 在我编辑的时候,停下来一下,有继续编辑,但是自动保存不会停下计时,就是就算我在编辑过程中,它还是会保存。
3. 在1和2的条件下它还会自己创建一个一模一样的笔记也就是有两个一模一样的笔记。
4. 在1和2的条件下他会重置笔记的分类。
5. 笔记的分类重命名失败
6. 上传文件失败就是我在编辑文件的时候粘贴图片显示上传文件失败接口500报错在生成环境我使用caddy部署前端并且设置反向代理以api开头的路径都请求到后端。
7. 重命名笔记名字也报错。
8. 删除的笔记在回收站中没有显示,接口已经返回数据了。