Files
biji/biji-qianduan/src/components/home/SidebarMenu.vue
ikmkj 25b52f87aa refactor: 统一错误处理并优化代码
- 移除重复的错误提示,统一在axios拦截器中处理
- 优化XSS拦截器,添加请求头白名单
- 修复注册码服务的日期处理问题
- 添加403权限错误处理
- 优化分组查询参数处理
2026-03-03 23:41:20 +08:00

480 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<el-aside class="sidebar" :class="{ 'is-collapsed': isCollapsed }" :width="isCollapsed ? (isMobile ? '0' : '72px') : '260px'">
<div class="sidebar-header">
<div class="header-left">
<transition name="fade">
<span v-if="!isCollapsed" class="sidebar-title">笔记分类</span>
</transition>
</div>
<div class="header-actions">
<el-tooltip content="新建分类" placement="top" :disabled="isCollapsed && !isMobile">
<el-button v-if="!isCollapsed || isMobile" type="primary" size="small" @click="$emit('show-create-group')" circle class="action-btn">
<el-icon><Plus /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip :content="isCollapsed ? '展开' : '收起'" placement="top" v-if="!isMobile">
<el-button @click="$emit('toggle-collapse')" type="primary" size="small" circle class="action-btn toggle-btn">
<el-icon :class="{ 'rotate-180': isCollapsed }">
<Fold />
</el-icon>
</el-button>
</el-tooltip>
</div>
</div>
<div class="sidebar-content">
<el-menu
:default-active="activeMenu"
class="el-menu-vertical-demo"
:collapse="isCollapsed && !isMobile"
popper-effect="light"
:collapse-transition="true"
>
<div class="menu-section" v-if="isMobile">
<div v-if="userStore.isLoggedIn" class="user-info">
<!-- 修复添加默认值防止空指针 -->
<el-avatar :size="40" class="user-avatar">{{ userStore.userInfo?.username?.charAt(0)?.toUpperCase() || '?' }}</el-avatar>
<span class="username">{{ userStore.userInfo?.username || '访客' }}</span>
</div>
<div v-else class="guest-info">
<el-button type="primary" @click="goToLogin">登录</el-button>
<el-button @click="goToRegister">注册</el-button>
</div>
</div>
<div class="menu-section">
<div class="section-title" v-if="!isCollapsed">分类列表</div>
<template v-for="menu in categoryTree" :key="menu.id">
<component :is="renderMenu(menu)" />
</template>
</div>
<div class="menu-section menu-footer">
<ElMenuItem index="trash" @click="goToTrash" class="menu-item-trash">
<ElIcon><Delete /></ElIcon>
<template #title>回收站</template>
</ElMenuItem>
<template v-if="isMobile && userStore.isLoggedIn">
<ElMenuItem index="system-settings" @click="$emit('show-system-settings')">
<ElIcon><Setting /></ElIcon>
<template #title>系统管理</template>
</ElMenuItem>
<ElMenuItem index="update-password" @click="$emit('show-update-password')">
<ElIcon><Lock /></ElIcon>
<template #title>修改密码</template>
</ElMenuItem>
<ElMenuItem index="logout" @click="$emit('logout')" class="menu-item-logout">
<ElIcon><SwitchButton /></ElIcon>
<template #title>退出登录</template>
</ElMenuItem>
</template>
</div>
</el-menu>
</div>
</el-aside>
</template>
<script setup>
import { h } from 'vue';
import { ElSubMenu, ElMenuItem, ElIcon, ElMessageBox, ElTooltip, ElMessage } from 'element-plus';
import { Folder, Delete, Edit, Plus, Fold, Expand, Setting, Lock, SwitchButton } from '@element-plus/icons-vue';
import { useUserStore } from '@/stores/user';
import { useRouter } from 'vue-router';
import { deleteGrouping } from '@/api/CommonApi.js';
const props = defineProps({
isCollapsed: Boolean,
isMobile: Boolean,
activeMenu: String,
categoryTree: Array,
});
const emit = defineEmits([
'select-file',
'show-rename-dialog',
'show-create-group',
'toggle-collapse',
'group-deleted',
'show-system-settings',
'show-update-password',
'logout'
]);
const userStore = useUserStore();
const router = useRouter();
const goToTrash = () => router.push({ name: 'Trash' });
const goToLogin = () => router.push('/login');
const goToRegister = () => router.push('/register');
const handleDeleteGroup = (group) => {
ElMessageBox.confirm(
`确定要删除分类 “${group.grouping}” 吗?这将同时删除该分类下的所有子分类和笔记。`,
'警告',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
).then(async () => {
try {
await deleteGrouping(group.id);
ElMessage.success('分类已删除');
emit('group-deleted');
} catch (error) {
// 错误已在 axios 拦截器中显示,这里不再重复显示
console.error('删除分类失败:', error);
}
});
};
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(); emit('show-rename-dialog', 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: !props.isCollapsed,
effect: 'dark',
offset: 15,
}, { default: titleContent });
if (item.children && item.children.length > 0) {
return h(ElSubMenu, {
index: `group-${item.id}`,
popperClass: props.isCollapsed ? 'hide-popper' : ''
}, {
title: () => h('div', {
class: 'submenu-title-wrapper',
onClick: () => emit('select-file', item),
style: 'width: 100%; display: flex; align-items: center;'
}, [ wrappedContent() ]),
default: () => item.children.map(child => renderMenu(child))
});
}
return h(ElMenuItem, { index: `group-${item.id}`, onClick: () => emit('select-file', item) }, {
default: wrappedContent
});
};
</script>
<style scoped>
.sidebar {
background: #ffffff;
border-right: 1px solid #e4e7ed;
transition: width 0.25s ease;
display: flex;
flex-direction: column;
position: relative;
z-index: 10;
}
.dark-theme .sidebar {
background: #1e1e2f;
border-right: 1px solid #2c2c3d;
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
flex-shrink: 0;
border-bottom: 1px solid #e4e7ed;
min-height: 64px;
transition: padding 0.25s ease;
}
.sidebar.is-collapsed .sidebar-header {
padding: 1rem 0.5rem;
justify-content: center;
}
.header-left {
flex: 1;
display: flex;
align-items: center;
overflow: hidden;
}
.sidebar.is-collapsed .header-left {
display: none;
}
.dark-theme .sidebar-header {
border-bottom-color: rgba(255, 255, 255, 0.06);
}
.sidebar-title {
font-weight: 700;
font-size: 16px;
color: var(--text-color);
letter-spacing: 0.3px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-color-light) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header-actions {
display: flex;
gap: 8px;
align-items: center;
}
.sidebar.is-collapsed .header-actions {
flex-direction: column;
gap: 12px;
}
.action-btn {
transition: transform 0.2s ease, color 0.2s ease;
}
.toggle-btn .el-icon {
transition: transform 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.toggle-btn .el-icon.rotate-180 {
transform: rotate(180deg);
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
transform: translateX(-10px);
}
.sidebar-content {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.el-menu {
border-right: none;
background: transparent;
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 0.75rem 0.25rem;
}
.el-menu::-webkit-scrollbar {
width: 4px;
}
.el-menu::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1);
border-radius: 2px;
}
.el-menu::-webkit-scrollbar-track {
background: transparent;
}
.menu-section {
padding: 0 0.25rem;
margin-bottom: 0.5rem;
}
.section-title {
font-size: 11px;
font-weight: 700;
color: var(--text-color-secondary);
text-transform: uppercase;
letter-spacing: 1.2px;
padding: 0.75rem 1rem 0.5rem;
opacity: 0.6;
}
.sidebar.is-collapsed .section-title {
display: none;
}
.menu-footer {
margin-top: auto;
border-top: 1px solid #e4e7ed;
padding-top: 0.75rem;
}
.dark-theme .menu-footer {
border-top-color: rgba(255, 255, 255, 0.06);
}
:deep(.el-menu-item), :deep(.el-sub-menu__title) {
height: 46px;
line-height: 46px;
border-radius: 12px;
margin: 2px 0.5rem;
color: var(--text-color-secondary);
transition: color 0.2s ease, background-color 0.2s ease, transform 0.2s ease;
position: relative;
}
:deep(.el-menu-item::before), :deep(.el-sub-menu__title::before) {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 0;
background: var(--primary-color);
border-radius: 0 3px 3px 0;
transition: height 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
:deep(.el-menu-item:hover::before), :deep(.el-sub-menu__title:hover::before) {
height: 20px;
}
:deep(.el-menu-item.is-active) {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-color-light) 100%);
color: #fff;
font-weight: 600;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.35);
}
:deep(.el-menu-item.is-active::before) {
height: 24px;
background: #fff;
}
:deep(.el-menu-item:hover), :deep(.el-sub-menu__title:hover) {
background-color: rgba(64, 158, 255, 0.08);
color: var(--primary-color);
transform: translateX(2px);
}
.dark-theme :deep(.el-menu-item:hover),
.dark-theme :deep(.el-sub-menu__title:hover) {
background-color: rgba(64, 158, 255, 0.15);
}
.menu-item-title {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
gap: 8px;
}
.menu-item-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
font-size: 14px;
font-weight: 500;
}
.menu-item-actions {
display: none;
align-items: center;
gap: 4px;
padding: 4px;
background: #f5f7fa;
border-radius: 8px;
}
.dark-theme .menu-item-actions {
background: #2c2c3d;
}
.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 {
opacity: 0;
width: 0;
}
.edit-icon, .delete-icon {
cursor: pointer;
color: var(--text-color-secondary);
padding: 6px;
border-radius: 6px;
transition: color 0.2s ease, background-color 0.2s ease, transform 0.2s ease;
}
.edit-icon:hover {
color: var(--primary-color);
background: rgba(64, 158, 255, 0.12);
transform: scale(1.1);
}
.delete-icon:hover {
color: #f56c6c;
background: rgba(245, 108, 108, 0.12);
transform: scale(1.1);
}
.user-info {
display: flex;
flex-direction: column;
align-items: center;
padding: 1.25rem 1rem;
gap: 0.75rem;
background: linear-gradient(135deg, rgba(64, 158, 255, 0.1) 0%, rgba(64, 158, 255, 0.05) 100%);
border-radius: 16px;
margin: 0.5rem;
border: 1px solid rgba(64, 158, 255, 0.1);
}
.user-avatar {
background: linear-gradient(135deg, var(--primary-color) 0%, #66b1ff 100%);
color: #fff;
font-weight: 700;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
.username {
font-weight: 600;
font-size: 14px;
color: var(--text-color);
}
.guest-info {
display: flex;
gap: 0.75rem;
padding: 1rem;
justify-content: center;
}
.menu-item-trash {
color: var(--text-color-secondary) !important;
}
.menu-item-trash:hover {
color: #e6a23c !important;
background-color: rgba(230, 162, 60, 0.12) !important;
}
.menu-item-logout:hover {
color: #f56c6c !important;
background-color: rgba(245, 108, 108, 0.12) !important;
}
.sidebar.is-collapsed :deep(.el-menu-item),
.sidebar.is-collapsed :deep(.el-sub-menu__title) {
margin: 4px 0.25rem;
justify-content: center;
}
.sidebar.is-collapsed :deep(.el-icon) {
font-size: 20px;
}
</style>