Files
biji/biji-qianduan/src/components/home/NoteEditor.vue
ikmkj 25b52f87aa refactor: 统一错误处理并优化代码
- 移除重复的错误提示,统一在axios拦截器中处理
- 优化XSS拦截器,添加请求头白名单
- 修复注册码服务的日期处理问题
- 添加403权限错误处理
- 优化分组查询参数处理
2026-03-03 23:41:20 +08:00

239 lines
6.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="note-editor-wrapper">
<el-header class="editor-header">
<h2 class="editor-title">{{ editData.title }}</h2>
<div class="actions">
<el-button type="primary" @click="handleBack">返回</el-button>
<el-button type="success" @click="save">保存</el-button>
<span class="save-status">{{ saveStatus }}</span>
</div>
</el-header>
<div id="vditor-editor" class="vditor" />
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
import Vditor from 'vditor';
import 'vditor/dist/index.css';
import { ElMessage } from 'element-plus';
import { updateMarkdown, uploadImage } from '@/api/CommonApi.js';
const props = defineProps({
editData: {
type: Object,
required: true,
},
});
const emit = defineEmits(['back', 'update:editData']);
const vditor = ref(null);
const currentId = ref(null);
const isInitialized = ref(false);
const saveStatus = ref('');
let saveTimeout = null;
let lastSavedContent = ref('');
let isSaving = ref(false);
// 维护当前最新的笔记数据
const currentData = ref({ ...props.editData });
const initVditor = () => {
if (vditor.value) {
vditor.value.destroy();
}
vditor.value = new Vditor('vditor-editor', {
height: 'calc(100vh - 120px)',
mode: 'ir',
cache: {
enable: false,
},
after: () => {
isInitialized.value = true;
if (currentData.value && currentData.value.content) {
vditor.value.setValue(currentData.value.content);
lastSavedContent.value = currentData.value.content;
}
if (currentData.value && currentData.value.id) {
currentId.value = currentData.value.id;
}
vditor.value.focus();
},
input: (value) => {
if (!isInitialized.value) return;
clearTimeout(saveTimeout);
saveStatus.value = '正在输入...';
saveTimeout = setTimeout(() => {
if (!isSaving.value && value !== lastSavedContent.value) {
save(value);
}
}, 3000);
},
upload: {
accept: 'image/*',
handler(files) {
const file = files[0];
if (!file) return;
uploadImage(file).then(res => {
const url = res.url;
const baseUrl = import.meta.env.VITE_API_BASE_URL || '';
vditor.value.insertValue(`![${file.name}](${baseUrl}${url})`);
}).catch((error) => {
// 错误已在 axios 拦截器中显示,这里不再重复显示
console.error('图片上传失败:', error);
});
},
},
});
};
const save = async (value) => {
if (isSaving.value) return;
// 修复:添加空值检查
if (!vditor.value) {
console.warn('编辑器未初始化');
return;
}
clearTimeout(saveTimeout);
const content = typeof value === 'string' ? value : vditor.value?.getValue() || '';
if (content === lastSavedContent.value && currentId.value) {
return;
}
isSaving.value = true;
try {
saveStatus.value = '正在保存...';
// 确保groupingId不会丢失优先使用currentData中的值
const groupingId = currentData.value.groupingId || props.editData.groupingId;
// 将ID转为字符串以避免JavaScript精度丢失
const idString = currentId.value ? String(currentId.value) : (currentData.value.id ? String(currentData.value.id) : null);
const groupingIdString = groupingId ? String(groupingId) : null;
const payload = {
id: idString,
content: content,
title: currentData.value.title || props.editData.title,
groupingId: groupingIdString,
fileName: currentData.value.fileName || props.editData.fileName,
isPrivate: currentData.value.isPrivate !== undefined ? currentData.value.isPrivate : props.editData.isPrivate
};
const response = await updateMarkdown(payload);
if (response && response.id) {
currentId.value = response.id;
lastSavedContent.value = content;
// 使用后端返回的数据但确保groupingId不会丢失
// 注意后端返回的ID是字符串保持字符串格式避免精度丢失
const updatedFile = {
...response,
content: content,
// 如果后端返回的groupingId为空使用原来的值保持字符串格式
groupingId: response.groupingId || groupingIdString,
groupingName: response.groupingName || currentData.value.groupingName
};
// 更新currentData为最新数据
currentData.value = updatedFile;
emit('update:editData', updatedFile);
saveStatus.value = '已保存';
}
} catch (error) {
saveStatus.value = '保存失败';
// 错误已在 axios 拦截器中显示,这里不再重复显示
console.error('保存失败:', error);
} finally {
isSaving.value = false;
}
};
const handleBack = async () => {
const content = vditor.value ? vditor.value.getValue() : '';
if (content !== lastSavedContent.value && !isSaving.value) {
await save(content);
}
// 确保groupingId不会丢失保持字符串格式
const groupingId = currentData.value.groupingId || props.editData.groupingId;
const groupingName = currentData.value.groupingName || props.editData.groupingName;
const returnData = {
...currentData.value,
...props.editData,
id: currentId.value ? String(currentId.value) : (currentData.value.id ? String(currentData.value.id) : null),
content: content,
groupingId: groupingId ? String(groupingId) : null,
groupingName: groupingName
};
emit('back', returnData);
};
onMounted(() => {
initVditor();
});
onBeforeUnmount(() => {
clearTimeout(saveTimeout);
if (vditor.value) {
vditor.value.destroy();
vditor.value = null;
}
currentId.value = null;
isInitialized.value = false;
});
watch(() => props.editData, (newVal, oldVal) => {
if (vditor.value && isInitialized.value && newVal && newVal.id !== oldVal?.id) {
// 更新currentData为最新的props数据
currentData.value = { ...newVal };
vditor.value.setValue(newVal.content || '');
lastSavedContent.value = newVal.content || '';
currentId.value = newVal.id;
saveStatus.value = '';
}
}, { deep: true });
</script>
<style scoped>
.note-editor-wrapper {
height: 100%;
display: flex;
flex-direction: column;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
border-bottom: 1px solid #dcdfe6;
}
.editor-title {
margin: 0;
font-size: 20px;
}
.actions .save-status {
margin-left: 10px;
color: #909399;
}
.vditor {
flex-grow: 1;
border: none;
}
</style>