vue3全栈后台管理系统

1.软件的安装

vue的安装

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

npm install @vue/cli -g

安装完成后使用

vue --version

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

yarn的安装

yarn的安装并查看版本

npm install -g yarn
yarn --version

在这里插入图片描述

vite的安装

npm install create-vite-app -g

在这里插入图片描述

1.项目的创建

采用vite创建vue项目

yarn create vite manager{项目名}

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

启动前端项目

yarn dev

2.安装项目依赖

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

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

Eslint
Vetur
TypeScript
Prettier

制定文件目录

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

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;

在main.js中挂载router

import router from "./router";

const app = createApp(App);
app.use(router).use(ElementPlus).mount("#app");

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],
};


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;



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();
 },
};

main.js挂载

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

app.config.globalProperties.$request = request;
app.config.globalProperties.$storage = storage;

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

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

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;
  }
}


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;
  }
}

main.js中引入样式文件

<style lang="scss">
@import "./assets/style/reset.css";
@import "./assets/style/index.scss";
</style>
完成登录页面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>


路由跳转的三种方式
router-link
<router-link to="/login">去登录</router-link>
传统跳转

<template>
<el-button @click="goHome">回首页</el-button>
</template>
<script>
export default{
name:'login',
methods:{
goHome(){
this.$router.push('/welcome')
}
}
}
</script>
Composition API跳转
<script setup>
import { useRouter } from 'vue-router'
let router = useRouter()
const goHome = ()=>{
router.push('/welcome')
}
</script>

3. koa2架构设计

1. 安装koa框架

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

npm install -g koa-generator

进行安装

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

koa2 manager-server

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

npm install

安装完成之后启动项目

npm start

将项目启动,默认端口号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

-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);
};

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

在app.js中引用

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

后端项目启动


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,
    };
  },
};

和文件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,
    });
  },
};


在main.js中挂载

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

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>

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,
});

moutations.js文件

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

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

在main.js中挂载vuex

import store from "./store";
app.use(store)

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

5.服务层的实现

1. 安装mongoosejs

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

npm install mongoose

进行安装

2.建立数据库连接

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

// 配置文件
// 采用mogos

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

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("***数据库连接成功");
});

在app.js中加载配置

require("./config/db");

执行命令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;

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());

通过一级路由定义一个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");

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()],
});

拦截后端接口,并关闭全局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>

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. 在父组件中,引入组件,并在components中注册组件,并且进行动态传值 :userMenu="userMenu"
<tree-menu :userMenu="userMenu"></tree-menu>

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

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},

子组件

<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>

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

1.vite别名
  1. vite可配置别名,解决./…/问题,类似于Vue里面的@
    参考
resolve: {
	alias:{
	'@': path.resolve( __dirname, './src' )
	}
}
  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. 全局的mixin 样式问题,可以通过vite进行配置
css: {
	preprocessorOptions: {
		scss: {
		additionalData: `@import '@/assets/style/base.scss';`
		}
	}
}

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. 安装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. 通过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. 首先通过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. 通过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. 进行安装,作用是在启动入口之前提前去加载这个中间件

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

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

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

  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. 然后进行返回的时候一个拦截请求
    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. 而通过.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;

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

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

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

<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>

2.getUserlist()函数Undefind报错

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

ctx.$api = api

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. scope是当前的插槽,即可通过scope.row取到当前删除的id
  // 用户单个删除方法
    const handleDel =async (row) => {
      await ctx.$api.userDel({
        userIds:[row.userId]  //可单个删除
      })
      ElMessage.success('删除成功')   
      getUserList()    // 重新获取用户列表
    }

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. 而在api.接口管理文件中,需接收一个params对象
 // 用户单个删除
  userDel(params) {
    return request({
      url: "/users/delete",
      method: "post",
      data: params,
      mock: true
    });
  },

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]
  }
},
])

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);
      })
    }

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.前端日期格式化
  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. 然后在前端文件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))
        }
      },
4.安装md5插件
  1. 通过安装命令
yarn add md5 -D
  1. 安装完毕后在头部进行引用
  2. 在使用时,直接括起来就行
const md5 = require('md5')

userPwd: md5('123456'),

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;

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 })

8.菜单管理前后台实现

1. 删除

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

2.mongo语法

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

$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();
      })
    },

2.rules验证

  1. 在表单中需定义一个ref='diaoform',在el-form中定义一个:rules=“rules”,然后在定义一下,并且定校验规则
