feat(security): 添加 JWT 认证功能

- 在后端添加 JWT 认证过滤器 JwtAuthenticationTokenFilter
- 创建 JwtTokenUtil 工具类用于生成和验证 JWT token
- 在 application.yml 中配置 JWT 相关参数
- 更新前端 HomePage 组件,增加用户认证相关逻辑
This commit is contained in:
2025-07-31 09:27:13 +08:00
parent 4e0977de85
commit ab4891d8db
14 changed files with 773 additions and 260 deletions

View File

@@ -1,76 +1,67 @@
<template>
<div class="home-page">
<div class="container">
<!-- 左侧菜单区域 -->
<div v-if="!isCollapsed" class="sidebar">
<div class="sidebar-header">
<span v-if="!isCollapsed" style="margin-right: 15px">笔记分类</span>
<el-button v-if="!isCollapsed" type="primary" size="small" @click="showCreateGroupDialog = true">
新建分类
</el-button>
<el-button v-if="!isCollapsed" @click="isCollapsed=!isCollapsed" type="primary" size="small">
收起
</el-button>
</div>
<el-menu
:default-active="activeMenu"
class="el-menu-vertical-demo"
:collapse="isCollapsed"
popper-effect="light"
collapse-transition
>
<!-- 分组分类 -->
<el-sub-menu v-for="group in groupings" :key="group.id" :index="`group-${group.id}`">
<template #title>
<span>{{ group.grouping }}</span>
</template>
<el-menu-item v-for="sub in jb22.filter(j => +j.parentId === +group.id)"
:key="sub.id"
:index="`sub-${sub.id}`"
@click="selectFile(sub);selectedFile=null"
>{{ sub.grouping }}</el-menu-item>
</el-sub-menu>
</el-menu>
<el-container class="home-page">
<!-- 左侧菜单区域 -->
<el-aside :width="isCollapsed ? '64px' : '250px'" class="sidebar">
<div class="sidebar-header">
<span v-if="!isCollapsed" style="margin-right: 15px; font-weight: bold;">笔记分类</span>
<el-button v-if="!isCollapsed" type="primary" size="small" @click="showCreateGroupDialog = true" circle>
<el-icon><Plus /></el-icon>
</el-button>
<el-button @click="isCollapsed = !isCollapsed" type="primary" size="small" circle>
<el-icon>
<Fold v-if="!isCollapsed" />
<Expand v-else />
</el-icon>
</el-button>
</div>
<el-icon :size="25" style="margin-top: 20px;margin-left: 10px" v-if="isCollapsed" @click="isCollapsed=!isCollapsed" ><DArrowRight /></el-icon>
<el-menu
:default-active="activeMenu"
class="el-menu-vertical-demo"
:collapse="isCollapsed"
popper-effect="light"
:collapse-transition="false"
>
<!-- 分组分类 -->
<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>
</template>
<el-menu-item
v-for="sub in jb22.filter(j => +j.parentId === +group.id)"
:key="sub.id"
:index="`sub-${sub.id}`"
@click="selectFile(sub); selectedFile = null"
>
<el-icon><Document /></el-icon>
{{ sub.grouping }}
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>
<!-- 右侧内容区域 -->
<div class="content">
<!-- 右侧内容区域 -->
<el-container>
<el-main class="content">
<div v-if="selectedFile" class="file-preview">
<div class="preview-header">
<el-header class="preview-header">
<h2>{{ selectedFile.title }}</h2>
<div class="actions">
<el-button v-if="!showEditor" type="primary" @click="selectedFile=null">清空</el-button>
<el-button v-if="!showEditor" type="primary" @click="editNote(selectedFile);isCollapsed=true">编辑</el-button>
<el-button v-if="!showEditor" 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="success" @click="handleSave(editData.content)">保存</el-button>
<el-button v-if="!showEditor" type="primary" @click="selectedFile = null">清空</el-button>
<el-button v-if="!showEditor" type="primary" @click="editNote(selectedFile); isCollapsed = true">编辑</el-button>
<el-button v-if="!showEditor" 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="success" @click="handleSave(vditor.getValue())">保存</el-button>
</div>
</div>
<v-md-preview
v-if="!showEditor"
:text="selectedFile.content"
class="markdown-preview"
@copy-code-success="handleCopyCodeSuccess"
></v-md-preview>
<!-- Markdown编辑器 -->
<v-md-editor
v-if="showEditor"
v-model="editData.content"
height="500px"
@upload-image="handleImageUpload"
@save="handleSave"
:disabled-menus="[]"
@copy-code-success="handleCopyCodeSuccess"
></v-md-editor>
</el-header>
<div v-if="!showEditor" v-html="previewHtml" class="markdown-preview"></div>
<!-- Vditor 编辑器 -->
<div v-show="showEditor" id="vditor" class="vditor" />
</div>
<div v-else>
<div class="header">
<el-header class="header">
<h1>我的笔记</h1>
<div class="actions">
<el-button type="primary" @click="showCreateNoteDialog = true">新建笔记</el-button>
@@ -84,78 +75,87 @@
<el-button type="success">上传Markdown</el-button>
</el-upload>
</div>
</div>
</el-header>
<div v-if="groupMarkdownFiles.length > 0" class="file-list">
<div v-for="file in groupMarkdownFiles" :key="file.id" 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>
</el-card>
</div>
<div v-else class="empty-tip">暂无笔记请创建或上传</div>
<el-empty v-else description="暂无笔记,请创建或上传" />
</div>
</div>
</div>
<!-- 分类创建对话框 -->
<el-dialog v-model="showCreateGroupDialog" title="新建分类" width="30%">
<el-form :model="newGroupForm" label-width="80px">
<el-switch v-model="isGroup1" active-text="一级分类" inactive-text="二级分类" style="margin-bottom: 20px;margin-left:30%" />
<el-form-item label="一级名称">
<el-input v-model="newGroupForm.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="一级名称">
<el-input v-model="newGroupForm.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item v-if="!isGroup1" label="二级名称">
<el-input v-model="newGroupForm.name" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showCreateGroupDialog = false">取消</el-button>
<el-button type="primary" @click="createGrouping">确定</el-button>
</template>
</el-dialog>
<!-- 分类创建对话框 -->
<el-dialog v-model="showCreateGroupDialog" title="新建分类" width="400px" @close="resetGroupForm">
<el-form :model="newGroupForm" :rules="groupFormRules" ref="groupFormRef" label-width="80px">
<el-form-item label="分类级别">
<el-radio-group v-model="isGroup1">
<el-radio :label="true">一级分类</el-radio>
<el-radio :label="false">二级分类</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="!isGroup1" label="父级分类" prop="parentId">
<el-select v-model="newGroupForm.parentId" placeholder="请选择父级分类">
<el-option
v-for="group in groupings"
:key="group.id"
:label="group.grouping"
:value="group.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="分类名称" prop="name">
<el-input v-model="newGroupForm.name" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showCreateGroupDialog = false">取消</el-button>
<el-button type="primary" @click="createGrouping">确定</el-button>
</template>
</el-dialog>
<!-- 笔记创建对话框 -->
<el-dialog v-model="showCreateNoteDialog" title="新建笔记" width="30%">
<el-form :model="newNoteForm" label-width="80px">
<el-form-item label="笔记标题">
<el-input v-model="newNoteForm.title" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="选择大分类">
<el-select v-model="fenlei1" :change="getjb2()" placeholder="请选择">
<el-option
v-for="item in groupings"
:key="item.id"
:label="item.grouping"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="选择分类">
<el-select v-model="newNoteForm.groupingId" placeholder="请选择">
<el-option
v-for="group in fenlei2"
:key="group.id"
:label="group.grouping"
:value="group.id"
></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showCreateNoteDialog = false">取消</el-button>
<el-button type="primary" @click="createNote">确定</el-button>
</template>
</el-dialog>
</div>
<!-- 笔记创建对话框 -->
<el-dialog v-model="showCreateNoteDialog" title="新建笔记" width="400px" @close="resetNoteForm">
<el-form :model="newNoteForm" :rules="noteFormRules" ref="noteFormRef" label-width="80px">
<el-form-item label="笔记标题" prop="title">
<el-input v-model="newNoteForm.title" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="选择大分类" prop="parentId">
<el-select v-model="fenlei1" placeholder="请选择" @change="getjb2">
<el-option
v-for="item in groupings"
:key="item.id"
:label="item.grouping"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="选择分类" prop="groupingId">
<el-select v-model="newNoteForm.groupingId" placeholder="请选择">
<el-option
v-for="group in fenlei2"
:key="group.id"
:label="group.grouping"
:value="group.id"
></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showCreateNoteDialog = false">取消</el-button>
<el-button type="primary" @click="createNote">确定</el-button>
</template>
</el-dialog>
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import {onMounted, ref} from 'vue';
import {onMounted, ref, nextTick} from 'vue';
import {ElMessage} from 'element-plus';
import '@kangc/v-md-editor/lib/style/preview.css';
import '@kangc/v-md-editor/lib/theme/style/github.css';
import Vditor from 'vditor';
import 'vditor/dist/index.css';
import {
addGroupings,
deleteImages, deleteMarkdown,
@@ -165,7 +165,7 @@ import {
Preview,
updateMarkdown, uploadImage
} from '@/api/CommonApi.js'
import {DArrowRight} from "@element-plus/icons-vue";
import { DArrowRight, Plus, Fold, Expand, Folder, Document } from "@element-plus/icons-vue";
const isGroup1=ref(true)
// 创建新文件中大分类的信息
@@ -182,14 +182,29 @@ const activeMenu = ref('all');
const isCollapsed = ref(false);
const showCreateGroupDialog = ref(false);
const showCreateNoteDialog = ref(false);
const newGroupForm = ref({ name: '' });
const groupFormRef = ref(null);
const newGroupForm = ref({ name: '', parentId: null });
const groupFormRules = ref({
name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }],
parentId: [{ required: true, message: '请选择父级分类', trigger: 'change' }],
});
const noteFormRef = ref(null);
const newNoteForm = ref({
id: null,
title: '',
groupingId: null ,
groupingId: null,
parentId: null,
fileName: '',
content: ''
});
const noteFormRules = ref({
title: [{ required: true, message: '请输入笔记标题', trigger: 'blur' }],
parentId: [{ required: true, message: '请选择大分类', trigger: 'change' }],
groupingId: [{ required: true, message: '请选择二级分类', trigger: 'change' }],
});
// 创建新笔记的多级菜单
const options=ref([])
// 编辑笔记的数据
@@ -201,6 +216,28 @@ const originalImages = ref([]);
// 分类为二级的数据
const jb22=ref([])
// Vditor 实例
const vditor = ref(null);
const previewHtml = ref('');
const initVditor = () => {
vditor.value = new Vditor('vditor', {
height: 'calc(100vh - 120px)',
mode: 'ir', // 即时渲染模式
after: () => {
if (editData.value) {
vditor.value.setValue(editData.value.content);
}
},
upload: {
accept: 'image/*',
handler(files) {
handleImageUpload(files);
},
},
});
};
// 创建md文件时通过大分类获取二级分类
const getjb2 = async () => {
if (fenlei1.value != null) {
@@ -246,11 +283,6 @@ const selectFile = async (data) => {
groupMarkdownFiles.value=promise.data
};
// 代码块复制成功回调
const handleCopyCodeSuccess = () => {
ElMessage.success('代码已复制到剪贴板');
};
// 获取所有Markdown文件确保ID为字符串
const fetchMarkdownFiles = async () => {
try {
@@ -267,29 +299,57 @@ const fetchMarkdownFiles = async () => {
// 创建新分类
const createGrouping = async () => {
// TODO 添加分类创建逻辑
try {
const response = await addGroupings(newGroupForm.value.name)
ElMessage.success('分类创建成功');
showCreateGroupDialog.value = false;
newGroupForm.value.name = '';
await fetchGroupings();
} catch (error) {
ElMessage.error('创建分类失败: ' + error.message);
if (!groupFormRef.value) return;
await groupFormRef.value.validate(async (valid) => {
if (valid) {
try {
const response = await addGroupings(newGroupForm.value)
ElMessage.success('分类创建成功');
showCreateGroupDialog.value = false;
await fetchGroupings();
} catch (error) {
ElMessage.error('创建分类失败: ' + error.message);
}
}
});
};
// 重置新建分类表单
const resetGroupForm = () => {
newGroupForm.value = { name: '', parentId: null };
if (groupFormRef.value) {
groupFormRef.value.resetFields();
}
};
// 创建新笔记
const createNote = async () => {
try {
newNoteForm.value.fileName = newNoteForm.value.title+'.md'
editData.value=newNoteForm.value
console.log(editData.value)
showCreateNoteDialog.value = false
showEditor.value = true;
selectedFile.value=editData.value
} catch (error) {
ElMessage.error('创建笔记失败: ' + error.message);
if (!noteFormRef.value) return;
await noteFormRef.value.validate(async (valid) => {
if (valid) {
try {
newNoteForm.value.fileName = newNoteForm.value.title+'.md'
editData.value=newNoteForm.value
showCreateNoteDialog.value = false
showEditor.value = true;
selectedFile.value=editData.value
await nextTick(() => {
initVditor();
});
} catch (error) {
ElMessage.error('创建笔记失败: ' + error.message);
}
}
});
};
// 重置新建笔记表单
const resetNoteForm = () => {
newNoteForm.value = { id: null, title: '', groupingId: null, parentId: null, fileName: '', content: '' };
fenlei1.value = null;
fenlei2.value = null;
if (noteFormRef.value) {
noteFormRef.value.resetFields();
}
};
@@ -318,22 +378,25 @@ const previewFile = async (file) => {
...file,
content: content
};
Vditor.preview(document.querySelector('.markdown-preview'), content);
} catch (error) {
ElMessage.error('获取笔记内容失败: ' + error.message);
}
};
// 编辑笔记
const editNote = (file) => {
const editNote = async (file) => {
editData.value = file
originalImages.value = extractImageUrls(file.content);
showEditor.value = true;
await nextTick(() => {
initVditor();
});
};
// 图片上传
const handleImageUpload=async (event, insertImage, files) => {
console.log(files)
const promise = await uploadImage(files[0]);
const handleImageUpload=async (files) => {
const promise = await uploadImage(files);
if (promise.code !== 200) {
ElMessage.error(promise.msg);
return;
@@ -342,13 +405,8 @@ const handleImageUpload=async (event, insertImage, files) => {
const imageUrl = promise.data.url.startsWith('/')
? `http://127.0.0.1:8084${promise.data.url}`
: promise.data.url;
// 插入图片
insertImage({
// 图片地址
url: imageUrl
// 图片描述
// desc: '七龙珠',
});
vditor.value.insertValue(`![](${imageUrl})`);
}
// 在编辑页面按Ctrl+S保存笔记或者点击保存对数据进行保存
@@ -387,21 +445,21 @@ const extractImageUrls = (data) => {
const mdRegex = /!\[.*?\]\((.*?)\)/g;
let mdMatch;
while ((mdMatch = mdRegex.exec(content)) !== null) {
urls.push(getPathFromUrl(mdMatch[1]))
urls.push(getPathFromUrl(mdMatch))
}
// 匹配HTML img标签
const htmlRegex = /<img[^>]+src="([^">]+)"/g;
let htmlMatch;
while ((htmlMatch = htmlRegex.exec(content)) !== null) {
urls.push(getPathFromUrl(htmlMatch[1]));
urls.push(getPathFromUrl(htmlMatch));
}
// 匹配base64图片
const base64Regex = /<img[^>]+src="(data:image\/[^;]+;base64[^">]+)"/g;
let base64Match;
while ((base64Match = base64Regex.exec(content)) !== null) {
urls.push(base64Match[1]);
urls.push(base64Match);
}
// 过滤和去重
@@ -473,7 +531,11 @@ const handleMarkdownUpload = (file) => {
};
onMounted(() => {
chushihua()
chushihua();
// 根据屏幕宽度初始化侧边栏状态
if (window.innerWidth < 768) {
isCollapsed.value = true;
}
});
const chushihua = async () => {
@@ -488,16 +550,12 @@ const chushihua = async () => {
background-color: #f5f7fa;
}
.container {
display: flex;
height: 100%;
}
.sidebar {
background: #fff;
border-right: 1px solid #e6e6e6;
display: flex;
flex-direction: column;
transition: width 0.3s;
}
.sidebar-header {
@@ -509,9 +567,8 @@ const chushihua = async () => {
}
.content {
padding: 20px;
height: 100vh;
margin-left: 20px;
flex: 1;
overflow-y: auto;
}
@@ -538,16 +595,12 @@ const chushihua = async () => {
}
.file-item {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 15px;
cursor: pointer;
transition: all 0.3s;
}
.file-item:hover {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
transform: translateY(-5px);
}
.file-title {
@@ -555,18 +608,8 @@ const chushihua = async () => {
font-weight: 500;
}
.empty-tip {
text-align: center;
padding: 50px;
color: #909399;
}
.file-preview {
height: 100vh;
padding: 20px;
border: 1px solid #ebeef5;
border-radius: 4px;
background: #fff;
height: 100%;
display: flex;
flex-direction: column;
}
@@ -580,17 +623,33 @@ const chushihua = async () => {
.markdown-preview {
flex: 1;
border: 1px solid #aa9898;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 20px;
background: #fff;
}
.el-menu-vertical-demo {
flex: 1;
overflow-y: auto;
border-right: none;
}
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 100%;
}
@media (max-width: 768px) {
.sidebar {
display: none;
}
.content {
padding: 10px;
}
.file-list {
grid-template-columns: 1fr;
}
}
</style>