前言
前面一段时间公司里忙得很晚,睡觉时间都不太够了,就隔了几天。服务端的用户系统已经完成。现在来一点前端的内容。我觉得前后端分离就可以各开发各的。相互之间用某种协议进行交互,整合。我的理解整个平台会由N个服务端项目、N个前端项目、加一些中间件构成。前端可能有WEB页面、手机APP、微信公众号页面、H5页面等等。
这里我们开始做PC端的后台管理页面。我把PC端分为前台、后台、登录三个项目。目前先这样,后续可能还会根据实际情况细分。几个项目都是Vue项目,就把它们合到一个工程中进行,省去共用模块、组件的重复管理。这个工程就需要多入口的Vue工程。
创建项目
因为我已经写过Vue多入口项目创建的文章,这里就不再重复了。移步vue多入口项目。项目名称huip。git地址:https://gitee.com/biboheart/huip-vue.git
创建完成后的目录结构:
引入图标
在应用型前端中,图标会用的比较多。fontawesome图标库中的免费图标是比较丰富的。只是npm中最高版本到4.7.2就没有再更新。而官网已经到了5.1.0 。所以我选择下载到本地静态引入的方法使用5.1.0(哪位朋友有好的方法还忘告知)。
从官网下载,拷贝到static目录中。在index.html文件中引入css
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link href="static/fontawesome/css/fontawesome-all.min.css" rel="stylesheet">
<title>huip</title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
引入前端框架
我采用element-ui作为vue前端框架。里面的组件比较丰富。包含了常用的管理组件了。
cnpm i element-ui -S
在三个main.js加入element-ui库,完成后的main.js内容如下
import Vue from 'vue'
import App from '@/components/App'
import router from './router'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.config.productionTip = false
Vue.use(ElementUI)
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
界面布局
在这个工程中,各项目(除了登录)的布局都是基本相同的。因此,需要一个公共的布局组件。利用element的Container 布局容器进行开发。
element-ui的NavMenu 导航菜单,官方提供的每个菜单项是需要自己去生成的,并不能通过树型结构的数据生成出整个菜单。因此,我们先来写个组件,递归生成树型菜单。
以下是一个菜单项的组件代码。
<template>
<component v-bind:is="currentItemComponent" :index="menu.path" :key="menu.path">
<i :class="menu.icon" v-if="menu.icon && !hc"></i><span v-if="!hc">{{menu.text}}</span>
<template slot="title" v-if="hc">
<i :class="menu.icon" v-if="menu.icon"></i><span slot="title">{{menu.text}}</span>
</template>
<tree-menu-item :menu="child" :key="child.name" v-for="child in menu.children" v-if="hc && child.menu"></tree-menu-item>
</component>
</template>
<script>
export default {
name: 'TreeMenuItem',
props: {
menu: Object
},
data () {
return {
hc: false
}
},
computed: {
currentItemComponent: function () {
return this.hasChildren() ? 'el-submenu' : 'el-menu-item'
}
},
methods: {
hasChildren () {
this.hc = !!this.menu.hasChildren && this.menu.children && this.menu.children.length > 0
return this.hc
}
}
}
</script>
<style scoped>
</style>
在公共布局组件中layout.vue中使用它:
<script>
import TreeMenuItem from '../packages/tree-menu/index.js'
export default {
name: 'HuipLayout',
components: {
TreeMenuItem
},
props: {
hmenus: Array,
vmenus: Array,
tips: Array,
logoSrc: String,
badgeValue: [String, Number]
},
data: function () {
return {
isCollapse: false,
asideWidth: '230px',
vDefActive: this.activePath(3),
hDefActive: this.activePath(2),
defaultLogo: require('@/assets/logo.png')
}
},
watch: {
'$route': function (val) {
this.vDefActive = this.activePath(3)
this.hDefActive = this.activePath(2)
}
},
methods: {
collapseChange: function () {
this.isCollapse = !this.isCollapse
this.$emit('changeCollapse', this.isCollapse)
if (this.isCollapse) {
this.asideWidth = '65px'
} else {
this.asideWidth = '230px'
}
},
activePath: function (max) {
let pathArr = this.$route.path.split('/')
let def = ''
if (max < 2) {
def = this.$route.path
} else {
if (pathArr && pathArr.length > max) {
for (let i = 1; i < max; i++) {
def += '/' + pathArr[i]
}
} else {
def = this.$route.path
}
}
return def
},
clickLogo: function (...args) {
this.$emit('logo-click', ...args)
}
}
}
</script>
<template>
<el-container>
<el-header class="layout-main-header clear">
<!-- logo -->
<div class="logo" @click="clickLogo">
<img :src="logoSrc" v-if="logoSrc"/>
<img :src="defaultLogo" v-else/>
</div>
<!-- 右侧菜单 -->
<div class="tool-body">
<div class="user-info-cell">
<el-badge :value="badgeValue" class="user-info-badge" v-if="badgeValue"></el-badge>
<slot name="user-info"></slot>
</div>
<ul class="tips-box clearfix">
<li class="tips-item" v-for="item in tips" :key="item.name" v-if="!!tips && tips.length > 0">
<a :href="item.url" :target="item.target || '_self'">{{item.text}}</a>
</li>
</ul>
</div>
<!-- 如果有顶菜单则显示顶菜单 -->
<div class="hmenu-wrap" v-if="hmenus && hmenus.length > 0">
<el-menu
:default-active="hDefActive"
unique-opened
router
mode="horizontal"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b">
<tree-menu-item :menu="item" :key="item.name" v-for="item in hmenus" v-if="item.menu"></tree-menu-item>
</el-menu>
</div>
</el-header>
<!-- 如果提供了左侧菜单的数据则显示左侧aside -->
<el-container v-if="vmenus && vmenus.length > 0">
<el-aside class="layout-main-aside" :width="asideWidth">
<div class="aside-container">
<div class="aside-header">
<div class="tools" @click.prevent="collapseChange">
<i :class="isCollapse ? 'el-icon-d-arrow-right' : 'el-icon-d-arrow-left'"></i>
</div>
</div>
<div class="aside-main aside-main-hasheader">
<el-scrollbar class="full-scrollbar">
<div :class="isCollapse ? 'menu-collapsed' : 'menu-expanded'">
<el-menu
:default-active="vDefActive"
class="el-menu-vertical-demo vmenu"
unique-opened
router
:collapse="isCollapse">
<tree-menu-item :menu="item" :key="item.name" v-for="item in vmenus" v-if="item.menu"></tree-menu-item>
</el-menu>
</div>
</el-scrollbar>
</div>
</div>
</el-aside>
<el-container>
<slot></slot>
</el-container>
</el-container>
<slot v-else></slot>
</el-container>
</template>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.layout-main-header {
background-color: #545c64;
color: #333;
line-height: 60px;
text-align: left;
font-size: 12px;
}
.layout-main-aside {
color: #333;
background-color: rgb(238, 241, 246);
}
.logo {
height:60px;
width:auto;
font-size: 12px;
padding-right:20px;
border-color: rgba(238,238,238,0.3);
border-right-width: 0px;
border-right-style: solid;
color: #fff;
float: left;
box-sizing: border-box;
cursor: pointer;
}
.logo img {
width: 40px;
height: 40px;
margin: 10px 10px 10px 10px;
}
.tool-body {
height: 60px;
width: auto;
float: right;
box-sizing: border-box;
text-align: right;
padding: 0;
}
.user-info-cell {
position: relative;
height:60px;
width:auto;
font-size: 12px;
border-color: rgba(238,238,238,0.3);
border-style: solid;
border-width: 0px;
border-left-width: 1px;
color: #fff;
float: right;
box-sizing: border-box;
}
.user-info-badge {
position: absolute;
top: -10px;
right: -10px;
}
.tool-body ul,
.tool-body li {
padding: 0;
margin: 0;
list-style: outside none none;
}
.tips-box {
float: right;
display: inline;
}
.tips-box .tips-item {
box-sizing: border-box;
float: left;
height: 60px;
line-height: 60px;
margin-right: 22px;
text-align: left;
}
.tips-box .tips-item a {
font-size: 14px;
text-decoration: none;
color: #bfcbd9;
}
.tips-box .tips-item a:hover {
color: #00a2ca;
}
.hmenu-wrap {
display: block;
float: left;
height: 60px;
line-height: 60px;
}
.tools{
width: 100%;
line-height: 44px;
cursor: pointer;
text-align: center;
font-size: 14px;
background: #e4e8f1;
}
.menu-expanded .vmenu {
position: relative;
}
.menu-collapsed .vmenu {
position: fixed;
z-index: 8888;
}
</style>
完成布局
/src/project/index/pages下创建index.vue文件,这是前台页面的主体组件
<script>
import HuipLayout from '@/components/HuipLayout'
export default {
name: 'AdminMain',
components: {
HuipLayout
},
data () {
return {
defaultLogo: require('@/assets/logo.png'),
user: {
name: 'admin'
},
tips: [{
text: '控制台',
name: 'admin',
url: '/admin.html'
}],
hmenus: [{
path: '/dashboard',
name: 'dashboard',
text: '仪表盘',
menu: true,
icon: 'fas fa-tachometer-alt'
}, {
path: '/core',
name: 'core',
text: '系统配置',
menu: true,
hasChildren: true,
icon: 'fa fa-cogs',
children: [{
path: '/core/app',
name: 'core_app',
text: '版本管理',
menu: true
}, {
path: '/core/reedback',
name: 'core_reedback',
text: '用户反馈',
menu: true
}]
}, {
path: '/user',
name: 'user',
text: '用户系统',
menu: true,
hasChildren: true,
icon: 'fa fa-users',
children: [{
path: '/user/client',
name: 'user_client',
text: '客户端管理',
menu: true
}, {
path: '/user/resource',
name: 'user_resource',
text: '资源管理',
menu: true
}, {
path: '/user/auth',
name: 'user_auth',
text: '权限管理',
menu: true,
hasChildren: true,
children: [{
path: '/user/auth/system',
name: 'user_auth_system',
text: '系统权限',
menu: true
}, {
path: '/user/auth/operation',
name: 'user_auth_operation',
text: '操作权限',
menu: true
}]
}, {
path: '/user/role',
name: 'user_role',
text: '角色管理',
menu: true
}, {
path: '/user/user',
name: 'user_user',
text: '用户管理',
menu: true
}]
}]
}
}
}
</script>
<template>
<huip-layout :hmenus="hmenus" :tips="tips">
<div v-popover:userInfoPopover class="user-info clear" slot="user-info">
<img v-if="user && user.pic" :src="user.pic" class="headimg">
<img v-else :src="defaultLogo" class="headimg">
</div>
<el-popover
ref="userInfoPopover"
placement="bottom-start"
trigger="hover"
width="240"
:visible-arrow="false"
popper-class="user-info-popover">
<div class="topbar-info-dropdown-memu">
<div class="topbar-user-info">
<img v-if="user && user.pic" :src="user.pic" class="topbar-user-avatar">
<img v-else :src="defaultLogo" class="topbar-user-avatar">
<p class="topbar-user-name">{{user ? user.name : ''}}</p>
</div>
<div class="topbar-user-entrance-list">
<div class="topbar-user-entrance clear">
<i class="fas fa-clipboard-list info-prefix-icon"></i>
<span class="left-text">个人中心</span>
</div>
<div class="topbar-user-entrance clear">
<i class="fas fa-comment-dots info-prefix-icon"></i>
<span class="left-text">消息中心</span>
<span class="right-text">20</span>
</div>
</div>
<div>
<div class="user-btn-list">
<span class="user-btn-link">退出登录</span>
</div>
</div>
</div>
</el-popover>
<router-view class="content-wrap"></router-view>
</huip-layout>
</template>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.user-info {
cursor: pointer;
color: #bfcbd9;
}
.user-info .headimg {
width: 40px;
height: 40px;
border-radius: 20px;
margin: 10px;
float: left;
}
.topbar-info-dropdown-memu {
padding: 0;
list-style: none;
background-color: #fff;
background-clip: padding-box;
font-size: 12px;
min-width: 100%;
margin: 0;
border: none;
-webkit-box-shadow: 0 1px 3px rgba(0,0,0,.1);
box-shadow: 0 1px 3px rgba(0,0,0,.2);
white-space: nowrap;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.topbar-info-dropdown-memu a,
.topbar-info-dropdown-memu li,
.topbar-info-dropdown-memu p,
.topbar-info-dropdown-memu span {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
letter-spacing: .02em;
text-decoration: none;
}
.topbar-info-dropdown-memu .topbar-user-info {
text-align: center;
padding-top: 16px;
border-bottom: 1px solid #eaeaea;
}
.topbar-info-dropdown-memu .topbar-user-info .topbar-user-avatar {
width: 36px;
height: 36px;
border-radius: 18px;
vertical-align: middle;
}
.topbar-info-dropdown-memu .topbar-user-info .topbar-user-name {
margin: 8px 0;
}
.topbar-info-dropdown-memu .topbar-user-entrance-list {
overflow: hidden;
width: 240px;
}
.topbar-info-dropdown-memu .topbar-user-entrance {
cursor: pointer;
height: 20px;
line-height: 20px;
padding: 0 16px;
margin: 12px 0;
font-size: 12px;
line-height: 16px;
position: relative;
}
.topbar-info-dropdown-memu .topbar-user-entrance .info-prefix-icon {
width: 16px;
height: 16px;
vertical-align: text-bottom;
margin-right: 8px;
color: #333;
}
.topbar-info-dropdown-memu .topbar-user-entrance .right-text {
float: right;
font-size: 10px;
}
.topbar-info-dropdown-memu .user-btn-link {
cursor: pointer;
height: 50px;
line-height: 50px;
display: block;
-webkit-transition: all .15s;
transition: all .15s;
text-align: center;
color: #333;
background-color: #f5f5f6;
border-top: #eaeaea;
}
</style>
后台(admin)中也同样创建一个主体页面组件。
这里的菜单数据是临时数据,为了看布局效果,最终是根据路由文件生成菜单的。
当前效果
前台页面:
后台页面:
总结
完成前端页面总体布局,公共组件开发。