本章目的: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、运行效果

(十).netcore+vue vue-cli@4+element-ui+router+vuex
原文:https://www.cnblogs.com/WorkhardNi/p/14690595.html