在上一章创建Vue项目之后,实现登录功能。
完成之后,整个项目的大致结构如下:
1.首先修改入口页面App.vue,修改之后如下
<template>
<div id="app">
<router-view/>
<!-- router-view 显示的是当前路由地址所对应的内容-->
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
然后修改入口js文件:main.js,修改之后如下
import Vue from 'vue'
import ElementUI from 'element-ui' //ui
import 'element-ui/lib/theme-chalk/index.css'
import locale from 'element-ui/lib/locale/lang/zh-CN' // lang i18n
import '@/styles/index.scss' // global css 核心css
import App from './App'
//import '@/icons' // icon
import router from './router/index' //router
// Vue.config.productionTip = false
Vue.use(ElementUI);
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
从上面可以看出引用了element-ui,所以事先要导入,导入有两种方式:
第一种,npm命令导入,可以指定版本号,在@后面加版本号
npm i element-ui@版本号
第二种,直接到项目的package.json配置文件的“dependencies”属性下添加
"dependencies": {
"element-ui": "^2.13.0",
},
然后npm命令手动导入,会将新加的js包下载到本项目,类似maven的pom.xml文件中添加新的jar依赖,再用maven重新导入。此处用的是最新的版本,版本号加在^后面。
npm i
到官网查看element-ui的历史版本号
2.添加核心css文件,新建styles文件夹,先展示需要添加的css文件目录结构
index.scss
@import './variables.scss';
@import './mixin.scss';
@import './transition.scss';
@import './element-ui.scss';
@import './sidebar.scss';
body {
margin: 0; //原始页面会多出8px边距
height: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}
.el-dialog__body {
padding: 20px 20px;
color: #606266;
font-size: 14px;
}
label {
font-weight: 700;
}
html {
height: 100%;
box-sizing: border-box;
}
#app{
height: 100%;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
outline: none;
text-decoration: none;
}
div:focus{
outline: none;
}
a:focus,
a:active {
outline: none;
}
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}
.clearfix {
&:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
}
.warn-content{
background: rgba(66,185,131,.1);
border-radius: 2px;
padding: 11px;
word-spacing: .05rem;
a{
color: #42b983;
font-weight: 400;
}
}
//main-container全局样式
.app-main{
height: 100%;
}
.app-container {
padding: 20px;
}
.head-container {
padding-bottom: 10px;
.filter-item {
display: inline-block;
vertical-align: middle;
margin-bottom: 10px;
}
input {
height: 30.5px;
line-height: 30.5px;
}
.el-input__icon {
line-height: 31px;
}
}
.el-avatar {
display: inline-block;
text-align: center;
background: #ccc;
color: #fff;
white-space: nowrap;
position: relative;
overflow: hidden;
vertical-align: middle;
width: 32px;
height: 32px;
line-height: 32px;
border-radius: 16px;
}
.logo-con{
height: 60px;
padding: 13px 0px 0px;
img{
height: 32px;
width: 135px;
display: block;
//margin: 0 auto;
}
}
#el-login-footer {
height: 40px;
line-height: 40px;
position: fixed;
bottom: 0;
width: 100%;
text-align: center;
color: #fff;
font-family: Arial;
font-size: 12px;
letter-spacing: 1px;
}
#el-main-footer {
background: none repeat scroll 0 0 white;
border-top: 1px solid #e7eaec;
overflow: hidden;
padding: 10px 6px 0px 6px;
height: 33px;
font-size: 0.7rem !important;
color: #7a8b9a;
letter-spacing: 0.8px;
font-family: Arial, sans-serif !important;
position: fixed;
bottom: 0;
z-index: 99;
width: 100%;
}
.eladmin-upload {
border: 1px dashed #c0ccda;
border-radius: 5px;
height: 45px;
line-height: 45px;
width: 368px;
}
variables.scss
#app {
// 主体区域
.main-container {
min-height: 100%;
transition: margin-left .28s;
margin-left: 193px;
position: relative;
}
// 侧边栏
.sidebar-container {
transition: width 0.28s;
width: 193px !important;
height: 100%;
position: fixed;
font-size: 0px;
top: 0;
bottom: 0;
left: 0;
z-index: 1001;
overflow: hidden;
//reset element-ui css
.horizontal-collapse-transition {
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
}
.el-scrollbar__bar.is-vertical{
right: 0px;
}
.scrollbar-wrapper {
overflow-x: hidden!important;
.el-scrollbar__view {
height: 100%;
}
}
.is-horizontal {
display: none;
}
a {
display: inline-block;
width: 100%;
overflow: hidden;
}
.svg-icon {
margin-right: 16px;
}
.el-menu {
border: none;
height: 100%;
width: 100% !important;
}
.is-active > .el-submenu__title{
color: #f4f4f5!important;
}
}
.hideSidebar {
.sidebar-container {
width: 36px !important;
}
.main-container {
margin-left: 36px;
}
.submenu-title-noDropdown {
padding-left: 10px !important;
position: relative;
.el-tooltip {
padding: 0 10px !important;
}
}
.el-submenu {
overflow: hidden;
&>.el-submenu__title {
padding-left: 10px !important;
.el-submenu__icon-arrow {
display: none;
}
}
}
.el-menu--collapse {
.el-submenu {
&>.el-submenu__title {
&>span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
}
}
}
}
.sidebar-container .nest-menu .el-submenu>.el-submenu__title,
.sidebar-container .el-submenu .el-menu-item {
min-width: 195px !important;
background-color: $subMenuBg !important;
&:hover {
background-color: $menuHover !important;
}
}
.el-menu--collapse .el-menu .el-submenu {
min-width: 195px !important;
}
//适配移动端
.mobile {
.main-container {
margin-left: 0px;
}
.sidebar-container {
transition: transform .28s;
width: 195px !important;
}
&.hideSidebar {
.sidebar-container {
transition-duration: 0.3s;
transform: translate3d(-195px, 0, 0);
}
}
}
.withoutAnimation {
.main-container,
.sidebar-container {
transition: none;
}
}
}
.el-menu--vertical{
& >.el-menu{
.svg-icon{
margin-right: 16px;
}
}
}
mixin.scss
@mixin clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}
@mixin scrollBar {
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
@mixin relative {
position: relative;
width: 100%;
height: 100%;
}
transition.scss
//globl transition css
/*fade*/
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.28s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
/*fade-transform*/
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all .5s;
}
.fade-transform-enter {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
/*fade*/
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all .5s;
}
.breadcrumb-enter,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
}
.breadcrumb-move {
transition: all .5s;
}
.breadcrumb-leave-active {
position: absolute;
}
element-ui.scss
//to reset element-ui default css
.el-upload {
input[type="file"] {
display: none !important;
}
}
.el-upload__input {
display: none;
}
//暂时性解决diolag 问题 https://github.com/ElemeFE/element/issues/2461
.el-dialog {
transform: none;
left: 0;
position: relative;
margin: 0 auto;
}
//element ui upload
.upload-container {
.el-upload {
width: 100%;
.el-upload-dragger {
width: 100%;
height: 200px;
}
}
}
sidebar.scss
#app {
// 主体区域
.main-container {
min-height: 100%;
transition: margin-left .28s;
margin-left: 193px;
position: relative;
}
// 侧边栏
.sidebar-container {
transition: width 0.28s;
width: 193px !important;
height: 100%;
position: fixed;
font-size: 0px;
top: 0;
bottom: 0;
left: 0;
z-index: 1001;
overflow: hidden;
//reset element-ui css
.horizontal-collapse-transition {
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
}
.el-scrollbar__bar.is-vertical{
right: 0px;
}
.scrollbar-wrapper {
overflow-x: hidden!important;
.el-scrollbar__view {
height: 100%;
}
}
.is-horizontal {
display: none;
}
a {
display: inline-block;
width: 100%;
overflow: hidden;
}
.svg-icon {
margin-right: 16px;
}
.el-menu {
border: none;
height: 100%;
width: 100% !important;
}
.is-active > .el-submenu__title{
color: #f4f4f5!important;
}
}
.hideSidebar {
.sidebar-container {
width: 36px !important;
}
.main-container {
margin-left: 36px;
}
.submenu-title-noDropdown {
padding-left: 10px !important;
position: relative;
.el-tooltip {
padding: 0 10px !important;
}
}
.el-submenu {
overflow: hidden;
&>.el-submenu__title {
padding-left: 10px !important;
.el-submenu__icon-arrow {
display: none;
}
}
}
.el-menu--collapse {
.el-submenu {
&>.el-submenu__title {
&>span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
}
}
}
}
.sidebar-container .nest-menu .el-submenu>.el-submenu__title,
.sidebar-container .el-submenu .el-menu-item {
min-width: 195px !important;
background-color: $subMenuBg !important;
&:hover {
background-color: $menuHover !important;
}
}
.el-menu--collapse .el-menu .el-submenu {
min-width: 195px !important;
}
//适配移动端
.mobile {
.main-container {
margin-left: 0px;
}
.sidebar-container {
transition: transform .28s;
width: 195px !important;
}
&.hideSidebar {
.sidebar-container {
transition-duration: 0.3s;
transform: translate3d(-195px, 0, 0);
}
}
}
.withoutAnimation {
.main-container,
.sidebar-container {
transition: none;
}
}
}
.el-menu--vertical{
& >.el-menu{
.svg-icon{
margin-right: 16px;
}
}
}
3.修改route,加载登录页面
import Vue from 'vue'
import Router from 'vue-router'
import login from '@/views/login' //导入login组件
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'login',
component: login //组件
}
]
})
4.编写login,新建views文件夹,添加login.vue,内容如下
<template>
<div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" label-position="left" label-width="0px" class="login-form">
<h3 class="title">EL-ADMIN 后台管理系统</h3>
<el-form-item prop="username">
<el-input v-model="loginForm.username" type="text" auto-complete="off" placeholder="账号">
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon"/>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" type="password" auto-complete="off" placeholder="密码" @keyup.enter.native="handleLogin">
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon"/>
</el-input>
</el-form-item>
<el-form-item prop="code">
<el-input v-model="loginForm.code" auto-complete="off" placeholder="验证码" style="width: 63%" @keyup.enter.native="handleLogin">
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon"/>
</el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode">
</div>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住我</el-checkbox>
<el-form-item style="width:100%;">
<el-button :loading="loading" size="medium" type="primary" style="width:100%;" @click.native.prevent="handleLogin">
<span v-if="!loading">登 录</span>
<span v-else>登 录 中...</span>
</el-button>
</el-form-item>
</el-form>
<!-- 底部 -->
<div id="el-login-footer">
<span>© 2020 Zhou Yi</span>
<span> ⋅ </span>
<a href="http://www.beian.miit.gov.cn" target="_blank">渝ICP备18005431号</a>
</div>
</div>
</template>
<script>
import { encrypt } from '@/utils/rsaEncrypt' //加密
import Config from '@/config' //全局配置
import { getCodeImg } from '@/api/login' //api
import Cookies from 'js-cookie' //cookie
export default {
name: 'Login',
data() {
return {
codeUrl: '',
cookiePass: '',
loginForm: {
username: 'admin',
password: '123456',
rememberMe: false,
code: '',
uuid: ''
},
loginRules: {
username: [{ required: true, trigger: 'blur', message: '用户名不能为空' }],
password: [{ required: true, trigger: 'blur', message: '密码不能为空' }],
code: [{ required: true, trigger: 'change', message: '验证码不能为空' }]
},
loading: false,
redirect: undefined
}
},
watch: {
$route: {
handler: function(route) {
this.redirect = route.query && route.query.redirect
},
immediate: true
}
},
created() {
// this.getCode()
// this.getCookie()
},
methods: {
getCode() {
getCodeImg().then(res => {
this.codeUrl = res.img
this.loginForm.uuid = res.uuid
})
},
getCookie() {
const username = Cookies.get('username')
let password = Cookies.get('password')
const rememberMe = Cookies.get('rememberMe')
// 保存cookie里面的加密后的密码
this.cookiePass = password === undefined ? '' : password
password = password === undefined ? this.loginForm.password : password
this.loginForm = {
username: username === undefined ? this.loginForm.username : username,
password: password,
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe),
code: ''
}
},
handleLogin() {
this.$refs.loginForm.validate(valid => {
const user = {
username: this.loginForm.username,
password: this.loginForm.password,
rememberMe: this.loginForm.rememberMe,
code: this.loginForm.code,
uuid: this.loginForm.uuid
}
if (user.password !== this.cookiePass) {
user.password = encrypt(user.password)
}
if (valid) {
this.loading = true
if (user.rememberMe) {
Cookies.set('username', user.username, { expires: Config.passCookieExpires })
Cookies.set('password', user.password, { expires: Config.passCookieExpires })
Cookies.set('rememberMe', user.rememberMe, { expires: Config.passCookieExpires })
} else {
Cookies.remove('username')
Cookies.remove('password')
Cookies.remove('rememberMe')
}
this.$store.dispatch('Login', user).then(() => {
this.loading = false
this.$router.push({ path: this.redirect || '/' })
}).catch(() => {
this.loading = false
this.getCode()
})
} else {
console.log('error submit!!')
return false
}
})
}
}
}
</script>
//scss语法
<style rel="stylesheet/scss" lang="scss">
.login {
display: flex; //flex布局
justify-content: center; //水平居中
align-items: center; //上下居中
height: 100%;
background-image:url(http://api.neweb.top/bing.php?type=rand );
background-size: cover;
}
.title {
margin: 0px auto 30px auto;
text-align: center;
color: #707070;
}
.login-form {
border-radius: 6px;
background: #ffffff;
width: 385px;
padding: 25px 25px 5px 25px;
.el-input {
height: 38px;
input {
height: 38px;
}
}
.input-icon{
height: 39px;width: 14px;margin-left: 2px;
}
}
.login-tip {
font-size: 13px;
text-align: center;
color: #bfbfbf;
}
.login-code {
width: 33%;
display: inline-block;
height: 38px;
float: right;
img{
cursor: pointer;
vertical-align:middle
}
}
#el-login-footer {
position: fixed;
bottom: 0;
width: 100%;
height: 40px;
line-height: 40px;
text-align: center;
color: #fff;
font-family: Arial,serif;
font-size: 12px;
letter-spacing: 1px;
}
a{
cursor: pointer;
color: inherit;
text-decoration: none;
}
</style>
首先样式使用scss语法,布局采用flex布局。
又引入了新的包,先解决js-cookie,注意:采用scss语法需要导入node-sass和sass-loader,直接在package.json中添加,npm i 命令导入
"dependencies": {
"js-cookie": "2.2.0",
},
"devDependencies": {
"node-sass": "^4.7.2",
"sass-loader": "7.0.3",
},
接下来新建utils文件夹,添加rsaEncrypt.js
import JSEncrypt from 'jsencrypt/bin/jsencrypt'
// 密钥对生成 http://web.chacuo.net/netrsakeypair
const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANL378k3RiZHWx5AfJqdH9xRNBmD9wGD\n' +
'2iRe41HdTNF8RUhNnHit5NpMNtGL0NPTSSpPjjI1kJfVorRvaQerUgkCAwEAAQ=='
const privateKey = 'MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEA0vfvyTdGJkdbHkB8\n' +
'mp0f3FE0GYP3AYPaJF7jUd1M0XxFSE2ceK3k2kw20YvQ09NJKk+OMjWQl9WitG9p\n' +
'B6tSCQIDAQABAkA2SimBrWC2/wvauBuYqjCFwLvYiRYqZKThUS3MZlebXJiLB+Ue\n' +
'/gUifAAKIg1avttUZsHBHrop4qfJCwAI0+YRAiEA+W3NK/RaXtnRqmoUUkb59zsZ\n' +
'UBLpvZgQPfj1MhyHDz0CIQDYhsAhPJ3mgS64NbUZmGWuuNKp5coY2GIj/zYDMJp6\n' +
'vQIgUueLFXv/eZ1ekgz2Oi67MNCk5jeTF2BurZqNLR3MSmUCIFT3Q6uHMtsB9Eha\n' +
'4u7hS31tj1UWE+D+ADzp59MGnoftAiBeHT7gDMuqeJHPL4b+kC+gzV4FGTfhR9q3\n' +
'tTbklZkD2A=='
// 加密
export function encrypt(txt) {
const encryptor = new JSEncrypt()
encryptor.setPublicKey(publicKey) // 设置公钥
return encryptor.encrypt(txt) // 对需要加密的数据进行加密
}
// 解密
export function decrypt(txt) {
const encryptor = new JSEncrypt()
encryptor.setPrivateKey(privateKey)
return encryptor.decrypt(txt)
}
再新建config文件夹,添加index.js全局配置文件
/**
* @description 系统全局配置
*/
export default {
/**
* @description 记住密码状态下的token在Cookie中存储的天数,默认1天
*/
tokenCookieExpires: 1,
/**
* @description 记住密码状态下的密码在Cookie中存储的天数,默认1天
*/
passCookieExpires: 1,
/**
* @description 此处修改网站名称
*/
webName: 'EL-ADMIN',
/**
* @description 是否只保持一个子菜单的展开
*/
uniqueOpened: true,
/**
* @description token key
*/
TokenKey: 'EL-ADMIN-TOEKN',
/**
* @description 请求超时时间,毫秒(默认2分钟)
*/
timeout: 1200000,
/**
* @description 是否显示 tagsView
*/
tagsView: true,
/**
* @description 固定头部
*/
fixedHeader: true,
/**
* @description 是否显示logo
*/
sidebarLogo: true,
/**
* 是否显示设置的悬浮按钮
*/
settingBtn: false,
}
新建api文件夹,添加login.js
import request from '@/utils/request'
export function login(username, password, code, uuid) {
return request({
url: 'auth/login',
method: 'post',
data: {
username,
password,
code,
uuid
}
})
}
export function getInfo() {
return request({
url: 'auth/info',
method: 'get'
})
}
export function getCodeImg() {
return request({
url: 'auth/captcha',
method: 'get'
})
}
export function logout() {
return request({
url: 'auth/logout',
method: 'delete'
})
}
在utils文件夹下添加request.js
import axios from 'axios'
import Config from '@/config'
// 创建axios实例
const service = axios.create({
baseURL: process.env.NODE_ENV === 'production' ? process.env.BASE_API : '/', // api 的 base_url
timeout: Config.timeout // 请求超时时间
})
export default service
此处需要引入axios
"dependencies": {
"axios": "^0.19.0",
},
5.启动项目测试
npm run dev
启动成功
浏览器输入:http://localhost:8080
后面还有svg-icon、store、以及与后台交互测试。