路由更新

This commit is contained in:
2025-10-17 12:05:04 +08:00
parent 0811af6d03
commit edadcc72ca
15 changed files with 456 additions and 557 deletions

View File

@@ -0,0 +1,357 @@
<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>

View File

@@ -3,7 +3,7 @@
<div class="nav-container">
<!-- Logo区域 -->
<div class="nav-logo">
<img src="@/assets/imgs/logo-icon.svg" alt="Logo" class="logo-icon" />
<img src="../../assets/imgs/logo-icon.svg" alt="Logo" class="logo-icon" />
<span class="logo-text">红色思政学习平台</span>
</div>
@@ -23,13 +23,12 @@
</div>
<!-- 下拉菜单 -->
<Teleport to="body">
<Teleport to="body" v-if="hasNavigationChildren(menu)">
<div
v-if="hasNavigationChildren(menu)"
class="dropdown-menu"
:class="{ show: activeDropdown === menu.menuID }"
:style="getDropdownPosition(menu)"
@mouseenter="handleMouseEnter(menu)"
@mouseenter="() => { if (menu.menuID) activeDropdown = menu.menuID }"
@mouseleave="handleMouseLeave"
>
<div
@@ -58,7 +57,7 @@
@keyup.enter="handleSearch"
/>
<div class="search-icon">
<img src="@/assets/imgs/search-icon.svg" alt="搜索" />
<img src="../../assets/imgs/search-icon.svg" alt="搜索" />
</div>
</div>
</div>
@@ -152,9 +151,11 @@ function handleMouseEnter(menu: SysMenu, event?: MouseEvent) {
activeDropdown.value = menu.menuID || null;
//
const target = event?.currentTarget as HTMLElement;
if (target && menu.menuID) {
const rect = target.getBoundingClientRect();
if (event && menu.menuID) {
const target = event.currentTarget as HTMLElement;
const navLink = target.querySelector('.nav-link') as HTMLElement;
const rect = (navLink || target).getBoundingClientRect();
dropdownPositions.value[menu.menuID] = {
left: rect.left,
top: rect.bottom,
@@ -167,11 +168,15 @@ function handleMouseEnter(menu: SysMenu, event?: MouseEvent) {
//
function getDropdownPosition(menu: SysMenu) {
const menuID = menu.menuID;
if (!menuID || !dropdownPositions.value[menuID]) {
return {};
const pos = menuID && dropdownPositions.value[menuID];
if (!pos) {
return {
position: 'fixed' as const,
visibility: 'hidden' as const
};
}
const pos = dropdownPositions.value[menuID];
return {
position: 'fixed' as const,
left: `${pos.left}px`,
@@ -356,15 +361,16 @@ function handleLogout() {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s;
transform: scaleY(0);
transform-origin: top center;
transition: opacity 0.2s, transform 0.2s, visibility 0.2s;
z-index: 10000;
pointer-events: none;
&.show {
opacity: 1;
visibility: visible;
transform: translateY(0);
transform: scaleY(1);
pointer-events: auto;
}
}

View File

@@ -0,0 +1,6 @@
export { default as Breadcrumb } from './Breadcrumb.vue';
export { default as FloatingSidebar } from './FloatingSidebar.vue';
export { default as MenuItem } from './MenuItem.vue';
export { default as MenuNav } from './MenuNav.vue';
export { default as TopNavigation } from './TopNavigation.vue';
export { default as UserDropdown } from './UserDropdown.vue';

View File

@@ -0,0 +1,3 @@
// 导出 base 基础组件
export * from './base';