Files
schoolNews/schoolNewsWeb/src/components/base/FloatingSidebar.vue

358 lines
6.4 KiB
Vue
Raw Normal View History

2025-10-17 12:05:04 +08:00
<template>
<aside class="floating-sidebar" :class="{ collapsed: collapsed, [type]: true }">
<!-- 折叠按钮 -->
<div class="sidebar-toggle-btn" @click="$emit('toggle')">
<i class="toggle-icon">{{ collapsed ? '▶' : '◀' }}</i>
</div>
<!-- 侧边栏内容 -->
<div class="sidebar-content" v-if="!collapsed">
<!-- 标题 -->
<div class="sidebar-header" v-if="title">
<h3 class="sidebar-title">{{ title }}</h3>
</div>
<!-- 菜单列表 -->
<nav class="sidebar-nav">
<div
v-for="menu in menus"
:key="menu.menuID"
class="sidebar-item"
:class="{ active: isActive(menu), 'has-children': hasChildren(menu) }"
>
<div class="sidebar-link" @click="handleClick(menu)">
<span class="link-text">{{ menu.name }}</span>
<i v-if="hasChildren(menu)" class="arrow-icon"></i>
</div>
<!-- 子菜单 -->
<div v-if="hasChildren(menu) && isExpanded(menu)" class="sidebar-submenu">
<div
v-for="child in menu.children"
:key="child.menuID"
class="submenu-item"
:class="{ active: isActive(child) }"
@click="handleClick(child)"
>
<span class="submenu-text">{{ child.name }}</span>
</div>
</div>
</div>
</nav>
</div>
<!-- 折叠状态的图标 -->
<div class="sidebar-icons" v-else>
<div
v-for="menu in menus"
:key="menu.menuID"
class="icon-item"
:class="{ active: isActive(menu) }"
:title="menu.name"
@click="handleClick(menu)"
>
<span class="icon-text">{{ menu.name?.charAt(0) }}</span>
</div>
</div>
</aside>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import type { SysMenu } from '@/types';
// Props
interface Props {
menus: SysMenu[];
collapsed?: boolean;
title?: string;
type?: 'nav' | 'sidebar';
activePath?: string;
}
const props = withDefaults(defineProps<Props>(), {
collapsed: false,
type: 'sidebar'
});
// Emits
const emit = defineEmits<{
'toggle': [];
'menu-click': [menu: SysMenu];
}>();
// 展开的菜单ID列表
const expandedMenus = ref<Set<string>>(new Set());
// 检查菜单是否有子菜单
function hasChildren(menu: SysMenu): boolean {
return !!(menu.children && menu.children.length > 0);
}
// 检查菜单是否激活
function isActive(menu: SysMenu): boolean {
if (!menu.url) return false;
return props.activePath === menu.url;
}
// 检查菜单是否展开
function isExpanded(menu: SysMenu): boolean {
return menu.menuID ? expandedMenus.value.has(menu.menuID) : false;
}
// 处理点击
function handleClick(menu: SysMenu) {
if (hasChildren(menu)) {
// 切换展开状态
if (menu.menuID) {
if (expandedMenus.value.has(menu.menuID)) {
expandedMenus.value.delete(menu.menuID);
} else {
expandedMenus.value.add(menu.menuID);
}
}
}
// 触发点击事件
emit('menu-click', menu);
}
</script>
<style lang="scss" scoped>
.floating-sidebar {
background: white;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
flex-shrink: 0;
display: flex;
flex-direction: column;
position: relative;
&.sidebar {
width: 260px;
&.collapsed {
width: 64px;
}
}
&.nav {
width: 200px;
&.collapsed {
width: 56px;
}
}
}
.sidebar-toggle-btn {
width: 24px;
height: 48px;
background: white;
border: 1px solid #e8e8e8;
border-radius: 0 12px 12px 0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
z-index: 10;
&:hover {
background: #f0f2f5;
border-color: #C62828;
}
.toggle-icon {
font-size: 12px;
color: #666;
font-style: normal;
}
}
.sidebar-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
padding: 20px 16px 16px;
border-bottom: 1px solid #f0f0f0;
.sidebar-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #141F38;
}
}
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 8px 0;
/* 滚动条样式 */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f5f5f5;
}
&::-webkit-scrollbar-thumb {
background: #ddd;
border-radius: 3px;
&:hover {
background: #bbb;
}
}
}
.sidebar-item {
margin: 4px 8px;
border-radius: 4px;
overflow: hidden;
transition: all 0.3s;
&.active {
background: #fff1f0;
.sidebar-link {
color: #C62828;
font-weight: 500;
}
}
&:hover {
background: #f5f5f5;
}
}
.sidebar-link {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
color: #333;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
user-select: none;
.link-text {
flex: 1;
}
.arrow-icon {
font-size: 10px;
font-style: normal;
transition: transform 0.3s;
color: #999;
}
&:hover {
color: #C62828;
}
}
.sidebar-submenu {
background: #fafafa;
margin: 0 8px 4px;
border-radius: 4px;
overflow: hidden;
}
.submenu-item {
padding: 10px 16px 10px 32px;
color: #666;
font-size: 13px;
cursor: pointer;
transition: all 0.3s;
user-select: none;
&:hover {
background: #f0f0f0;
color: #C62828;
}
&.active {
background: #fff1f0;
color: #C62828;
font-weight: 500;
}
.submenu-text {
display: block;
}
}
.sidebar-icons {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 8px;
gap: 8px;
overflow-y: auto;
}
.icon-item {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
background: #f5f5f5;
&:hover {
background: #e8e8e8;
}
&.active {
background: #C62828;
.icon-text {
color: white;
}
}
.icon-text {
font-size: 16px;
font-weight: 500;
color: #666;
user-select: none;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.floating-sidebar {
&.sidebar {
width: 200px;
&.collapsed {
width: 56px;
}
}
&.nav {
width: 180px;
&.collapsed {
width: 50px;
}
}
}
}
</style>