接上篇vue-cli3 + express + mongodb小型全栈项目(一)
1、创建vue项目
在node_app文件夹下使用vue create client
命令生成vue项目。client是本次客户端项目名称。
安装后的项目目录:
2、使用concurrently连接前、后台项目同时启动
2.1 安装concurrently。
2.2 修改client项目的package.json
增加一个脚本命令
"start":"npm run serve"
2.3 node_app 项目package.json文件的脚本修改如下:
"client-install":"npm install --prefix client",
"client":"npm start --prefix client",
"server": "nodemon server.js",
"start": "node server.js",
"dev":"concurrently \"npm run server\" \"npm run client\""
2.4 使用npm run dev
同时启动两个项目
3、安装element-ui
使用 vue add element
命令,安装element
4、创建Register组件
views文件夹下新建Register.vue
<template>
<div class="register">
<section class="form_container">
<div class="manage_tip">
<span class="title">在线后台管理系统</span>
<el-form :model="registerUser" :rules="rules" ref="registerForm" label-width="80px" class="registerForm">
<el-form-item label="用户名" prop="name">
<el-input v-model="registerUser.name" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="registerUser.email" placeholder="请输入email"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="registerUser.password" placeholder="请输入密码"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="password2">
<el-input type="password" v-model="registerUser.password2" placeholder="请确认密码"></el-input>
</el-form-item>
<el-form-item label="选择身份">
<el-select v-model="registerUser.identity" placeholder="请选择身份">
<el-option label="管理员" value="manager"></el-option>
<el-option label="员工" value="employee"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" class="submit_btn" @click="submitForm('registerForm')">注册</el-button>
</el-form-item>
</el-form>
</div>
</section>
</div>
</template>
<script>
export default {
name: 'register',
data() {
var validatePass2 = (rule,value,callback)=>{
if(value!== this.registerUser.password){
callback(new Error("两次密码不一致"))
}else{
callback();
}
}
return {
registerUser:{
name:'',
email:'',
password:'',
password2:'',
identity:''
},
rules:{
name:[
{
required:true,
message:'用户名不能为空',
trigger:'blur'
},{
min:2,
max:16,
message:"长度需在2-16位之间",
trigger:'blur'
}
],
email:[
{
type:"email",
required:true,
message:'邮箱格式不正确',
trigger:'blur'
}
],
password:[
{
required:true,
message:'密码不能为空',
trigger:'blur'
},{
min:6,
max:30,
message:"长度需在6-30位之间",
trigger:'blur'
}
],
password2:[
{
required:true,
message:'确认密码不能为空',
trigger:'blur'
},
{
min:6,
max:30,
message:"长度需在6-30位之间",
trigger:'blur'
},
{
validator:validatePass2,
trigger:'blur'
}
]
}
}
},
methods: {
submitForm(formName){
this.$refs[formName].validate(valid =>{
if (valid) {
// 表单验证通过
}else{
console.log("error 没有通过校验");
return false;
}
})
}
},
}
</script>
<style scoped>
.register {
position: relative;
width: 100%;
height: 100%;
background: url(../assets/bg.jpg) no-repeat center center;
background-size: 100% 100%;
}
.form_container {
width: 370px;
height: 210px;
position: absolute;
top: 10%;
left: 34%;
padding: 25px;
border-radius: 5px;
text-align: center;
}
.form_container .manage_tip .title {
font-family: "Microsoft YaHei";
font-weight: bold;
font-size: 26px;
color: #fff;
}
.registerForm {
margin-top: 20px;
background-color: #fff;
padding: 20px 40px 20px 20px;
border-radius: 5px;
box-shadow: 0px 5px 10px #cccc;
}
.submit_btn {
width: 100%;
}
</style>
配置路由 router.js文件添加如下代码:
import Register from './../views/Register'
const routes = [
{
path:'/register',
component:Register
},
]
5 、创建404页面组件
views文件夹下新建404.vue
<template>
<div class="nofind">
<img src="../assets/404.gif" alt="">
</div>
</template>
<style scoped>
.nofind {
width: 100%;
height: 100%;
overflow: hidden;
}
.nofind img {
width: 100%;
height: 100%;
}
</style>
路由文件修改,增加如下代码
import NotFound from './../views/404.vue'
const routes = [
{
path:'*',
component:NotFound
}
]
6、安装axios,并实现注册接口
6.1 通过vue add axios安装
6.2 Register.vue组件submitForm方法修改为:
submitForm(formName){
this.$refs[formName].validate(valid =>{
if (valid) {
// 表单验证通过
this.axios.post('/api/users/register',this.registerUser).then(res=>{
this.$message({
message:'注册成功',
type:"success"
})
this.$router.push('/login')
})
}else{
console.log("error 没有通过校验");
return false;
}
})
}
6.3 在client文件夹下创建vue.config.js,内容如下
const path = require('path')
const debug = process.env.NODE_ENV !== 'production'
module.exports = {
publicPath: '/', // 根域上下文目录
outputDir: 'dist', // 构建输出目录
assetsDir: 'assets', // 静态资源目录 (js, css, img, fonts)
lintOnSave: false, // 是否开启eslint保存检测,有效值:ture | false | 'error'
runtimeCompiler: true, // 运行时版本是否需要编译
transpileDependencies: [], // 默认babel-loader忽略mode_modules,这里可增加例外的依赖包名
productionSourceMap: true, // 是否在构建生产包时生成 sourceMap 文件,false将提高构建速度
configureWebpack: config => { // webpack配置,值位对象时会合并配置,为方法时会改写配置
if (debug) { // 开发环境配置
config.devtool = 'cheap-module-eval-source-map'
} else { // 生产环境配置
}
// Object.assign(config, { // 开发生产共同配置
// resolve: {
// alias: {
// '@': path.resolve(__dirname, './src'),
// '@c': path.resolve(__dirname, './src/components'),
// 'vue$': 'vue/dist/vue.esm.js'
// }
// }
// })
},
chainWebpack: config => { // webpack链接API,用于生成和修改webapck配置,https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
if (debug) {
// 本地开发配置
} else {
// 生产开发配置
}
},
parallel: require('os').cpus().length > 1, // 构建时开启多进程处理babel编译
pluginOptions: { // 第三方插件配置
},
pwa: { // 单页插件相关配置 https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa
},
devServer: {
open: true,
host: 'localhost',
port: 8081,
https: false,
hotOnly: false,
proxy: { // 配置跨域
'/api': {
target: 'http://localhost:5000/api/',
ws: true,
changOrigin: true,
pathRewrite: {
'^/api': ''
}
}
},
before: app => { }
}
}
接口调试OK。
7、创建Login.vue 组件并实现登录接口
7.1 创建Login.vue 组件,内容如下:
<template>
<div class="login">
<section class="form_container">
<div class="manage_tip">
<span class="title">在线后台管理系统</span>
</div>
<el-form :model="loginUser" :rules="rules" ref="loginForm" class="loginForm" label-width="60px">
<el-form-item label="邮箱" prop="email">
<el-input v-model="loginUser.email" placeholder="请输入邮箱"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="loginUser.password" placeholder="请输入密码" type="password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('loginForm')" class="submit_btn">登 录</el-button>
</el-form-item>
<div class="tiparea">
<p>还没有账号?现在<router-link to='/register'>注册</router-link></p>
</div>
</el-form>
</section>
</div>
</template>
<script>
import jwt_decode from "jwt-decode";
export default {
name: "login",
data() {
return {
loginUser: {
email: "",
password: ""
},
rules: {
email: [
{
type: "email",
required: true,
message: "邮箱格式不正确",
trigger: "change"
}
],
password: [
{ required: true, message: "密码不能为空", trigger: "blur" },
{ min: 6, max: 30, message: "长度在 6 到 30 个字符", trigger: "blur" }
]
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate(valid => {
if (valid) {
this.axios.post("/api/users/login", this.loginUser).then(res => {
// 登录成功
const { token } = res.data;
localStorage.setItem("eleToken", token);
// 解析token
const decode = jwt_decode(token);
// 存储数据
this.$store.dispatch("setIsAutnenticated", !this.isEmpty(decode));
this.$store.dispatch("setUser", decode);
// 页面跳转
this.$router.push("/index");
});
} else {
console.log("error submit!!");
return false;
}
});
},
isEmpty(value) {
return (
value === undefined ||
value === null ||
(typeof value === "object" && Object.keys(value).length === 0) ||
(typeof value === "string" && value.trim().length === 0)
);
}
}
};
</script>
<style scoped>
.login {
position: relative;
width: 100%;
height: 100%;
background: url(../assets/bg.jpg) no-repeat center center;
background-size: 100% 100%;
}
.form_container {
width: 370px;
height: 210px;
position: absolute;
top: 20%;
left: 34%;
padding: 25px;
border-radius: 5px;
text-align: center;
}
.form_container .manage_tip .title {
font-family: "Microsoft YaHei";
font-weight: bold;
font-size: 26px;
color: #fff;
}
.loginForm {
margin-top: 20px;
background-color: #fff;
padding: 20px 40px 20px 20px;
border-radius: 5px;
box-shadow: 0px 5px 10px #cccc;
}
.submit_btn {
width: 100%;
}
.tiparea {
text-align: right;
font-size: 12px;
color: #333;
}
.tiparea p a {
color: #409eff;
}
</style>
7.2 配置路由
import Login from './../views/Login'
const routes = [
{
path:'/login',
component:Login
},
]
8、设置路由守卫及token过期处理
8.1 路由守卫 router/index.js文件添加如下代码:
router.beforeEach((to,from,next)=>{
const token = localStorage.getItem('eleToken');
// 去登录页或者注册页 不需要校验
if (to.path === '/login' || to.path === '/register') {
next();
}
if(token){
next()
}else{
next('/login')
}
})
8.2 token过期处理 ,添加axios请求、响应拦截。
在main.js文件中添加如下代码:
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
console.log("请求前");
if (localStorage.eleToken) {
config.headers.Authorization = localStorage.eleToken;
}
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
Message.error(error.response.data)
const {status } = error.response;
if (status == 401) {
Message.error('token 过期')
localStorage.removeItem('eleToken')
router.push('/login')
}
// 对响应错误做点什么
return Promise.reject(error);
});
store文件下index.js文件
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const types = {
SET_AUTHENTICATED:"SET_AUTHENTICATED",
SET_USER:"SET_USER"
}
const state = {
isAuthenticated:false,
user:{}
}
const mutations = {
[types.SET_AUTHENTICATED](state,isAuthenticated){
if (isAuthenticated) state.isAuthenticated = isAuthenticated;
else state.isAuthenticated = false;
},
[types.SET_USER](state,user){
if (user) {
state.user = user
}else{
state.user = {}
}
}
}
const actions = {
setAutnenticated({commit},isAuthenticated){
commit(types.SET_AUTHENTICATED,isAuthenticated)
},
setUser({commit},user){
commit(types.SET_USER,user)
},
clearCurrentState({commit}){
commit(types.SET_AUTHENTICATED,false)
commit(types.SET_USER,null)
}
}
const getters = {
isAuthenticated:state=>state.isAuthenticated,
user:state=>state.user
}
export default new Vuex.Store({
state,
mutations,
actions,
getters
})
9、头部组件及头部下拉选项的实现
9.1 src目录下新建components文件夹下新建HeadNav.vue文件
<template>
<header class="head-nav">
<el-row>
<el-col :span="6" class='logo-container'>
<img src="../assets/logo.png" class='logo' alt="">
<span class='title'>在线后台管理系统</span>
</el-col>
<el-col :span='6' class="user">
<div class="userinfo">
<img :src="user.avatar" class='avatar' alt="">
<div class='welcome'>
<p class='name comename'>欢迎</p>
<p class='name avatarname'>{{user.name}}</p>
</div>
<span class='username'>
<el-dropdown
trigger="click"
@command='setDialogInfo'>
<span class="el-dropdown-link">
<i class="el-icon-caret-bottom el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command='info'>个人信息</el-dropdown-item>
<el-dropdown-item command='logout'>退出</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</span>
</div>
</el-col>
</el-row>
</header>
</template>
<script>
export default {
name: "head-nav",
computed: {
user() {
return this.$store.getters.user;
}
},
methods: {
setDialogInfo(cmditem) {
if (!cmditem) {
this.$message("菜单选项缺少command属性");
return;
}
switch (cmditem) {
case "info":
this.showInfoList();
break;
case "logout":
this.logout();
break;
}
},
showInfoList() {
// 个人信息
this.$router.push("/infoShow");
},
logout() {
// 清除token
localStorage.removeItem("eleToken");
this.$store.dispatch("clearCurrentState");
// 页面跳转
this.$router.push("/login");
}
}
};
</script>
<style scoped>
.head-nav {
width: 100%;
height: 60px;
min-width: 600px;
padding: 5px;
background: #324057;
color: #fff;
border-bottom: 1px solid #1f2d3d;
}
.logo-container {
line-height: 60px;
min-width: 400px;
}
.logo {
height: 50px;
width: 50px;
margin-right: 5px;
vertical-align: middle;
display: inline-block;
}
.title {
vertical-align: middle;
font-size: 22px;
font-family: "Microsoft YaHei";
letter-spacing: 3px;
}
.user {
line-height: 60px;
text-align: right;
float: right;
padding-right: 10px;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
vertical-align: middle;
display: inline-block;
}
.welcome {
display: inline-block;
width: auto;
vertical-align: middle;
padding: 0 5px;
}
.name {
line-height: 20px;
text-align: center;
font-size: 14px;
}
.comename {
font-size: 12px;
}
.avatarname {
color: #409eff;
font-weight: bolder;
}
.username {
cursor: pointer;
margin-right: 5px;
}
.el-dropdown {
color: #fff;
}
</style>
这里图标用的是在线的 ,在index.html文件加入
<link href="//cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.css" rel="stylesheet">
9.2 views文件夹下新建Home.vue 文件
<template>
<div class="home">
<div class="container">
<h1 class="title">在线后台</h1>
<p class="lead"> 专注于线上教育, 用心做课程, 用心做服务! </p>
</div>
</div>
</template>
<style scoped>
.home {
width: 100%;
height: 100%;
background: url(../assets/showcase.png) no-repeat;
background-size: 100% 100%;
}
.container {
width: 100%;
height: 100%;
box-sizing: border-box;
padding-top: 100px;
background-color: rgba(0, 0, 0, 0.7);
text-align: center;
color: white;
}
.title {
font-size: 30px;
}
.lead {
margin-top: 50px;
font-size: 22px;
}
</style>
9.3 views文件夹下新建InfoShow.vue 文件
<template>
<div class="infoshow">
<el-row type="flex" class="row-bg" justify="center">
<el-col :span='8'>
<div class="user">
<img :src="user.avatar" class='avatar' alt="">
</div>
</el-col>
<el-col :span='16'>
<div class="userinfo">
<div class="user-item">
<i class="fa fa-user"></i>
<span>{{user.name}}</span>
</div>
<div class="user-item">
<i class="fa fa-cog"></i>
<span>{{user.identity == 'manager' ? '管理员' : '普通员工'}}</span>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
name: "infoshow",
computed: {
user() {
return this.$store.getters.user;
}
}
};
</script>
<style scoped>
.infoshow {
width: 100%;
height: 100%;
box-sizing: border-box;
/* padding: 16px; */
}
.row-bg {
width: 100%;
height: 100%;
}
.user {
text-align: center;
position: relative;
top: 30%;
}
.user img {
width: 150px;
border-radius: 50%;
}
.user span {
display: block;
text-align: center;
margin-top: 20px;
font-size: 20px;
font-weight: bold;
}
.userinfo {
height: 100%;
background-color: #eee;
}
.user-item {
position: relative;
top: 30%;
padding: 26px;
font-size: 28px;
color: #333;
}
</style>
9.4 router/index.js文件增加代码
import Home from './../views/Home'
import InfoShow from './../views/InfoShow'
'
Vue.use(VueRouter)
const routes = [
{
path:'/index',
component:Index,
children:[
{
path:'',
component:Home,
},
{
path:'/home',
component:Home,
},
{
path:'/infoShow',
component:InfoShow,
}
]
},
]
9.5 views文件夹下Index.vue 文件引入HeadNav .vue 组件
<template>
<div class="index">
<HeadNav></HeadNav>
<div class="rightContainer">
<router-view></router-view>
</div>
</div>
</template>
<script>
import HeadNav from "../components/HeadNav";
export default {
name: "index",
components: {
HeadNav
}
};
</script>
<style scoped>
.index {
width: 100%;
height: 100%;
overflow: hidden;
}
.rightContainer {
position: relative;
top: 0;
left: 180px;
width: calc(100% - 180px);
height: calc(100% - 71px);
overflow: auto;
}
</style>