vue-cli3 + express + mongodb小型全栈项目(二)

4 篇文章 0 订阅

接上篇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

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>


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值