当前位置:   article > 正文

vue3全栈后台管理系统_开源vue3后台管理

开源vue3后台管理

1.软件的安装

vue的安装

必须在管理员的命令下进行安装

npm install @vue/cli -g
  • 1

安装完成后使用

vue --version
  • 1

检查安装版本
在这里插入图片描述

yarn的安装

yarn的安装并查看版本

npm install -g yarn
yarn --version
  • 1
  • 2

在这里插入图片描述

vite的安装

npm install create-vite-app -g
  • 1

在这里插入图片描述

1.项目的创建

采用vite创建vue项目

yarn create vite manager{项目名}
  • 1

一直Enter会有两个选项
在这里插入图片描述
直到这样项目创建成功

启动前端项目

yarn dev
  • 1

2.安装项目依赖

# 安装项目生产依赖
yarn add vue-router@next vuex@next element-plus axios -s
#安装项目开发依赖
yarn add sass -D
  • 1
  • 2
  • 3
  • 4

在这里插入图片描述
vscode安装插件

Eslint
Vetur
TypeScript
Prettier
  • 1
  • 2
  • 3
  • 4

制定文件目录

dist  	打包完成的包
node_modules
public
src
	api  管理接口的
	assets   静态资源文件
	components   组件
	config   项目配置
	router   工程路由
	store  状态管理
	utils  工具函数
	views  界面结构
	App.vue
	main.js
.gitignore  
.env.dev  环境变量
.env.test
.env.prod
index.html
package.json
vite.config.js
yarn.lock
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

3.修改端口号

vitejs
在这里插入图片描述端口号修改成功后,需重新启动端口号

2. 前端架构的设计

1.router路由

简称路由的封装在src目录下创建router文件夹,并创建index.js文件

import { createRouter, createWebHashHistory } from "vue-router";
import Home from "./../components/Home.vue";
const routes = [
  {
    name: "home",
    path: "/",
    meta: {
      title: "首页",
    },
    component: Home,
    redirect: "/welcome",
    children: [
      {
        name: "welcome",
        path: "/welcome",
        meta: {
          title: "欢迎页",
        },
        component: () => import("./../views/welcome.vue"),
      },
    ],
  },
  {
    name: "login",
    path: "/login",
    meta: {
      title: "登录",
    },
    component: () => import("./../views/Login.vue"),
  },
];

const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

export default router;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

在main.js中挂载router

import router from "./router";

const app = createApp(App);
app.use(router).use(ElementPlus).mount("#app");
  • 1
  • 2
  • 3
  • 4

2.axios的封装

在src文件下新建config文件,并创建index.js文件夹
并且编写环境配置

// 环境配置文件
// 一般在企业级项目里面有三个环境,分别是开发环境,测试环境,线上环境

// env当前的环境
const env = import.meta.env.MODE || "prod";

const EnvConfig = {
  // 测试环境
  development: {
    baseApi: "/api",
    mockApi:
      "https://www.fastmock.site/mock/7a0ea3a39c0a0f79524bd73f034b38c0/api",
  },
  // 测试环境
  test: {
    baseApi: "//future.com//api",
    mockApi:
      "https://www.fastmock.site/mock/7a0ea3a39c0a0f79524bd73f034b38c0/api",
  },
  // 开发环境
  pro: {
    baseApi: "//future.com/api",
    mockApi:
      "https://www.fastmock.site/mock/7a0ea3a39c0a0f79524bd73f034b38c0/api",
  },
};

export default {
  env,
  // mock的总开关
  mock: true,
  namespacs: "manage", //命名空间
  ...EnvConfig[env],
};


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

utils下面创建一份request.js文件

import axios from "axios";
import config from "../config";
import { ElMessage } from "element-plus";
import router from "../router";
const TOKEN_INVALID = "Token认证失败,请重新登录";
const NERWORK_ERROR = "网络请求异常,稍后从试";

// axios二次封装
// 创建axios的实例对象,添加全局配置
const service = axios.create({
  baseURL: config.baseApi,
  timeout: 8000,
});

// 请求拦截在请求之前做一些事情
service.interceptors.request.use((req) => {
  // TO-DO
  const headers = req.headers;
  if (!headers.Authorization) headers.Authorization = "Bear Jack";
  return req;
});

// 响应拦截在请求之后做一些拦截
service.interceptors.response.use((res) => {
  const { code, data, msg } = res.data;
  if (code === 200) {
    return data;
  } else if (code === 40001) {
    ElMessage.error(msg || TOKEN_INVALID);
    setTimeout(() => {
      router.push("/login");
    }, 2000);
    return Promise.reject(msg || TOKEN_INVALID);
  } else {
    ElMessage.error(msg || NERWORK_ERROR);
  }
});

// 请求核心函数
// @param {*} options请求配置
function request(options) {
  options.method = options.method || "get";
  if (options.method.toLowerCase() === "get") {
    options.params = options.data;
  }

  // 配置单个接口是否可以使用mock
  if (typeof options.mock != "undefined") {
    config.mock = options.mock;
  }

  if (config.env === "prod") {
    service.defaults.baseURL = config.baseApi;
  } else {
    service.defaults.baseURL = config.mock ? config.mockApi : config.baseApi;
  }
  return service(options);
}

["get", "post", "put", "delete", "patch"].forEach((item) => {
  request[item] = (url, data, options) => {
    return request({
      url,
      data,
      method: item,
      ...options,
    });
  };
});

export default request;


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

3.storage的封装

作用: 主要是用于缓存的,用"命名空间"

utils文件夹下创建storage.js文件

// Storage二次封装,命名空间

import config from "../config";

