feat(qianduan): 重构前端结构并添加新功能

- 新增分类创建功能
- 新增笔记创建功能
- 优化笔记列表展示
- 改进笔记预览界面
- 添加删除笔记功能
- 重构菜单选择逻辑
- 优化文件上传处理
- 更新Markdown编辑器配置
This commit is contained in:
ikmkj
2025-06-19 15:02:51 +08:00
parent b1b74f5efd
commit 827c661e5c
19 changed files with 776 additions and 276 deletions

View File

@@ -0,0 +1,47 @@
import axiosApi from '@/utils/axios.js'
export const groupingId = (data) => axiosApi.get(`/api/markdown/grouping/${data}`)
// 获取所有分组
export const groupingAll = () => axiosApi.get(`/api/groupings`)
// 获取所有Markdown文件
export const markdownAll = () => axiosApi.get(`/api/markdown`);
// 预览markdown文件
export const Preview = (id) => axiosApi.get(`/api/markdown/${id}`);
// 创建分类分组
export const addGroupings = (name) => {
const formData = new FormData()
if (name) formData.append('grouping', name)
return axiosApi.post('/api/groupings', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// MD5哈希
export const MD5 = (data, file) => {
const formData = new FormData()
if (data) formData.append('input', data)
if (file) formData.append('file', file)
return axiosApi.post('/api/common/md5', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}

View File

@@ -1,253 +1,385 @@
<template>
<div class="home-page">
<el-container style="height: 100vh;">
<!-- 左侧菜单 -->
<el-aside :width="isCollapsed ? '64px' : '300px'" style="background-color: #f5f7fa; border-right: 1px solid #ebeef5; transition: width 0.3s;">
<div class="sidebar-header" :style="{ justifyContent: isCollapsed ? 'center' : 'space-between' }">
<h3 v-if="!isCollapsed">笔记分类</h3>
<el-button @click="isCollapsed = !isCollapsed" size="small" type="text">
<el-icon v-if="isCollapsed">
<Expand />
</el-icon>
<span v-else>收起</span>
<div class="container">
<!-- 左侧菜单区域 -->
<div class="sidebar">
<div class="sidebar-header">
<span>笔记分类</span>
<el-button type="primary" size="small" @click="showCreateGroupDialog = true">
新建分类
</el-button>
</div>
<el-menu
v-show="!isCollapsed"
default-active="all"
class="el-menu-vertical-demo"
:default-active="activeMenu"
class="el-menu-vertical-demo"
:collapse="isCollapsed"
@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">
<!-- 分组分类 -->
<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="file in groupFiles[group.id] || []"
<!-- 分组下的文件 -->
<el-menu-item
v-for="file in groupFiles[group.id] || []"
:key="file.id"
:index="'file-'+file.id"
:index="`file-${file.id}`"
>
{{ file.title }}
</el-menu-item>
</el-submenu>
</el-sub-menu>
</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-list">
<div v-for="group in groupings" :key="group.id" class="group-section">
<h3>{{ group.grouping }}</h3>
<ul>
<li v-for="file in groupFiles[group.id]" :key="file.id" @click="selectFile(file)">
{{ file.title }}
</li>
</ul>
</div>
</div>
</div>
<!-- 右侧内容区 -->
<div class="content">
<div v-if="selectedFile" class="file-preview">
<div class="preview-header">
<h2>{{ selectedFile.title }}</h2>
<div>
<div class="actions">
<el-button type="primary" @click="editNote(selectedFile)">编辑</el-button>
<el-button @click="selectedFile = null">返回列表</el-button>
<el-button type="danger" @click="deleteNote(selectedFile)">删除</el-button>
</div>
</div>
<v-md-preview :text="selectedFile.content" class="markdown-preview"></v-md-preview>
</div>
<div v-else>
<div class="header">
<h1>我的笔记</h1>
<div class="actions">
<el-button type="primary" @click="showCreateNoteDialog = true">新建笔记</el-button>
<el-upload
class="upload-btn"
action=""
:show-file-list="false"
:before-upload="handleMarkdownUpload"
accept=".md"
>
<el-button type="success">上传Markdown</el-button>
</el-upload>
</div>
</div>
<div class="markdown-preview">
<v-md-preview :text="selectedFile.content"></v-md-preview>
<div v-if="markdownFiles.length > 0" class="file-list">
<div v-for="file in markdownFiles" :key="file.id" class="file-item">
<div @click="selectFile(file)" class="file-title">{{ file.title }}</div>
</div>
</div>
<div v-else class="empty-tip">暂无笔记请创建或上传</div>
</div>
</el-main>
</el-container>
<el-dialog v-model="showEditor" title="Markdown编辑器" width="80%">
<MarkdownEditor v-if="showEditor" :fileId="currentFileId" @close="showEditor = false" />
</div>
</div>
<!-- 分类创建对话框 -->
<el-dialog v-model="showCreateGroupDialog" title="新建分类" width="30%">
<el-form :model="newGroupForm" label-width="80px">
<el-form-item 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="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="newNoteForm.groupingId" placeholder="请选择">
<el-option
v-for="group in groupings"
:key="group.id"
:label="group.name"
: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>
<!-- Markdown编辑器 -->
<markdown-editor
v-if="showEditor"
:file-id="currentFileId"
@close="showEditor = false"
@saved="handleNoteSaved"
/>
</div>
</template>
<script>
<script setup>
import { ref, onMounted } from 'vue';
import axios from 'axios';
import { ElMessage } from 'element-plus';
import { Expand } from '@element-plus/icons-vue';
import MarkdownEditor from './MarkdownEditor.vue';
import MarkdownEditor from '@/components/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';
import {groupingId, groupingAll, markdownAll, addGroupings, Preview} from '@/api/CommonApi.js'
VMdPreview.use(githubTheme);
const markdownFiles = ref([]);
const groupings = ref([]);
const groupFiles = ref({});
const showEditor = ref(false);
const currentFileId = ref(null);
const selectedFile = ref(null);
const activeMenu = ref('all');
const isCollapsed = ref(false);
const showCreateGroupDialog = ref(false);
const showCreateNoteDialog = ref(false);
const newGroupForm = ref({ name: '' });
const newNoteForm = ref({ title: '', groupingId: null });
export default {
components: {
MarkdownEditor,
[VMdPreview.name]: VMdPreview,
Expand
},
setup() {
const API_BASE_URL = 'http://localhost:8084';
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 () => {
// 获取所有分组
const fetchGroupings = async () => {
try {
const response = await groupingAll()
// 确保分组数据是数组
groupings.value = response.data
// 为每个分组获取文件
for (const group of groupings.value) {
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);
const filesRes = await groupingId(group.id);
// 确保文件ID为字符串
groupFiles.value[group.id] = (filesRes.data).map(file => ({
...file,
id: file.id
}));
} catch (fileError) {
console.error(`获取分组 ${group.id} 文件失败:`, fileError);
groupFiles.value[group.id] = [];
}
};
}
// 获取所有Markdown文件
const fetchMarkdownFiles = async () => {
// 添加"全部"分类 (ID=1)
if (!groupings.value.some(g => g.id === 1)) {
// groupings.value.unshift({ id: 1, name: '全部' });
// 获取所有文件作为"全部"分类的内容
try {
const response = await axios.get(`${API_BASE_URL}/api/markdown`);
markdownFiles.value = response.data;
} catch (error) {
ElMessage.error('获取笔记列表失败: ' + error.message);
const allFilesRes = await markdownAll()
groupFiles.value[1] = (allFilesRes.data).map(file => ({
...file,
id: file.id
}));
} catch (allFilesError) {
console.error('获取全部笔记失败:', allFilesError);
groupFiles.value[1] = [];
}
};
// 选择文件预览
const selectFile = (file) => {
selectedFile.value = file;
};
// 编辑笔记
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) {
selectFile(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,
selectFile,
editNote,
deleteNote,
handleMarkdownUpload,
handleMenuSelect
};
}
} catch (error) {
console.error('获取分组失败:', error);
ElMessage.error('获取分组失败: ' + (error.response?.data?.message || error.message));
groupings.value = [];
}
};
// 获取所有Markdown文件确保ID为字符串
const fetchMarkdownFiles = async () => {
try {
const response = await markdownAll()
// 确保文件ID为字符串
markdownFiles.value = (response.data || []).map(file => ({
...file,
id: String(file.id)
}));
} catch (error) {
ElMessage.error('获取笔记列表失败: ' + error.message);
}
};
// 创建新分类
const createGrouping = async () => {
try {
const response = await addGroupings(newGroupForm.value.name)
ElMessage.success('分类创建成功');
showCreateGroupDialog.value = false;
newGroupForm.value.name = '';
fetchGroupings();
} catch (error) {
ElMessage.error('创建分类失败: ' + error.message);
}
};
// 创建新笔记
const createNote = async () => {
try {
await axios.post(`${API_BASE_URL}/api/markdown`, '# 新笔记内容', {
params: {
groupingId: newNoteForm.value.groupingId,
title: newNoteForm.value.title,
fileName: newNoteForm.value.title
}
});
ElMessage.success('笔记创建成功');
showCreateNoteDialog.value = false;
newNoteForm.value = { title: '', groupingId: null };
await fetchMarkdownFiles();
await fetchGroupings();
} catch (error) {
ElMessage.error('创建笔记失败: ' + error.message);
}
};
// 选择文件预览
const selectFile = async (file) => {
try {
const response = await Preview(file.id)
// 确保内容为字符串
let content = response.data;
if (typeof content !== 'string') {
// 如果返回的是对象,尝试转换为字符串
if (content && typeof content === 'object') {
content = JSON.stringify(content);
} else {
content = String(content);
}
}
selectedFile.value = {
...file,
content: content
};
} catch (error) {
ElMessage.error('获取笔记内容失败: ' + error.message);
}
};
// 编辑笔记
const editNote = (file) => {
currentFileId.value = file.id;
showEditor.value = true;
};
// 笔记保存后处理
const handleNoteSaved = () => {
showEditor.value = false;
fetchMarkdownFiles();
fetchGroupings();
if (selectedFile.value) {
selectFile(selectedFile.value); // 刷新当前预览内容
}
};
// 删除笔记
const deleteNote = async (file) => {
try {
await axios.delete(`${API_BASE_URL}/api/markdown/${file.id}`);
ElMessage.success('删除成功');
selectedFile.value = null;
fetchMarkdownFiles();
fetchGroupings();
} catch (error) {
ElMessage.error('删除失败: ' + error.message);
}
};
// 菜单选择处理
const handleMenuSelect = (index) => {
if (index === 'all') {
selectedFile.value = null;
activeMenu.value = 'all';
} else if (index.startsWith('file-')) {
const fileId = index.split('-')[1];
// 确保markdownFiles.value是数组
if (Array.isArray(markdownFiles.value)) {
const file = markdownFiles.value.find(f => f.id === fileId);
if (file) {
selectFile(file);
activeMenu.value = index;
}
}
} else if (index.startsWith('group-')) {
activeMenu.value = index;
}
};
// 上传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('上传成功');
fetchMarkdownFiles();
fetchGroupings();
} catch (error) {
ElMessage.error('上传失败: ' + error.message);
}
};
reader.readAsText(file);
return false; // 阻止默认上传行为
};
onMounted(() => {
fetchMarkdownFiles();
fetchGroupings();
});
</script>
<style scoped>
.home-page {
height: 100vh;
background-color: #f5f7fa;
}
.container {
display: flex;
height: 100%;
}
.sidebar {
width: 280px;
background: #fff;
border-right: 1px solid #e6e6e6;
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 15px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #ebeef5;
}
.content {
height: 100vh;
margin-left: 20px;
flex: 1;
overflow-y: auto;
}
.header {
display: flex;
justify-content: space-between;
@@ -262,68 +394,68 @@ export default {
.upload-btn {
display: inline-block;
margin-left: 10px;
}
.file-list {
margin-top: 20px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 15px;
}
.group-section {
margin-bottom: 20px;
}
.group-section h3 {
margin-bottom: 10px;
font-size: 18px;
color: #333;
}
.group-section ul {
list-style: none;
padding: 0;
}
.group-section li {
padding: 8px 0;
border-bottom: 1px solid #eee;
.file-item {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 15px;
cursor: pointer;
transition: all 0.3s;
}
.group-section li:hover {
background-color: #f5f7fa;
.file-item:hover {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.file-title {
font-size: 16px;
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;
display: flex;
flex-direction: column;
}
.preview-header {
display: flex;
margin-bottom: 10px;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.markdown-preview {
min-height: 500px;
padding: 20px;
flex: 1;
border: 1px solid #f0f0f0;
border-radius: 4px;
background: #fff;
}
/* 修复菜单折叠样式 */
.el-menu-vertical-demo {
height: calc(100vh - 60px);
flex: 1;
overflow-y: auto;
}
.sidebar-header {
height: 60px;
display: flex;
align-items: center;
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 100%;
}
</style>

View File

@@ -1,18 +1,33 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
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';
import githubTheme from '@kangc/v-md-editor/lib/theme/github.js';
import '@kangc/v-md-editor/lib/theme/style/github.css';
import VMdPreview from '@kangc/v-md-editor/lib/preview';
import '@kangc/v-md-editor/lib/style/preview.css';
import '@kangc/v-md-editor/lib/theme/style/github.css';
const app = createApp(App)
// 配置Markdown编辑器
VMdEditor.use(vuepressTheme);
// highlightjs
import hljs from 'highlight.js';
VMdEditor.use(githubTheme, {
Hljs: hljs,
});
VMdPreview.use(githubTheme, {
Hljs: hljs,
});
// // 配置Markdown编辑器
app.use(VMdEditor);
app.use(VMdPreview);
// 使用Element Plus和路由
app.use(ElementPlus)

View File

@@ -0,0 +1,10 @@
/**
* 处理参数的其他处理逻辑,如编码、格式化等
* @param data
* @returns {string}
*/
const handleCopy =(data) =>{
return encodeURIComponent( data)
};
export default handleCopy;

View File

@@ -0,0 +1,32 @@
import axios from 'axios'
const instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
// 开发环境使用withCredentials生产环境关闭
withCredentials: import.meta.env.DEV,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
instance.interceptors.request.use(
config => {
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
instance.interceptors.response.use(
response => {
return response.data
},
error => {
return Promise.reject(error)
}
)
export default instance

View File

@@ -0,0 +1,11 @@
/**
* 设备检测工具
*/
export function detectDeviceType() {
// 基础检测User Agent
const ua = navigator.userAgent;
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
return isMobile ? 'mobile' : 'desktop';
}

View File

@@ -0,0 +1,55 @@
/**
* 处理复制
* @param data
* @param message
* @returns {Promise<void>}
*/
const handleCopy = async (data, message) => {
// 检查是否在安全上下文中 (HTTPS 或 localhost)
const isSecureContext = window.isSecureContext || location.protocol === 'https:';
if (isSecureContext) {
try {
// 使用现代剪贴板 API
await navigator.clipboard.writeText(data);
message.success('已复制到剪贴板');
} catch (err) {
message.error('现代剪贴板API错误:', err);
useFallbackCopy(data, message);
}
} else {
// 非安全上下文直接使用回退方法
useFallbackCopy(data, message);
}
}
// 回退复制方法
function useFallbackCopy(data, message) {
try {
const textarea = document.createElement('textarea');
textarea.value = data;
// 设置样式确保元素在视口外但可交互
textarea.style.position = 'fixed';
textarea.style.top = '0';
textarea.style.left = '0';
textarea.style.opacity = '0';
textarea.style.pointerEvents = 'none';
document.body.appendChild(textarea);
textarea.select();
// 尝试执行复制
const success = document.execCommand('copy');
document.body.removeChild(textarea);
if (success) {
message.success('已复制');
} else {
throw new Error('回退复制方法失败');
}
} catch (err) {
message.error('复制失败,请手动复制内容');
}
}
export default handleCopy