文章目录
1. 修改App.vue
项目采用vue router 来管理路由, 作为单页面应用, 其页面为vue组件,通过路由动态渲染,App.vue作为根组件,需要配置路由出口
2. 布局页面
2.1 src\views\layout\Layout.vue
<template>
<el-container style="height: 100vh">
<el-aside width="auto">
<el-scrollbar>
<LeftLayout :isCollapse='isCollapse' />
</el-scrollbar>
</el-aside>
<el-container>
<el-header style="height: 5vh; padding: 0 5px 0 5px"><Header @parentClick='parentClick' /></el-header>
<el-main style="padding: 0;"><MainView /></el-main>
<el-footer style="height: 3vh; padding: 0;"><Footer/></el-footer>
</el-container>
</el-container>
</template>
<script setup lang="ts">
import LeftLayout from './Aside.vue'
import Header from './Header.vue'
import MainView from './MainView.vue'
import Footer from './Footer.vue'
import { ref } from 'vue'
const isCollapse = ref<any>(false)
const parentClick = (isCollapseValue: any) => {
isCollapse.value = isCollapseValue
}
</script>
<style lang="scss" scoped>
</style>
2.2 src\views\layout\Aside.vue
使用前边自定义菜单组件
<template>
<div class="menu-container">
<div class="up-title" v-if="!isCollapse">
<!-- <div>
<svg-icon name="" width="20px" height="20px" />
</div> -->
<div class="title-text">{{ !isCollapse ? "Vue3 boot3 快速开发平台" : "" }}</div>
</div>
<div class="down-menu">
<el-menu :default-active="route.path" class="el-menu-vertical-demo" :collapse="isCollapse" :router="true"
:collapse-transition="false">
<MenuTree :menuList="menuList"></MenuTree>
</el-menu>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted} from 'vue'
//渲染菜单的组件
import MenuTree from "@/components/menuTree/index.vue"
import { useMenuStore } from '@/stores/menu.js'
import { useRoute } from 'vue-router'
const menuStore = useMenuStore()
const route = useRoute()
// 获取pinia的缓存的菜单数据
const menuList = menuStore.menuList
const props = defineProps({
isCollapse: Boolean
})
</script>
<style lang="scss">
.menu-container {
display: flex;
flex-direction: column;
height: 100vh;
.up-title {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 5vh;
// padding: 3px;
/* 内边距 */
border-bottom: 2px solid;
border-right: 1px solid;
/* 设置下边框宽度和样式 */
border-color: #f5f5f5;
.title-text {
display: inline-block;
vertical-align: middle;
/* 文字与图片垂直居中对齐 */
font-weight: bold;
/* 加粗文字 */
font-size: 13px;
margin-left: 5px;
}
}
.down-menu {
flex-grow: 1;
}
}
.el-menu-vertical-demo {
height: 100%;
overflow: auto;
}
</style>
2.3 src\views\layout\Header.vue
<template>
<div class="header-container">
<!-- div left -->
<div class="left">
<!-- 折叠按钮-->
<div class="div-item" @click="toggleCollapse()">
<el-icon :size="22" v-show="!isMenuOpen"><Fold /></el-icon>
<el-icon :size="22" v-show="isMenuOpen"><Expand /></el-icon>
</div>
<!-- 面包屑 -->
<div class="div-item">
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }"></el-breadcrumb-item>
<template v-for="(item, index) in breadList">
<el-breadcrumb-item v-if="item.menuName" :key="index" >{{ item.menuName }}</el-breadcrumb-item>
</template>
</el-breadcrumb>
</div>
</div>
<!-- div right -->
<div class="right">
<div>
<el-icon size="20" @click="fullScreenHander"><FullScreen /></el-icon>
</div>
<div>
<!-- <el-avatar> {{ userData.username }} </el-avatar> -->
</div>
<div>
<span style="font-size: 18px;">{{ userData.username }}</span>
</div>
<div>
<el-dropdown>
<!-- <el-icon size="24"><MoreFilled /></el-icon> -->
<el-icon size="20"><i-ep-User /></el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="selfInfoHander">
<el-icon><UserFilled /></el-icon>个人信息
</el-dropdown-item>
<el-dropdown-item @click="exitHander">
<el-icon><ArrowLeft /></el-icon>退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Fold, Expand, FullScreen, ArrowDown, ArrowLeft, UserFilled } from '@element-plus/icons-vue';
import { ref } from 'vue';
import { useRoute } from 'vue-router'
import {loginOutService } from '@/api/auth/index'
// 从stors中获取tab数据
import { useTokenStore } from "@/stores/token"
const tokenStore = useTokenStore()
import { useRouter } from 'vue-router';
const router = useRouter()
import { useMenuStore } from '@/stores/menu'
import { storeToRefs } from 'pinia';
const menuStore = useMenuStore();
let { breadList } = storeToRefs(menuStore)
const route = useRoute();
import {useUserInfoStore } from '@/stores/userInfo';
const userInfoStore = useUserInfoStore();
// 用户数据模型
let userData = ref({
id: '',
username: '',
nickname: '',
})
// 获取登录用户信息
const getUser = async () => {
//console.log('getUser userInfo:', userInfoStore.userInfo)
userData.value.username = userInfoStore.userInfo!.username
userData.value.id = userInfoStore.userInfo!.id
}
getUser()
// 折叠按钮处理
const emits = defineEmits(['parentClick']);
const isMenuOpen = ref(false);
const toggleCollapse = () => {
isMenuOpen.value = !isMenuOpen.value;
//console.log(isMenuOpen.value);
emits('parentClick', isMenuOpen.value);
}
// 全屏----------------
const fullScreenHander = () => {
let full = document.fullscreenElement
//console.log(full)
if(!full) {
// document自带的全屏方法
document.documentElement.requestFullscreen()
}else {
// document自带的推出全屏方法
document.exitFullscreen()
}
}
// 退出登录
const exitHander = async () => {
await loginOutService()
//console.log("exit ==================")
// 清空 token 用户 菜单 路由
tokenStore.removeToke()
userInfoStore.removeInfo()
menuStore.removeMenuRouter()
// 跳转登录页面
router.push("/login")
}
// 个人信息
const selfInfoHander = () => {
let data = { title: '个人中心', path: '/userInfo', isClose: false, menuName: '个人中心', parentId: 0 }
menuStore.tabList.push(data)
menuStore.activeTab = "/userInfo"
router.push("/userInfo")
}
</script>
<style lang="scss" scope>
.header-container {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
height: 5vh;
border-bottom: 2px solid;
border-color: #f5f5f5;
}
.left {
display: flex;
align-items: center; /* 垂直居中子项 */
justify-content: center; /* 水平居中子项(如果需要)*/
height: 100%;
}
.left > div {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 100%;
margin: 5px;
}
.right {
display: flex;
justify-content: center; /* 水平居中子项(如果需要)*/
align-items: center; /* 垂直居中子项 */
}
.right > div {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 100%;
margin: 3px;
margin-left: 5px;
}
.right > div:last-child {
margin-right: 10px;
}
</style>
2.4 src\views\layout\MainView.vue
使用自定义tabs
<template>
<el-container style="height: 99%">
<el-header style="height: 30px; padding: 0;">
<div class="tabs-container">
<tabs :tagsList="tabList" :activePath="activeTab" @childEvent="handChildEvent"></tabs>
</div>
<ul v-show="contextMenuVisible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
<li @click="closeAllTabs">关闭所有</li>
<li @click="closeOtherTabs">关闭其他</li>
</ul>
</el-header>
<el-main>
<el-scrollbar>
<RouterView/>
</el-scrollbar>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import tabs from '@/components/tabs/index.vue'
// 从stors中获取tab数据
import { useMenuStore } from '@/stores/menu'
import { storeToRefs } from 'pinia'
import { useRouter, useRoute } from 'vue-router'
import { nextTick, onMounted, ref, watch } from 'vue';
const router = useRouter()
const menuStore = useMenuStore();
let { tabList, activeTab } = storeToRefs(menuStore)
const route = useRoute()
watch(() => route.path, newRoute=> {
console.log("watch route: ", newRoute)
menuStore.routeCheage(newRoute)
})
// 选中
function gotoTab(path: string) {
// 修改activeTab
menuStore.setActiveTab(path);
// 路由页面
router.push(path);
console.log("current tablist======", tabList.value.filter(item => item.path == path))
let currentTab = tabList.value.filter(item => item.path == path)
// 添加面包屑
menuStore.addBreadList(currentTab[0])
}
// 移除
function closeTab(path: string) {
// 移除的是否是当前激活的tab,激活首页
console.log("closetab path=", path)
if (path == menuStore.activeTab) {
menuStore.setActiveTab("index")
router.push("index")
}
menuStore.delTabList(path)
}
// 关闭contextMenu
const closeContextMenu = () => {
contextMenuVisible.value = false
}
// 关闭所有标签页
const closeAllTabs = () => {
contextMenuVisible.value = false;
menuStore.initBreadcrumbAndTabs()
router.push("/index")
}
// 关闭其它标签页
const closeOtherTabs = () => {
menuStore.colseOthersTabs(currrentPath.value)
contextMenuVisible.value = false
}
// 子组件传值
const handChildEvent = (data: any) => {
console.log("handChildEvent,", data)
if (data.type == 'close') {
menuStore.delTabList(data.path)
}
if (data.type == 'active') {
menuStore.setActiveTab(data.path)
router.push(data.path)
}
if (data.type == 'rightClick') {
currrentPath.value = data.path
contextMenuVisible.value = true
left.value = data.left
top.value = data.top
}
}
// 右键标签
const contextMenuVisible = ref(false)
const left = ref()
const top = ref()
const currrentPath = ref()
watch(contextMenuVisible, (newValue, oldValue) => {
if (newValue) {
document.body.addEventListener("click", closeContextMenu)
} else {
document.body.removeEventListener("click", closeContextMenu)
}
})
</script>
<style lang="scss" >
.tabs-container {
height: 30px;
margin: 5px;
}
.el-tabs {
--el-tabs-header-height: 30px;
}
.demo-tabs > .el-tabs__content {
padding: 0;
}
.demo-tabs > .el-tabs__header {
margin: 0 0 5px 0;
}
/* 弹出框 */
.contextmenu {
width: 100px;
margin: 0;
border: 1px solid #ccc;
background: #fff;
z-index: 3000;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 14px;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.2);
}
.contextmenu li {
margin: 0;
padding: 7px 16px;
}
.contextmenu li:hover {
background: #f2f2f2;
cursor: pointer;
}
</style>
2.5 src\views\layout\Footer.vue
<template>
<div class="footer-container">
<span class="footer-span">Vue3 TS Vite Springboot3 快速开发平台 ©2024 Created by cc</span>
</div>
</template>
<style lang="scss">
.footer-container {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 90%;
border-top: 2px solid;
border-color: #f5f5f5;
.footer-span {
font-size: 11px;
}
}
</style>