wip: 临时提交

This commit is contained in:
Ray.Hao
2025-05-23 09:51:13 +08:00
parent 54b2164d25
commit af30411317
14 changed files with 809 additions and 308 deletions

View File

@@ -0,0 +1,46 @@
<template>
<div class="layout" :class="layoutClass">
<!-- 移动端遮罩层 -->
<div v-if="isMobile && isSidebarOpen" class="layout__overlay" @click="closeSidebar" />
<!-- 布局内容插槽 -->
<slot></slot>
<!-- 设置面板 -->
<Settings v-if="isShowSettings" />
<!-- 返回顶部按钮 -->
<el-backtop target=".app-main">
<div class="i-svg:backtop w-6 h-6" />
</el-backtop>
</div>
</template>
<script setup lang="ts">
import { useLayout } from "../composables/useLayout";
import { useLayoutResponsive } from "../composables/useLayoutResponsive";
import Settings from "@/layout/components/Settings/index.vue";
// 布局相关
const { layoutClass, isShowSettings, isSidebarOpen, closeSidebar } = useLayout();
// 响应式处理
const { isMobile } = useLayoutResponsive();
</script>
<style lang="scss" scoped>
.layout {
width: 100%;
height: 100%;
&__overlay {
position: fixed;
top: 0;
left: 0;
z-index: 999;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
}
}
</style>

View File

@@ -0,0 +1,130 @@
<!-- 菜单组件 -->
<template>
<el-menu
ref="menuRef"
:default-active="currentRoute.path"
:collapse="!appStore.sidebar.opened"
:background-color="
theme === 'dark' || sidebarColorScheme === SidebarColor.CLASSIC_BLUE
? variables['menu-background']
: undefined
"
:text-color="
theme === 'dark' || sidebarColorScheme === SidebarColor.CLASSIC_BLUE
? variables['menu-text']
: undefined
"
:active-text-color="
theme === 'dark' || sidebarColorScheme === SidebarColor.CLASSIC_BLUE
? variables['menu-active-text']
: undefined
"
:popper-effect="theme"
:unique-opened="false"
:collapse-transition="false"
:mode="menuMode"
@open="onMenuOpen"
@close="onMenuClose"
>
<!-- 菜单项 -->
<SidebarMenuItem
v-for="route in data"
:key="route.path"
:item="route"
:base-path="resolveFullPath(route.path)"
/>
</el-menu>
</template>
<script lang="ts" setup>
import { ref, computed, watch, PropType } from "vue";
import { useRoute } from "vue-router";
import path from "path-browserify";
import type { MenuInstance } from "element-plus";
import type { RouteRecordRaw } from "vue-router";
import { SidebarColor } from "@/enums/settings/theme.enum";
import { useSettingsStore, useAppStore } from "@/store";
import { isExternal } from "@/utils/index";
import SidebarMenuItem from "@/layout/components/Sidebar/components/SidebarMenuItem.vue";
import variables from "@/styles/variables.module.scss";
const props = defineProps({
data: {
type: Array as PropType<RouteRecordRaw[]>,
default: () => [],
},
basePath: {
type: String,
required: true,
example: "/system",
},
menuMode: {
type: String as PropType<"vertical" | "horizontal">,
default: "vertical",
validator: (value: string) => ["vertical", "horizontal"].includes(value),
},
});
const menuRef = ref<MenuInstance>();
const settingsStore = useSettingsStore();
const appStore = useAppStore();
const currentRoute = useRoute();
// 存储已展开的菜单项索引
const expandedMenuIndexes = ref<string[]>([]);
// 获取主题
const theme = computed(() => settingsStore.theme);
// 获取浅色主题下的侧边栏配色方案
const sidebarColorScheme = computed(() => settingsStore.sidebarColorScheme);
/**
* 获取完整路径
*
* @param routePath 当前路由的相对路径 /user
* @returns 完整的绝对路径 D://vue3-element-admin/system/user
*/
function resolveFullPath(routePath: string) {
if (isExternal(routePath)) {
return routePath;
}
if (isExternal(props.basePath)) {
return props.basePath;
}
// 解析路径,生成完整的绝对路径
return path.resolve(props.basePath, routePath);
}
/**
* 打开菜单
*
* @param index 当前展开的菜单项索引
*/
const onMenuOpen = (index: string) => {
expandedMenuIndexes.value.push(index);
};
/**
* 关闭菜单
*
* @param index 当前收起的菜单项索引
*/
const onMenuClose = (index: string) => {
expandedMenuIndexes.value = expandedMenuIndexes.value.filter((item) => item !== index);
};
/**
* 监听菜单模式变化:当菜单模式切换为水平模式时,关闭所有展开的菜单项,
* 避免在水平模式下菜单项显示错位。
*/
watch(
() => props.menuMode,
(newMode) => {
if (newMode === "horizontal" && menuRef.value) {
expandedMenuIndexes.value.forEach((item) => menuRef.value!.close(item));
}
}
);
</script>

