本章目的:UI整体框架搭起来
1、安装并引用element-ui
需注意,vue-cli@4+的版本,在创建项目时,选择vue2的版本,如果选择vue3的版本就不能这样引入element-ui了
npm i element-ui -S
main.js 引入element-ui
import ElementUI from ‘element-ui‘; import ‘element-ui/lib/theme-chalk/index.css‘;
Vue.use(ElementUI);
2、router/index.js 设置路由
菜单先写死,然后根据菜单集合动态配置路由
import Vue from ‘vue‘ import Router from ‘vue-router‘ import Home from ‘../views/Home.vue‘ //import User from ‘../views/User/Users.vue‘ //import Role from ‘../views/User/Roles.vue‘ //import Menu from ‘../views/Permission/Menu.vue‘ //import RoleMenu from ‘../views/Permission/RoleMenu.vue‘ //import Action from ‘../views/Permission/Action.vue‘ import Layout from "../views/Layout/Layout"; const _import = require(‘@/router/_importview.js‘)//获取组件的方法 Vue.use(Router) //菜单集合 const routesList = [ { id: "1", path: ‘/‘, name: ‘首页‘, iconCls: ‘el-icon-s-home‘, IsHide: false, meta: { title: ‘首页‘ } }, { id: "2", path: ‘-‘, name: ‘用户角色管理‘, iconCls: ‘el-icon-user-solid‘, IsHide: false, children: [ { id: "3", path: ‘/User/Users‘, name: ‘用户管理‘, IsHide: false, meta: { title: ‘用户管理‘ } }, { id: "4", path: ‘/User/Roles‘, name: ‘角色管理‘, IsHide: false, meta: { title: ‘角色管理‘ } } ] }, { id: "5", path: ‘-‘, name: ‘授权管理‘, iconCls: ‘el-icon-menu‘, IsHide: false, children: [ { id: "6", path: ‘/Permission/Menu‘, name: ‘菜单管理‘, IsHide: false, meta: { title: ‘菜单管理‘ } }, { id: "7", path: ‘/Permission/Action‘, name: ‘接口管理‘, IsHide: false, meta: { title: ‘接口管理‘ } }, { id: "8", path: ‘/Permission/RoleMenu‘, name: ‘菜单授权‘, IsHide: false, meta: { title: ‘菜单授权‘ } } ] } ] const createRouter = () => new Router({ mode: ‘history‘, routes: [{ id: "1", path: ‘/‘, name: ‘首页‘, component: Home, iconCls: ‘el-icon-s-home‘, IsHide: false, meta: { title: ‘首页‘ } }] }) const router = createRouter() /*--------------------------根据菜单集合动态配置路由----------------------*/ export function filterAsyncRouter(asyncRouterMap) { const accessedRouters = asyncRouterMap.filter(route => { if (route.path) { if (route.path === ‘-‘) {//一级菜单 route.component = Layout } else { try { route.component = _import(route.path.replace(‘/:id‘, ‘‘)) } catch (e) { try { route.component = () => import(‘@/views‘ + route.path.replace(‘/:id‘, ‘‘) + ‘.vue‘); } catch (error) { } } } } if (route.children && route.children.length) { route.children = filterAsyncRouter(route.children) } return true }) return accessedRouters } router.$addRoutes = (params) => { var f = item => { if (item[‘children‘]) { item[‘children‘] = item[‘children‘].filter(f); return true; }else { return true; } } var params = params.filter(f); router.addRoutes(params) } let getRouter = filterAsyncRouter(routesList); //过滤路由 router.$addRoutes(getRouter) //动态添加路由 /*--------------------------根据菜单集合动态配置路由----------------------*/ window.localStorage.router = (JSON.stringify(routesList)); export default router
3、模板页 App.vue
<template> <div id="app"> <transition v-if="!$route.meta.NoNeedHome" name="fade" mode="out-in"> <el-row class="container"> <el-col :span="24" class="header"> <el-col :span="10" class="logo collapsedLogo" :class="collapsed?‘logo-collapse-width‘:‘logo-width‘"> <div @click="toindex"> {{collapsed?sysNameShort:sysName}}</div> </el-col> <el-col :span="10" class="logoban"> <div :class=" collapsed?‘tools collapsed‘:‘tools‘" @click="collapse"> <i class="fa el-icon-s-operation"></i> </div> <el-breadcrumb separator="/" class="breadcrumb-inner collapsedLogo"> <el-breadcrumb-item v-for="item in $route.matched" :key="item.path"> <span style=""> {{ item.name }}</span> </el-breadcrumb-item> </el-breadcrumb> </el-col> <el-col :span="4" class="userinfo"> <el-dropdown trigger="hover"> <span class="el-dropdown-link userinfo-inner"> {{sysUserName}} <img src="./assets/logo.png" height="128" width="128" /> </span> <el-dropdown-menu slot="dropdown"> <el-dropdown-item divided @click.native="logout">退出登录</el-dropdown-item> </el-dropdown-menu> </el-dropdown> </el-col> </el-col> <el-col :span="24" class="main"> <aside :class="collapsedClass "> <el-scrollbar style="height:100%;background: #2f3e52;" class="scrollbar-handle"> <el-menu :default-active="$route.path" class="el-menu-vertical-demo" unique-opened router :collapse="isCollapse" background-color="#2f3e52" style="border-right: none;" text-color="#fff" active-text-color="#ffd04b"> <sidebar v-for="(menu,index) in routes" @collaFa="collapseFa" :key="index" :item="menu" /> </el-menu> </el-scrollbar> </aside> <el-col :span="24" class="content-wrapper" :class="collapsed?‘content-collapsed‘:‘content-expanded‘"> <div class="tags" v-if="showTags"> <div id="tags-view-container" class="tags-view-container"> <scroll-pane ref="scrollPane" class="tags-view-wrapper"> <router-link v-for="(tag,index) in tagsList" ref="tag" :key="tag.path" :class="{‘active‘: isActive(tag.path)}" :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" tag="span" @click.middle.native="closeTags(index)" class="tags-view-item"> {{ tag.title }} <span class="el-icon-close" @click.prevent.stop="closeTags(index)" /> </router-link> </scroll-pane> </div> <!-- 其他操作按钮 --> <div class="tags-close-box"> <el-dropdown @command="handleTags"> <el-button size="mini"> <i class="el-icon-arrow-down el-icon--right"></i> </el-button> <el-dropdown-menu size="small" slot="dropdown"> <el-dropdown-item command="other">关闭其他</el-dropdown-item> <el-dropdown-item command="all">关闭所有</el-dropdown-item> </el-dropdown-menu> </el-dropdown> </div> </div> <transition name="fade" mode="out-in"> <div class="content-az router-view-withly"> <!-- 含有母版的路由页面 --> <router-view></router-view> </div> </transition> </el-col> </el-col> </el-row> </transition> <transition v-else name="fade" mode="out-in"> <div class="content-az router-view-noly"> <!-- 单独的页面 --> <router-view></router-view> </div> </transition> <div class="v-modal " @click="collapse" v-show="SidebarVisible" tabindex="0" style="z-index: 2917;"></div> </div> </template> <style lang="css"> @import "./style/home.css"; .el-menu-vertical-demo { /*width: 230px;*/ } .el-breadcrumb { line-height: 60px !important; } </style> <script> import Sidebar from ‘./components/Sidebar‘ import ScrollPane from ‘./components/ScrollPane‘ export default { components: { Sidebar, ScrollPane }, data() { return { sysName: ‘后台管理系统‘, sysNameShort: ‘ET‘, SidebarVisible: false,//左侧是否可见 collapsed: false,//左侧菜单是否折叠 collapsedClass: ‘menu-expanded‘,//折叠样式 ispc: false, sysUserName: ‘管理员‘, isCollapse: false, tagsList: [],//标签页 routes: [] } }, methods: { //首页 toindex() { this.$router.replace({ path: "/", }); }, //折叠导航栏 collapse: function () { this.collapsed = !this.collapsed; if (this.ispc) { if (this.collapsed) { this.collapsedClass = ‘menu-collapsed‘; } else { this.collapsedClass = ‘menu-expanded‘; } } else { // mobile if (this.collapsed) { this.SidebarVisible = true; this.collapsedClass = ‘menu-collapsed-mobile‘; } else { this.SidebarVisible = false; this.collapsedClass = ‘menu-expanded-mobile‘; } this.collapsedClass += ‘ mobile-ex ‘; } this.isCollapse = !this.isCollapse; }, collapseFa: function () { if (!this.ispc) { this.collapse(); } }, //退出登录 logout: function () { var _this = this; this.$confirm(‘确认退出吗?‘, ‘提示‘, { }).then(() => { this.tagsList = []; this.routes = []; }).catch(() => { }); }, isActive(path) { return path === this.$route.fullPath; }, // 关闭单个标签 closeTags(index) { const delItem = this.tagsList.splice(index, 1)[0]; const item = this.tagsList[index] ? this.tagsList[index] : this.tagsList[index - 1]; if (item) { delItem.path === this.$route.fullPath && this.$router.push(item.path); this.$store.commit("saveTagsData", JSON.stringify(this.tagsList)); } else { this.$router.push(‘/‘); } }, // 设置标签 setTags(route) { if (!route.meta.NoTabPage) { const isExist = this.tagsList.some(item => { return item.path === route.fullPath; }) !isExist && this.tagsList.push({ title: route.meta.title, path: route.fullPath, }) } }, // 关闭全部标签 closeAll() { this.tagsList = []; this.$router.push(‘/‘); sessionStorage.removeItem("Tags"); }, // 关闭其他标签 closeOther() { const curItem = this.tagsList.filter(item => { return item.path === this.$route.fullPath; }) this.tagsList = curItem; sessionStorage.setItem("Tags", JSON.stringify(this.tagsList)) }, // 当关闭所有页面时隐藏 handleTags(command) { command === ‘other‘ ? this.closeOther() : this.closeAll(); } }, mounted() { console.log(this.$route) var tags = sessionStorage.getItem(‘Tags‘) ? JSON.parse(sessionStorage.getItem(‘Tags‘)) : []; if (tags && tags.length > 0) { this.tagsList = tags; } var NavigationBar = JSON.parse(window.localStorage.router ? window.localStorage.router : null); if (this.routes.length <= 0 && NavigationBar && NavigationBar.length >= 0) { this.routes = NavigationBar; } // 折叠菜单栏 this.collapse(); }, updated() { var user = JSON.parse(window.localStorage.user ? window.localStorage.user : null); if (user) { this.sysUserName = user.uRealName || ‘未登录‘; this.sysUserAvatar = user.avatar || ‘../assets/user.png‘; } var NavigationBar = JSON.parse(window.localStorage.router ? window.localStorage.router : null); if (NavigationBar && NavigationBar.length >= 0) { if (this.routes.length <= 0 || (JSON.stringify(this.routes) != JSON.stringify((NavigationBar)))) { this.routes = NavigationBar; } } }, computed: { showTags() { if (this.tagsList.length > 1) { this.$store.commit("saveTagsData", JSON.stringify(this.tagsList)); } return this.tagsList.length > 0; } }, watch: { // 对router进行监听,每当访问router时,对tags的进行修改 $route: async function (newValue, from) { if (global.IS_IDS4) { await this.refreshUserInfo(); } this.setTags(newValue); const tags = this.$refs.tag this.$nextTick(() => { if (tags) { for (const tag of tags) { if (tag.to.path === this.$route.path) { this.$refs.scrollPane.moveToTarget(tag, tags) break } } } }) } }, created() { this.setTags(this.$route); this.ispc = window.screen.width > 680; if (this.ispc) { this.collapsedClass = ‘menu-expanded‘; } else { this.collapsedClass = ‘menu-expanded-mobile mobile-ex‘; this.collapsed = true; this.collapse(); } } } </script> <style lang="css"> @import "./style/home.css"; .el-menu-vertical-demo { /*width: 230px;*/ } .el-breadcrumb { line-height: 60px !important; } </style> <style> .menu-collapsed .el-icon-arrow-right:before { display: none; } .tags { position: relative; overflow: hidden; border: 1px solid #f0f0f0; background: #f0f0f0; } .tags ul { box-sizing: border-box; width: 100%; height: 100%; padding: 0; margin: 0; display: none; } .tags-li { float: left; margin: 3px 5px 2px 3px; border-radius: 3px; font-size: 13px; overflow: hidden; height: 23px; line-height: 23px; border: 1px solid #e9eaec; background: #fff; padding: 3px 5px 4px 12px; vertical-align: middle; color: #666; -webkit-transition: all .3s ease-in; transition: all .3s ease-in; } .tags-li-icon { cursor: pointer; } .tags-li:not(.active):hover { background: #f8f8f8; } .tags-li-title { float: left; max-width: 80px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; margin-right: 5px; color: #666; text-decoration: none; } .tags-li.active { /*color: #fff;*/ /*border: 1px solid #10B9D3;*/ /*background-color: #10B9D3;*/ } .tags-li.active .tags-li-title { /*color: #fff;*/ } .tags-close-box { box-sizing: border-box; text-align: center; z-index: 10; float: right; margin-right: 1px; line-height: 2; } </style> <style> /*.logoban{*/ /*width: auto !important;*/ /*}*/ .news-dialog { background: #fff; z-index: 3000 !important; position: fixed; height: 100vh; width: 100%; max-width: 260px; top: 60px !important; left: 0 !important; ; -webkit-box-shadow: 0 0 15px 0 rgba(0, 0, 0, .05); box-shadow: 0 0 15px 0 rgba(0, 0, 0, .05); -webkit-transition: all .25s cubic-bezier(.7, .3, .1, 1); transition: all .25s cubic-bezier(.7, .3, .1, 1); -webkit-transform: translate(100%); z-index: 40000; left: auto !important; ; right: 0 !important; ; transform: translate(0); } .news-dialog .el-dialog { margin: auto !important; -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, .3); box-shadow: none; width: 100%; } .news-dialog.show { transform: translate(0); } .tag-new { width: 100%; margin: 10px 0; } @media screen and (max-width: 680px) { .collapsedLogo { display: none; } .el-dialog { width: 90% !important; } .content-expanded { max-width: 100% !important; max-height: calc(100% - 60px); } .mobile-ex { background: #fff; z-index: 3000; position: fixed; height: 100vh; width: 100%; max-width: 260px; top: 0; left: 0; -webkit-box-shadow: 0 0 15px 0 rgba(0, 0, 0, .05); box-shadow: 0 0 15px 0 rgba(0, 0, 0, .05); -webkit-transition: all .25s cubic-bezier(.7, .3, .1, 1); transition: all .25s cubic-bezier(.7, .3, .1, 1); -webkit-transform: translate(100%); z-index: 40000; left: auto; right: 0; transform: translate(100%); } .mobile-ex.menu-collapsed-mobile { transform: translate(0); } .el-menu--collapse { width: 100% !important; } .el-date-editor.el-input, .el-date-editor.el-input__inner, .el-cascader.el-cascader--medium { width: 100% !important; } .toolbar.roles { width: 100% !important; } .toolbar.perms { width: 800px !important; } .toolbar.perms .box-card { width: 100% !important; } .login-container { width: 300px !important; } .count-test label { } .content-wrapper .tags { margin: 0px; padding: 0px; } .activeuser { display: none !important; } } </style> <style> .tags-view-container { height: 34px; width: calc(100% - 60px); /*background: #fff;*/ /*border-bottom: 1px solid #d8dce5;*/ /*box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);*/ float: left; } .tags-view-container .tags-view-wrapper .tags-view-item { display: inline-block; position: relative; cursor: pointer; height: 26px; line-height: 26px; border: 1px solid #d8dce5; color: #495060; background: #fff; padding: 0 8px; font-size: 12px; margin-left: 5px; margin-top: 4px; } .tags-view-container .tags-view-wrapper .tags-view-item:first-of-type { margin-left: 15px; } .tags-view-container .tags-view-wrapper .tags-view-item:last-of-type { margin-right: 15px; } .tags-view-container .tags-view-wrapper .tags-view-item.active { /*background-color: #42b983;*/ /*color: #fff;*/ /*border-color: #42b983;*/ } .tags-view-container .tags-view-wrapper .tags-view-item.active::before { content: ""; background: #2d8cf0; display: inline-block; width: 10px; height: 10px; border-radius: 50%; position: relative; margin-right: 2px; } .tags-view-container .contextmenu { margin: 0; background: #fff; z-index: 3000; position: absolute; list-style-type: none; padding: 5px 0; border-radius: 4px; font-size: 12px; font-weight: 400; color: #333; box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3); } .tags-view-container .contextmenu li { margin: 0; padding: 7px 16px; cursor: pointer; } .tags-view-container .contextmenu li:hover { background: #eee; } </style> <style> .tags-view-wrapper .tags-view-item .el-icon-close { width: 16px; height: 16px; vertical-align: 2px; border-radius: 50%; text-align: center; transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); transform-origin: 100% 50%; } .tags-view-wrapper .tags-view-item .el-icon-close:before { transform: scale(0.6); display: inline-block; vertical-align: -3px; } .tags-view-wrapper .tags-view-item .el-icon-close:hover { background-color: #ef2b74; color: #fff; } </style>
4、菜单组件、标签页组件
<template> <div> <!-- if 有子节点,渲染节点递归 --> <template v-if="item.children"> <!--一级菜单--> <el-submenu v-if="!(item.path!=‘‘&&item.path!=‘ ‘&&item.path!=‘-‘)" :index="item.id+‘index‘" :key="item.path" > <template slot="title"> <i v-if="item.children&&item.children.length>0&&item.iconCls" class="fa" :class="item.iconCls" ></i> <span class="title-name" slot="title">{{item.name}}</span> </template> <template v-for="child in item.children"> <!-- 递归嵌套子菜单 --> <template v-if="!child.IsHide"> <sidebar v-if="child.children&&child.children.length>0" :item="child" :index="child.id" :key="child.path" /> <app-link :to="child.path" v-else :key="child.path" :data-link="child.path"> <el-menu-item :key="child.path" :index="isExternalLink(child.path)? ‘‘:child.path" @click="cop"> <i class="fa" :class="child.iconCls"></i> <template slot="title"> <span class="title-name" slot="title">{{child.name}}</span> </template> </el-menu-item> </app-link> </template> </template> </el-submenu> <template v-else> <!--一级菜单path不等于空或- --> <app-link :to="item.path" :key="item.path+‘d‘" :data-link="item.path"> <el-menu-item :index="isExternalLink(item.path)? ‘‘:item.path" :key="item.path+‘d‘"> <i class="fa" :class="item.iconCls"></i> <template slot="title"> <span class="title-name" slot="title">{{item.name}}33</span> </template> </el-menu-item> </app-link> </template> </template> <!-- else 没有子节点,直接输出:首页 --> <template v-else> <app-link :to="item.path" :key="item.path+‘d‘"> <el-menu-item :index="isExternalLink(item.path)? ‘‘:item.path" :key="item.path+‘d‘" @click="cop" > <i class="fa" :class="item.iconCls"></i> <template slot="title"> <span class="title-name" slot="title">{{item.name}}</span> </template> </el-menu-item> </app-link> </template> </div> </template> <script> import AppLink from "./AppLink"; import { isExternal } from "../js/validate"; export default { name: "Sidebar", components: { AppLink }, props: { item: { type: Object, required: true } }, methods: { isExternalLink(to) { return isExternal(to); }, cop: function() { // 子组件中触发父组件方法collaFa并传值123 this.$emit("collaFa", "123"); } } }; </script>
<template> <el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll"> <slot /> </el-scrollbar> </template> <script> const tagAndTagSpacing = 4 // tagAndTagSpacing export default { name: ‘ScrollPane‘, data() { return { left: 0 } }, computed: { scrollWrapper() { return this.$refs.scrollContainer.$refs.wrap } }, methods: { handleScroll(e) { const eventDelta = e.wheelDelta || -e.deltaY * 40 const $scrollWrapper = this.scrollWrapper $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4 }, moveToTarget(currentTag,tagList) { const $container = this.$refs.scrollContainer.$el const $containerWidth = $container.offsetWidth const $scrollWrapper = this.scrollWrapper // const tagList = this.$parent.$refs.tag let firstTag = null let lastTag = null // find first tag and last tag if (tagList.length > 0) { firstTag = tagList[0] lastTag = tagList[tagList.length - 1] } if (firstTag === currentTag) { $scrollWrapper.scrollLeft = 0 } else if (lastTag === currentTag) { $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth } else { // find preTag and nextTag const currentIndex = tagList.findIndex(item => item === currentTag) const prevTag = tagList[currentIndex - 1] const nextTag = tagList[currentIndex + 1] // the tag‘s offsetLeft after of nextTag const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing // the tag‘s offsetLeft before of prevTag const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) { $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) { $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft } } } } } </script> <style > .scroll-container { white-space: nowrap; position: relative !important; overflow: hidden !important; width: 100%; } .scroll-container .el-scrollbar__bar { bottom: 0px; } .scroll-container .el-scrollbar__wrap { height: 49px; } </style>
<template> <component :is="type" v-bind="linkProps(to)"> <slot /> </component> </template> <script> import { isExternal } from ‘../js/validate‘ export default { props: { to: { type: String, required: true } }, computed: { isExternal() { return isExternal(this.to) }, type() { if (this.isExternal) { return ‘a‘ } return ‘router-link‘ } }, methods: { linkProps(to) { if (this.isExternal) { return { href: to, target: ‘_blank‘, style:‘color:#fff;‘ } } return { to: to } } } } </script>
5、运行效果