rules:{
        roleName: [{
          required: true,   //开启校验规则
            message:'请输入角色名称',
          }]
      }
  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();
      })
    },

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. 后端请求数据
// 角色操作
  roleOperate(params) {
    return request({
      url: "/roles/operate",
      method: "post",
      data: params, 
      mock: true
    });
  }

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();   //调用新增,编辑,删除接口
    },

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();
    },

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;

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. 而在重置方法中,需要接受一个值,然后调用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. :formatter="item.formatter"属性为后面留下的时间插槽,为后期时间转换留下了好的窗口,只用在前面引入工具类util
import utils from '../utils/utils';

{
          label: "创建时间",
          prop: "createTime",
          formatter(row, colum, value) {
          return utils.formateDate(new Date(value))
        }

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. 在对应的: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. 表单在新增过程中,定义了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. 首先需要对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. 删除方法需要传入一份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();
 },

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. 在数据库中创建集合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. 在app.js中定义路由,通过router进行挂载
const depts = require('./routes/depts');


router.use(depts.routes(), depts.allowedMethods());

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. 通过前端传入的数据,结构出,_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);

  }
})

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;
}

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. 返回之后得到的是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, [])
}

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;
  }

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. 根据用户对应的角色有哪些,通过find({ _id: { $in: roleKeys } })查找role表中全部与登录用户相等的用户角色_id,然后声明一个新的数据,存放用户菜单。
  2. 将获得的用户角色进行一个循环,得到其中的checkedKeys,checkedKeys
  3. 通过concat连接 ,形成一个新的数组,
  4. 通过new Set(permissionList)去重
  5. 然后通过id查找符合角色相应的菜单

3.按钮权限

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
}
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. 在store/index.js文件中,也是定义两个menuList,和actionList
const state = {
  userInfo: storage.getItem("userInfo") || {}, //获取用户信息
  menuList: storage.getItem("menuList") || [],
  actionList: storage.getItem("actionList") || []
};
  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)
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)
    }
  }
})

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

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

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


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. 在前端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.动态路由(未成功)

  在index.js文件中

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. 导航守卫、权限拦截、动态路由

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

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)
	}
	}
})

理解指令:

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)

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>

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. 表单提交之前需进行表单校验,通过,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);
        }
      })
    }

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. 后在方法中定义方法了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 + '天';
      }
    }

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. 通过 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;

    }

然后通过数据字典对传过来的值进行处理,在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>

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");

2. 创建leave.js文件

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

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

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;
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(',', '操作成功')
  }
})

13.代我审批前后端实现

13 .4已结束

1.

14.造轮子

c1c302e8baed9894c48c17e4738c092e

  • 5
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Vue 3是一种流行的JavaScript框架,用于构建用户界面。它提供了一些强大的工具和功能,使开发人员能够轻松地构建现代、高效的Web应用程序。 对于后台管理系统Vue 3提供了许多有用的功能和组件,使开发人员能够快速构建出功能丰富、易于维护的系统。以下是一些常见的Vue 3后台管理系统的特点和技术: 1. 组件化开发:Vue 3使用组件化的开发模式,使得代码可复用,并且更易于维护和扩展。你可以将不同的功能模块封装成独立的组件,然后在应用中进行组合和重用。 2. 路由管理:Vue Router是Vue 3的官方路由管理器,它允许你通过定义路由来管理应用的导航。你可以根据路由配置来渲染不同的组件,并且支持动态路由和嵌套路由。 3. 状态管理:Vue 3使用Vuex进行状态管理,它提供了一个集中式的存储机制,用于在应用程序中管理共享状态。你可以在任何组件中访问和修改这些状态,并且支持异步操作和插件扩展。 4. UI库和组件:Vue 3有许多优秀的UI库和组件库可供选择,例如Element Plus、Ant Design VueVuetify等。这些库提供了丰富的UI组件和样式,可用于构建美观和交互丰富的后台管理系统。 5. 状态响应性:Vue 3引入了响应式API,使得数据的变化可以自动地反映在用户界面上。你可以将数据绑定到模板中,并在数据发生变化时自动更新视图。 总之,使用Vue 3开发后台管理系统可以帮助你更快速、高效地构建出现代化的用户界面。它提供了丰富的工具和功能,使得开发过程更加简单和愉快。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值