使用vue3创建后台管理项目
最后案例:
一:创建一个 Vue 应用
打开控制台:
npm init vue@latest
输入你需要创建的项目名称,一路回车
下载需要的包,如下:
"dependencies": { "@element-plus/icons-vue": "^2.1.0", "axios": "^1.3.5", "element-plus": "^2.3.3", "qs": "^6.11.1", "vue": "^3.2.47", "vue-router": "^4.1.6", "vuex": "^4.0.2" },
main.js:
import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) //创建VUE对象 //导入路由配置 import router from './router' app.use(router) // import VueWechatTitle from 'vue-wechat-title'; // app.use(VueWechatTitle) import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' app.use(ElementPlus) import * as ElementPlusIconsVue from '@element-plus/icons-vue' for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } app.mount('#app')
App.vue
<script setup> import HelloWorld from './components/HelloWorld.vue' import TheWelcome from './components/TheWelcome.vue' </script> <template> <!--<header> <img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" /> <div class="wrapper"> <HelloWorld msg="You did it!" /> </div> </header> <main> <TheWelcome /> </main>--> <router-view v-wechat-title="$route.meta.title" /> </template> <style scoped> @import url("./assets/base.css"); header { line-height: 1.5; } .logo { display: block; margin: 0 auto 2rem; } @media (min-width: 1024px) { header { display: flex; place-items: center; padding-right: calc(var(--section-gap) / 2); } .logo { margin: 0 2rem 0 0; } header .wrapper { display: flex; place-items: flex-start; flex-wrap: wrap; } } </style>
router / index.js
// import Vue from 'vue' //引入Vue import { createRouter, createWebHashHistory } from 'vue-router' //引入vue-router // Vue.use(Router) //Vue全局使用Router import home from '../views/home.vue' // import login from '../views/login.vue' import index from '../views/index.vue' import collect from '../views/collect.vue' import set from '../views/set.vue' import img1 from '../views/img1.vue' const routes = [{ path: '', redirect: "home" }, { path: '/', redirect: "home" }, { path: '/login', name: 'login', // component: login, component: () => import('../views/login.vue'), meta: { title: '登录' } }, { path: '/home', name: 'home', component: home, /* 子路由 */ children: [{ path: '/', redirect: "index" },{ path: '', redirect: "index" }, { path: '/index', name: 'index', component: index, meta: { title: '首页', } }, { path: '/collect', name: 'collect', component: collect, meta: { title: '收藏', isTab: true } }, { path: '/img1', name: 'img1', component: img1, meta: { title: '图片1', isTab: true } }, { path: '/set', name: 'set', component: set, meta: { title: '设置', isTab: true } } ] } ]; // 导航守卫 // 使用 router.beforeEach 注册一个全局前置守卫,判断用户是否登陆 /* router.beforeEach((to, from, next) => { if (to.path === '/login') { next(); } else { let token = localStorage.getItem('Authorization'); if (token === null || token === '') { next('/login'); } else { next(); } } }); */ const router = createRouter({ history: createWebHashHistory(), routes }) export default router;
menu.js
var mu = { longTitle: '管理控制台', littleTitle: '控制台', items: [{ iconName: 'home', name: '首页', routerName: 'index', disabled: false }, { iconName: 'img', name: '图片管理', submenu: [{ iconName: 'img', name: '图片一', routerName: 'img1', disabled: false }, { iconName: 'img', name: '图片二', routerName: 'img2', disabled: false }, { iconName: 'img', name: '图片三管理', submenu: [{ iconName: 'img', name: '图片三', routerName: 'img1', disabled: true }] }] }, { iconName: 'collection', name: '收藏管理', submenu: [{ iconName: 'collection', name: '收藏', routerName: 'collect', disabled: false }] }, { iconName: 'about', name: '设置', routerName: 'set', disabled: false } ] } export default mu;
utils / api.js
/** * api接口的统一封装 */ import axios from './request.js'; import Qs from 'qs'; const urlApi = 'http://localhost:3000/';//本地测试 // 登录 export function UserLogin(query) { return axios({ url: urlApi + 'loginData',//模拟数据接口 method: 'post', data: Qs.stringify(query) }) }
request.js
import axios from 'axios' import router from '../router/index.js' let loading // 请求拦截 axios.interceptors.request.use( (confing) => { //设置请求头 if (localStorage.Authorization) { //判断本地缓存的token是否存在 confing.headers.Authorization = localStorage.Authorization } return confing }, (error) => { //token不存在,设置为网络报错 return Promise.reject(error) } ) //响应拦截 axios.interceptors.response.use( (res) => { //响应处理 if (res.status === 200) { //响应码200请求成功 if (res.data.code == '200') { //接口请求成功 return Promise.resolve(res.data) } else if (res.data.code == '10000' || res.data.code == '10001') {//token验证失败,根据自己实际的修改 //清除token localStorage.removeItem('Authorization') //跳转到登录页面 router.push('/login') } else { //Message.error(res.data.msg); } return Promise.reject(res) } else { return Promise.reject(res) } // return res }, (error) => { Message.error('网络出错') // endLoading() // 获取状态码 const { status } = error.response if (status === 401) { //Message.error('请重新登录') //清楚token localStorage.removeItem('Authorization') //跳转到登录页面 router.push('/login') } return Promise.reject(error) } ) export default axios
hemo.vue
<template v-slot:default> <div :class="['content',isCollapse?'menu--fold':'menu--unfold']"> <!-- 侧边菜单栏 --> <div class="menuLeft"> <!--<div class="menu-nav-header"> <span>{{isCollapse?'控制台':'管理控制台'}}</span> </div>--> <div class="menu-nav-header"> <span>{{isCollapse?littleTitle:longTitle}}</span> </div> <!--todo 菜单栏组件 --> <el-menu active-text-color="#fff" background-color="#263238" class="el-menu-vertical-demo" :collapse-transition="false" default-active="2" text-color="#96a4ab " @open="handleOpen" @close="handleClose" :collapse="isCollapse"> <el-menu-item index="1" @click="$router.push({ name: 'index' })"> <!--<SvgIcon name="home" class="icon-svg" />--> <el-icon :size="size" :color="color"> <Edit /> </el-icon> <span slot=""> 首页</span> </el-menu-item> <el-sub-menu index="2"> <template #title> <!--<SvgIcon name="img" class="icon-svg" />--> <span> 图片管理</span> </template> <el-menu-item index="1-1" @click="$router.push({ name: 'img1' })"> <SvgIcon name="img" class="icon-svg" /> <span> 图片1</span> </el-menu-item> <el-menu-item index="1-2"> <SvgIcon name="img" class="icon-svg" /> <span> 图片2</span> </el-menu-item> <el-sub-menu index="1-4"> <template #title> <SvgIcon name="img" class="icon-svg" /> <span> 图片3</span> </template> <el-menu-item index="1-4-1"> <SvgIcon name="img" class="icon-svg" /> <span> 图片三级子菜单</span> </el-menu-item> </el-sub-menu> </el-sub-menu> <el-sub-menu index="3"> <template #title> <SvgIcon name="collection" class="icon-svg" /> <span> 收藏管理</span> </template> <el-menu-item index="3" @click="$router.push({ name: 'collect' })"> <SvgIcon name="collection" class="icon-svg" /> <span class="icon-text"> 收藏</span> </el-menu-item> </el-sub-menu> <el-menu-item index="4" @click="$router.push({ name: 'set' })"> <SvgIcon name="about" class="icon-svg" /> <span> 设置</span> </el-menu-item> </el-menu> </div> <!-- 右边内容 --> <div class="content-main"> <div class="navTop horizontalView"> <div class="nav_tools horizontalView"> <el-icon :name="isCollapse?'expand':'fold'" class="icon-svg" @click="isCollapse=!isCollapse" /> </div> <!--<el-breadcrumb separator="/"> <el-breadcrumb-item v-if="!breadcrumbList.size && breadcrumbList[0]!='首页'" :to="{ name: 'index' }"> 首页 </el-breadcrumb-item> <el-breadcrumb-item v-for="it in breadcrumbList">{{it}}</el-breadcrumb-item> </el-breadcrumb>--> </div> <!-- todo 内容组件 --> <el-tabs v-if="$route.meta.isTab" v-model="mainTabsActiveName" :closable="true" @tab-click="selectedTabHandle" @tab-remove="removeTabHandle"> <el-scrollbar ref="scroll" :height="siteContentViewHeight+32+'px'" @scroll="scroll"> <el-tab-pane v-for="item in mainTabs" :label="item.title" :name="item.name"> <el-card :style="'min-height:'+siteContentViewHeight + 'px'"> <router-view v-if="item.name === mainTabsActiveName" /> </el-card> </el-tab-pane> </el-scrollbar> </el-tabs> <div v-else> <el-scrollbar ref="scroll" :height="siteContentViewHeight+32+'px'" @scroll="scroll"> <!-- 主入口标签页 e --> <el-card :style="'min-height:'+ siteContentViewHeight + 'px'"> <router-view /> </el-card> </el-scrollbar> </div> <router-view /> </div> </div> </template> <script> import mu from '../router/menu'; export default { components: { }, data: function() { return { isCollapse: false, mainTabs: [], mainTabsActiveName: '', menuActiveName: '', menus: [], longTitle: '', littleTitle: '', breadcrumbObj: {}, breadcrumbList:[] } }, created() { let that = this; that.routeHandle(that.$route); that.menus = mu.items; that.longTitle = mu.longTitle; that.littleTitle = mu.littleTitle; that.breadcrumbList = [ that.$route.meta.title ] //菜单项层级处理,做一个面包屑集合保存 var mus=that.menus for (let i1 of mus) { if (i1.submenu) { for (let i2 of i1.submenu) { if (i2.submenu) { for (let i3 of i2.submenu) { if (!i3.submenu) { that.breadcrumbObj[i3.name] = [i1.name, i2.name, i3.name]; } } } else { that.breadcrumbObj[i2.name] = [i1.name, i2.name]; console.log(i2.name) } } } else { that.breadcrumbObj[i1.name] = [i1.name]; console.log(i1.name) } } }, // 监听路由变化 watch: { $route: { handler(to, from) { if (to.path != from.path) { // 处理路由 this.routeHandle(to); this.breadcrumbList = this.breadcrumbObj[to.meta.title] } } } }, methods: { resetDocumentClientHeight: function() { this.documentClientHeight = document.documentElement['clientHeight']; window.onresize = () => { this.documentClientHeight = document.documentElement['clientHeight']; this.loadSiteContentViewHeight(); }; }, loadSiteContentViewHeight: function() { let height = this.documentClientHeight - 52; //减去导航栏高度50 console.log(this.$route.meta.isTab) if (this.$route.meta.isTab) { height -= 70; //减去tab栏高度40,减去上下边距30 /* this.siteContentViewHeight = { 'min-height': height + 'px' }; */ this.siteContentViewHeight = height; } else { height -= 30; //给内容区设置高度 this.siteContentViewHeight = height; } }, routeHandle: function(route) { //每次切换页面,重新计算页面高度和内容区高度 this.resetDocumentClientHeight(); this.loadSiteContentViewHeight(); if (route.meta.isTab) { // tab选中, 不存在先添加 var tab = this.mainTabs.filter(item => item.name === route.name)[0]; if (!tab) { if (route.meta.isDynamic) { route = this.dynamicMenuRoutes.filter(item => item.name === route.name)[0]; if (!route) { return console.error('未能找到可用标签页!'); } } tab = { menuId: route.meta.menuId || route.name, name: route.name, title: route.meta.title, iframeUrl: route.meta.iframeUrl || '', params: route.params, query: route.query }; this.mainTabs = this.mainTabs.concat(tab); } this.menuActiveName = tab.menuId + ''; this.mainTabsActiveName = tab.name; } }, mounted: function() { let that = this; that.resetDocumentClientHeight(); that.loadSiteContentViewHeight(); }, selectedTabHandle: function(tab, e) { tab = this.mainTabs.filter(item => item.name === tab.paneName); if (tab.length >= 1) { this.$router.push({ name: tab[0].name, query: tab[0].query, params: tab[0].params }); } }, removeTabHandle: function(tabName) { this.mainTabs = this.mainTabs.filter(item => item.name !== tabName); if (this.mainTabs.length >= 1) { // 当前选中tab被删除 if (tabName === this.mainTabsActiveName) { var tab = this.mainTabs[this.mainTabs.length - 1]; this.$router.push({ name: tab.name, query: tab.query, params: tab.params }, () => { this.mainTabsActiveName = this.$route.name; } ); } } else { this.menuActiveName = ''; this.$router.push({ name: 'Home' }); } }, } } </script> <style> /*@import url('../assets/css/home.css');*/ /* -------侧边栏 折叠 */ .menu--fold .menuLeft { width: 64px; } .menu--fold .content-main { margin-left: 64px; } /* --------------------- */ /* ---------侧边栏 展开 */ .menu--unfold .menuLeft { width: 230px; } .menu--unfold .content-main { margin-left: 230px; } /* ------------- */ .navTop { position: relative; width: 100%; height: 50px; z-index: 100; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); box-sizing: border-box; background: white; } .nav_tools { height: 100%; /* width: 100%; */ } .nav_tools .icon-svg { margin-left: 10px; color: #5b5b5b; } .menuLeft { position: fixed; top: 0; left: 0; bottom: 0; z-index: 1020; overflow: hidden; background-color: #263238; } .content-main { position: relative; background: #f1f4f5; height: 100%; } .menu-nav-header { color: white; height: 50px; line-height: 50px; text-align: center; font-size: 20px; font-weight: bold; /* background-color: #9fbea7; */ background-color: #566f7e; } /* 动画 */ .nav_tools, .menuLeft, .content-main { transition: inline-block 0.3s, left 0.3s, width 0.3s, margin-left 0.3s, font-size 0.3s; } /* 修改菜单栏样式 */ .menuLeft .el-menu { border-right: none; } .el-menu-vertical-demo:not(.el-menu--collapse) { border-right: none; width: 230px; } .el-menu .icon-text { margin-left: 10px; } /* 修改标签栏样式 */ .content-main .el-tabs .el-tabs__header { z-index: 90; padding: 0 55px 0 15px; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12), 0 0 6px 0 rgba(0, 0, 0, 0.04); background-color: #fff; } .content-main .el-tabs .el-tabs__nav-wrap::after { width: 0px; } .content-main .el-scrollbar .el-card { margin: 15px 15px; } .content-main .el-tabs .el-tabs__header{ margin: unset; } .content-main .el-tabs .el-tab-pane{ } </style>