路由更新
This commit is contained in:
357
schoolNewsWeb/src/components/base/FloatingSidebar.vue
Normal file
357
schoolNewsWeb/src/components/base/FloatingSidebar.vue
Normal 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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
6
schoolNewsWeb/src/components/base/index.ts
Normal file
6
schoolNewsWeb/src/components/base/index.ts
Normal 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';
|
||||
3
schoolNewsWeb/src/components/index.ts
Normal file
3
schoolNewsWeb/src/components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// 导出 base 基础组件
|
||||
export * from './base';
|
||||
|
||||
Reference in New Issue
Block a user