Files
biji/biji-qianduan/src/components/HomePage.vue
ikmkj b95ca5678a refactor(layout): 重构首页布局和菜单项样式
- 更新了侧边栏和内容区域的样式
- 优化了菜单项的展示方式,增加工具提示和响应式布局
- 改进了文件列表和预览区域的样式- 统一了全局样式,包括按钮、表单等元素
2025-07-31 18:43:12 +08:00

1035 lines
29 KiB
Vue

<template>
<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-menu
:default-active="activeMenu"
class="el-menu-vertical-demo"
:collapse="isCollapsed"
popper-effect="light"
:collapse-transition="false"
>
<!-- 递归菜单组件 -->
<template v-for="menu in categoryTree" :key="menu.id">
<component :is="renderMenu(menu)" />
</template>
</el-menu>
</el-aside>
<!-- 右侧内容区域 -->
<el-container>
<el-main class="content">
<div v-if="selectedFile" class="file-preview">
<el-header class="preview-header">
<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="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="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>
<!-- Vditor 编辑器 -->
<div v-show="showEditor" id="vditor" class="vditor" />
</div>
<div v-else>
<el-header class="header">
<h1 @click="resetToHomeView" style="cursor: pointer;">我的笔记</h1>
<div class="actions">
<el-input
v-model="searchKeyword"
placeholder="搜索笔记标题"
class="search-input"
@keyup.enter="handleSearch"
>
<template #append>
<el-button @click="handleSearch">
<el-icon><Search /></el-icon>
</el-button>
</template>
</el-input>
<div v-if="userStore.isLoggedIn">
<span>欢迎, {{ userStore.userInfo?.username }}</span>
<el-button type="danger" @click="handleLogout">退出</el-button>
</div>
<div v-else>
<el-button type="primary" @click="goToLogin">登录</el-button>
<el-button @click="goToRegister">注册</el-button>
</div>
<el-button v-if="userStore.isLoggedIn" type="primary" @click="showCreateNoteDialog = true">新建笔记</el-button>
<el-upload
v-if="userStore.isLoggedIn"
class="upload-btn"
action=""
:show-file-list="false"
:before-upload="handleMarkdownUpload"
accept=".md"
>
<el-button type="success">上传Markdown</el-button>
</el-upload>
</div>
</el-header>
<div v-if="groupMarkdownFiles.length > 0" class="file-list">
<el-card v-for="file in groupMarkdownFiles" :key="file.id" shadow="hover" class="file-item">
<div @click="previewFile(file)" class="file-title">
<span>{{ file.title }}</span>
<span class="file-group-name">{{ file.groupingName }}</span>
</div>
</el-card>
</div>
<el-empty v-else description="暂无笔记,请创建或上传" />
</div>
<!-- 分类创建对话框 -->
<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-cascader
v-model="newGroupForm.parentId"
:options="categoryCascaderOptions"
:props="{ checkStrictly: true, emitPath: false }"
clearable
placeholder="不选则为一级分类"
style="width: 100%;"
></el-cascader>
</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="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="groupingId">
<el-cascader
v-model="newNoteForm.groupingId"
:options="categoryTree"
:props="{ checkStrictly: true, emitPath: false }"
clearable
placeholder="请选择笔记所属分类"
style="width: 100%;"
></el-cascader>
</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-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-cascader
v-model="importGroupId"
:options="categoryTree"
:props="{ checkStrictly: true, emitPath: false }"
clearable
placeholder="请选择要导入的分类"
style="width: 100%;"
></el-cascader>
<template #footer>
<el-button @click="showSelectGroupDialog = false">取消</el-button>
<el-button type="primary" @click="confirmImport">确定</el-button>
</template>
</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-container>
</el-container>
</template>
<script setup>
import {onMounted, ref, nextTick, watch, h, computed} from 'vue';
import {ElMessage, ElSubMenu, ElMenuItem, ElIcon, ElMessageBox, ElTooltip} from 'element-plus';
import Vditor from 'vditor';
import 'vditor/dist/index.css';
import {
addGroupings,
deleteImages, deleteMarkdown,
groupingAll,
markdownAll, markdownList,
Preview,
updateMarkdown, uploadImage,
searchMarkdown,
updateGroupingName,
updateMarkdownTitle,
deleteGrouping as apiDeleteGrouping,
getRecentFiles
} from '@/api/CommonApi.js'
import { Plus, Fold, Expand, Folder, Document, Search, Edit, Delete } from "@element-plus/icons-vue";
import { useUserStore } from '../stores/user';
import { useRouter } from 'vue-router';
const userStore = useUserStore();
const router = useRouter();
const searchKeyword = ref('');
const markdownFiles = ref([]);
const categoryTree = ref([]);
const groupMarkdownFiles = ref([]);
const showEditor = ref(false);
const selectedFile = ref(null);
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 showMoveNoteDialog = ref(false);
const moveToGroupId = ref(null);
const currentGroupName = ref('');
const groupFormRef = ref(null);
const newGroupForm = ref({ name: '', parentId: null });
const groupFormRules = ref({
name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }],
});
const noteFormRef = ref(null);
const newNoteForm = ref({
id: null,
title: '',
groupingId: null,
fileName: '',
content: ''
});
const noteFormRules = ref({
title: [{ required: true, message: '请输入笔记标题', trigger: 'blur' }],
groupingId: [{ required: true, message: '请选择分类', trigger: 'change' }],
});
const editData=ref(null)
const imageUrls = ref([]);
const originalImages = ref([]);
const vditor = ref(null);
const previewHtml = ref('');
const saveStatus = ref('空闲');
let debounceTimer = null;
const categoryCascaderOptions = computed(() => categoryTree.value);
const initVditor = () => {
vditor.value = new Vditor('vditor', {
height: 'calc(100vh - 120px)',
mode: 'ir',
after: () => {
if (editData.value) {
vditor.value.setValue(editData.value.content);
}
},
input: (value) => {
debouncedSave(value);
},
upload: {
accept: 'image/*',
handler(files) {
handleImageUpload(files);
},
},
});
};
const buildTree = (items) => {
const tree = [];
const itemMap = new Map();
// First, map all items by their id
items.forEach(item => {
itemMap.set(String(item.id), {
...item,
value: item.id,
label: item.grouping,
children: [], // Initialize children array
});
});
// Then, build the tree structure
itemMap.forEach(item => {
const parentId = String(item.parentId);
if (parentId !== '0' && itemMap.has(parentId)) {
const parent = itemMap.get(parentId);
parent.children.push(item);
} else {
// If it's a root node or an orphan, add it to the top level
tree.push(item);
}
});
// Helper to remove empty children arrays
const cleanTree = (nodes) => {
nodes.forEach(node => {
if (node.children.length === 0) {
delete node.children;
} else {
cleanTree(node.children);
}
});
};
cleanTree(tree);
return tree;
};
const fetchGroupings = async () => {
try {
const response = await groupingAll("");
const allCategories = response.data || [];
categoryTree.value = buildTree(allCategories);
} catch (error) {
console.error('获取分组失败:', error);
ElMessage.error('获取分组失败: ' + (error.response?.data?.message || error.message));
categoryTree.value = [];
}
};
const selectFile = async (data) => {
const promise = await markdownList(data.id);
groupMarkdownFiles.value = promise.data;
currentGroupName.value = data.grouping;
selectedFile.value = null;
};
const fetchMarkdownFiles = async () => {
try {
const response = await markdownAll()
markdownFiles.value = (response.data || []).map(file => ({
...file,
id: String(file.id)
}));
} catch (error) {
ElMessage.error('获取笔记列表失败: ' + error.message);
}
};
const createGrouping = async () => {
if (!groupFormRef.value) return;
await groupFormRef.value.validate(async (valid) => {
if (valid) {
try {
const payload = {
grouping: newGroupForm.value.name, // 将 name 映射到 grouping
parentId: newGroupForm.value.parentId || 0
};
await addGroupings(payload);
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 () => {
if (!noteFormRef.value) return;
await noteFormRef.value.validate(async (valid) => {
if (valid) {
try {
const groupingId = newNoteForm.value.groupingId;
if (!groupingId) {
ElMessage.error('必须选择一个分类');
return;
}
const payload = {
id: null,
title: newNoteForm.value.title,
groupingId: groupingId,
fileName: newNoteForm.value.title + '.md',
content: ''
};
editData.value = payload;
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, fileName: '', content: '' };
if (noteFormRef.value) {
noteFormRef.value.resetFields();
}
};
const renderMenu = (item) => {
const titleContent = () => h('div', { class: 'menu-item-title' }, [
h(ElIcon, () => h(Folder)),
h('span', { class: 'menu-item-text' }, item.grouping),
h('div', { class: 'menu-item-actions' }, [
h(ElIcon, { class: 'edit-icon', onClick: (e) => { e.stopPropagation(); openRenameDialog(item, 'group'); } }, () => h(Edit)),
h(ElIcon, { class: 'delete-icon', onClick: (e) => { e.stopPropagation(); handleDeleteGroup(item); } }, () => h(Delete))
])
]);
const wrappedContent = () => h(ElTooltip, {
content: item.grouping,
placement: 'right',
disabled: !isCollapsed.value,
effect: 'dark',
offset: 15,
}, {
default: titleContent
});
if (item.children && item.children.length > 0) {
return h(ElSubMenu, { index: `group-${item.id}` }, {
title: () => h('div', { onClick: () => selectFile(item), style: 'width: 100%;' }, [ wrappedContent() ]),
default: () => item.children.map(child => renderMenu(child))
});
}
return h(ElMenuItem, { index: `group-${item.id}`, onClick: () => selectFile(item) }, {
default: wrappedContent
});
};
// 选择文件预览
const previewFile = async (file) => {
if (file.id === null){
editData.value=file
selectedFile.value=null
return;
}
try {
const response = await Preview(file.id)
// 确保内容为字符串
const content = String(response.data || '');
selectedFile.value = {
...file,
content: content
};
await nextTick();
const previewElement = document.querySelector('.markdown-preview');
if (previewElement) {
Vditor.preview(previewElement, content, {
// 在这里提供一个基本的配置对象
mode: 'light', // 或者 'dark',可以根据当前主题动态设置
hljs: {
enable: true,
style: 'github'
}
});
}
} catch (error) {
ElMessage.error('获取笔记内容失败: ' + error.message);
}
};
// 编辑笔记
const editNote = async (file) => {
editData.value = file
originalImages.value = extractImageUrls(file.content);
showEditor.value = true;
await nextTick(() => {
initVditor();
});
};
// 图片上传
const handleImageUpload=async (files) => {
const promise = await uploadImage(files);
if (promise.code !== 200) {
ElMessage.error(promise.msg);
return;
}
const url = promise.data;
imageUrls.value.push(url);
vditor.value.insertValue(`![${files[0].name}](${url})`);
}
const debouncedSave = (value) => {
saveStatus.value = '正在输入...';
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
handleSave(value);
}, 2000);
};
const handleSave = async (content) => {
saveStatus.value = '正在保存...';
try {
const newImageUrls = extractImageUrls(content);
const deletedImages = originalImages.value.filter(url => !newImageUrls.includes(url));
if (deletedImages.length > 0) {
await deleteImages({ imageUrls: deletedImages });
}
originalImages.value = newImageUrls;
const payload = {
...editData.value,
content: content,
};
const response = await updateMarkdown(payload);
editData.value = response.data;
selectedFile.value = editData.value;
saveStatus.value = '已保存';
ElMessage.success('保存成功');
await fetchGroupings();
await fetchMarkdownFiles();
} catch (error) {
saveStatus.value = '保存失败';
ElMessage.error('保存失败: ' + error.message);
}
};
const extractImageUrls = (markdown) => {
const regex = /!\[.*?\]\((.*?)\)/g;
const urls = [];
let match;
while ((match = regex.exec(markdown)) !== null) {
urls.push(match);
}
return urls;
};
const openRenameDialog = (item, type) => {
itemToRename.value = { ...item, type };
newName.value = type === 'file' ? item.title : item.grouping;
showRenameDialog.value = true;
};
const handleRename = async () => {
if (!newName.value.trim()) {
ElMessage.error('名称不能为空');
return;
}
try {
if (itemToRename.value.type === 'file') {
await updateMarkdownTitle({ id: itemToRename.value.id, title: newName.value });
if (selectedFile.value && selectedFile.value.id === itemToRename.value.id) {
selectedFile.value.title = newName.value;
}
} else {
await updateGroupingName({ id: itemToRename.value.id, grouping: newName.value });
}
ElMessage.success('重命名成功');
await fetchGroupings();
await fetchMarkdownFiles();
} catch (error) {
ElMessage.error('重命名失败: ' + error.message);
} finally {
showRenameDialog.value = false;
}
};
const handleDeleteGroup = (group) => {
ElMessageBox.confirm(
`确定要删除分类 “${group.grouping}” 吗?这将同时删除该分类下的所有子分类和笔记。`,
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
await apiDeleteGrouping(group.id);
ElMessage.success('分类已删除');
await fetchGroupings();
await fetchMarkdownFiles();
if (activeMenu.value.startsWith('group-') && activeMenu.value.endsWith(group.id)) {
activeMenu.value = 'all';
groupMarkdownFiles.value = markdownFiles.value;
}
} catch (error) {
ElMessage.error('删除分类失败: ' + error.message);
}
}).catch(() => {
// 用户取消操作
});
};
const deleteNote = (file) => {
ElMessageBox.confirm(
`确定要删除笔记 “${file.title}” 吗?`,
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
await deleteMarkdown(file.id);
ElMessage.success('笔记已删除');
selectedFile.value = null;
await fetchMarkdownFiles();
// Optionally, refresh the current group's file list
if (activeMenu.value.startsWith('group-')) {
const groupId = activeMenu.value.split('-');
await selectFile({ id: groupId });
}
} catch (error) {
ElMessage.error('删除笔记失败: ' + error.message);
}
});
};
const handleMarkdownUpload = (file) => {
fileToImport.value = file;
showSelectGroupDialog.value = true;
return false; // 阻止 el-upload 自动上传
};
const confirmImport = async () => {
if (!importGroupId.value) {
ElMessage.error('请选择要导入的分类');
return;
}
const reader = new FileReader();
reader.onload = async (e) => {
const content = e.target.result;
const payload = {
title: fileToImport.value.name.replace(/\.md$/, ''),
groupingId: importGroupId.value,
content: content,
fileName: fileToImport.value.name
};
try {
await updateMarkdown(payload);
ElMessage.success('Markdown 文件导入成功');
await fetchGroupings();
await fetchMarkdownFiles();
showSelectGroupDialog.value = false;
} catch (error) {
ElMessage.error('导入失败: ' + error.message);
}
};
reader.readAsText(fileToImport.value);
};
const handleMoveNote = async () => {
if (!moveToGroupId.value) {
ElMessage.error('请选择目标分类');
return;
}
try {
const payload = {
...selectedFile.value,
groupingId: moveToGroupId.value
};
await updateMarkdown(payload);
ElMessage.success('笔记移动成功');
showMoveNoteDialog.value = false;
selectedFile.value = null;
await fetchGroupings();
await fetchMarkdownFiles();
} catch (error) {
ElMessage.error('移动失败: ' + error.message);
}
};
const handleSearch = async () => {
if (!searchKeyword.value.trim()) {
groupMarkdownFiles.value = markdownFiles.value;
return;
}
try {
const response = await searchMarkdown(searchKeyword.value);
groupMarkdownFiles.value = response.data;
} catch (error) {
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 = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${selectedFile.value.title}.md`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const goToLogin = () => {
router.push('/login');
};
const goToRegister = () => {
router.push('/register');
};
const handleLogout = () => {
userStore.logout();
ElMessage.success('已退出登录');
router.push('/login');
};
const resetToHomeView = async () => {
selectedFile.value = null;
showEditor.value = false;
searchKeyword.value = '';
try {
const response = await getRecentFiles();
groupMarkdownFiles.value = response.data;
} catch (error) {
ElMessage.error('获取最近文件失败: ' + error.message);
groupMarkdownFiles.value = [];
}
};
onMounted(async () => {
await fetchGroupings();
await resetToHomeView();
});
watch(activeMenu, (newVal) => {
if (newVal === 'all') {
resetToHomeView();
}
});
</script>
<style>
/* 全局 Element Plus 样式覆盖 */
.el-dialog {
border-radius: 8px !important;
box-shadow: var(--box-shadow-dark) !important;
}
.el-dialog__header {
border-bottom: 1px solid var(--border-color-light);
padding: 15px 20px !important;
margin-right: 0 !important;
}
.el-dialog__title {
color: var(--text-color) !important;
font-weight: 600;
}
.el-dialog__body {
padding: 20px !important;
}
.el-form-item__label {
color: var(--text-color-secondary);
}
.el-input__wrapper, .el-cascader {
background-color: var(--bg-color-secondary) !important;
box-shadow: none !important;
border-radius: 6px !important;
}
.el-input__inner {
color: var(--text-color) !important;
}
.el-button--primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
transition: all var(--transition-duration) ease;
}
.el-button--primary:hover {
background-color: var(--primary-color-dark);
border-color: var(--primary-color-dark);
}
.el-button {
border-radius: 6px;
}
.el-dialog__footer {
padding: 10px 20px 20px !important;
border-top: 1px solid var(--border-color-light);
}
/* Vditor 暗黑模式适配 */
.dark-theme .vditor {
--panel-background-color: var(--bg-color-secondary) !important;
--textarea-background-color: var(--bg-color) !important;
--toolbar-background-color: var(--bg-color-tertiary) !important;
--border-color: var(--border-color) !important;
color: var(--text-color) !important;
}
.dark-theme .vditor-toolbar,
.dark-theme .vditor-content,
.dark-theme .vditor-preview {
background-color: var(--bg-color) !important;
}
.dark-theme .vditor-toolbar__item:hover,
.dark-theme .vditor-toolbar__item--current {
background-color: var(--primary-color-light) !important;
}
/* 移除预览中代码块的内部滚动条 */
.markdown-preview .vditor-reset pre {
max-height: none !important;
overflow: visible !important;
}
.markdown-preview .vditor-reset pre code {
max-height: none !important;
overflow-y: visible !important;
word-break: break-all !important;
white-space: pre-wrap !important;
display: block;
}
</style>
<style scoped>
.home-page {
height: 100vh;
overflow: hidden;
}
.sidebar {
border-right: 1px solid var(--border-color);
transition: width 0.3s ease;
background-color: var(--bg-color);
display: flex;
flex-direction: column;
overflow-x: hidden;
}
.sidebar-header {
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-color-light);
flex-shrink: 0;
}
.el-menu-vertical-demo {
border-right: none;
flex-grow: 1;
overflow-y: auto;
overflow-x: hidden;
}
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 100%;
}
:deep(.el-menu-item),
:deep(.el-sub-menu__title) {
width: 100%;
}
.menu-item-title {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
gap: 5px;
}
.menu-item-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
}
.menu-item-actions {
display: none;
align-items: center;
gap: 5px;
}
.el-menu:not(.el-menu--collapse) .el-menu-item:hover .menu-item-actions,
.el-menu:not(.el-menu--collapse) .el-sub-menu__title:hover .menu-item-actions {
display: flex;
}
.el-menu--collapse .menu-item-text,
.el-menu--collapse .menu-item-actions {
display: none;
}
.edit-icon, .delete-icon {
cursor: pointer;
color: var(--text-color-secondary);
}
.edit-icon:hover, .delete-icon:hover {
color: var(--primary-color);
}
.content {
padding: 0;
background-color: var(--bg-color-secondary);
height: 100vh;
display: flex;
flex-direction: column;
}
.header, .preview-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background-color: var(--bg-color);
border-bottom: 1px solid var(--border-color);
}
.preview-title {
display: flex;
align-items: center;
gap: 10px;
}
.preview-title .edit-icon {
visibility: visible;
opacity: 0.6;
}
.preview-title .edit-icon:hover {
opacity: 1;
}
.actions {
display: flex;
gap: 10px;
align-items: center;
}
.search-input {
width: 250px;
}
.file-list {
padding: 20px;
flex-grow: 1;
overflow-y: auto;
}
.file-item {
margin-bottom: 15px;
cursor: pointer;
transition: all 0.2s ease;
}
.file-item:hover {
transform: translateY(-2px);
box-shadow: var(--box-shadow-light);
}
.file-title {
display: flex;
justify-content: space-between;
align-items: center;
}
.file-group-name {
font-size: 0.8em;
color: var(--text-color-secondary);
background-color: var(--bg-color-mute);
padding: 2px 6px;
border-radius: 4px;
}
.file-preview {
display: flex;
flex-direction: column;
height: 100%;
}
.markdown-preview {
padding: 20px;
flex-grow: 1;
overflow-y: auto;
background-color: var(--bg-color);
}
.vditor {
flex-grow: 1;
border: none;
}
.save-status {
font-size: 14px;
color: var(--text-color-secondary);
}
.upload-btn {
display: inline-block;
margin-left: 10px;
}
</style>