refactor(layout): 重构首页布局和菜单项样式
- 更新了侧边栏和内容区域的样式 - 优化了菜单项的展示方式,增加工具提示和响应式布局 - 改进了文件列表和预览区域的样式- 统一了全局样式,包括按钮、表单等元素
This commit is contained in:
@@ -196,7 +196,7 @@
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref, nextTick, watch, h, computed} from 'vue';
|
||||
import {ElMessage, ElSubMenu, ElMenuItem, ElIcon, ElMessageBox} from 'element-plus';
|
||||
import {ElMessage, ElSubMenu, ElMenuItem, ElIcon, ElMessageBox, ElTooltip} from 'element-plus';
|
||||
import Vditor from 'vditor';
|
||||
import 'vditor/dist/index.css';
|
||||
import {
|
||||
@@ -428,24 +428,34 @@ const resetNoteForm = () => {
|
||||
};
|
||||
|
||||
const renderMenu = (item) => {
|
||||
if (item.children && item.children.length > 0) {
|
||||
return h(ElSubMenu, { index: `group-${item.id}` }, {
|
||||
title: () => h('div', { class: 'menu-item-title', onClick: () => selectFile(item) }, [
|
||||
h(ElIcon, () => h(Folder)),
|
||||
h('span', null, item.grouping),
|
||||
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))
|
||||
]),
|
||||
default: () => item.children.map(child => renderMenu(child))
|
||||
});
|
||||
}
|
||||
return h(ElMenuItem, { index: `group-${item.id}`, onClick: () => selectFile(item) }, {
|
||||
default: () => h('div', { class: 'menu-item-title' }, [
|
||||
h(ElIcon, () => h(Folder)),
|
||||
h('span', null, item.grouping),
|
||||
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
|
||||
});
|
||||
};
|
||||
|
||||
@@ -500,132 +510,61 @@ const handleImageUpload=async (files) => {
|
||||
ElMessage.error(promise.msg);
|
||||
return;
|
||||
}
|
||||
// 插入图片(确保URL格式正确)
|
||||
const imageUrl = promise.data.url.startsWith('/')
|
||||
? `http://127.0.0.1:8084${promise.data.url}`
|
||||
: promise.data.url;
|
||||
|
||||
vditor.value.insertValue(``);
|
||||
const url = promise.data;
|
||||
imageUrls.value.push(url);
|
||||
vditor.value.insertValue(`![${files[0].name}](${url})`);
|
||||
}
|
||||
|
||||
// 在编辑页面,按Ctrl+S保存笔记,或者点击保存,对数据进行保存
|
||||
const handleSave= async (content, isAutoSave = false) => {
|
||||
imageUrls.value = extractImageUrls(content);
|
||||
extractDeletedImageUrls(imageUrls.value)
|
||||
editData.value.content = content
|
||||
const filesRes = await updateMarkdown(editData.value);
|
||||
if (filesRes.code === 200) {
|
||||
// 关键修复:用后端返回的、带有ID的最新数据更新本地状态
|
||||
editData.value = filesRes.data;
|
||||
if (selectedFile.value) {
|
||||
selectedFile.value.id = filesRes.data.id; // 确保预览对象也有ID
|
||||
}
|
||||
|
||||
if (!isAutoSave) {
|
||||
ElMessage.success(filesRes.msg);
|
||||
}
|
||||
await chushihua();
|
||||
} else {
|
||||
if (!isAutoSave) {
|
||||
ElMessage.error(filesRes.msg);
|
||||
}
|
||||
throw new Error(filesRes.msg);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存时获取所有的图片url
|
||||
const extractImageUrls = (data) => {
|
||||
const content = data
|
||||
const urls = [];
|
||||
|
||||
// 处理 URL 的内部函数
|
||||
const getPathFromUrl = (url) => {
|
||||
try {
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
// 使用 URL 对象解析路径
|
||||
return new URL(url).pathname;
|
||||
}
|
||||
return url; // 非 HTTP(S) URL 直接返回(如 base64)
|
||||
} catch (e) {
|
||||
return url; // 解析失败时返回原始 URL
|
||||
}
|
||||
};
|
||||
|
||||
// 匹配Markdown图片语法
|
||||
const mdRegex = /!\[.*?\]\((.*?)\)/g;
|
||||
let mdMatch;
|
||||
while ((mdMatch = mdRegex.exec(content)) !== null) {
|
||||
urls.push(getPathFromUrl(mdMatch))
|
||||
}
|
||||
|
||||
// 匹配HTML img标签
|
||||
const htmlRegex = /<img[^>]+src="([^">]+)"/g;
|
||||
let htmlMatch;
|
||||
while ((htmlMatch = htmlRegex.exec(content)) !== null) {
|
||||
urls.push(getPathFromUrl(htmlMatch));
|
||||
}
|
||||
|
||||
// 匹配base64图片
|
||||
const base64Regex = /<img[^>]+src="(data:image\/[^;]+;base64[^">]+)"/g;
|
||||
let base64Match;
|
||||
while ((base64Match = base64Regex.exec(content)) !== null) {
|
||||
urls.push(base64Match);
|
||||
}
|
||||
|
||||
// 过滤和去重
|
||||
const validUrls = urls.filter(url => {
|
||||
return url.startsWith('http') || url.startsWith('data:image');
|
||||
});
|
||||
return [...new Set(validUrls)]
|
||||
};
|
||||
|
||||
// 笔记保存时获取要删除的图片url
|
||||
const extractDeletedImageUrls = (data) => {
|
||||
const delImages = []
|
||||
// 原来的url originalImages.value
|
||||
for (let i = 0; i <originalImages.value.length; i++) {
|
||||
for (let j = 0; j <data.length; j++) {
|
||||
if (originalImages.value[i] !== data[j]){
|
||||
delImages.push(originalImages.value[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
if (delImages.length>0){
|
||||
deleteImages(delImages)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除笔记
|
||||
const deleteNote = async (file) => {
|
||||
try {
|
||||
await deleteMarkdown(file.id);
|
||||
ElMessage.success('删除成功');
|
||||
selectedFile.value = null;
|
||||
await chushihua()
|
||||
await selectFile(groupingId)
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedSave = (content) => {
|
||||
const debouncedSave = (value) => {
|
||||
saveStatus.value = '正在输入...';
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(async () => {
|
||||
saveStatus.value = '正在保存...';
|
||||
try {
|
||||
// 直接使用 vditor.value.getValue() 获取最新内容
|
||||
await handleSave(vditor.value.getValue(), true);
|
||||
saveStatus.value = '已保存';
|
||||
} catch (error) {
|
||||
saveStatus.value = '保存失败';
|
||||
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 });
|
||||
}
|
||||
}, 2000); // 2-second delay
|
||||
|
||||
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 === 'group' ? item.grouping : item.title;
|
||||
newName.value = type === 'file' ? item.title : item.grouping;
|
||||
showRenameDialog.value = true;
|
||||
};
|
||||
|
||||
@@ -634,87 +573,135 @@ const handleRename = async () => {
|
||||
ElMessage.error('名称不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (itemToRename.value.type === 'group') {
|
||||
await updateGroupingName(itemToRename.value.id, newName.value);
|
||||
ElMessage.success('分类重命名成功');
|
||||
await fetchGroupings();
|
||||
} else if (itemToRename.value.type === 'file') {
|
||||
await updateMarkdownTitle(itemToRename.value.id, newName.value);
|
||||
ElMessage.success('笔记重命名成功');
|
||||
selectedFile.value.title = newName.value;
|
||||
await fetchMarkdownFiles();
|
||||
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 });
|
||||
}
|
||||
showRenameDialog.value = false;
|
||||
ElMessage.success('重命名成功');
|
||||
await fetchGroupings();
|
||||
await fetchMarkdownFiles();
|
||||
} catch (error) {
|
||||
ElMessage.error('重命名失败: ' + error.message);
|
||||
} finally {
|
||||
showRenameDialog.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 上传Markdown文件处理
|
||||
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) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
fileToImport.value = {
|
||||
content: e.target.result,
|
||||
title: file.name.replace('.md', ''),
|
||||
fileName: file.name,
|
||||
};
|
||||
showSelectGroupDialog.value = true;
|
||||
};
|
||||
reader.readAsText(file);
|
||||
return false; // 阻止默认上传行为
|
||||
fileToImport.value = file;
|
||||
showSelectGroupDialog.value = true;
|
||||
return false; // 阻止 el-upload 自动上传
|
||||
};
|
||||
|
||||
const confirmImport = async () => {
|
||||
if (!importGroupId.value) {
|
||||
ElMessage.error('请选择一个分类');
|
||||
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 {
|
||||
await updateMarkdown({
|
||||
...fileToImport.value,
|
||||
groupingId: importGroupId.value,
|
||||
});
|
||||
ElMessage.success('导入成功');
|
||||
showSelectGroupDialog.value = false;
|
||||
await chushihua();
|
||||
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);
|
||||
ElMessage.error('移动失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
chushihua();
|
||||
// 根据屏幕宽度初始化侧边栏状态
|
||||
if (window.innerWidth < 768) {
|
||||
isCollapsed.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
const chushihua = async () => {
|
||||
await fetchMarkdownFiles();
|
||||
await fetchGroupings();
|
||||
await fetchRecentFiles();
|
||||
}
|
||||
|
||||
const goToLogin = () => {
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
const goToRegister = () => {
|
||||
router.push('/register');
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
userStore.logout();
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!searchKeyword.value) {
|
||||
await fetchMarkdownFiles();
|
||||
if (!searchKeyword.value.trim()) {
|
||||
groupMarkdownFiles.value = markdownFiles.value;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -728,106 +715,66 @@ const handleSearch = async () => {
|
||||
const handleExportMd = () => {
|
||||
if (!selectedFile.value) return;
|
||||
const blob = new Blob([selectedFile.value.content], { type: 'text/markdown;charset=utf-8' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `${selectedFile.value.title}.md`);
|
||||
link.download = `${selectedFile.value.title}.md`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
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);
|
||||
}
|
||||
const goToLogin = () => {
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
const handleDeleteGroup = async (group) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定要删除这个分类吗?分类下的所有笔记将被移动到“未分类”。',
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
);
|
||||
|
||||
const unclassifiedGroup = categoryTree.value.find(g => g.grouping === '未分类');
|
||||
if (!unclassifiedGroup) {
|
||||
ElMessage.error('未找到“未分类”目录,无法移动笔记。');
|
||||
return;
|
||||
}
|
||||
|
||||
const notesToMove = await markdownList(group.id);
|
||||
if (notesToMove.data && notesToMove.data.length > 0) {
|
||||
for (const note of notesToMove.data) {
|
||||
await updateMarkdown({ ...note, groupingId: unclassifiedGroup.id });
|
||||
}
|
||||
}
|
||||
|
||||
await apiDeleteGrouping(group.id);
|
||||
ElMessage.success('分类删除成功');
|
||||
await fetchGroupings();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败: ' + (error.message || ''));
|
||||
}
|
||||
}
|
||||
const goToRegister = () => {
|
||||
router.push('/register');
|
||||
};
|
||||
|
||||
const fetchRecentFiles = async () => {
|
||||
try {
|
||||
const res = await getRecentFiles();
|
||||
// 与 selectFile 等接口保持一致,axios拦截器已处理一层data
|
||||
groupMarkdownFiles.value = res.data;
|
||||
} catch (error) {
|
||||
console.error('获取最近文件失败:', error);
|
||||
ElMessage.error('获取最近文件失败');
|
||||
}
|
||||
const handleLogout = () => {
|
||||
userStore.logout();
|
||||
ElMessage.success('已退出登录');
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
const resetToHomeView = () => {
|
||||
const resetToHomeView = async () => {
|
||||
selectedFile.value = null;
|
||||
fetchRecentFiles();
|
||||
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 {
|
||||
background-color: var(--bg-color-tertiary) !important;
|
||||
border-radius: 12px !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: var(--box-shadow-dark) !important;
|
||||
animation: fadeIn 0.3s ease-out, slideInUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
.el-dialog__header {
|
||||
padding: 20px 20px 10px !important;
|
||||
margin-right: 0 !important;
|
||||
border-bottom: 1px solid var(--border-color-light);
|
||||
padding: 15px 20px !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.el-dialog__title {
|
||||
@@ -836,10 +783,37 @@ const resetToHomeView = () => {
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 25px 20px !important;
|
||||
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);
|
||||
@@ -881,247 +855,180 @@ const resetToHomeView = () => {
|
||||
<style scoped>
|
||||
.home-page {
|
||||
height: 100vh;
|
||||
background-color: var(--bg-color-secondary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* --- Sidebar --- */
|
||||
.sidebar {
|
||||
background: var(--bg-color);
|
||||
border-right: 1px solid var(--border-color-light);
|
||||
border-right: 1px solid var(--border-color);
|
||||
transition: width 0.3s ease;
|
||||
background-color: var(--bg-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width var(--transition-duration) ease;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--border-color-light);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-header span {
|
||||
color: var(--text-color);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.sidebar-header .el-button {
|
||||
transition: all var(--transition-duration) ease;
|
||||
}
|
||||
.sidebar-header .el-button:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.el-menu-vertical-demo {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
border-right: none;
|
||||
background-color: transparent;
|
||||
padding: 8px;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu__title),
|
||||
.el-menu-item {
|
||||
border-radius: 6px;
|
||||
margin-bottom: 4px;
|
||||
color: var(--text-color-secondary);
|
||||
transition: all var(--transition-duration) ease;
|
||||
.el-menu-vertical-demo:not(.el-menu--collapse) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu__title:hover),
|
||||
.el-menu-item:hover {
|
||||
background-color: var(--bg-color-tertiary);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
background-color: var(--primary-color-light);
|
||||
color: var(--primary-color) !important;
|
||||
font-weight: bold;
|
||||
: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-title span {
|
||||
flex: 1;
|
||||
.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;
|
||||
margin-left: 8px;
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-duration) ease;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
.delete-icon:hover {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
.edit-icon:hover {
|
||||
|
||||
.edit-icon:hover, .delete-icon:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.el-sub-menu__title:hover .edit-icon,
|
||||
.el-sub-menu__title:hover .delete-icon,
|
||||
.el-menu-item:hover .edit-icon,
|
||||
.el-menu-item:hover .delete-icon,
|
||||
.preview-title:hover .edit-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* --- Content Area --- */
|
||||
.content {
|
||||
padding: 24px;
|
||||
padding: 0;
|
||||
background-color: var(--bg-color-secondary);
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
.header, .preview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding: 15px 20px;
|
||||
background-color: var(--bg-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: var(--text-color);
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
.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;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.actions span {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
margin-bottom: 15px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color-light);
|
||||
background-color: var(--bg-color);
|
||||
box-shadow: var(--box-shadow-light);
|
||||
transition: all var(--transition-duration) ease;
|
||||
animation: slideInUp 0.4s ease-out;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: var(--box-shadow);
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--box-shadow-light);
|
||||
}
|
||||
|
||||
.file-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-group-name {
|
||||
font-size: 12px;
|
||||
font-size: 0.8em;
|
||||
color: var(--text-color-secondary);
|
||||
background-color: var(--bg-color-tertiary);
|
||||
padding: 2px 8px;
|
||||
background-color: var(--bg-color-mute);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* --- File Preview --- */
|
||||
.file-preview {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--bg-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--box-shadow);
|
||||
overflow: hidden;
|
||||
animation: fadeIn 0.5s;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
padding: 12px 20px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border-color-light);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.preview-title .edit-icon {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.markdown-preview, .vditor {
|
||||
flex: 1;
|
||||
.markdown-preview {
|
||||
padding: 20px;
|
||||
background: var(--bg-color);
|
||||
border: none;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.vditor {
|
||||
flex-grow: 1;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.save-status {
|
||||
margin-left: 10px;
|
||||
font-size: 14px;
|
||||
color: var(--text-color-placeholder);
|
||||
transition: all var(--transition-duration) ease;
|
||||
}
|
||||
.save-status:not(:empty) {
|
||||
animation: fadeIn 0.5s;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
/* --- Responsive --- */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
position: absolute;
|
||||
z-index: 1001;
|
||||
box-shadow: var(--box-shadow-dark);
|
||||
}
|
||||
.sidebar:not(.el-aside--collapse) {
|
||||
width: 250px;
|
||||
}
|
||||
.content {
|
||||
padding: 15px;
|
||||
}
|
||||
.file-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 22px;
|
||||
}
|
||||
.upload-btn {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user