初始化项目
This commit is contained in:
@@ -0,0 +1,665 @@
|
||||
<template>
|
||||
<a-layout class="app-wrapper" :class="layoutClass">
|
||||
<!-- 默认布局:左侧双栏布局 -->
|
||||
<template v-if="layoutMode === 'default'">
|
||||
<!-- 第一个侧边栏:显示一级菜单 -->
|
||||
<a-layout-sider theme="dark" width="70" class="left-sidebar">
|
||||
<div class="logo-box">
|
||||
<img src="@/assets/images/logo.png" alt="logo" class="logo-image" />
|
||||
</div>
|
||||
<ul class="left-nav">
|
||||
<li v-for="(item, index) in menuList" :key="index"
|
||||
:class="{ active: selectedParentMenu?.path === item.path }"
|
||||
@click="handleParentMenuClick(item)">
|
||||
<component :is="getIconComponent(item.meta?.icon)" />
|
||||
<span>{{ item.meta?.title }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</a-layout-sider>
|
||||
|
||||
<!-- 第二个侧边栏:显示选中的父菜单的子菜单 -->
|
||||
<a-layout-sider
|
||||
v-if="selectedParentMenu && selectedParentMenu.children && selectedParentMenu.children.length > 0"
|
||||
theme="light" :collapsed="sidebarCollapsed" :collapsible="true" @collapse="handleCollapse" width="200"
|
||||
:collapsed-width="64" class="right-sidebar">
|
||||
<div class="parent-title">
|
||||
<component :is="getIconComponent(selectedParentMenu.meta?.icon)" />
|
||||
<span v-if="!sidebarCollapsed">{{ selectedParentMenu.meta?.title }}</span>
|
||||
</div>
|
||||
<a-menu v-model:openKeys="openKeys" v-model:selectedKeys="selectedKeys" mode="inline"
|
||||
:selected-keys="[route.path]">
|
||||
<navMenu :menu-items="selectedParentMenu.children" :active-path="route.path" />
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
|
||||
<a-layout class="main-layout">
|
||||
<a-layout-header class="app-header">
|
||||
<div class="header-left">
|
||||
<breadcrumb />
|
||||
</div>
|
||||
<userbar />
|
||||
</a-layout-header>
|
||||
<tags />
|
||||
<a-layout-content class="app-main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="cachedViews">
|
||||
<component :is="Component" :key="refreshKey" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<!-- Menu布局:左侧菜单栏布局 -->
|
||||
<template v-else-if="layoutMode === 'menu'">
|
||||
<a-layout-sider theme="light" style="border-right: 1px solid #f0f0f0" :collapsed="sidebarCollapsed"
|
||||
:collapsible="true" @collapse="handleCollapse" class="full-menu-sidebar" width="200"
|
||||
:collapsed-width="64">
|
||||
<div class="logo-box-full">
|
||||
<img src="@/assets/images/logo.png" alt="logo" class="logo-image" />
|
||||
<span v-if="!sidebarCollapsed" class="app-name">{{ config.APP_NAME }}</span>
|
||||
</div>
|
||||
<a-menu v-model:openKeys="openKeys" v-model:selectedKeys="selectedKeys" mode="inline"
|
||||
:selected-keys="[route.path]">
|
||||
<navMenu :menu-items="menuList" :active-path="route.path" />
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
<a-layout class="main-layout">
|
||||
<a-layout-header class="app-header">
|
||||
<div class="header-left">
|
||||
<breadcrumb />
|
||||
</div>
|
||||
<userbar />
|
||||
</a-layout-header>
|
||||
<tags />
|
||||
<a-layout-content class="app-main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="cachedViews">
|
||||
<component :is="Component" :key="$route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<!-- Top布局:顶部菜单栏布局 -->
|
||||
<template v-else-if="layoutMode === 'top'">
|
||||
<a-layout-header class="app-header top-header">
|
||||
<div class="top-header-left">
|
||||
<div class="logo-box-top">
|
||||
<img src="@/assets/images/logo.png" alt="logo" class="logo-image" />
|
||||
<span class="app-name">{{ config.APP_NAME }}</span>
|
||||
</div>
|
||||
<a-menu v-model:selectedKeys="selectedKeys" mode="horizontal" :selected-keys="[route.path]"
|
||||
style="line-height: 60px">
|
||||
<navMenu :menu-items="menuList" :active-path="route.path" />
|
||||
</a-menu>
|
||||
</div>
|
||||
<userbar />
|
||||
</a-layout-header>
|
||||
<tags />
|
||||
<a-layout-content class="app-main top-content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="cachedViews">
|
||||
<component :is="Component" :key="refreshKey" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</a-layout-content>
|
||||
</template>
|
||||
|
||||
<!-- 漂浮的设置按钮 -->
|
||||
<a-float-button type="primary" @click="openSetting">
|
||||
<template #icon>
|
||||
<SettingOutlined />
|
||||
</template>
|
||||
</a-float-button>
|
||||
|
||||
<!-- 布局设置组件 -->
|
||||
<setting ref="settingRef" />
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useLayoutStore } from '@/stores/modules/layout'
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
import { SettingOutlined } from '@ant-design/icons-vue'
|
||||
import * as icons from '@ant-design/icons-vue'
|
||||
import config from '@/config/index.js'
|
||||
|
||||
import userbar from './components/userbar.vue'
|
||||
import navMenu from './components/navMenu.vue'
|
||||
import breadcrumb from './components/breadcrumb.vue'
|
||||
import tags from './components/tags.vue'
|
||||
import setting from './components/setting.vue'
|
||||
|
||||
// 定义组件名称(多词命名)
|
||||
defineOptions({
|
||||
name: 'AppLayouts',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const layoutStore = useLayoutStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const settingRef = ref(null)
|
||||
|
||||
const layoutMode = computed(() => layoutStore.layoutMode)
|
||||
const sidebarCollapsed = computed(() => layoutStore.sidebarCollapsed)
|
||||
const selectedParentMenu = computed(() => layoutStore.selectedParentMenu)
|
||||
|
||||
// 缓存的视图列表
|
||||
const cachedViews = computed(() => {
|
||||
return layoutStore.viewTags.filter((tag) => !tag.meta?.noCache).map((tag) => tag.name)
|
||||
})
|
||||
|
||||
// 布局类名
|
||||
const layoutClass = computed(() => {
|
||||
return {
|
||||
'layout-default': layoutMode.value === 'default',
|
||||
'layout-menu': layoutMode.value === 'menu',
|
||||
'layout-top': layoutMode.value === 'top',
|
||||
'is-collapse': sidebarCollapsed.value,
|
||||
}
|
||||
})
|
||||
|
||||
// 获取刷新 key
|
||||
const refreshKey = computed(() => layoutStore.refreshKey)
|
||||
|
||||
const openKeys = ref([])
|
||||
const selectedKeys = ref([])
|
||||
const menuList = computed(() => {
|
||||
return userStore.menu
|
||||
})
|
||||
|
||||
// 获取图标组件
|
||||
const getIconComponent = (iconName) => {
|
||||
return icons[iconName] || icons.FileTextOutlined
|
||||
}
|
||||
|
||||
// 处理父菜单点击(默认布局的第一级菜单)
|
||||
const handleParentMenuClick = (item) => {
|
||||
// 设置选中的父菜单
|
||||
layoutStore.setSelectedParentMenu(item)
|
||||
// 如果没有子菜单,直接跳转
|
||||
if (!item.children || item.children.length === 0) {
|
||||
if (item.path) {
|
||||
router.push(item.path)
|
||||
}
|
||||
} else {
|
||||
// 默认展开第一个子菜单
|
||||
if (item.children.length > 0 && item.children[0].path) {
|
||||
router.push(item.children[0].path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理折叠
|
||||
const handleCollapse = (collapsed) => {
|
||||
layoutStore.sidebarCollapsed = collapsed
|
||||
}
|
||||
|
||||
// 打开设置抽屉
|
||||
const openSetting = () => {
|
||||
settingRef.value?.openDrawer()
|
||||
}
|
||||
|
||||
// 更新选中的菜单和展开的菜单
|
||||
const updateMenuState = () => {
|
||||
selectedKeys.value = [route.path]
|
||||
|
||||
// 获取所有父级路径
|
||||
const matched = route.matched.filter((item) => item.path !== '/' && item.path !== route.path)
|
||||
const parentPaths = matched.map((item) => item.path)
|
||||
|
||||
// 对于不同的布局模式,处理方式不同
|
||||
if (layoutMode.value === 'default') {
|
||||
// 默认布局:找到当前路由对应的父菜单
|
||||
const currentMenu = findMenuByPath(menuList.value, route.path)
|
||||
if (currentMenu) {
|
||||
// 如果当前菜单有子菜单,设置为选中的父菜单
|
||||
if (currentMenu.children && currentMenu.children.length > 0) {
|
||||
layoutStore.setSelectedParentMenu(currentMenu)
|
||||
} else {
|
||||
// 如果当前菜单是子菜单,找到它的父菜单
|
||||
const parentMenu = findParentMenu(menuList.value, route.path)
|
||||
if (parentMenu) {
|
||||
layoutStore.setSelectedParentMenu(parentMenu)
|
||||
} else {
|
||||
layoutStore.setSelectedParentMenu(currentMenu)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!sidebarCollapsed.value) {
|
||||
// 其他布局模式:展开所有父级菜单
|
||||
openKeys.value = parentPaths
|
||||
}
|
||||
}
|
||||
|
||||
// 根据路径查找菜单
|
||||
const findMenuByPath = (menus, path) => {
|
||||
for (const menu of menus) {
|
||||
if (menu.path === path) {
|
||||
return menu
|
||||
}
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const found = findMenuByPath(menu.children, path)
|
||||
if (found) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 查找父菜单
|
||||
const findParentMenu = (menus, path) => {
|
||||
for (const menu of menus) {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
for (const child of menu.children) {
|
||||
if (child.path === path) {
|
||||
return menu
|
||||
}
|
||||
if (child.children && child.children.length > 0) {
|
||||
const found = findParentMenu([child], path)
|
||||
if (found) {
|
||||
return menu
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 监听路由变化,更新菜单状态
|
||||
watch(
|
||||
() => route.path,
|
||||
(newPath) => {
|
||||
console.log('路由变化:', newPath)
|
||||
updateMenuState()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 监听布局模式变化,确保菜单状态正确
|
||||
watch(
|
||||
() => layoutMode.value,
|
||||
() => {
|
||||
updateMenuState()
|
||||
},
|
||||
)
|
||||
|
||||
// 监听折叠状态
|
||||
watch(
|
||||
() => sidebarCollapsed.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
openKeys.value = []
|
||||
} else {
|
||||
updateMenuState()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
// 如果还没有选中的父菜单,默认选中第一个
|
||||
if (layoutMode.value === 'default' && !selectedParentMenu.value && menuList.value.length > 0) {
|
||||
layoutStore.setSelectedParentMenu(menuList.value[0])
|
||||
}
|
||||
updateMenuState()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
.app-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
height: 60px;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.collapse-btn {
|
||||
font-size: 16px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-main {
|
||||
background-color: #f0f2f5;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
height: calc(100vh - 106px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 默认布局 - 双栏菜单 */
|
||||
&.layout-default {
|
||||
.left-sidebar {
|
||||
background-color: #001529;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 10;
|
||||
|
||||
.logo-box {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
.logo-image {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.left-nav {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
li {
|
||||
height: 70px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border-left: 3px solid transparent;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #ffffff;
|
||||
background-color: #1890ff;
|
||||
border-left-color: #ffffff;
|
||||
}
|
||||
|
||||
:deep(.anticon) {
|
||||
font-size: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
word-break: break-all;
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-sidebar {
|
||||
background-color: #ffffff;
|
||||
box-shadow: 1px 0 6px rgba(0, 0, 0, 0.1);
|
||||
z-index: 9;
|
||||
|
||||
.parent-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
gap: 5px;
|
||||
height: 60px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
:deep(.ant-menu) {
|
||||
border-right: none;
|
||||
|
||||
.ant-menu-item {
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
|
||||
&.ant-menu-item-selected {
|
||||
background-color: #e6f7ff;
|
||||
|
||||
&::after {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-menu-submenu {
|
||||
>.ant-menu-submenu-title {
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-menu-submenu-open {
|
||||
>.ant-menu-submenu-title {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
height: calc(100vh - 106px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Menu布局 */
|
||||
&.layout-menu {
|
||||
.full-menu-sidebar {
|
||||
background-color: #ffffff;
|
||||
z-index: 10;
|
||||
transition: all 0.2s;
|
||||
|
||||
.logo-box-full {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: all 0.2s;
|
||||
gap: 10px;
|
||||
padding: 0 10px;
|
||||
|
||||
.logo-image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
color: #1890ff;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-menu) {
|
||||
border-right: none;
|
||||
height: calc(100% - 60px);
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.ant-menu-item {
|
||||
margin: 0;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
padding-left: 20px !important;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
|
||||
&.ant-menu-item-selected {
|
||||
background-color: #e6f7ff;
|
||||
|
||||
&::after {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-menu-submenu {
|
||||
>.ant-menu-submenu-title {
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
margin: 0;
|
||||
padding-left: 20px !important;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-menu-submenu-open {
|
||||
>.ant-menu-submenu-title {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-menu-sub {
|
||||
background-color: #fafafa;
|
||||
|
||||
.ant-menu-item {
|
||||
padding-left: 40px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
height: calc(100vh - 106px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Top布局 */
|
||||
&.layout-top {
|
||||
flex-direction: column;
|
||||
|
||||
.top-header {
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
border-bottom: none;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
|
||||
.top-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
|
||||
.logo-box-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.logo-image {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.top-content {
|
||||
height: calc(100vh - 116px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 通用菜单样式优化 */
|
||||
:deep(.ant-menu-item-icon) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user