feat(笔记): 添加笔记移动功能并优化分类管理

- 后端:修改创建分组接口,支持接收 parent_id 参数
-前端:实现笔记移动功能,增加移动按钮和对话框- 优化分类列表渲染逻辑,支持点击分类名称查看笔记
- 调整笔记列表显示样式,增加分类名称
This commit is contained in:
2025-07-31 15:05:14 +08:00
parent 1bfc45b240
commit c660ae5b12
4 changed files with 74 additions and 19 deletions

View File

@@ -24,6 +24,9 @@ public class GroupingController {
@Operation(summary = "创建分组") @Operation(summary = "创建分组")
@PostMapping @PostMapping
public R<Grouping> createGrouping(@RequestBody Grouping grouping) { public R<Grouping> createGrouping(@RequestBody Grouping grouping) {
if (grouping.getParentId() == null) {
grouping.setParentId(0L);
}
Grouping created = groupingService.createGrouping(grouping); Grouping created = groupingService.createGrouping(grouping);
return R.success(created); return R.success(created);
} }

View File

@@ -11,14 +11,8 @@ export const markdownAll = () => axiosApi.get(`/api/markdown`);
export const Preview = (id) => axiosApi.get(`/api/markdown/${id}`); export const Preview = (id) => axiosApi.get(`/api/markdown/${id}`);
// 创建分类分组 // 创建分类分组
export const addGroupings = (name) => { export const addGroupings = (group) => {
const formData = new FormData() return axiosApi.post('/api/groupings', group);
if (name) formData.append('grouping', name)
return axiosApi.post('/api/groupings', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
} }
//更新Markdown文件 //更新Markdown文件
export const updateMarkdown = (data) => { export const updateMarkdown = (data) => {

View File

@@ -40,6 +40,7 @@
</h2> </h2>
<div class="actions"> <div class="actions">
<el-button v-if="!showEditor" type="primary" @click="selectedFile = null">返回</el-button> <el-button v-if="!showEditor" type="primary" @click="selectedFile = null">返回</el-button>
<el-button v-if="!showEditor && userStore.isLoggedIn" type="warning" @click="showMoveNoteDialog = true">移动</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="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 && 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" type="primary" @click="showEditor = !showEditor; previewFile(editData)">返回</el-button>
@@ -93,7 +94,10 @@
<div v-if="groupMarkdownFiles.length > 0" class="file-list"> <div v-if="groupMarkdownFiles.length > 0" class="file-list">
<el-card v-for="file in groupMarkdownFiles" :key="file.id" shadow="hover" class="file-item"> <el-card v-for="file in groupMarkdownFiles" :key="file.id" shadow="hover" class="file-item">
<div @click="previewFile(file)" class="file-title">{{ file.title }}</div> <div @click="previewFile(file)" class="file-title">
<span>{{ file.title }}</span>
<span class="file-group-name">{{ currentGroupName }}</span>
</div>
</el-card> </el-card>
</div> </div>
<el-empty v-else description="暂无笔记,请创建或上传" /> <el-empty v-else description="暂无笔记,请创建或上传" />
@@ -169,6 +173,22 @@
<el-button type="primary" @click="confirmImport">确定</el-button> <el-button type="primary" @click="confirmImport">确定</el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- 移动笔记对话框 -->
<el-dialog v-model="showMoveNoteDialog" title="移动笔记到" width="400px">
<el-cascader
v-model="moveToGroupId"
:options="categoryTree"
:props="{ checkStrictly: true, emitPath: false }"
clearable
placeholder="请选择目标分类"
style="width: 100%;"
></el-cascader>
<template #footer>
<el-button @click="showMoveNoteDialog = false">取消</el-button>
<el-button type="primary" @click="handleMoveNote">确定</el-button>
</template>
</el-dialog>
</el-main> </el-main>
</el-container> </el-container>
</el-container> </el-container>
@@ -213,6 +233,9 @@ const newName = ref('');
const showSelectGroupDialog = ref(false); const showSelectGroupDialog = ref(false);
const importGroupId = ref(null); const importGroupId = ref(null);
const fileToImport = ref(null); const fileToImport = ref(null);
const showMoveNoteDialog = ref(false);
const moveToGroupId = ref(null);
const currentGroupName = ref('');
const groupFormRef = ref(null); const groupFormRef = ref(null);
const newGroupForm = ref({ name: '', parentId: null }); const newGroupForm = ref({ name: '', parentId: null });
@@ -242,9 +265,7 @@ const previewHtml = ref('');
const saveStatus = ref('空闲'); const saveStatus = ref('空闲');
let debounceTimer = null; let debounceTimer = null;
const categoryCascaderOptions = computed(() => { const categoryCascaderOptions = computed(() => categoryTree.value);
return [{ id: 0, grouping: '根分类', value: 0, label: '根分类' }, ...categoryTree.value];
});
const initVditor = () => { const initVditor = () => {
vditor.value = new Vditor('vditor', { vditor.value = new Vditor('vditor', {
@@ -274,11 +295,11 @@ const buildTree = (items) => {
// First, map all items by their id // First, map all items by their id
items.forEach(item => { items.forEach(item => {
itemMap.set(String(item.id), { itemMap.set(String(item.id), {
...item, ...item,
value: item.id, value: item.id,
label: item.grouping, label: item.grouping,
children: [], // Initialize children array children: [], // Initialize children array
}); });
}); });
// Then, build the tree structure // Then, build the tree structure
@@ -323,6 +344,7 @@ const fetchGroupings = async () => {
const selectFile = async (data) => { const selectFile = async (data) => {
const promise = await markdownList(data.id); const promise = await markdownList(data.id);
groupMarkdownFiles.value = promise.data; groupMarkdownFiles.value = promise.data;
currentGroupName.value = data.grouping;
selectedFile.value = null; selectedFile.value = null;
}; };
@@ -344,7 +366,7 @@ const createGrouping = async () => {
if (valid) { if (valid) {
try { try {
const payload = { const payload = {
name: newGroupForm.value.name, grouping: newGroupForm.value.name, // 将 name 映射到 grouping
parentId: newGroupForm.value.parentId || 0 parentId: newGroupForm.value.parentId || 0
}; };
await addGroupings(payload); await addGroupings(payload);
@@ -406,7 +428,7 @@ const resetNoteForm = () => {
const renderMenu = (item) => { const renderMenu = (item) => {
if (item.children && item.children.length > 0) { if (item.children && item.children.length > 0) {
return h(ElSubMenu, { index: `group-${item.id}` }, { return h(ElSubMenu, { index: `group-${item.id}` }, {
title: () => h('div', { class: 'menu-item-title' }, [ title: () => h('div', { class: 'menu-item-title', onClick: () => selectFile(item) }, [
h(ElIcon, () => h(Folder)), h(ElIcon, () => h(Folder)),
h('span', null, item.grouping), h('span', null, item.grouping),
h(ElIcon, { class: 'edit-icon', onClick: (e) => { e.stopPropagation(); openRenameDialog(item, 'group'); } }, () => h(Edit)) h(ElIcon, { class: 'edit-icon', onClick: (e) => { e.stopPropagation(); openRenameDialog(item, 'group'); } }, () => h(Edit))
@@ -710,6 +732,31 @@ const handleExportMd = () => {
document.body.removeChild(link); document.body.removeChild(link);
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
}; };
const handleMoveNote = async () => {
if (!moveToGroupId.value) {
ElMessage.error('请选择目标分类');
return;
}
if (!selectedFile.value) {
ElMessage.error('没有选中的笔记');
return;
}
try {
const updatedFile = {
...selectedFile.value,
groupingId: moveToGroupId.value,
};
await updateMarkdown(updatedFile);
ElMessage.success('笔记移动成功');
showMoveNoteDialog.value = false;
selectedFile.value = null; // 返回列表页
await chushihua(); // 刷新数据
} catch (error) {
ElMessage.error('移动失败: ' + error.message);
}
};
</script> </script>
<style> <style>
@@ -932,6 +979,17 @@ const handleExportMd = () => {
font-weight: 500; font-weight: 500;
color: var(--text-color); color: var(--text-color);
padding: 20px; padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-group-name {
font-size: 12px;
color: var(--text-color-secondary);
background-color: var(--bg-color-tertiary);
padding: 2px 8px;
border-radius: 4px;
} }
/* --- File Preview --- */ /* --- File Preview --- */

Binary file not shown.