View File

@@ -0,0 +1,110 @@
<template>
<LayoutBase>
<!-- 左侧菜单栏 -->
<div class="layout__sidebar" :class="{ 'layout__sidebar--collapsed': !isSidebarOpen }">
<LayoutSidebar :show-logo="isShowLogo" :is-collapsed="!isSidebarOpen">
<el-scrollbar>
<SidebarMenu :data="routes" base-path="" />
</el-scrollbar>
</LayoutSidebar>
</div>
<!-- 主内容区 -->
<div
:class="{
hasTagsView: isShowTagsView,
'layout__main--collapsed': !isSidebarOpen,
}"
class="layout__main"
>
<NavBar />
<TagsView v-if="isShowTagsView" />
<AppMain />
</div>
</LayoutBase>
</template>
<script setup lang="ts">
import { useLayout } from "../../composables/useLayout";
import { useLayoutMenu } from "../../composables/useLayoutMenu";
import LayoutBase from "../LayoutBase.vue";
import LayoutSidebar from "../common/LayoutSidebar.vue";
import NavBar from "@/layout/components/NavBar/index.vue";
import TagsView from "@/layout/components/TagsView/index.vue";
import AppMain from "@/layout/components/AppMain/index.vue";
import SidebarMenu from "../LayoutMenu.vue";
// 布局相关参数
const { isShowTagsView, isShowLogo, isSidebarOpen } = useLayout();
// 菜单相关
const { routes } = useLayoutMenu();
</script>
<style lang="scss" scoped>
.layout {
&__sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 999;
width: $sidebar-width;
background-color: $menu-background;
transition: width 0.28s;
&--collapsed {
width: $sidebar-width-collapsed;
}
}
&__main {
position: relative;
height: 100%;
margin-left: $sidebar-width;
overflow-y: auto;
transition: margin-left 0.28s;
&--collapsed {
margin-left: $sidebar-width-collapsed;
}
.fixed-header {
position: sticky;
top: 0;
z-index: 9;
transition: width 0.28s;
}
}
}
/* 移动端样式 */
:deep(.mobile) {
.layout__sidebar {
position: fixed;
left: 0;
transition: transform 0.28s;
}
&.hideSidebar {
.layout__sidebar {
width: $sidebar-width !important;
transform: translateX(-$sidebar-width);
}
.layout__main {
margin-left: 0 !important;
}
}
.layout__main {
margin-left: 0;
}
}
:deep(.hasTagsView) {
.app-main {
height: calc(100vh - $navbar-height - $tags-view-height) !important;
}
}
</style>

View File