export default {
 // 添加缓存
 setItem(key, val) {
   let storage = this.getStroage();
   storage[key] = val;
   window.localStorage.setItem(config.namespacs, JSON.stringify(storage));
 },
 // 获取缓存
 getItem(key) {
   return this.getStroage()[key];
 },
 getStroage() {
   return JSON.parse(window.localStorage.getItem(config.namespacs) || "{}");
 },
 // 清空所选
 clearItem(key) {
   let storage = this.getStroage();
   delete storage[key];
   window.localStorage.setItem(config.namespacs, JSON.stringify(storage));
 },
 // 清空所有
 clearAll() {
   window.localStorage.clear();
 },
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

main.js挂载

import request from "./utils/request";
import storage from "./utils/storage";

app.config.globalProperties.$request = request;
app.config.globalProperties.$storage = storage;
  • 1
  • 2
  • 3
  • 4
  • 5

config/index.js文件中创建名称为manage的命名空间

export default {
  env,
  // mock的总开关
  mock: true,
  namespacs: "manage", //命名空间
  ...EnvConfig[env],
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

4.页面的编写

首先在src/assets创建一个style文件夹,并在其下创建index.scss公共样式文件,和页面cssreset.css文件

reset.css文件

reset.css 可以该后缀为.less

/*  请尽量不要更改此文件夹 */
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption, 
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video{
  margin: 0;
  padding: 0;
  border: 0;
  font-size: 100%;
  font: inherit;
  vertical-align: baseline;

}
article,
aside{
  margin: 0;
  padding: 0;
}


blockquote::before,
blockquote::after,
q:before,
q:after{
  content: '';
  content: none;
}

a,
a:hover{
  color: inherit;
  text-decoration: none;
}

table{
  border-collapse: collapse;
  border-spacing: 0;
}

html,body{
  width: 100%;
  height: 100%;
  background-color: #f5f5f5;
  font-family: 'PingFangSC-Light','PingFang-SC','STHeitiSC-Light',
  'Helvetica-Light','Arial','sans-serif';

}


/* // 公共样式 */
.f1{
  float: left;
}

.fr{
  float: right;
  .button-group-item{
    padding-left: 3px;
  }
}

/* // 清除浮动 */
.clearfix{
  zoom: 1;
  &::after{
    display: block;
    clear: both;
    content: "";
    visibility: hidden;
    height: 0;
  }
}


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
index.scss文件

index.scss

*{
  margin: 0;
  padding: 0;
}
html,body{
  height: 100%;
  width: 100%;
}

*:not([class^='el-']){
  box-sizing: border-box;
}
.white{
  background-color: #ffff;
}
a{
  text-decoration: none;
}
.gray{
  background-color: #eef0f3;
}
.mr10{
  margin-right: 10px;
}
.mr20{
  margin-right: 20px;
}
.mb20{
  margin-bottom: 20px;
}
.m-lr10{
  margin-left: 10px;
}
.p20{
  padding: 20px;
}
.pl20{
  padding-left: 20px;
}
.text-right{
  text-align: right;
}
.fr{
  float: right;
}
.flex{
  display: flex;
}
.flex-between{
  display: flex;
  justify-content: space-between;
}
.flex-center{
  display: flex;
  justify-content: center;
}
.tips{
  margin-left: 150px;
  color: #787878;
}

// 公共样式
.query-form{
  background-color: #ffffff;
  padding: 22px 20px 0;
  border-radius: 5px;
}
.base-table{
  border-radius: 5px;
  background: #ffffff;
  margin-top: 20px;
  margin-bottom: 20px;
  .action{
    border-radius: 5px 5px 0px 0px;
    background: #ffffff;
    padding: 20px;
    border-bottom: 1px solid #ece8e8;
  }
  .pagination{
    text-align: right;
    padding: 10px;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83

main.js中引入样式文件

<style lang="scss">
@import "./assets/style/reset.css";
@import "./assets/style/index.scss";
</style>
  • 1
  • 2
  • 3
  • 4
完成登录页面home.vue的编写
<template>
  <div class="basic-layout">
    <div class="nav-side"></div>
    <div class="content-right">
      <div class="nav-top">
        <div class="bread">面包屑</div>
        <div class="user-info">用户</div>
      </div>
      <div class="wrapper">
        <div class="main-page">
          <router-view></router-view>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import { useRouter } from "vue-router";
export default {
  name: "Home",
};
</script>

<style lang="scss">
.basic-layout {
  // 相对定位
  position: relative;
  .nav-side {
    // 固定定位
    position: fixed;
    width: 200px;
    height: 100vh;
    background-color: #001529;
    color: #fff;
    // 滚动条
    overflow-y: auto;
    // 动画
    transition: width 0.5s;
  }
  .content-right {
    margin-left: 200px;
    .nav-top {
      height: 50px;
      line-height: 50px;
      // 两端对齐
      display: flex;
      justify-content: space-between;
      border-bottom: 1px solid #ddd;
      background-color: #fff;
      padding: 0 20px;
    }
    .wrapper {
      background: #eef0f3;
      padding: 20px;
      height: calc(100vh - 50px);
      .main-page {
        height: 100%;
        background-color: #fff;
      }
    }
  }
}
</style>


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
路由跳转的三种方式
router-link
<router-link to="/login">去登录</router-link>
  • 1
传统跳转

<template>
<el-button @click="goHome">回首页</el-button>
</template>
<script>
export default{
name:'login',
methods:{
goHome(){
this.$router.push('/welcome')
}
}
}
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
Composition API跳转
<script setup>
import { useRouter } from 'vue-router'
let router = useRouter()
const goHome = ()=>{
router.push('/welcome')
}
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

3. koa2架构设计

1. 安装koa框架

使用管理员权限进入cmd,然后进入安装目录
使用命令

npm install -g koa-generator
  • 1

进行安装

使用koa-generator生成koa2项目,输入命令:

koa2 manager-server
  • 1

manager-server是项目名称
创建项目成功之后进入到mangager-server目录下,安装项目依赖

npm install
  • 1

安装完成之后启动项目

npm start
  • 1

将项目启动,默认端口号http://localhost:3000/
在这里插入图片描述

koa2不是内部命令

安装完毕后如果发现不能使用koa2命令,需配置环境变量
将找到koa-generator文件夹下的bin文件夹目录下的koa2添加到环境变量添加到环境变量path

D:\software\Yarn\Data\global\node_modules\koa-generator\bin

在这里插入图片描述

2.安装log4js-node 插件

插件官网

使用命令安装

yarn add log4js -D
  • 1

-D保存到开发依赖中
在这里插入图片描述

创建utils文件夹并创建logj.js文件

/**
 * 日志存储
 * @author JackBean
 */
const log4js = require("log4js");

const levels = {
  trace: log4js.levels.TRACE,
  debug: log4js.levels.DEBUG,
  info: log4js.levels.INFO,
  warn: log4js.levels.WARN,
  error: log4js.levels.ERROR,
  fatal: log4js.levels.FATAL,
};

log4js.configure({
  appenders: {
    console: { type: "console" },
    info: {
      type: "file",
      filename: "logs/all-logs.log",
    },
    error: {
      type: "dateFile",
      filename: "logs/log",
      pattern: "yyyy-MM-dd.log",
      alwaysIncludePattern: true, // 设置文件名称为 filename + pattern
    },
  },
  categories: {
    default: { appenders: ["console"], level: "debug" },
    info: {
      appenders: ["info", "console"],
      level: "info",
    },
    error: {
      appenders: ["error", "console"],
      level: "error",
    },
  },
});

/**
 * 日志输出,level为debug
 * @param {string} content
 */
exports.debug = (content) => {
  let logger = log4js.getLogger();
  logger.level = levels.debug;
  logger.debug(content);
};

/**
 * 日志输出,level为info
 * @param {string} content
 */
exports.info = (content) => {
  let logger = log4js.getLogger("info");
  logger.level = levels.info;
  logger.info(content);
};

/**
 * 日志输出,level为error
 * @param {string} content
 */
exports.error = (content) => {
  let logger = log4js.getLogger("error");
  logger.level = levels.error;
  logger.error(content);
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72

用处可以在打印并存储日志文件

在app.js中引用

const log4js = require("./utils/log4j");
  • 1

后端项目启动


  • 1

3. 安装MongoDB

参考文章

1.下载安装

MongoDB官网进行下载,

在这里插入图片描述

无脑安装

1.安装完毕后需在安装目录下的bin目录下添加到全局的环境变量path中

然后在

2.Compass-图形化界面客户端

下载地址
在这里插入图片描述
在这里插入图片描述

然后安装后直接点击进行,然后在桌面会直接建立一个连接,点击进去之后直接连接就行

3.Mongo语法

跟SQL语句对比
SQLMongo
表(Tbale)集合(collection)
行(Row)文档(Document)
列(Col)字段(Field)
主键(Primary)对象ID(Objectld)
数据库操作
创建数据库use demo
查看数据库show dbs
删除数据库db.dropDatabase()
集合操作
创建集合db.createCollection(name)
查看集合show collections
删除集合db.collection.drop()

collection集合名称

文档操作
创建文档db.collection.insertOne({})
db.collection.insertMany({})
查看文档db.collections.find()
删除文档db.collection.deleteOne({})
db.collection.deleMany({})
更新文档db.collection.update({},{},false,true)
条件操作
大于$gt
小于$It
大于等于$gte
小于等于$lte

图形工具robo3T

4.封装通用工具函数

在utils文件夹下,创建util.js文件,并封装公共函数

// 通用工具函数

const log4js = require("./log4j");

const CODE = {
  SUCCESS: 200,
  PARAM_ERROP: 10001, //参数错误
  USER_ACCOUNT_ERROR: 20001, //账号或密码错误
  USER_LOGIN_ERROR: 30001, //用户未登录
  BUSINESS_ERROR: 40001, //业务请求失败
  AUTH_ERROR: 500001, //认证失败或TORK过期
};

// 分页功能封装
module.exports = {
  // @param{number} pageNum
  // @param{number} pageSize
  pager({ pageNum = 1, pageSize = 10 }) {
    pageNu *= 1;
    pageSize = 1;
    const skipIndex = (pageNum - 1) * pageSize;
    return {
      page: {
        pageNum,
        pageSize,
      },
      skipIndex,
    };
  },
  success(data = "", msg = "", code = CODE.SUCCESS) {
    log4js.debug(data);
    return {
      code,
      data,
      msg,
    };
  },
  fail(msg = "", code = CODE.BUSINESS_ERROR) {
    log4js.debug(msg);
    return {
      code,
      data,
      msg,
    };
  },
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

和文件util.s

4.用户登录前后台实现

1. 页面的编写

2.api接口的封装

在api目录下创建index.js文件夹
并导出api接口

// api管理

// api管理

import request from "../utils/request";
export default {

  // 登录接口
  login(params) {
    return request({
      url: "/users/login",
      method: "post",
      data: params,
      mock: false,
    });
  },
};


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

在main.js中挂载

import api from "./api/index";
app.config.globalProperties.$api = api;

  • 1
  • 2
  • 3

3.发送登录请求

login页面直接引用

<template>
  <div class="login-wrapper">
    <div class="modal">
      <el-form ref="userForm" :model="user" status-icon :rules="rules">
        <div class="title">火星</div>
        <el-form-item prop="userName">
          <el-input type="text" v-model="user.userName">
            <template #prefix>
              <el-icon class="el-input__View"><Sunrise /></el-icon>
            </template>
          </el-input>
        </el-form-item>
        <el-form-item prop="userPwd">
          <el-input type="password" v-model="user.userPwd">
            <template #prefix>
              <el-icon class="el-input__icon"><View /></el-icon>
            </template>
          </el-input>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" class="btn-login" @click="login">
            登录</el-button
          >
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>
<script>
export default {
  name: "login",
  data() {
    return {
      user: {
        userName: "",
        userPwd: "",
      },
      rules: {
        userName: [
          {
            required: true,
            message: "请输入用户名",
            trigger: "blur",
          },
        ],
        userPwd: [
          {
            required: true,
            message: "请输入密码",
            trigger: "blur",
          },
        ],
      },
    };
  },
  methods: {
    login() {
      this.$refs.userForm.validate((valid) => {
        if (valid) {
          this.$api.login(this.user).then((res) => {
            console.log(res);
          });
        } else {
          return false;
        }
      });
    },
  },
};
</script>
<style lang="scss">
.login-wrapper {
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #f9fcff;
  width: 100vw;
  height: 100vh;
  .modal {
    width: 500px;
    padding: 50px;
    background-color: #fff;
    border-radius: 4px;
    box-shadow: 0px 0px 10px 3px #c7c9c4;
    .title {
      font-size: 50px;
      line-height: 1.5;
      text-align: center;
      margin-bottom: 30px;
    }
    .btn-login {
      width: 100%;
    }
  }
}
</style>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97

4. 前台实现

封装vuex
在store中创建index.js和mutations.js两个文件

index.js文件

// Vuex状态管理
import { createStore } from "vuex";
import mutations from "./mutations";
// vuex刷新没有,结合storage做存储
import storage from "./../utils/storage";

const state = {
  userInfo: "" || storage.getItem("userInfo"), //获取用户信息
};
export default createStore({
  state,
  mutations,
});

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

moutations.js文件

// Mutations业务层数据提交
import storage from "./../utils/storage";

export default {
  saveUserInfo(state, userInfo) {
    state.userInfo = userInfo;
    storage.setItem("userInfo", userInfo);
  },
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

在main.js中挂载vuex

import store from "./store";
app.use(store)
  • 1
  • 2

在页面中使用
在这里插入图片描述
页面中获取成功之后,将返回值存储,并跳转到首页

5.服务层的实现

1. 安装mongoosejs

官方文档
下载插件mongoose
采用命令

npm install mongoose
  • 1

进行安装

2.建立数据库连接

创建文件夹config,创建index.js文件

// 配置文件
// 采用mogos

module.exports = {
  URL: "mongodb://127.0.0.1:27017/imooc-manager ",
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

imooc-manager为数据库名称
在config中创建db.js文件

// 数据库连接接
const mongoose = require("mongoose");
const config = require(".");
const log4js = require("./../utils/log4j");


mongoose.connect(config.URL, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

const db = mongoose.connection;

db.on("error", () => {
  log4js.error("***数据库连接失败");
});

db.on("open", () => {
  log4js.info("***数据库连接成功");
});

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在app.js中加载配置

require("./config/db");
  • 1

执行命令yarn dev 连接数据库
在这里插入图片描述

3.用户的开发
1.定义用户模块

routes文件夹下创建users.js文件

// 用户管理模块
const router = require("koa-router")();
const User = require("./../models/userSchems");
const util = require("./../utils/util");
router.prefix("/users");   //二级路由

router.post("/login", async (ctx) => {
  try {
    const { userName, userPwd } = ctx.request.body;
    const res = await User.findOne({
      userName,
      userPwd,
    });
    if (res) {
      ctx.body = util.success(res);
    } else {
      ctx.body = util.fail("账号或密码不正确");
    }
  } catch (error) {
    ctx.body = util.fail(error.msg);
  }
});
module.exports = router;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

router.prefix("/users"); 定义为二级路由 ,通过router.post定义一个login接口,监听try里的参数,通过ctx.request.body里查找数据,如果查找成功就直接返回,如果没有查找到,就抛出一个异常

在app.js中定义一个一级路由

const users = require("./routes/users");
const router = require("koa-router")();  //一级路由

require("./config/db");

router.prefix("/api");
router.use(users.routes(), users.allowedMethods());
app.use(router.routes(), router.allowedMethods());

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

通过一级路由定义一个require("./config/db");
通过router挂载一个二级路由,然后app.加载全局rouer

2.创建数据库schems

新建一个models文件夹
并创建userSchems.js文件
建立用户users的对应数据库结构

const mongoose = require("mongoose");

const userSchema = mongoose.Schema({
  userId: Number, //用户ID,自增长
  userName: String, //用户名称
  userPwd: String, //用户密码,md5加密
  userEmail: String, //用户邮箱
  mobile: String, //手机号
  sex: Number, //性别 0:男 1:女
  deptId: [String], //部门
  job: String, //岗位
  state: {
    type: Number,
    default: 1,
  }, // 1: 在职 2: 离职 3: 试用期
  role: {
    type: Number,
    default: 1,
  }, // 用户角色 0:系统管理员 1: 普通用户
  roleList: [], //系统角色
  createTime: {
    type: Date,
    default: Date.now(),
  }, //创建时间
  lastLoginTime: {
    type: Date,
    default: Date.now(),
  }, //更新时间
  remark: String,
});

module.exports = mongoose.model("users", userSchema, "users");

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
3.前端代理

vite.config.js
中定义代理

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

// https://vitejs.dev/config/
export default defineConfig({
  server: {
    host: "localhost",
    port: 8080,
    proxy: {
      "/api": {
        target: "http://localhost:3000",
      },
    },
  },
  plugins: [vue()],
});

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

拦截后端接口,并关闭全局mock

5.前台首页实现

1. 首页局部

<template>
  <div class="basic-layout">
    <div class="nav-side"></div>
    <div class="content-right">
      <div class="nav-top">
        <div class="bread">面包屑</div>
        <div class="user-info">用户</div>
      </div>
      <div class="wrapper">
        <div class="main-page">
          <router-view></router-view>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import { useRouter } from "vue-router";
export default {
  name: "Home",
};
</script>

<style lang="scss">
.basic-layout {
  // 相对定位
  position: relative;
  .nav-side {
    // 固定定位
    position: fixed;
    width: 200px;
    height: 100vh;
    background-color: #001529;
    color: #fff;
    // 滚动条
    overflow-y: auto;
    // 动画
    transition: width 0.5s;
  }
  .content-right {
    margin-left: 200px;
    .nav-top {
      height: 50px;
      line-height: 50px;
      // 两端对齐
      display: flex;
      justify-content: space-between;
      border-bottom: 1px solid #ddd;
      background-color: #fff;
      padding: 0 20px;
    }
    .wrapper {
      background: #eef0f3;
      padding: 20px;
      height: calc(100vh - 50px);
      .main-page {
        height: 100%;
        background-color: #fff;
      }
    }
  }
}
</style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

2.侧边栏组件化

1.父子组件之间的传值
  1. 在components文件夹下,创建一个组件TreeMenu.vue组件,然后在里面输入初始结构

  2. 子组件
    props接受父组件传递过来的数据,
    type是接受类型,
    default是默认值,且必须是函数

<template>
  <template v-for="menu in userMenu"  >
    <el-sub-menu v-if="menu.children && menu.children.length>0 && menu.children[0].menuType == 1" :key="menu._id" :index="menu.path">
    <template #title>
      <i :class="menu.icon"></i>
      <!-- <el-icon><setting /></el-icon> -->
      <span>{{menu.menuName}}</span>
    </template>
    <tree-menu :userMenu="menu.children" />
  </el-sub-menu>
  <el-menu-item v-else-if="menu.menuType==1" :index="menu.path" :key="menu.path">{{menu.menuName}}</el-menu-item>
  </template>

  
</template>
<script>
export default {
  name: 'TreeMenu',   //组件名称
  props: {      
    userMenu: {
      type: Array,
      default() {
        return []
      }
    }
  }
}
</script>
<style></style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  1. 在父组件中,引入组件,并在components中注册组件,并且进行动态传值 :userMenu="userMenu"
<tree-menu :userMenu="userMenu"></tree-menu>

import TreeMenu from "./TreeMenu.vue";
export default {
  name: "Home",
  components:{TreeMenu},
  data() {
    return {
    	 userMenu: [],
    }
  },
  mounted() {},
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
4.查看当前页面的路由
  1. location.hash.slice(1)

3.面包屑的实现

  1. 在components文件夹中新建文件BreadCrumb.vue组件,然后在父组件中,引入并注册子组件,不需要传值
    父组件
<div class="bread">
    <BreadCrumb></BreadCrumb>
</div>
import BreadCrumb from './BreadCrumb.vue';
components:{TreeMenu,BreadCrumb},
  • 1
  • 2
  • 3
  • 4
  • 5

子组件

<template>
  <el-breadcrumb   separator="/" >
    <el-breadcrumb-item  v-for="(item,index) in breadList" :key="item.path">
      <router-link to="/welcome" v-if="index == 0">{{item.meta.title }}</router-link>
      <span v-else>{{item.meta.title }}</span>
    </el-breadcrumb-item>
  </el-breadcrumb>
</template>
<script>
export default {
  name: 'BreadCrumb',
  computed: {
    breadList() {
      return this.$route.matched;
    }
  },
  mounted() {
    // console.log('routes=>',this.$route.path);  //查看当前路由
  }
}
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

3.本章重难点总结 (vite)别名

1.vite别名
  1. vite可配置别名,解决./…/问题,类似于Vue里面的@
    参考
resolve: {
	alias:{
	'@': path.resolve( __dirname, './src' )
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  1. 而在改写过程中,其中的 需要path需要通过 import引入
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from "path";

// https://vitejs.dev/config/
export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  },
  server: {
    host: "localhost",
    port: 8080,
    // hmr: true, // 开启热更新
    proxy: {
      "/api": {
        target: "http://localhost:3000",
      },
    }
  },
  plugins: [vue()],
});

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  1. 全局的mixin 样式问题,可以通过vite进行配置
css: {
	preprocessorOptions: {
		scss: {
		additionalData: `@import '@/assets/style/base.scss';`
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

6.JWT方案讲解

1.关键问题

1.什么是jwt?
  • JWT是一种跨域认真解决方案
2.解决问题
  • 数据传输简单,高效
  • jwt会生成签名,保证传输安全
  • jwt具有时效性
  • jwt更高效利用集群做好单点登录
3. 原理
  • 服务器认真后,认证一个json对象,后续通过json进行通信
4.数据结构
  • Header(头部)
  • Payload(负载)
  • Signature(签名)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
5.使用方式
  • /api?token = xxx
  • cookie写入token
  • storage写入token,请求头添加:Authorization:Bearer < token >

2.jwt的使用

在这个项目中,使用jwt是使用jwt的插件来使用jwt
jsonwebtoken插件地址

1. 安装插件
  1. 在后端文件manager-server中使用命令
yarn add jsonwebtoken -S
  • 1
  1. 安装jsonwebtoken插件
2.jsonwebtoken生成token
  1. 打开后端manger-server文件,在routes文件夹下的user.js文件中引入jsonwentoken
// 用户管理模块
const router = require("koa-router")();
const { log } = require("debug/src/browser");
const User = require("./../models/userSchems");
const util = require("./../utils/util");
const jwt = require('jsonwebtoken');   //引入jsonwebtoken
router.prefix("/users");

router.post("/login", async (ctx) => {
  try {
    const { userName, userPwd } = ctx.request.body;
    const res = await User.findOne({
      userName,
      userPwd,
    },'userId userName  userEmail state role deptId roleList');
    if (res) {
      const data = res._doc;
	  // 生成token
	  const token = jwt.sign({
	    data: data,
	  }, 'imooc', { expiresIn: 30 })

      data.token = token
      ctx.body = util.success(data);
    } else {
      ctx.body = util.fail("账号或密码不正确");
    }
  } catch (error) {
    ctx.body = util.fail(error.msg);
  }
});
module.exports = router;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  1. 通过jwt.sign()函数生成token,‘imooc’是秘钥,expiresIn是过期时间
  2. 通过token将userId userName userEmail state role deptId roleList等值赋值给data,然后利用data生成token,
  3. 所以token中的信息包含,userId userName userEmail state role deptId roleList,等信息

3.解析token

  1. 在前端文件manager文件中,打开utils/request.js文件中

  2. 在请求拦截之后的TO-DO拦截之后需要做一些事情

// 请求拦截在请求之前做一些事情
service.interceptors.request.use((req) => {
 // TO-DO
 const headers = req.headers;
 const { token } = storage.getItem('userInfo')
 // console.log('token=>', token);
 if (!headers.Authorization) headers.Authorization = "Bearer " + token;
 return req;
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  1. 首先通过storage.getItem()获取缓存信息中的token,将token 拼接到headers.Authorization 请求头文件中,获取到的信息是“bearer ”+ token
1. token解密测试
  1. 在后端文件app.js中创建创建leave/count接口,
router.prefix("/api");

router.get('/leave/count', (ctx) => {
  // console.log('=>', ctx.request.headers);
  const token = ctx.request.headers.authorization.split(' ')[1];
  const payload = jwt.verify(token, 'imooc')
  ctx.body = payload
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  1. 通过ctx.request.headers.authorization.split(' ')[1];获取头部信息的token ,利用split(‘ ’ )的空格分割Bearertoken,然后获取到下标为1的token,然后用jwt.verify()函数进行token解密,秘钥是’imooc’
    然后将信息进行打印,会得到token过期时间

4. token过期拦截

  1. 首先需在后端文件中安装一个中间件,利用命令
yarn add koa-jwt -S
  • 1
  1. 进行安装,作用是在启动入口之前提前去加载这个中间件

  2. 在后端文件app.js中首先进行声明

const koajwt =require('koa-jwt')   //引入

app.use(koajwt({ secret: 'imooc' }))
router.prefix("/api");

  • 1
  • 2
  • 3
  • 4
  • 5
  1. router.prefix("/api");上面首先进行一下声明和引入

  2. 在后端文件工具类util.js中对错误值CODE进行一个返回

// 通用工具函数

const log4js = require("./log4j");

const CODE = {
  SUCCESS: 200,
  PARAM_ERROP: 10001, //参数错误
  USER_ACCOUNT_ERROR: 20001, //账号或密码错误
  USER_LOGIN_ERROR: 30001, //用户未登录
  BUSINESS_ERROR: 40001, //业务请求失败
  AUTH_ERROR: 500001, //认证失败或TORK过期
};

// 分页功能封装
module.exports = {
  // @param{number} pageNum
  // @param{number} pageSize
  pager({ pageNum = 1, pageSize = 10 }) {
    pageNu *= 1;
    pageSize = 1;
    const skipIndex = (pageNum - 1) * pageSize;
    return {
      page: {
        pageNum,
        pageSize,
      },
      skipIndex,
    };
  },
  success(data = "", msg = "", code = CODE.SUCCESS) {
    log4js.debug(data);
    return {
      code,
      data,
      msg,
    };
  },
  fail(msg = "", code = CODE.BUSINESS_ERROR, data = "") {
    log4js.debug(msg);
    return {
      code,
      data,
      msg,
    };
  },
  CODE
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

  1. 然后进行返回的时候一个拦截请求
    app.js文件
// logger
app.use(async (ctx, next) => {
  log4js.info(`get params:${JSON.stringify(ctx.request.query)}`);
  log4js.info(`post params:${JSON.stringify(ctx.request.body)}`);
  await next().catch((err) => {
    if (err.status == '401') {
      ctx.status = 200
      ctx.body = util.fail('Token认证失败', util.CODE.AUTH_ERROR)
    } else {
      throw err
    }
  });
});

app.use(koajwt({ secret: 'imooc' }).unless({
  path: [/^\/api\/users\/login/]
}))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  1. 而通过.unless通过正则表达式表示对登录页面的排除登录请求是否过期

  2. 然后在user.js文件中,通过三种方式可以对返回字段进行一个筛选

// 用户管理模块
const router = require("koa-router")();
const { log } = require("debug/src/browser");
const User = require("./../models/userSchems");
const util = require("./../utils/util");
const jwt = require('jsonwebtoken');
router.prefix("/users");




router.post("/login", async (ctx) => {
  try {
    /*** 
     * 返回数据库指定字段,有三种方式
     * 1.'userId userName  userEmail state role deptId roleList'
     * 2.[userId:1,state:0 】 // 1代表返回,0代表不返回
     * 3.
     * 
    */
    const { userName, userPwd } = ctx.request.body;
    const res = await User.findOne({
      userName,
      userPwd,
    }, 'userId userName  userEmail state role deptId roleList');

    const data = res._doc;

    // 生成token
    const token = jwt.sign({
      data: data,
    }, 'imooc', { expiresIn: '1h' })

    if (res) {
      data.token = token
      ctx.body = util.success(data);
    } else {
      ctx.body = util.fail("账号或密码不正确");
    }
  } catch (error) {
    ctx.body = util.fail(error.msg);
  }
});
module.exports = router;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

7. 用户管理及前后端实现

1.user列表的获取,函数的调用

  1. 在user.vue中中,首先需导入 getCurrentInstance, onMounted, reactive, ref,
    然后再setup()函数中引入ctx,才能在后续操作中才能使用ctx.
    并且在setup中所有的变量都得进行返回
    const { ctx } = getCurrentInstance();

  • 1
  • 2
<script>
import { getCurrentInstance, onMounted, reactive, ref } from 'vue'
import api from './../api'
export default {
  name: 'user',
  //入口函数
  setup() {
    const { ctx } = getCurrentInstance();
    const user = reactive({
      state:0
    });
    const userList = ref([]);
    const columus = reactive([
      {
        label: '用户ID',
        prop: 'userId',
        // width:180
      },
      {
        label: '用户名称',
        prop: 'userName',
        // width:180
      },
      {
        label: '用户邮箱',
        prop: 'userEmail',
        // width:180
      },
      {
        label: '用户角色',
        prop: 'role',
        // width:80
      },
      {
        label: '用户状态',
        prop: 'state',
        // width:80
      },
      {
        label: '注册时间',
        prop: 'createTime',
        // width:170
      },
      {
        label: '最后登录时间',
        prop: 'lastLoginTime',
        // width:200
      },
    ])
    const pager = reactive({
      pageNum: 1,
      pageSize:10
    })
    // onMountedDom渲染完之后会执行onMounted
    onMounted(() => {
      getUserList()
    })
    const getUserList = async () => {
      ctx.$api = api
      try {
        const { list, page } = await ctx.$api.getUserList();
        userList.value = list;
        pager.total = page.total;
      } catch(error){

      }

      
    }

    return {
      user,userList,columus,pager,getUserList
    }
  }
  }
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76

2.getUserlist()函数Undefind报错

  1. ctx.$api有时候进行保存,无法找到函数,就得首先进行api引入,然后进行api的局部声明
import api from './../api'

ctx.$api = api
  • 1
  • 2
  • 3

3.删除单条数据

  1. 首先在删除按钮中绑定一个点击事件
<template #default="scope">
              <el-button @click="handleQuery(scope.row)" >编辑</el-button>
              <el-button type="danger" @click="handleDel(scope.row)">删除</el-button>
            </template>
  • 1
  • 2
  • 3
  • 4
  1. scope是当前的插槽,即可通过scope.row取到当前删除的id
  // 用户单个删除方法
    const handleDel =async (row) => {
      await ctx.$api.userDel({
        userIds:[row.userId]  //可单个删除
      })
      ElMessage.success('删除成功')   
      getUserList()    // 重新获取用户列表
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

4. 删除多条数据

  1. 需要对表格定义一个多选删除对象的id数组checkedUserIds
  2. 对表格绑定一个 @selection-change="handleSelectionChange事件,会给返回选择的对象
  3. 需对选择的对象进行.map遍历,先定义一个id数组,然后把遍历后的Id,push进数组,然后将数组,赋值给checkedUserIds
   // 选中列表对象
   const checkedUserIds = ref([])

    // 批量删除
   const handlePatchDel = async () => {
     if (checkedUserIds.value.length == 0) {
       ElMessage.error('请选择要删除的用户')
       return
     } else {
       await ctx.$api.userDel({
       userIds:checkedUserIds.value  //可单个删除,也可批量删除
     })
       ElMessage.success('删除成功')
       getUserList()
     }  
   }
   // 表格多选
   const handleSelectionChange = (list) => {
     let arr = [];
     list.map(item => {
       arr.push(item.userId)
     })
     checkedUserIds.value = arr;
   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  1. 而在api.接口管理文件中,需接收一个params对象
 // 用户单个删除
  userDel(params) {
    return request({
      url: "/users/delete",
      method: "post",
      data: params,
      mock: true
    });
  },
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

5.表格0-1对应响应的格式

  1. 需在表格循环中定义formatter属性
  2. 然后在对应的循环列表中定义formatter
 <el-table-column 
    v-for="item in columus"
    :key="item.prop"
    :prop="item.prop" 
    :label="item.label" 
    :width="item.width"
    :formatter="item.formmtter" />


const columus = reactive([
{
 label: '用户角色',
  prop: 'role',
  // width:80
  formmtter(row, colum, value) {         
    return {
      0: '管理员',
      1:'普通用户'
    }[value]
  }
},
{
  label: '用户状态',
  prop: 'state',
  // width:80
  formmtter(row, colum, value) {         
    return {
      0: '所有',
      1: '在职',
      2: '离职',
      3:'试用期'
    }[value]
  }
},
])

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

6.新增编辑

  1. ctx.$nextTick(() => { }控制在DOM渲染完毕之后再把数据渲染给控件
  2. 而在Object.assign(userForm, row);使用的是浅拷贝,将点击事件获取到的数据渲染给userForm表格
 // 用户编辑
    const handleEdit = (row) => {
      action.value = 'edit';   //控制是编辑还是新增
      showModal.value = true;  //打开弹窗
      ctx.$nextTick(() => { 
        Object.assign(userForm, row);
      })
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

7.后台用户列表

  1. user/list的编写
    后端user.js
// 用户管理模块
const router = require("koa-router")();
const { log } = require("debug/src/browser");
const User = require("./../models/userSchems");
const util = require("./../utils/util");
const jwt = require('jsonwebtoken');
router.prefix("/users");

// 用户登录
router.post("/login", async (ctx) => {
  try {
    /*** 
     * 返回数据库指定字段,有三种方式
     * 1.'userId userName  userEmail state role deptId roleList'
     * 2.[userId:1,state:0 】 // 1代表返回,0代表不返回
     * 3.
     * 
    */
    const { userName, userPwd } = ctx.request.body;
    const res = await User.findOne({
      userName,
      userPwd,
    }, 'userId userName  userEmail state role deptId roleList');

    const data = res._doc;

    // 生成token
    const token = jwt.sign({
      data: data,
    }, 'imooc', { expiresIn: '1h' })

    if (res) {
      data.token = token
      ctx.body = util.success(data);
    } else {
      ctx.body = util.fail("账号或密码不正确");
    }
  } catch (error) {
    ctx.body = util.fail(error.msg);
  }
});

// 用户列表
router.get('/list', async (ctx) => {
  // 解构获取的对象
  const { userId, userName, state } = ctx.request.query;
  const { page, skipIndex } = util.pager(ctx.request.query);
  let params = {};
  if (userId) params.userId = userId;
  if (userName) params.userName = userName;
  if (state && state != '0') params.state = state;

  try {
    // 根据条件查询所有用户列表
    const query = User.find(params, { userPwd: 0, _id: 0 })
    const list = await query.skip(skipIndex).limit(page.pageSize);
    // 统计
    const total = await User.countDocuments(params)
    ctx.body = util.success({
      page: {
        ...page,
        total
      },
      list
    })
  } catch (error) {
    ctx.body = util.fail(`查询异常:${error.stack}`)
  }

})

module.exports = router;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
1.前端日期格式化
  1. 在utils的文件夹下,创建utils.js文件
/**
 * 工具函数封装
 */
export default {
  formateDate(date, rule) {
    let fmt = rule || 'yyyy-MM-dd hh:mm:ss'
    // 判断年份
    if (/(y+)/.test(fmt)) {
      fmt = fmt.replace(RegExp.$1, date.getFullYear())
    }
    const o = {
      'y+': date.getFullYear(),
      'M+': date.getMonth() + 1,
      'd+': date.getDate(),
      'h+': date.getHours(),
      'm+': date.getMinutes(),
      's+': date.getSeconds()
    }
    for (let k in o) {
      if (new RegExp(`(${k})`).test(fmt)) {
        const val = o[k] + '';
        fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? val : ('00' + val).substring(val.length));
      }
    }
    return fmt
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  1. 然后在前端文件user.vue中引入
import utils from './../utils/utils'


//在插槽中引入时间格式化文件
{
        label: '用户状态',
        prop: 'state',
        // width:80
        formmtter(row, colum, value) {
          return {
            0: '所有',
            1: '在职',
            2: '离职',
            3: '试用期'
          }[value]
        }
      },
      {
        label: '注册时间',
        prop: 'createTime',
        // width:170
        formmtter: (row,columu,value) => {
          return  utils.formateDate(new Date(value))
        }
      },
      {
        label: '最后登录时间',
        prop: 'lastLoginTime',
        // width:200
        formmtter: (row,columu,value) => {
          return  utils.formateDate(new Date(value))
        }
      },
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
4.安装md5插件
  1. 通过安装命令
yarn add md5 -D
  • 1
  1. 安装完毕后在头部进行引用
  2. 在使用时,直接括起来就行
const md5 = require('md5')

userPwd: md5('123456'),

  • 1
  • 2
  • 3
  • 4
8.用户列表的后台接口编写
/**
 *  用户管理模块
 */
const router = require("koa-router")();
const User = require("./../models/userSchems");
const Counter = require('./../models/counterSchema');
const util = require("./../utils/util");
const jwt = require('jsonwebtoken');
const md5 = require('md5');
router.prefix("/users");

// 用户登录
router.post("/login", async (ctx) => {
  try {
    /*** 
     * 返回数据库指定字段,有三种方式
     * 1.'userId userName  userEmail state role deptId roleList'
     * 2.[userId:1,state:0 ]  // 1代表返回,0代表不返回
     * 3.
     * 
    */
    const { userName, userPwd } = ctx.request.body;
    const res = await User.findOne({
      userName,
      userPwd:md5(userPwd),
    }, 'userId userName  userEmail state role deptId roleList');
    if (res) {
    
	    const data = res._doc;
	
	    // 生成token
	    const token = jwt.sign({
	      data: data,
	    }, 'imooc', { expiresIn: '1h' })

      data.token = token
      ctx.body = util.success(data);
    } else {
      ctx.body = util.fail("账号或密码不正确");
    }
  } catch (error) {
    ctx.body = util.fail(error.msg);
  }
});

// 用户列表
router.get('/list', async (ctx) => {
  // 解构获取的对象
  const { userId, userName, state } = ctx.request.query;
  const { page, skipIndex } = util.pager(ctx.request.query);
  let params = {};
  if (userId) params.userId = userId;
  if (userName) params.userName = userName;
  if (state && state != '0') params.state = state;
  try {
    // 根据条件查询所有用户列表
    const query = User.find(params, { userPwd: 0, _id: 0 })
    const list = await query.skip(skipIndex).limit(page.pageSize);
    // 统计所有条数
    const total = await User.countDocuments(params)
    ctx.body = util.success({
      page: {
        ...page,
        total
      },
      list
    })
  } catch (error) {
    ctx.body = util.fail(`查询异常:${error.stack}`)
  }
})

// 用户删除和批量删除
router.post('/delete', async (ctx) => {
  // 待删除的用户id数组
  const { userIds } = ctx.request.body;
  // User.updateMany({ $or: [{ userId: '10001' }, { userId: '10002' }] })
  const res = await User.updateMany({ userId: { $in: userIds } }, { state: 2 });
  if (res) {
    ctx.body = util.success(res, `共删除成功${res.nModified}条`);
    return;
  }
  ctx.body = util.fail('删除失败1', res)
})

// 用户新增/编辑
router.post('/operate', async (ctx) => {
  const { userId, userName, mobile, userEmail, job, state, roleList, deptId, action } = ctx.request.body;
  // 判断是新增还是编辑
  if (action == 'add') {
    if (!userName || !userEmail || !deptId) {
      ctx.body = util.fail('参数错误', util.CODE.BUSINESS_ERROR)
      return;
    }
    // 查询表中是否有username和useremail重名
    const res = await User.findOne({ $or: [{ userName }, { userEmail }] }, '_id userId userEmail')
    if (res) {
      ctx.body = util.fail(`系统检测到有重读的用户,信息如下:${res.userName}- ${res.userEmail}`)
    } else {
      // 自增id
      const doc = await Counter.findOneAndUpdate({ _id: 'userId' }, { $inc: { sequence_value: 1 } }, { new: true })
      try {
        const user = new User({
          userId: doc.sequence_value,
          userPwd: md5('123456'),
          userName,
          userEmail,
          role: 1, //默认普通用户
          roleList,
          job,
          state,
          deptId,
          mobile
        })
        user.save();
        ctx.body = util.success('', '用户创建成功')
      } catch (error) {
        ctx.body = util.fail('error', '用户创建失败')
      }
    }

  } else {
    if (!deptId) {
      ctx.body = util.fail('部门不能为空', util.CODE.BUSINESS_ERROR)
      return;
    }
    try {
      // 更新数据,不讲更新后的数据进行返回
      const res = await User.findOneAndUpdate({ userId }, { job, state, roleList, deptId, mobile })
      ctx.body = util.success({}, '更新成功');
    } catch (error) {
      ctx.body = util.fail('更新失败')
    }
  }

})

module.exports = router;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
9.重难点Mogo语法
  1. User.findOne() //查询一条数据
  2. User.find() // 查询所有符合条件的数据
  3. User.find().skip().limit() // 专门用于数据分页
  4. User.countDocuments({}) // 统计总数量
  5. User.updateMany() // 更新用户信息
  6. { userId: { $in: [100001,100002] } // 判断userId在[100001,100002]中间
  7. { $or: [{ userName:‘jack’ }, { userEmail:‘jack@imooc.com’ }] } // 或 条件判断
  8. { $inc: { sequence_value: 1 } // 更新值 +1
1. mongo返回字段的四种方式
  1. ‘userId userName userEmail state role deptId roleList’
  2. { userId:1,_id:0 }
  3. select(‘userId’)
  4. select({ userId:1,_id:0 })
User.findOne({ userName,userPwd }, 'userId userName userEmail state role deptId roleList')
// Or
User.findOne({ userName,userPwd }, { userId:1,_id:0 })
// Or
User.findOne({ userName,userPwd }).select('userId userName userEmail')
// Or
User.findOne({ userName,userPwd }).select({ userId:1,_id:0 })
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

8.菜单管理前后台实现

1. 删除

  1. 删除列表下面的所有数据
      await Menu.deleteMany({ parentId: { $all: [_id] } })  //删除保护的id 
  • 1
  1. _id父菜单,parentId子菜单id

2.mongo语法

  1. 根据id查找并更新
Menu.findByIdAndUpdate(_id, params)
  • 1
  1. 根据ID查找并删除
Menu.findByIdAndRemove(_id)
  • 1
  1. 查找表中parentId包含[id]的数据,并批量删除
Menu.deleteMany({ parentId: { $all: [_id] } })
  • 1

$all 指的是表中某一列包含[id]的数据,例如:parentId:[1,3,5] 包含 [3]
$in 指的是表中某一列在[id]这个数组中,例如:_id:3 在[1,3,5]中

9.角色管理

1.前端实现

1. 清空表单resetFields

  1. 清空表单要使用属性resetFields,而使用resetFields属性时 ,需要对方法传入一个ref属性
  2. 使用ref属性需要对表单中定义这个ref="form",然后对表单中提交的方法传入这个form对象(hangleReset('form')")
  3. 然后在方法中使用
 // 重置表单
    hangleReset(form) {
      this.$nextTick(() => {
        this.$refs[form].resetFields();
      })
    },
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2.rules验证

  1. 在表单中需定义一个ref='diaoform',在el-form中定义一个:rules=“rules”,然后在定义一下,并且定校验规则
rules:{
        roleName: [{
          required: true,   //开启校验规则
            message:'请输入角色名称',
          }]
      }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  1. 提交表单之前检查一下校验规则,通过dialogForm是在表单中定义的ref指,通过对valid判断是否为teue,如果是true则进行,否则就不能提交
  2. 重置方法中[form]中的form是传入的定义的ref值
// 提交
    handleSubmit() {
      this.$refs.dialogForm.validate((valid) => {
        if (valid) {
          
        }
      })
    },
 // 取消
    handleClose() {
      this.hangleReset('dialogForm')
      this.showModal = false;
      
    }
  // 重置表单
    hangleReset(form) {
      this.$nextTick(() => {
        this.$refs[form].resetFields();
      })
    },
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

3. 创建功能

  1. 在对valid进行验证之后,就进行到提交
  2. 首先对acion,和roleForm中的内容进行赋值
  3. 然后定义params,通过...进行结构,传回到后端,然后对res进行判断,看是否请求成功
// 表单提交
 // 角色提交
    handleSubmit() {
      this.$refs.dialogForm.validate( async (valid) => {
        if (valid) {
          let { roleForm, action } = this;
          let params = { ...roleForm, action }
          let res = await this.$api.roleOperate(params);
          if (res) {
            this.showModal = false;
            this.hangleReset('dialogForm');//重置表单
            ElMessage({
            message: '提交成功',
            type: 'success',
            })
            this.getRoleList();
          }
        }
      })
    },
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  1. 后端请求数据
// 角色操作
  roleOperate(params) {
    return request({
      url: "/roles/operate",
      method: "post",
      data: params, 
      mock: true
    });
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

4.编辑

  1. 编辑时,需对绑定的按钮传入scope.row
  <el-button type="primary" @click="handleEdit(scope.row)" >编辑</el-button>

   // 编辑
    handleEdit(row) {
      this.action = 'edit';
      this.showModal = true;  //打开弹窗
      this.$nextTick(() => {
        this.roleForm = row;   //给表单赋值
      })
      this.handleSubmit();   //调用新增,编辑,删除接口
    },

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

5.删除

  1. 通过scope.row._id传入当前数据的id,然后删除方法中接收一个id
  2. 在后端中对action的参数做一个判断,调用/新增,编辑,删除,修改接口,做出相应的功能
              <el-button type="danger" @click="handleDel(scope.row._id)">删除</el-button>


// 删除
    async handleDel(_id) {
      await this.$api.roleOperate({ _id, action: 'delete' });
      ElMessage({
        message: '删除成功',
        type: 'success',
      })
      this.roleList();
    },
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

2.后端

后端所有接口的实现

1.后端接口

/**
 *  用户管理模块
 */
const router = require("koa-router")();
const { log } = require("debug/src/browser");
const Role = require("../models/roleSchems");
const util = require("../utils/util");
const jwt = require('jsonwebtoken');
const md5 = require('md5');
router.prefix("/roles");

//查询所有角色列表
router.get('/allList', async (ctx) => {
  try {
    const list = await Role.find({}, "_id roleName");
    ctx.body = util.success(list);
  } catch (error) {
    ctx.body = util.fail(`查询失败:${error.stacks}`)
  }
})

// 获取角色列表
router.get('/list', async (ctx) => {
  const { roleName } = ctx.request.query;
  const { page, skipIndex } = util.pager(ctx.request.query);
  try {
    let params = {}
    if (roleName) params.roleName = roleName;
    const query = Role.find(params);
    const list = await query.skip(skipIndex).limit(page.pageSize);
    const total = await Role.countDocuments(params);
    ctx.body = util.success({
      list,
      page: {
        ...page,
        total
      }
    })
  } catch (error) {
    ctx.body = util.fail(`查询失败:${error.stack}`)
  }
})

// 角色的操作/创建/编辑/删除
router.post('/operate', async (ctx) => {
  const { _id, roleName, remark, action } = ctx.request.body;
  let res, info;
  try {
    if (action == 'create') {
      res = await Role.create({ roleName, remark });
      info = "创建成功";
    } else if (action == 'edit') {
      if (_id) {
        let params = { roleName, remark };
        params.updateTime = new Date();
        res = await Role.findByIdAndUpdate(_id, params);
        info = "编辑成功";
      } else {
        ctx.body = util.fail('确实参数params_id');
        return
      }
    } else {
      if (_id) {
        res = await Role.findByIdAndRemove(_id);
        info = "删除成功";
      } else {
        ctx.body = util.fail('确实参数params_id');
        return

      }
    }
    ctx.body = util.success(res, info);
  } catch (error) {
    ctx.body = util.fail(error.stack);
  }
})

// 权限设置
router.post('/update/permission', async (ctx) => {
  const { _id, permissionList } = ctx.request.body;
  console.log('permissionList=>', permissionList);
  try {
    let params = { permissionList, updateTime: new Date() }
    let res = await Role.findByIdAndUpdate(_id, params);
    ctx.body = util.success('', '权限设置成功')
  } catch (error) {
    ctx.body = util.fail("权限设置失败");
  }
})

module.exports = router;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92

3.角色管理总结

角色列表: /roles/list
菜单列表: /menu/list
角色操作: /roles/operate
权限设置: /roles/update/permission
所有角色列表: /roles/allList

注意事项:

  1. 分页参数 { ...this.queryForm, ...this.pager, }
  2. 角色列表展示菜单权限,递归调用actionMap
  3. 角色编辑 nextTick
  4. 理解权限设置中 checkedKeyshalfCheckedKeys

RBAC模型
Role-Base-Access-Control
用户 分配角色 -> 角色 分配权限 -> 权限 对应菜单、按钮
用户登录以后,根据对应角色,拉取用户的所有权限列表,对菜单、按钮进行动态渲染。

10. 部门管理

1.静态页面

1.form表单

  1. 在el-form 中设置inline="true"设置为行内样式
  2. placeholder用来定义输入框中的值
  3. 定义一个ref可以用来重置表单值,label可以设置input输入框中前面的名称
  4. 重置事件 需要传入一个值queryForm,就是定义的那个ref的值
<el-form :inline="true" ref="deptform" :model="queryForm">
   <el-form-item label="部门名称"  prop="deptname">
      <el-input placeholder="请输入部门名称" v-model="queryForm.deptname"> </el-input>
   </el-form-item>
   <el-form-item>
      <el-button  type="primary" @click="getDeptList">查询</el-button>
      <el-button type="warning"  @click="hangleReset('deptform')">重置</el-button>
   </el-form-item>
</el-form>


methods: {
    // 表单重置
    handleReset(form) {
      this.$refs[form].resetFields();
    }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  1. 而在重置方法中,需要接受一个值,然后调用refs方法,使input清空,要想使resetFields()方法生效,必须定义prop

2.table表格

  1. 在表格中,如果是树形结构需要一个row-key="_id" 属性,定义tree-props返回的是一个children属性(树形),如果不是children属性可以进行定义
  2. 插槽#default="scope",需在表格中定义一个插槽属性,并且需要定义一个方法,返回一个scope.row,用来后面在编辑和删除中通过row,接受传过来的值
  3. 在获取部门列表使,传入分页参数
 <div class="base-table">
   <div class="action">
      <el-button type="primary">创建</el-button>
    </div>
    <el-table 
      :data="deptList"
      row-key="_id"  
      :tree-props="{children:'children'}" stripe >
      <el-table-column 
        v-for="item in columns"
        :key="item.prop"
        v-bind="item"
        :formatter="item.formatter" />
      <el-table-column label="操作" width="200">
        <template #default="scope">
          <el-button type="primary" @click="handleEdit(scope.row)">新增</el-button>
          <el-button type="danger" @click="handleDel(scope.row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table> 
  </div>
 </div>

pager: {
  pageNum: 1,
  pageSizr:10
}

methods: {
    // 获取部门列表
    async getDeptList() {
      let res = await this.$api.getDeptList({
        ...this.queryForm,
        ...this.pager
      });
      this.deptList = res;
    },
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  1. :formatter="item.formatter"属性为后面留下的时间插槽,为后期时间转换留下了好的窗口,只用在前面引入工具类util
import utils from '../utils/utils';

{
          label: "创建时间",
          prop: "createTime",
          formatter(row, colum, value) {
          return utils.formateDate(new Date(value))
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3.select组件

  1. 默认对相应的负责人,设置对应的负责人邮箱

在这里插入图片描述

	<el-form-item label="负责人" prop="user">
     <el-select 
        placeholder="请选择部门负责人" 
        @change="handldUser"
        v-model="deptForm.user">
        <el-option v-for="item in userList" 
          :key="item.userId" 
          :label="item.userName"
          :value="`${item.userId}/${item.userName}/${item.userEmail}`">
        </el-option>
      </el-select>
    </el-form-item>
        
	handldUser(val) {
	  const [userId, userName,userEmail] = val.split('/');
	   console.log('userEmail', userEmail);
	   Object.assign(this.deptForm, { userId,  userName,userEmail});
	 },
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  1. 在对应的:value绑定好对应的userid,username,和usename
  2. userlist是获取用户列表
  3. 在el-select中设置change事件,并用模板字符串分割方法,和浅拷贝给赋值

2. 新增/编辑/删除

1.新增

<!-- 弹窗 -->
    <el-dialog :title="action=='create'?'创建部门':'编辑部门'" v-model="showModal">
      <el-form ref="dialogForm" :model="deptForm" :rules="rules" label-width="120px">
        <el-form-item label="上级部门" prop="parentId">
          <el-cascader 
            placeholder="请选择上级部门" 
            v-model="deptForm.parentId"
            clearable
            :options="deptList"
            :show-all-levels="true"
            :props="{checkStrictly:true,value:'_id',label:'deptName'}">
          </el-cascader>
        </el-form-item>
        <el-form-item label="部门名称" prop="deptName">
          <el-input placeholder="请输入部门名称" 
            v-model="deptForm.deptName"></el-input>
        </el-form-item>
        <el-form-item label="负责人" prop="user">
          <el-select 
            placeholder="请选择部门负责人" 
            @change="handldUser"
            v-model="deptForm.user">
            <el-option v-for="item in userList" 
              :key="item.userId" 
              :label="item.userName"
              :value="`${item.userId}/${item.userName}/${item.userEmail}`">
            </el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="负责人邮箱" prop="userEmail">
          <el-input placeholder="请输入负责人邮箱" 
            v-model="deptForm.userEmail"
            disabled></el-input>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary" @click="handldClose" >取消</el-button>
          <el-button type="primary" @click="handleSubmit" >确定</el-button>
        </span>
      </template>
    </el-dialog>




rules: {
        parentId: [
          {
            required: true,
            message: '请选择上级部门',
            trigger:'blur'
          }
        ],
        deptName: [
          {
            required: true,
            message: '请输入部门名称',
            trigger:'blur'
          }
        ],
        user: [
          {
            required: true,
            message: '请选择负责人',
            trigger:'blur'
          }
        ],
      }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  1. 表单在新增过程中,定义了rulus,表单定义规则。required,表示必填,trigger表示失去焦点时触发
  2. 然后定义提交方法handleSubmit()方法
handleSubmit() {
this.$refs.dialogForm.validate(async (valid) => {
   if (valid) {
     let params = { ...this.deptForm, action: this.action };
     delete params.user;
     let res = await this.$api.deptOperate(params)
     if (res) {
       ElMessage({
         message: '操作成功',
         type: 'success',
       })
       this.handldClose();  //关闭弹窗,清空表单
       this.getDeptList();  //重新获取用户列表
     }
   }
 })
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  1. 首先需要对rules表单进行验证,通过refs对表单中定义ref=fidloForm进行验证,判断valid是否为true,如果为true则进行后续操作,
  2. 对表单deptForm进行结构,并添加action为create代表新增,将这两项数据都添加到params中
  3. 删除params中user的(为给表单赋值,拼接的用户id,用户名,用户邮箱),通过delete即可删除
  4. 调用方法,方法类型为post,需要传入params

2.删除

// 删除
async handleDel(_id) {
  this.action = 'delete';
  await this.$api.deptOperate({ _id, action: this.action });
  ElMessage({
    message: '删除成功',
    type: 'success',
  })
  this.getDeptList();
},
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  1. 删除方法需要传入一份id,然后对action进行赋值为delete,
  2. 调用方法跟新增同一个接口,类型为post请求,需要传入一份id和action就行,然后重新刷新列表

3.编辑(未完成)

  1. 需要将action改为edit,并且用$nextTick()对和浅拷贝对表单进行赋值,并且将用户名,id,email邮箱赋值到表单中
// 编辑
 handleEdit(row) {
   this.action = 'edit';
   this.showModal = true
   this.$nextTick(()=>{
     Object.assign(this.deptForm, row, {
       user:`${row.userId}/${row.userName}/${row.userEmail}`
     })
   })
   // this.handleSubmit();
 },
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

3.后端编写

1.创建schems对象

  1. 首先先创建mongoo对象
  2. 然后通过mongoose定义schema对象
  3. 声明deptSchema
  4. 通过mongoose指定表,进行输出,通过mongoose.model声明一个模型
  5. 第一个表名称为depts,自己取的名字,二是定义好的模型,三表集合名称,与数据库中的结构是一一匹配的
const mongoose = require('mongoose');  //先创建mongoose对象

// 然后通过mongoose定义Schema对象
const deptSchema= mongoose.Schema({
  deptName: String,
  userId: String,
  userName: String,
  userEmail: String,
  parentId: [mongoose.Types.ObjectId], //自动生成id
  updateTime: {
    type: Date,
    default: Date.now()
  },
  createTime: {
    type: Date,
    default: Date.now()
  }
})

module.exports = mongoose.model("depts", deptSchema, "depts");
// 第一个是自己取的名字,二是定义好的模型,三表集合名称
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  1. 在数据库中创建集合depts
  2. 在routes文件夹中创建depts.js
const router = require("koa-router")();
const util = require("./../utils/util");
const Dept = require('./../models/deptSchems');

router.prefix('/dept');

module.exports = router;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  1. 在app.js中定义路由,通过router进行挂载
const depts = require('./routes/depts');


router.use(depts.routes(), depts.allowedMethods());
  • 1
  • 2
  • 3
  • 4

2. 部门操作,编辑,删除

// 部门操作/创建/删除 
router.post('/operate', async (ctx) => {
  const { _id, action, ...params } = ctx.request.body;
  let res, info;
  try {
    if (action == 'create') {
      await Dept.create(params);
      info = "创建成功";
    } else if (action == 'edit') {
      params.updateTime = new Date();
      await Dept.findByIdAndUpdate(_id, params);
      info = '编辑成功'
    } else if (action == 'delete') {
      res = await Dept.findByIdAndRemove(_id);
      await Dept.deleteMany({ parentId: { $all: [_id] } });
      info = '删除成功'
    }
    ctx.body = util.success('', info)
  } catch (error) {
    ctx.body = util.fail('', error.stack)

  }
})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  1. 通过前端传入的数据,结构出,_id,action,params,数据,然后定义一个res,和返回的类型定义
  2. 通过判断action的值,来判定该接口实现的是什么功能,
  3. 新增通过.create()来新增一条数据
  4. 编辑 通过.findByIdAndUpdate(_id,params)方法来根据 ID 来修改编辑一条数据
  5. 删除 通过.findByIdAndRemove(_id)根据ID来删除一条数据,
  6. 删除所有的含有父ID的元素.deleteMany({ parentId: { $all: [_id] } }),parentId 是父元素包含子元素定义的标签

3. 关联用户列表

  1. 通过find({}, "userId userName userEmail")进行查询,只返回,userId,userName,userEmail这三个字段
  2. 通过 util.success(list);进行返回
//获取全量用户列表
router.get('/all/list', async (ctx) => {
  try {
    const list = await User.find({}, "userId userName userEmail");
    ctx.body = util.success(list);
  } catch (error) {
    ctx.body = util.fail(error.stack);

  }
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

4.返回树形菜单

// 部门树形列表
router.get('/list', async (ctx) => {
  let { deptName } = ctx.request.query;
  let params = {}
  if (deptName) params.deptName = deptName;
  let rootList = await Dept.find(params)
  if (deptName) {
    ctx.body = util.success(rootList)
  } else {
    let tressList = getTreeDept(rootList, null, []);
    ctx.body = util.success(tressList);
  }
})

// 递归拼接树形菜单
function getTreeDept(rootList, id, list) {
  for (let i = 0; i < rootList.length; i++) {
    let item = rootList[i]
    if (String(item.parentId.slice().pop()) == String(id)) {
      list.push(item._doc);
    }
  }
  list.map(item => {
    item.children = []
    getTreeDept(rootList, item._id, item.children)
    if (item.children.length == 0) {
      delete item.children;
    }
  })
  return list;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

11.动态路由,导航守卫

1.理论

  1. 权限 RBAC(Rile Based Access Conteol)
    用户 角色 权限
    菜单权限 按钮权限 数据权限

公司现状

  • 一个系统一套权限
  • 不同系统权限各不相同
  • 很多系统前端是同一个团队,后端不同权限

大厂做法

  • 通一各个系统权限
  • 搭建权限中心,用户系统,实现单点登录,通一权限分配
  • 业务图对只负责业务开发

工作流

什么是工作流?

部分或整体业务实现计算机环境下的自动化


那些场景或系统会使用工作流?
OA HR ERP CRM

加班,报销,出差,采购,报价,培训,考核,付款


工作流七要素

角色 场景 节点 环节 必要信息 通知 操作

角色:发起人,审批人
场景:请假,出差
节点:审批单节点,多节点
环节:审批单环节,多环节
必要信息:申请理由,申请时长
通知:申请人,审批人
操作:未审批,已驳回,已审批


在这里插入图片描述

2. 根据角色获取用户动态菜单

1.获取用户对应的权限菜单

  1. 在后端user.js方法中新建getPermissionList()方法,想解出token中的信息,首先需要获取到authorization(不区分大小写),得到含有Bearer token 一段字符串
  2. 在util.js中公共方法中定义decoded()方法,以便复用,接收一个authorization,判断是否存在,如果存在,通过split()利用空格进行分割,分割完成后取第一个字符串就是token,然后利用verify()进行解密,imooc就是秘钥,有值的话则进行返回,如果没有,则返回一个空字符串
decoded(authorization) {
 if (authorization) {
   let token = authorization.split(' ')[1];
   return jwt.verify(token, 'imooc')
 } else {
   return '';
 }
},
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  1. 返回之后得到的是token中含有的信息,得到其中的内容data,
  2. 创建一个getMenuList()方法,判断是否是管理员,0是管理员,1是普通用户,调用getMenuList()方法时,需要传入role (0是管理员,1是普通用户),和roleList权限列表,在getMenuList()中判断,如果是管理员,则在menu集合中,查找所有菜单
  3. 然后通过调用公共方法中的,util.getmenuList()方法进行拼接树形菜单
// 获取用户对应的权限菜单
router.get('/getPermissionList', async (ctx) => {
  let authorization = ctx.request.headers.authorization;
  let { data } = util.decoded(authorization);
  let menuList = await getMenuList(data.role, data.roleList);
  ctx.body = util.success(menuList);
})

async function getMenuList(userRole, roleKeys) {
  let rootList = [];
  // 判断是否是管理员  0是管理员
  if (userRole == 0) {
    rootList = await Menu.find({},) || [];
  }
  return util.getTreeMenu(rootList, null, [])
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

2.封装公共的递归拼接树形菜单方法

  1. menu.js中,将getTreeMenu()方法进行提取到util.js文件中,则在下面的递归中,再次进行getTreeMenu()调用时,需要指用this.getTreeMenu()进行调用
  2. 而在user.js中,调用getTreeMenu()时,则需要利用util.getTreeMenu(进行调用
CODE,

  decoded(authorization) {
    if (authorization) {
      let token = authorization.split(' ')[1];
      return jwt.verify(token, 'imooc')
    } else {
      return '';
    }
  },

  // 递归拼接树形菜单
  getTreeMenu(rootList, id, list) {
    for (let i = 0; i < rootList.length; i++) {
      let item = rootList[i]
      if (String(item.parentId.slice().pop()) == String(id)) {
        list.push(item._doc);
      }
    }
    list.map(item => {
      item.children = []
      this.getTreeMenu(rootList, item._id, item.children)
      if (item.children.length == 0) {
        delete item.children;
      } else if (item.children[0].menuType == 2) {
        // 快速区分按你和菜单,用与后期做菜单按钮权限控制
        item.action = item.children
      }
    })
    return list;
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

3.role = 0,不是管理员

如果是管理员则返回所有的菜单

async function getMenuList(userRole, roleKeys) {
  let rootList = [];
  // 判断是否是管理员  0是管理员
  if (userRole == 0) {
    rootList = await Menu.find({},) || [];
  } else {
    // 根据用户拥有的角色,获取权限列表
    // 先查找用户对应的角色有哪些
    let roleList = await Role.find({ _id: { $in: roleKeys } });
    let permissionList = [];
    roleList.map(role => {
      let { checkedKeys, halfCheckedKeys } = role.permissionList;
      permissionList = permissionList.concat([...checkedKeys, ...halfCheckedKeys]);
    })
    // 聚合,去重
    permissionList = [...new Set(permissionList)];
    // console.log('permissionList', permissionList);
    rootList = await Menu.find({ _id: { $in: permissionList } });

  }
  return util.getTreeMenu(rootList, null, [])
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  1. 根据用户对应的角色有哪些,通过find({ _id: { $in: roleKeys } })查找role表中全部与登录用户相等的用户角色_id,然后声明一个新的数据,存放用户菜单。
  2. 将获得的用户角色进行一个循环,得到其中的checkedKeys,checkedKeys
  3. 通过concat连接 ,形成一个新的数组,
  4. 通过new Set(permissionList)去重
  5. 然后通过id查找符合角色相应的菜单

3.按钮权限

1.后端设计
 首先需要拉取到用户完整的菜单权限,才能知道用户有哪些按钮
  • 1
  1. 对后端的权限标识进行递归,生成一个menuList,actionList进行返回
router.get('/getPermissionList', async (ctx) => {
  let authorization = ctx.request.headers.authorization;
  let { data } = util.decoded(authorization);
  let menuList = await getMenuList(data.role, data.roleList);
  let actionList = getActionList(JSON.parse(JSON.stringify(menuList)))
  ctx.body = util.success({ menuList, actionList });
})

async function getMenuList(userRole, roleKeys) {
  let rootList = [];
  // 判断是否是管理员  0是管理员
  if (userRole == 0) {
    rootList = await Menu.find({},) || [];
  } else {
    // 根据用户拥有的角色,获取权限列表
    // 先查找用户对应的角色有哪些
    let roleList = await Role.find({ _id: { $in: roleKeys } });
    let permissionList = [];
    roleList.map(role => {
      let { checkedKeys, halfCheckedKeys } = role.permissionList;
      permissionList = permissionList.concat([...checkedKeys, ...halfCheckedKeys]);
    })
    // 聚合,去重
    permissionList = [...new Set(permissionList)];
    rootList = await Menu.find({ _id: { $in: permissionList } });
  }
  return util.getTreeMenu(rootList, null, [])
}

// 获取所有按钮权限
function getActionList(list) {
  const actionList = []
  const deep = (arr) => {
    while (arr.length) {
      let item = arr.pop()
      if (item.action) {
        item.action.map(action => {
          actionList.push(action.menuCode)
        })
      }
      if (item.children && !item.action) {
        deep(item.children)
      }
    }
  }
  deep(list)
  return actionList
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
2.前度进行缓存actionList
  1. 首先在mutations.js中定义两个方法,分别是saveUserMenu,saveUserAction
  saveUserMenu(state, menuList) {
    state.menuList = menuList;
    storage.setItem("menuList", menuList);
  },
  saveUserAction(state, actionList) {
    state.actionList = actionList;
    storage.setItem("actionList", actionList);
  },
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  1. 在store/index.js文件中,也是定义两个menuList,和actionList
const state = {
  userInfo: storage.getItem("userInfo") || {}, //获取用户信息
  menuList: storage.getItem("menuList") || [],
  actionList: storage.getItem("actionList") || []
};
  • 1
  • 2
  • 3
  • 4
  • 5
  1. 而在home.vue中,对获取到的两个数据通过this.$store.commit()进行缓存
const { menuList, actionList } = await this.$api.getPermissionList()
this.userMenu = menuList   
this.$store.commit("saveUserMenu", menuList)
this.$store.commit("saveUserAction",actionList)
  • 1
  • 2
  • 3
  • 4
2.判断按钮权限 动态指令
  1. 在前端页面中,在main.js中定义一个全局指令,
  • 第一个是指令名称,可以随便改
  • 第二个可以定义指令相关的钩子,
  1. 通过 storage.getItem("actionList");获取缓存到的actionList ,也就是权限列表
  2. 然后判断是否含有权限,如果没有就进行隐藏,通过DOM节点进行删除
  3. 而在beforeMount节点中,不能进行删除,而需要定义宏任务 **setTimeout **进行删除
app.directive('has', {
  beforeMount: (el, binding) => {
    // 获取按钮权限
    let actionList = storage.getItem("actionList");
    let value = binding.value;
    // 判断列表中是否有对应的按钮权限标识
    let hasPermission = actionList.includes(value);
    if (!hasPermission) {   //没有就隐藏掉
      el.style = "display:none";
      setTimeout(() => {
        el.parentNode.removeChild(el);
      }, 0)
    }
  }
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

5.在前端页面中定义v-has="'user-edit'",而user-edit为权限标识

<el-button v-has=" 'user-edit' ">编辑</el-button>

  • 1
  • 2

这样则完成了权限按钮控制


3.404 路由守卫

  1. 创建一个404页面,以便在跳转页面出错时,会跳到404
<template>
  <div class="exception">
    <img src="./../assets/img/404.png" alt="">
    <el-button class="btn-home" @click="goHome">回首页</el-button>
  </div>
</template>
<script>
export default {
  name: '404',
  methods: {
    goHome() {
      this.$router.push('/');
    }
  }
}
</script>
<style lang="scss">
.exception{
  position: relative;
  img{
    width: 100%;
    height: 100vh;  
  }

  .btn-home{
    position: fixed;
    bottom: 100px;
    left: 50%;
    margin-left: -34px;
  }
}

</style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  1. 在前端index.js文件中,定义导航守卫
  2. 通过router.beforeEach((to,from,next)=>()} 来定义路由守卫而第一个参数是去哪,from是哪去,next是执行
  3. 定义checkPermission判断当期路由是否在路由当中,to.path则代表当前页面路由,传到函数中,进行filter,与当前所有路由进行对比,看是否存在,如果存在则代表是,不存在则返回false
  4. 通过DMO原生,对当前页面的title进行设置,之前在路由中定义的meta起到了作用,则可以作为当前页面的title
// 判断当前地址是否可以访问
function checkPermission(path) {
  let hasPermission = router.getRoutes().filter(route => route.path == path).length;
  if (hasPermission) {
    return true
  } else {
    return false
  }
}


// 导航守卫
router.beforeEach((to, from, next) => {
  if (checkPermission(to.path)) {
    document.title = to.meta.title
    next()
  } else {
    next('/404');
  }
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

1.动态路由(未成功)

  在index.js文件中
  • 1

import storage from "../utils/storage";
import API from "./../api"


// 页面刷新就调用
await loadAsyncRouters();

function genrateRoute(menuList) {
  let routes = []
  const deepList = (list) => {
    while (list.length) {
      let item = list.pop();
      if (item.action) {
        routes.push({
          name: item.component,
          path: item.path,
          meta: {
            title: item.menuName,
          },
          component: item.component,
        })
      }
      if (item.children && !item.action) {
        deepList(item.children)
      }
    }
  }
  deepList(menuList)
  return routes;
}



async function loadAsyncRouters() {
  let userInfo = storage.getItem('userInfo') || {}
  if (userInfo.token) {
    try {
      const { menuList } = await API.getPermissionList()
      let routes = genrateRoute(menuList)
      routes.map(route => {
        console.log('routesmap', route);
        let url = `./../views/${route.component}.vue`
        route.component = () => import(url);
        router.addRoute("home", route)
      })
    } catch (error) {
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

总结

内容介绍

  1. 权限&工作流知识介绍
  2. 动态菜单渲染
  3. 按钮权限控制
  4. 导航守卫、权限拦截、动态路由

      接口调用:权限列表: /users/getPermissionList
  • 1

2.重难点

用户菜单权限:
用户登录 -> 获取用户身份(管理员和普通用户) -> 调用 权限列表 接口 -> 递归生成菜单和按钮list -> 前端进行菜单渲染
动态指令: v-has

app.directive('has', {
beforeMount: function (el, binding) {
	// 获取按钮列表,注意按钮的key不可以重复,必须唯一
	let actionList = storage.getItem('actionList');
	// 获取质量的值
	let value = binding.value;
	// 判断值是否在按钮列表里面
	let hasPermission = actionList.includes(value)
	if (!hasPermission) {
		// 隐藏按钮
		el.style = 'display:none';
		setTimeout(() => {
		// 删除按钮
		el.parentNode.removeChild(el);
		}, 0)
	}
	}
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

理解指令:

v-on:click = "handleUser"
click 对应binding.arg ,表示指令参数
handleUser 对应binding.value ,表示指令值

导航守卫

常用API:beforeEach()afterEach()getRoutes()push()back()addRoute()
我们判断当前路由是否存在时,也可以使用hasRoute()
原代码: router.getRoutes().filter(route => route.path == path).length;
更改后代码: router.hasRoute(to.name)

注意事项

动态加载路由时,切记compoent的地址

1. url必须提取出来
2. 地址需要添加.vue后缀
3. 不可以使用@/views

let url = ./../views/${route.component}.vue
route.component = ()=>import(url)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

12. 休假申请,前后端实现

1.工作流的介绍

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

2. 申请休假

1. 创建弹窗

<el-form ref="dialogFrom" 
        :model="leaveForm" 
        :rules="rules"
        label-width="120px" >
        <el-form-item label="休假类型" prop="applyType" required>
          <el-select v-model="leaveForm.applyType">
            <el-option label="事假" :value="1"></el-option>
            <el-option label="调休" :value="2"></el-option>
            <el-option label="年假" :value="3"></el-option>
          </el-select>
      </el-form-item>
 
      <el-form-item label="休假时间"   required>
        <el-row>
          <el-col :span="8" >
            <el-form-item prop="startTime" >
                <el-date-picker
                  v-model="leaveForm.startTime"
                  type="date"
                  placeholder="选择开始日期"
                  @change=" (val) => handleDateChanges('startTime',val)"
                />
            </el-form-item>
          </el-col>
          <el-col :span="1"> <span>--</span>  </el-col>
          <el-col :span="8">
            <el-form-item prop="endTime" required> 
              <el-date-picker
                v-model="leaveForm.endTime"
                type="date"
                placeholder="选择结束日期"
                @change=" (val) => handleDateChanges('endTime',val)"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form-item>
      <el-form-item label="休假时长" required>
        {{ leaveForm.leaveTime }}
      </el-form-item>

      <el-form-item label="休假原因" prop="reasons" required>
        <el-input type="textarea" :rows="3"  placeholder="请输入休假原因"
          v-model="leaveForm.reasons"
        ></el-input>
      </el-form-item>
    </el-form>
    <template #footer>
      <span class="dialog-footer">
        <el-button @click=" handleClose">取消</el-button>
        <el-button type="primary" @click="handleSubmit">
          确认
        </el-button>
      </span>
    </template>
  </el-dialog>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

2.表单校验

  1. 参考上述代码
    :rules="rules" prop="applyType"都是用于表单校验的 表格前面的 * 则是用 required定义
    而在data中定义rules
// 定义表单校验规格
    const rules = reactive({
      applyType: [
        {
          required: true,
          message: '请选择休假事由',
          // 鼠标移出校验
          trigger:'blur'
        }
      ],
      startTime: [
        {
          type:"date",
          required: true,
          message: '请输入开始日期',
          // 鼠标移出校验
          trigger:'change'
        }
      ],
      endTime: [
        {
          type:"date",
          required: true,
          message: '请输入结束日期',
          // 鼠标移出校验
          trigger:'change'
        }
      ],
      reasons: [
        {
          required: true,
          message: '请输入休假原因',
          trigger:['blur','change']
        }
      ]
    })
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  1. 表单提交之前需进行表单校验,通过,validate来校验是否通过,通过value值是false还是ture来判断是否通过校验
const handleSubmit = () => {  
      ctx.$refs.dialogFrom.validate(async (value) => {
        if (value) {
          try {
            console.log('成功了value',value);
            let params = { ...leaveForm, action: action.value }
            let res = await ctx.$api.leaveOperate(params)
            ElMessage.success('创建成功');
            handleClose();// 关闭表单 
            getApplyList()
          } catch (error){
          }
        } else {
          console.log('失败了value',value);
        }
      })
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

3.休假时间的计算

在这里插入图片描述

  1. 休假时间的计算使用了<el-date-picker >组件中的 @change=" (val) => handleDateChanges('endTime',val)"事件,
<el-form-item prop="startTime" >
    <el-date-picker
      v-model="leaveForm.startTime"
      type="date"
      placeholder="选择开始日期"
      @change=" (val) => handleDateChanges('startTime',val)"
    />
</el-form-item>

<el-form-item prop="endTime" required> 
  <el-date-picker
     v-model="leaveForm.endTime"
     type="date"
     placeholder="选择结束日期"
     @change=" (val) => handleDateChanges('endTime',val)"
   />
 </el-form-item>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  1. 后在方法中定义方法了handleDateChanges()方法,key是点击了那个时间开始还是结束,val是选择的时间,然后进行计算
const handleDateChanges = (key, val) => {
      let { startTime, endTime } = leaveForm
      if (!startTime || !endTime) return;
      if (startTime > endTime) {
        ElMessage.error('开始日期不能晚于借宿日期');
        leaveForm.leaveTime = "0天"
        setTimeout(() => {
          leaveForm[key] = '';
        }, 0);
      } else {
        leaveForm.leaveTime = (endTime - startTime) / (24 * 60 * 60 * 1000) + 1 + '天';
      }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

3.查看详情

1.对象解构 数据字典

  1. 通过对查看绑定点击事件,通过scope.row点击查看,但是传过去的值是一个对象形式,
  <el-button  @click="handleDetail(scope.row)">查看</el-button>
  <el-button type="danger"  @click="handleDelete(scope.row._id)" >作废</el-button>
  • 1
  • 2

在这里插入图片描述

  1. 对上述传过来的数据进行处理
  2. 通过 let data = { ...row };进行对象结构
  // 查看详情
    const handleDetail = (row) => {
      let data = { ...row };
      data.applyTypeName = {
        1: '事假',
        2: '调休',
        3: '年假'
      }[data.applyType]    //1,2,3
      data.time = (utils.formateDate(new Date(data.startTime), "yyyy-MM-dd") +
        "到" + utils.formateDate(new Date(data.endTime), "yyyy-MM-dd"));
        // 1:待审批,2:审批中,3.审批拒绝,4.审批通过,5.作废
      data.applyStateName = {
        1: "待审批",
        2: "审批中",
        3: "审批拒绝",
        4: "审批通过",
        5: "作废",
      }[data.applyState];
      detail.value = data;
      showDetailModal.value = true;

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

然后通过数据字典对传过来的值进行处理,在applyTypeName 值中是新定义的显示字段,而后面的[data.applyType]是传过来的字段

2. Steps 步骤条组件

  • :active="detail.applyState"是Number值,显示当前的步骤
  • finish-status="success" 组件颜色
  • align-center居中
  • destroy-on-close 清除缓存
<el-steps :active="detail.applyState" finish-status="success" align-center destroy-on-close>
  <el-step title="待审批" />
  <el-step title="审批中" />
  <el-step title="审批通过/审批拒绝" />
</el-steps>
  • 1
  • 2
  • 3
  • 4
  • 5

4.后端接口的编写

1. 创建Schems文件

  1. 在schems文件夹下创建leaveSchems.js文件
const mongoose = require("mongoose");

const leaveSchema = mongoose.Schema({
  orderNo: String, //申请单号
  applyType: Number,  //申请类型,1:事假  2:调休  3:年假
  startTime: { type: Date, default: Date.now }, //开始时间
  endTime: { type: Date, default: Date.now },  //结束时间
  applyUser: {  //申请人信息
    userId: String,
    userName: String,
    userEmail: String
  },
  leaveTime: String,  //休假时间
  reasons: String,  //休假原因
  auditUsers: String,   //完整审批人
  curAuditUserName: String,  //当前审批人
  auditFlows: [  //审批流
    {
      userId: String,
      userName: String,
      userEmail: String
    }
  ],
  auditLogs: [
    {
      userId: String,
      userName: String,
      createTime: Date,  //时间
      remark: String, //同意
      action: String   //审核通过
    }
  ],
  applyState: { type: Number, default: 1 },
  createTime: { type: Date, default: Date.now }
});

module.exports = mongoose.model("leaves", leaveSchema, "roles");

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

2. 创建leave.js文件

  1. 在routes文件夹中创建leave.js文件
const leave = require('./routes/leave');

router.use(leave.routes(), leave.allowedMethods());
  • 1
  • 2
  • 3
  1. 在app.js中引入
const leave = require('./routes/leave');
router.use(leave.routes(), leave.allowedMethods());
  • 1
  • 2

3.编写接口

1. 查询接口
  1. 通过ctx.request.query接收传过来的参数,然后解出applyState,判断当前休假申请的状态,
/**
 *  休假申请模块
 */
const router = require("koa-router")();
const { log } = require("debug/src/browser");
const Leave = require("../models/leaveSchems");
const Dept = require('./../models/deptSchems');
const util = require("../utils/util");
const jwt = require('jsonwebtoken');
const md5 = require('md5');
router.prefix("/leave");


// 查询申请表
router.get('/list', async (ctx) => {
	//判断休假申请的状态
  const { applyState } = ctx.request.query;
   //分页功能
  const { page, skipIndex } = util.pager(ctx.request.query);
   //取出当前登录的token
  let authorization = ctx.request.headers.authorization;
   //tokenj加密时是含有数据的,通过decoded解密数据 ,得到用户信息
  let { data } = util.decoded(authorization);
  try {
    let params = {
      "applyUser.userId": data.userId
    }
    if (applyState) params.applyState = applyState
    // const query = Leave.find();   查找全部数据
    const query = Leave.find(params);

	//对查找到的数据做枫叶
    const list = await query.skip(skipIndex).limit(page.pageSize);
    const total = await Leave.countDocuments(params);
    ctx.body = util.success({
      page: {
        ...page,
        total
      },
      list
    })
  } catch (error) {
    ctx.body = util.fail(`查询失败:${error.stacks}`)
  }
})


module.exports = router;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
2.申请接口
// 申请表单
router.post('/operate', async (ctx) => {
  const { _id, action, ...params } = ctx.request.body;
  // 获取用户信息  通过decode解密拿到data
  let authorization = ctx.request.headers.authorization;
  let { data } = util.decoded(authorization);
  if (action == 'create') {
    // 生成申请单号
    let orderNo = "XJ"
    orderNo += util.formateDate(new Date(), "yyyyMMdd");
    const total = await Leave.countDocuments()
    params.orderNo = orderNo + total;

    // 获取用户当前部门ID
    let id = data.deptId.pop();
    // 查找负责人信息
    let dept = await Dept.findById(id)

    // 获取人事部门和财务部门负责人信息
    let userList = await Dept.find({ deptName: { $in: ['人事部门', '财务部门'] } })

    let auditUsers = dept.userName;
    let auditFlows = [
      { userId: dept.userId, userName: dept.userName, userEmail: dept.userEmail }
    ]

    userList.map(item => {
      auditFlows.push({
        userId: item.userId, userName: item.userName, userEmail: item.userEmail
      })
      auditUsers += ',' + item.userName;
    })

    params.auditUsers = auditUsers;
    params.curAuditUserName = dept.userName
    params.auditFlows = auditFlows
    params.auditLogs = []
    params.applyUser = {
      userId: data.userId,
      userName: data.userName,
      userEmail: data.userEmail
    }
    let res = await Leave.create(params)
    ctx.body = util.success("", "创建成功")
  } else {
    let res = await Leave.findByIdAndUpdate(_id, { applyState: 5 })
    ctx.body = util.success(',', '操作成功')
  }
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

13.代我审批前后端实现

13 .4已结束

1.

14.造轮子

c1c302e8baed9894c48c17e4738c092e

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/98581
推荐阅读
相关标签
  

闽ICP备14008679号