feat(grouping): 新增分组功能并优化 Markdown 文件操作
- 新增分组实体、控制器、服务和映射器 - 实现分组创建、获取、更新和删除接口 - 优化 Markdown 文件创建、获取和删除接口- 新增全局异常处理和日志记录 - 更新数据库表结构和字段类型 - 重构前端页面,支持分组和 Markdown 文件展示
This commit is contained in:
3530
biji-qianduan/package-lock.json
generated
3530
biji-qianduan/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kangc/v-md-editor": "^2.3.18",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
|
||||
|
||||
<template>
|
||||
<div id="app"></div>
|
||||
<div id="app">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
|
||||
|
||||
// 这里可以添加全局逻辑
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.vue:hover {
|
||||
filter: drop-shadow(0 0 2em #42b883aa);
|
||||
<style>
|
||||
/* 全局样式 */
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #2c3e50;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
266
biji-qianduan/src/components/HomePage.vue
Normal file
266
biji-qianduan/src/components/HomePage.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<el-container style="height: 100vh;">
|
||||
<!-- 左侧菜单栏 -->
|
||||
<el-aside width="300px" style="background-color: #f5f7fa; border-right: 1px solid #ebeef5;">
|
||||
<div class="sidebar-header">
|
||||
<h3>笔记分类</h3>
|
||||
<el-button @click="isCollapsed = !isCollapsed" size="small" type="text">
|
||||
{{ isCollapsed ? '展开' : '收起' }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-menu
|
||||
v-if="!isCollapsed"
|
||||
default-active="all"
|
||||
class="el-menu-vertical-demo"
|
||||
@select="handleMenuSelect"
|
||||
>
|
||||
<el-menu-item index="all">
|
||||
<span>全部笔记</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-submenu v-for="group in groupings" :key="group.id" :index="'group-'+group.id">
|
||||
<template #title>
|
||||
<span>{{ group.grouping }}</span>
|
||||
</template>
|
||||
<el-menu-item
|
||||
v-for="file in groupFiles[group.id] || []"
|
||||
:key="file.id"
|
||||
:index="'file-'+file.id"
|
||||
>
|
||||
{{ file.title }}
|
||||
</el-menu-item>
|
||||
</el-submenu>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<el-main style="padding: 20px;">
|
||||
<div class="header" v-if="!selectedFile">
|
||||
<h1>我的笔记</h1>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="showEditor = true">新建笔记</el-button>
|
||||
<el-upload
|
||||
action=""
|
||||
:show-file-list="false"
|
||||
:before-upload="handleMarkdownUpload"
|
||||
accept=".md"
|
||||
class="upload-btn"
|
||||
>
|
||||
<el-button type="success">上传Markdown</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedFile" class="file-preview">
|
||||
<div class="preview-header">
|
||||
<h2>{{ selectedFile.title }}</h2>
|
||||
<div>
|
||||
<el-button type="primary" @click="editNote(selectedFile)">编辑</el-button>
|
||||
<el-button @click="selectedFile = null">返回列表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="markdown-preview">
|
||||
<v-md-preview :text="selectedFile.content"></v-md-preview>
|
||||
</div>
|
||||
</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
|
||||
<el-dialog v-model="showEditor" title="Markdown编辑器" width="80%">
|
||||
<MarkdownEditor v-if="showEditor" :fileId="currentFileId" @close="showEditor = false" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import MarkdownEditor from './MarkdownEditor.vue';
|
||||
import VMdPreview from '@kangc/v-md-editor/lib/preview';
|
||||
import '@kangc/v-md-editor/lib/style/preview.css';
|
||||
import githubTheme from '@kangc/v-md-editor/lib/theme/github.js';
|
||||
import '@kangc/v-md-editor/lib/theme/style/github.css';
|
||||
|
||||
VMdPreview.use(githubTheme);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MarkdownEditor,
|
||||
[VMdPreview.name]: VMdPreview
|
||||
},
|
||||
setup() {
|
||||
const API_BASE_URL = 'http://localhost:8083';
|
||||
const markdownFiles = ref([]);
|
||||
const groupings = ref([]);
|
||||
const groupFiles = ref({});
|
||||
const showEditor = ref(false);
|
||||
const currentFileId = ref(null);
|
||||
const selectedFile = ref(null);
|
||||
const isCollapsed = ref(false);
|
||||
|
||||
// 获取所有分组
|
||||
const fetchGroupings = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/api/groupings`);
|
||||
groupings.value = response.data;
|
||||
|
||||
// 为每个分组获取文件
|
||||
for (const group of groupings.value) {
|
||||
const filesRes = await axios.get(`${API_BASE_URL}/api/markdown/grouping/${group.id}`);
|
||||
groupFiles.value[group.id] = filesRes.data;
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取分组失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取所有Markdown文件
|
||||
const fetchMarkdownFiles = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/api/markdown`);
|
||||
markdownFiles.value = response.data;
|
||||
} catch (error) {
|
||||
ElMessage.error('获取笔记列表失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑笔记
|
||||
const editNote = (file) => {
|
||||
currentFileId.value = file.id;
|
||||
showEditor.value = true;
|
||||
};
|
||||
|
||||
// 删除笔记
|
||||
const deleteNote = async (file) => {
|
||||
try {
|
||||
await axios.delete(`${API_BASE_URL}/api/markdown/${file.id}`);
|
||||
ElMessage.success('删除成功');
|
||||
fetchMarkdownFiles();
|
||||
fetchGroupings(); // 重新加载分组和文件
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 菜单选择处理
|
||||
const handleMenuSelect = (index) => {
|
||||
if (index === 'all') {
|
||||
selectedFile.value = null;
|
||||
} else if (index.startsWith('file-')) {
|
||||
const fileId = index.split('-')[1];
|
||||
const file = markdownFiles.value.find(f => f.id == fileId);
|
||||
if (file) {
|
||||
selectedFile.value = file;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 上传Markdown文件处理
|
||||
const handleMarkdownUpload = (file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
const content = e.target.result;
|
||||
const fileName = file.name.replace('.md', '');
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('userId', '1');
|
||||
params.append('title', fileName);
|
||||
params.append('fileName', fileName);
|
||||
|
||||
await axios.post(`${API_BASE_URL}/api/markdown`, content, {
|
||||
params: params,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
});
|
||||
|
||||
ElMessage.success('上传成功');
|
||||
fetchMarkdownFiles();
|
||||
fetchGroupings(); // 重新加载分组和文件
|
||||
} catch (error) {
|
||||
ElMessage.error('上传失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
return false; // 阻止默认上传行为
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchMarkdownFiles();
|
||||
fetchGroupings();
|
||||
});
|
||||
|
||||
return {
|
||||
markdownFiles,
|
||||
groupings,
|
||||
groupFiles,
|
||||
showEditor,
|
||||
currentFileId,
|
||||
selectedFile,
|
||||
isCollapsed,
|
||||
editNote,
|
||||
deleteNote,
|
||||
handleMarkdownUpload,
|
||||
handleMenuSelect
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-page {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
padding: 20px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.markdown-preview {
|
||||
min-height: 500px;
|
||||
padding: 20px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
141
biji-qianduan/src/components/MarkdownEditor.vue
Normal file
141
biji-qianduan/src/components/MarkdownEditor.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div class="markdown-editor">
|
||||
<div class="editor-header">
|
||||
<el-input v-model="title" placeholder="标题" class="title-input" />
|
||||
<el-input v-model="fileName" placeholder="文件名" class="file-input" />
|
||||
</div>
|
||||
|
||||
<v-md-editor
|
||||
v-model="content"
|
||||
height="500px"
|
||||
:disabled-menus="[]"
|
||||
@upload-image="handleImageUpload"
|
||||
></v-md-editor>
|
||||
|
||||
<div class="editor-footer">
|
||||
<el-button type="primary" @click="saveMarkdown">保存</el-button>
|
||||
<el-button @click="previewMarkdown">预览</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: 'MarkdownEditor',
|
||||
setup() {
|
||||
const title = ref('');
|
||||
const fileName = ref('');
|
||||
const content = ref('');
|
||||
const userId = ref(1); // 示例用户ID,实际应用中从登录信息获取
|
||||
const currentFileId = ref(null);
|
||||
|
||||
const saveMarkdown = async () => {
|
||||
try {
|
||||
const API_BASE_URL = 'http://localhost:8083';
|
||||
let response;
|
||||
|
||||
if (currentFileId.value) {
|
||||
// 更新已有文件:content作为请求体
|
||||
response = await axios.post(`${API_BASE_URL}/api/markdown/${currentFileId.value}`,
|
||||
content.value, {
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// 创建新文件:userId, title, fileName作为查询参数,content作为请求体
|
||||
const params = new URLSearchParams();
|
||||
params.append('userId', userId.value);
|
||||
params.append('title', title.value);
|
||||
params.append('fileName', fileName.value);
|
||||
|
||||
response = await axios.post(`${API_BASE_URL}/api/markdown`,
|
||||
content.value, {
|
||||
params: params,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
}
|
||||
);
|
||||
currentFileId.value = response.data.id;
|
||||
}
|
||||
|
||||
ElMessage.success('保存成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const previewMarkdown = () => {
|
||||
if (!currentFileId.value) {
|
||||
ElMessage.warning('请先保存文档');
|
||||
return;
|
||||
}
|
||||
window.open(`/api/markdown/${currentFileId.value}`, '_blank');
|
||||
};
|
||||
|
||||
const handleImageUpload = async (event, insertImage, files) => {
|
||||
const file = files[0];
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('userId', userId.value);
|
||||
formData.append('markdownId', currentFileId.value);
|
||||
|
||||
try {
|
||||
const API_BASE_URL = 'http://localhost:8083';
|
||||
const response = await axios.post(`${API_BASE_URL}/api/images`, formData, {
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
// 插入图片到编辑器
|
||||
insertImage({
|
||||
url: response.data.url,
|
||||
desc: response.data.originalName
|
||||
});
|
||||
} catch (error) {
|
||||
ElMessage.error('图片上传失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
title,
|
||||
fileName,
|
||||
content,
|
||||
saveMarkdown,
|
||||
previewMarkdown,
|
||||
handleImageUpload
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.markdown-editor {
|
||||
padding: 20px;
|
||||
}
|
||||
.editor-header {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.title-input {
|
||||
flex: 2;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.file-input {
|
||||
flex: 1;
|
||||
}
|
||||
.editor-footer {
|
||||
margin-top: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,21 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import VMdEditor from '@kangc/v-md-editor';
|
||||
import '@kangc/v-md-editor/lib/style/base-editor.css';
|
||||
import vuepressTheme from '@kangc/v-md-editor/lib/theme/vuepress.js';
|
||||
import '@kangc/v-md-editor/lib/theme/style/vuepress.css';
|
||||
|
||||
createApp(App).mount('#app')
|
||||
const app = createApp(App)
|
||||
|
||||
// 配置Markdown编辑器
|
||||
VMdEditor.use(vuepressTheme);
|
||||
app.use(VMdEditor);
|
||||
|
||||
// 使用Element Plus和路由
|
||||
app.use(ElementPlus)
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
33
biji-qianduan/src/router/index.js
Normal file
33
biji-qianduan/src/router/index.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import HomePage from '../components/HomePage.vue';
|
||||
import MarkdownEditor from '../components/MarkdownEditor.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/home'
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
name: 'Home',
|
||||
component: HomePage
|
||||
},
|
||||
{
|
||||
path: '/editor',
|
||||
name: 'Editor',
|
||||
component: MarkdownEditor
|
||||
},
|
||||
{
|
||||
path: '/editor/:id',
|
||||
name: 'EditMarkdown',
|
||||
component: MarkdownEditor,
|
||||
props: true
|
||||
}
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user