@@ -0,0 +1,143 @@
<template>
<LayoutBase>
<!-- 顶部菜单栏 -->
<div class="layout__header">
<LayoutSidebar :show-logo="isShowLogo" :is-collapsed="false">
<SidebarMixTopMenu />
<NavbarActions />
</LayoutSidebar>
</div>
<!-- 主内容区容器 -->
<div class="layout__container">
<!-- 左侧菜单栏 -->
<div class="layout__sidebar--left" :class="{ 'layout__sidebar--collapsed': !isSidebarOpen }">
<el-scrollbar>
<SidebarMenu :data="sideMenuRoutes" :base-path="activeTopMenuPath" />
</el-scrollbar>
<!-- 侧边栏切换按钮 -->
<div class="layout__sidebar-toggle">
<Hamburger :is-active="isSidebarOpen" @toggle-click="toggleSidebar" />
</div>
</div>
<!-- 主内容区 -->
<div :class="{ hasTagsView: isShowTagsView }" class="layout__main">
<TagsView v-if="isShowTagsView" />
<AppMain />
</div>
</div>
</LayoutBase>
</template>
<script setup lang="ts">
import { useLayout } from "../../composables/useLayout";
import { useLayoutMenu } from "../../composables/useLayoutMenu";
import LayoutBase from "../LayoutBase.vue";
import LayoutSidebar from "../common/LayoutSidebar.vue";
import SidebarMixTopMenu from "@/layout/components/Sidebar/components/SidebarMixTopMenu.vue";
import NavbarActions from "@/layout/components/NavBar/components/NavbarActions.vue";
import TagsView from "@/layout/components/TagsView/index.vue";
import AppMain from "@/layout/components/AppMain/index.vue";
import SidebarMenu from "../LayoutMenu.vue";
import Hamburger from "@/components/Hamburger/index.vue";
// 布局相关参数
const { isShowTagsView, isShowLogo, isSidebarOpen, toggleSidebar } = useLayout();
// 菜单相关
const { sideMenuRoutes, activeTopMenuPath } = useLayoutMenu();
</script>
<style lang="scss" scoped>
.layout {
&__header {
position: sticky;
top: 0;
z-index: 999;
width: 100%;
height: $navbar-height;
background-color: $menu-background;
:deep(.layout-sidebar) {
display: flex;
width: 100% !important;
height: $navbar-height;
}
}
&__container {
display: flex;
height: calc(100vh - $navbar-height);
padding-top: 0;
.layout__sidebar--left {
position: relative;
width: $sidebar-width;
height: 100%;
background-color: var(--menu-background);
transition: width 0.28s;
&--collapsed {
width: $sidebar-width-collapsed !important;
}
:deep(.el-scrollbar) {
height: calc(100vh - $navbar-height - 50px);
}
:deep(.el-menu) {
height: 100%;
border: none;
}
.layout__sidebar-toggle {
position: absolute;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 50px;
line-height: 50px;
box-shadow: 0 0 6px -2px var(--el-color-primary);
}
}
.layout__main {
flex: 1;
min-width: 0;
height: 100%;
margin-left: 0;
overflow-y: auto;
}
}
}
/* 移动端样式 */
:deep(.mobile) {
.layout__container {
.layout__sidebar--left {
position: fixed;
top: $navbar-height;
bottom: 0;
left: 0;
z-index: 1000;
transition: transform 0.28s;
}
}
&.hideSidebar {
.layout__sidebar--left {
width: $sidebar-width !important;
transform: translateX(-$sidebar-width);
}
}
}
:deep(.hasTagsView) {
.app-main {
height: calc(100vh - $navbar-height - $tags-view-height) !important;
}
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<LayoutBase>
<!-- 顶部菜单栏 -->
<div class="layout__header">
<LayoutSidebar :show-logo="isShowLogo" :is-collapsed="false">
<el-scrollbar>
<SidebarMenu :data="routes" base-path="" menu-mode="horizontal" />
</el-scrollbar>
<NavbarActions />
</LayoutSidebar>
</div>
<!-- 主内容区 -->
<div :class="{ hasTagsView: isShowTagsView }" class="layout__main">
<TagsView v-if="isShowTagsView" />
<AppMain />
</div>
</LayoutBase>
</template>
<script setup lang="ts">
import { useLayout } from "../../composables/useLayout";
import { useLayoutMenu } from "../../composables/useLayoutMenu";
import LayoutBase from "../LayoutBase.vue";
import LayoutSidebar from "../common/LayoutSidebar.vue";
import NavbarActions from "@/layout/components/NavBar/components/NavbarActions.vue";
import TagsView from "@/layout/components/TagsView/index.vue";
import AppMain from "@/layout/components/AppMain/index.vue";
import SidebarMenu from "../LayoutMenu.vue";
// 布局相关参数
const { isShowTagsView, isShowLogo } = useLayout();
// 菜单相关
const { routes } = useLayoutMenu();
</script>
<style lang="scss" scoped>
.layout {
&__header {
position: sticky;
top: 0;
z-index: 999;
width: 100%;
height: $navbar-height;
background-color: $menu-background;
:deep(.layout-sidebar) {
display: flex;
width: 100% !important;
height: $navbar-height;
.el-scrollbar {
flex: 1;
height: $navbar-height;
}
.el-menu-item,
.el-sub-menu__title,
.el-menu--horizontal {
height: $navbar-height;
line-height: $navbar-height;
}
}
}
&__main {
position: relative;
height: calc(100vh - $navbar-height);
margin-left: 0;
overflow-y: auto;
.fixed-header {
position: sticky;
top: 0;
z-index: 9;
}
}
}
:deep(.hasTagsView) {
.app-main {
height: calc(100vh - $navbar-height - $tags-view-height) !important;
}
}
</style>

View File

@@ -0,0 +1,46 @@
<template>
<div :class="{ 'has-logo': showLogo }" class="layout-sidebar">
<!-- Logo -->
<SidebarLogo v-if="showLogo" :collapse="isCollapsed" />
<!-- 主菜单内容 -->
<slot></slot>
</div>
</template>
<script setup lang="ts">
defineProps({
/**
* 是否显示Logo
*/
showLogo: {
type: Boolean,
default: true,
},
/**
* 是否折叠
*/
isCollapsed: {
type: Boolean,
default: false,
},
});
</script>
<style lang="scss" scoped>
.layout-sidebar {
position: relative;
height: 100%;
background-color: var(--menu-background);
transition: width 0.28s;
&.has-logo {
.el-scrollbar {
height: calc(100vh - $navbar-height);
}
}
:deep(.el-menu) {
border: none;
}
}
</style>