feat(功能): 增加笔记重命名和导出功能- 在笔记列表和预览页面添加重命名功能
- 实现笔记内容自动保存机制 -增加笔记导出为 Markdown 文件的功能 - 优化后端接口,支持更新笔记标题
This commit is contained in:
@@ -74,9 +74,25 @@ export const register = (data) => {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// 更新分组名称
|
||||
export const updateGroupingName = (id, newName) => {
|
||||
return axiosApi.put(`/api/groupings/${id}`, { grouping: newName });
|
||||
}
|
||||
|
||||
// 更新Markdown文件标题
|
||||
export const updateMarkdownTitle = (id, newTitle) => {
|
||||
return axiosApi.put(`/api/markdown/${id}/title`, newTitle, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -25,8 +25,11 @@
|
||||
<!-- 分组分类 -->
|
||||
<el-sub-menu v-for="group in groupings" :key="group.id" :index="`group-${group.id}`">
|
||||
<template #title>
|
||||
<el-icon><Folder /></el-icon>
|
||||
<span>{{ group.grouping }}</span>
|
||||
<div class="menu-item-title">
|
||||
<el-icon><Folder /></el-icon>
|
||||
<span>{{ group.grouping }}</span>
|
||||
<el-icon class="edit-icon" @click.stop="openRenameDialog(group, 'group')"><Edit /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<el-menu-item
|
||||
v-for="sub in jb22.filter(j => +j.parentId === +group.id)"
|
||||
@@ -46,13 +49,18 @@
|
||||
<el-main class="content">
|
||||
<div v-if="selectedFile" class="file-preview">
|
||||
<el-header class="preview-header">
|
||||
<h2>{{ selectedFile.title }}</h2>
|
||||
<h2 class="preview-title">
|
||||
{{ selectedFile.title }}
|
||||
<el-icon class="edit-icon" @click="openRenameDialog(selectedFile, 'file')"><Edit /></el-icon>
|
||||
</h2>
|
||||
<div class="actions">
|
||||
<el-button v-if="!showEditor" type="primary" @click="selectedFile = null">清空</el-button>
|
||||
<el-button v-if="!showEditor && userStore.isLoggedIn" type="primary" @click="editNote(selectedFile); isCollapsed = true">编辑</el-button>
|
||||
<el-button v-if="!showEditor && userStore.isLoggedIn" type="danger" @click="deleteNote(selectedFile)">删除</el-button>
|
||||
<el-button v-if="showEditor" type="primary" @click="showEditor = !showEditor; previewFile(editData)">返回</el-button>
|
||||
<el-button v-if="showEditor && userStore.isLoggedIn" type="success" @click="handleSave(vditor.getValue())">保存</el-button>
|
||||
<span v-if="showEditor" class="save-status">{{ saveStatus }}</span>
|
||||
<el-button v-if="!showEditor" type="success" @click="handleExportMd">导出为.md</el-button>
|
||||
</div>
|
||||
</el-header>
|
||||
<div v-if="!showEditor" v-html="previewHtml" class="markdown-preview"></div>
|
||||
@@ -167,13 +175,38 @@
|
||||
<el-button type="primary" @click="createNote">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 重命名对话框 -->
|
||||
<el-dialog v-model="showRenameDialog" title="重命名" width="400px">
|
||||
<el-input v-model="newName" placeholder="请输入新名称"></el-input>
|
||||
<template #footer>
|
||||
<el-button @click="showRenameDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleRename">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 导入选择分类对话框 -->
|
||||
<el-dialog v-model="showSelectGroupDialog" title="选择导入的分类" width="400px">
|
||||
<el-select v-model="importGroupId" placeholder="请选择分类">
|
||||
<el-option
|
||||
v-for="group in jb22"
|
||||
:key="group.id"
|
||||
:label="group.grouping"
|
||||
:value="group.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
<template #footer>
|
||||
<el-button @click="showSelectGroupDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmImport">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref, nextTick} from 'vue';
|
||||
import {onMounted, ref, nextTick, watch} from 'vue';
|
||||
import {ElMessage} from 'element-plus';
|
||||
import Vditor from 'vditor';
|
||||
import 'vditor/dist/index.css';
|
||||
@@ -185,9 +218,11 @@ import {
|
||||
markdownAll, markdownList,
|
||||
Preview,
|
||||
updateMarkdown, uploadImage,
|
||||
searchMarkdown
|
||||
searchMarkdown,
|
||||
updateGroupingName,
|
||||
updateMarkdownTitle
|
||||
} from '@/api/CommonApi.js'
|
||||
import { DArrowRight, Plus, Fold, Expand, Folder, Document, Search } from "@element-plus/icons-vue";
|
||||
import { DArrowRight, Plus, Fold, Expand, Folder, Document, Search, Edit } from "@element-plus/icons-vue";
|
||||
import { useUserStore } from '../stores/user';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
@@ -210,6 +245,12 @@ const activeMenu = ref('all');
|
||||
const isCollapsed = ref(false);
|
||||
const showCreateGroupDialog = ref(false);
|
||||
const showCreateNoteDialog = ref(false);
|
||||
const showRenameDialog = ref(false);
|
||||
const itemToRename = ref(null);
|
||||
const newName = ref('');
|
||||
const showSelectGroupDialog = ref(false);
|
||||
const importGroupId = ref(null);
|
||||
const fileToImport = ref(null);
|
||||
|
||||
const groupFormRef = ref(null);
|
||||
const newGroupForm = ref({ name: '', parentId: null });
|
||||
@@ -247,6 +288,8 @@ const jb22=ref([])
|
||||
// Vditor 实例
|
||||
const vditor = ref(null);
|
||||
const previewHtml = ref('');
|
||||
const saveStatus = ref('空闲');
|
||||
let debounceTimer = null;
|
||||
|
||||
const initVditor = () => {
|
||||
vditor.value = new Vditor('vditor', {
|
||||
@@ -257,6 +300,9 @@ const initVditor = () => {
|
||||
vditor.value.setValue(editData.value.content);
|
||||
}
|
||||
},
|
||||
input: (value) => {
|
||||
debouncedSave(value);
|
||||
},
|
||||
upload: {
|
||||
accept: 'image/*',
|
||||
handler(files) {
|
||||
@@ -438,16 +484,21 @@ const handleImageUpload=async (files) => {
|
||||
}
|
||||
|
||||
// 在编辑页面,按Ctrl+S保存笔记,或者点击保存,对数据进行保存
|
||||
const handleSave= async (file) => {
|
||||
imageUrls.value = extractImageUrls(file);
|
||||
const handleSave= async (content, isAutoSave = false) => {
|
||||
imageUrls.value = extractImageUrls(content);
|
||||
extractDeletedImageUrls(imageUrls.value)
|
||||
editData.value.content = file
|
||||
editData.value.content = content
|
||||
const filesRes = await updateMarkdown(editData.value);
|
||||
if (filesRes.code===200){
|
||||
ElMessage.success(filesRes.msg);
|
||||
if (!isAutoSave) {
|
||||
ElMessage.success(filesRes.msg);
|
||||
}
|
||||
await chushihua()
|
||||
}else {
|
||||
ElMessage.error(filesRes.msg);
|
||||
if (!isAutoSave) {
|
||||
ElMessage.error(filesRes.msg);
|
||||
}
|
||||
throw new Error(filesRes.msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,38 +577,83 @@ const deleteNote = async (file) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------TODO 下面的还没有还是看
|
||||
const debouncedSave = (content) => {
|
||||
saveStatus.value = '正在输入...';
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(async () => {
|
||||
saveStatus.value = '正在保存...';
|
||||
try {
|
||||
// 直接使用 vditor.value.getValue() 获取最新内容
|
||||
await handleSave(vditor.value.getValue(), true);
|
||||
saveStatus.value = '已保存';
|
||||
} catch (error) {
|
||||
saveStatus.value = '保存失败';
|
||||
}
|
||||
}, 2000); // 2-second delay
|
||||
};
|
||||
|
||||
const openRenameDialog = (item, type) => {
|
||||
itemToRename.value = { ...item, type };
|
||||
newName.value = type === 'group' ? item.grouping : item.title;
|
||||
showRenameDialog.value = true;
|
||||
};
|
||||
|
||||
const handleRename = async () => {
|
||||
if (!newName.value.trim()) {
|
||||
ElMessage.error('名称不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (itemToRename.value.type === 'group') {
|
||||
await updateGroupingName(itemToRename.value.id, newName.value);
|
||||
ElMessage.success('分类重命名成功');
|
||||
await fetchGroupings();
|
||||
} else if (itemToRename.value.type === 'file') {
|
||||
await updateMarkdownTitle(itemToRename.value.id, newName.value);
|
||||
ElMessage.success('笔记重命名成功');
|
||||
selectedFile.value.title = newName.value;
|
||||
await fetchMarkdownFiles();
|
||||
}
|
||||
showRenameDialog.value = false;
|
||||
} catch (error) {
|
||||
ElMessage.error('重命名失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 上传Markdown文件处理
|
||||
const handleMarkdownUpload = (file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
const content = e.target.result;
|
||||
const fileName = file.name.replace('.md', '');
|
||||
|
||||
try {
|
||||
await axios.post(`${API_BASE_URL}/api/markdown`, content, {
|
||||
params: {
|
||||
groupingId: 1, // 默认放入"全部"分类
|
||||
title: fileName,
|
||||
fileName: fileName
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
});
|
||||
|
||||
ElMessage.success('上传成功');
|
||||
await chushihua()
|
||||
} catch (error) {
|
||||
ElMessage.error('上传失败: ' + error.message);
|
||||
}
|
||||
reader.onload = (e) => {
|
||||
fileToImport.value = {
|
||||
content: e.target.result,
|
||||
title: file.name.replace('.md', ''),
|
||||
fileName: file.name,
|
||||
};
|
||||
showSelectGroupDialog.value = true;
|
||||
};
|
||||
reader.readAsText(file);
|
||||
return false; // 阻止默认上传行为
|
||||
};
|
||||
|
||||
const confirmImport = async () => {
|
||||
if (!importGroupId.value) {
|
||||
ElMessage.error('请选择一个分类');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await createMarkdown({
|
||||
...fileToImport.value,
|
||||
groupingId: importGroupId.value,
|
||||
});
|
||||
ElMessage.success('导入成功');
|
||||
showSelectGroupDialog.value = false;
|
||||
await chushihua();
|
||||
} catch (error) {
|
||||
ElMessage.error('导入失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
chushihua();
|
||||
// 根据屏幕宽度初始化侧边栏状态
|
||||
@@ -596,6 +692,19 @@ const handleSearch = async () => {
|
||||
ElMessage.error('搜索失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportMd = () => {
|
||||
if (!selectedFile.value) return;
|
||||
const blob = new Blob([selectedFile.value.content], { type: 'text/markdown;charset=utf-8' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `${selectedFile.value.title}.md`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -683,6 +792,35 @@ const handleSearch = async () => {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.save-status {
|
||||
margin-left: 10px;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.menu-item-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
cursor: pointer;
|
||||
margin-left: 8px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-sub-menu__title:hover .edit-icon,
|
||||
.preview-title:hover .edit-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-menu-vertical-demo {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
Reference in New Issue
Block a user