vue3全栈后台管理系统
在routes文件夹下创建users.js文件// 用户管理模块//二级路由try {userName,userPwd,});if (res) {} else {ctx.body = util.fail("账号或密码不正确");});将定义为二级路由 ,通过定义一个login接口,监听try里的参数,通过里查找数据,如果查找成功就直接返回,如果没有查找到,就抛出一个异常在app.js中定义一个一级
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语句对比
SQL | Mongo |
---|---|
表(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
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.父子组件之间的传值
-
在components文件夹下,创建一个组件TreeMenu.vue组件,然后在里面输入初始结构
-
子组件
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>
- 在父组件中,引入组件,并在
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.查看当前页面的路由
location.hash.slice(1)
3.面包屑的实现
- 在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别名
- vite可配置别名,解决./…/问题,类似于Vue里面的@
参考
resolve: {
alias:{
'@': path.resolve( __dirname, './src' )
}
}
- 而在改写过程中,其中的 需要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()],
});
- 全局的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. 安装插件
- 在后端文件
manager-server
中使用命令
yarn add jsonwebtoken -S
- 安装jsonwebtoken插件
2.jsonwebtoken生成token
- 打开后端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;
- 通过jwt.sign()函数生成token,‘imooc’是秘钥,expiresIn是过期时间
- 通过token将userId userName userEmail state role deptId roleList等值赋值给data,然后利用data生成token,
- 所以token中的信息包含,userId userName userEmail state role deptId roleList,等信息
3.解析token
-
在前端文件manager文件中,打开utils/request.js文件中
-
在请求拦截之后的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;
});
- 首先通过
storage.getItem()
获取缓存信息中的token,将token 拼接到headers.Authorization
请求头文件中,获取到的信息是“bearer ”+ token
1. token解密测试
- 在后端文件
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
})
- 通过
ctx.request.headers.authorization.split(' ')[1];
获取头部信息的token ,利用split(‘ ’ )
的空格分割Bearer
和token
,然后获取到下标为1的token,然后用jwt.verify()
函数进行token解密,秘钥是’imooc’
然后将信息进行打印,会得到token过期时间
4. token过期拦截
- 首先需在后端文件中安装一个中间件,利用命令
yarn add koa-jwt -S
-
进行安装,作用是在启动入口之前提前去加载这个中间件
-
在后端文件app.js中首先进行声明
const koajwt =require('koa-jwt') //引入
app.use(koajwt({ secret: 'imooc' }))
router.prefix("/api");
-
在
router.prefix("/api");
上面首先进行一下声明和引入 -
在后端文件工具类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
};
- 然后进行返回的时候一个拦截请求
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/]
}))
-
而通过.
unless
通过正则表达式表示对登录页面的排除登录请求是否过期 -
然后在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列表的获取,函数的调用
- 在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报错
ctx.$api
有时候进行保存,无法找到函数,就得首先进行api引入,然后进行api的局部声明
import api from './../api'
ctx.$api = api
3.删除单条数据
- 首先在删除按钮中绑定一个点击事件
<template #default="scope">
<el-button @click="handleQuery(scope.row)" >编辑</el-button>
<el-button type="danger" @click="handleDel(scope.row)">删除</el-button>
</template>
scope
是当前的插槽,即可通过scope.row
取到当前删除的id
// 用户单个删除方法
const handleDel =async (row) => {
await ctx.$api.userDel({
userIds:[row.userId] //可单个删除
})
ElMessage.success('删除成功')
getUserList() // 重新获取用户列表
}
4. 删除多条数据
- 需要对表格定义一个多选删除对象的id数组
checkedUserIds
- 对表格绑定一个
@selection-change="handleSelectionChange
事件,会给返回选择的对象 - 需对选择的对象进行
.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;
}
- 而在
api.
接口管理文件中,需接收一个params对象
// 用户单个删除
userDel(params) {
return request({
url: "/users/delete",
method: "post",
data: params,
mock: true
});
},
5.表格0-1对应响应的格式
- 需在表格循环中定义
formatter
属性 - 然后在对应的循环列表中定义
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.新增编辑
ctx.$nextTick(() => { }
控制在DOM渲染完毕之后再把数据渲染给控件- 而在
Object.assign(userForm, row);
使用的是浅拷贝
,将点击事件获取到的数据渲染给userForm
表格
// 用户编辑
const handleEdit = (row) => {
action.value = 'edit'; //控制是编辑还是新增
showModal.value = true; //打开弹窗
ctx.$nextTick(() => {
Object.assign(userForm, row);
})
}
7.后台用户列表
- 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.前端日期格式化
- 在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
}
}
- 然后在前端文件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插件
- 通过安装命令
yarn add md5 -D
- 安装完毕后在头部进行引用
- 在使用时,直接括起来就行
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语法
- User.findOne() //查询一条数据
- User.find() // 查询所有符合条件的数据
- User.find().skip().limit() // 专门用于数据分页
- User.countDocuments({}) // 统计总数量
- User.updateMany() // 更新用户信息
- { userId: { $in: [100001,100002] } // 判断userId在[100001,100002]中间
- { $or: [{ userName:‘jack’ }, { userEmail:‘jack@imooc.com’ }] } // 或 条件判断
- { $inc: { sequence_value: 1 } // 更新值 +1
1. mongo返回字段的四种方式
- ‘userId userName userEmail state role deptId roleList’
- { userId:1,_id:0 }
- select(‘userId’)
- 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. 删除
- 删除列表下面的所有数据
await Menu.deleteMany({ parentId: { $all: [_id] } }) //删除保护的id
_id
父菜单,parentId
子菜单id
2.mongo语法
- 根据id查找并更新
Menu.findByIdAndUpdate(_id, params)
- 根据ID查找并删除
Menu.findByIdAndRemove(_id)
- 查找表中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
- 清空表单要使用属性
resetFields
,而使用resetFields
属性时 ,需要对方法传入一个ref属性 - 使用ref属性需要对表单中定义这个
ref="form"
,然后对表单中提交的方法传入这个form对象(hangleReset('form')")
- 然后在方法中使用
// 重置表单
hangleReset(form) {
this.$nextTick(() => {
this.$refs[form].resetFields();
})
},
2.rules验证
- 在表单中需定义一个
ref='diaoform'
,在el-form中定义一个:rules=“rules”,然后在定义一下,并且定校验规则
rules:{
roleName: [{
required: true, //开启校验规则
message:'请输入角色名称',
}]
}
- 提交表单之前检查一下校验规则,通过
dialogForm
是在表单中定义的ref指,通过对valid判断是否为teue,如果是true则进行,否则就不能提交 - 重置方法中
[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. 创建功能
- 在对valid进行验证之后,就进行到提交
- 首先对acion,和roleForm中的内容进行赋值
- 然后定义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();
}
}
})
},
- 后端请求数据
// 角色操作
roleOperate(params) {
return request({
url: "/roles/operate",
method: "post",
data: params,
mock: true
});
}
4.编辑
- 编辑时,需对绑定的按钮传入
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.删除
- 通过scope.row._id传入当前数据的id,然后删除方法中接收一个id
- 在后端中对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
注意事项:
- 分页参数
{ ...this.queryForm, ...this.pager, }
- 角色列表展示菜单权限,递归调用
actionMap
- 角色编辑
nextTick
- 理解权限设置中
checkedKeys
和halfCheckedKeys
RBAC模型:
Role-Base-Access-Control
用户 分配角色 -> 角色 分配权限 -> 权限 对应菜单、按钮
用户登录以后,根据对应角色,拉取用户的所有权限列表,对菜单、按钮进行动态渲染。
10. 部门管理
1.静态页面
1.form表单
- 在el-form 中设置
inline="true"
设置为行内样式 placeholder
用来定义输入框中的值- 定义一个
ref
可以用来重置表单值,label
可以设置input输入框中前面的名称 - 重置事件 需要传入一个值
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();
}
}
- 而在重置方法中,需要接受一个值,然后调用refs方法,使input清空,要想使
resetFields()
方法生效,必须定义prop
2.table表格
- 在表格中,如果是树形结构需要一个
row-key="_id"
属性,定义tree-props
返回的是一个children
属性(树形),如果不是children
属性可以进行定义 - 插槽
#default="scope"
,需在表格中定义一个插槽属性,并且需要定义一个方法,返回一个scope.row
,用来后面在编辑和删除中通过row,接受传过来的值 - 在获取部门列表使,传入分页参数
<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;
},
}
:formatter="item.formatter"
属性为后面留下的时间插槽,为后期时间转换留下了好的窗口,只用在前面引入工具类util
import utils from '../utils/utils';
{
label: "创建时间",
prop: "createTime",
formatter(row, colum, value) {
return utils.formateDate(new Date(value))
}
3.select组件
- 默认对相应的负责人,设置对应的负责人邮箱
<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});
},
- 在对应的:value绑定好对应的userid,username,和usename
- userlist是获取用户列表
- 在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'
}
],
}
- 表单在新增过程中,定义了rulus,表单定义规则。
required
,表示必填,trigger
表示失去焦点时触发 - 然后定义提交方法
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(); //重新获取用户列表
}
}
})
}
- 首先需要对
rules
表单进行验证,通过refs对表单中定义ref=fidloForm
进行验证,判断valid是否为true,如果为true则进行后续操作, - 对表单
deptForm
进行结构,并添加action为create
代表新增,将这两项数据都添加到params中 - 删除params中user的(为给表单赋值,拼接的用户id,用户名,用户邮箱),通过
delete
即可删除 - 调用方法,方法类型为
post
,需要传入params
2.删除
// 删除
async handleDel(_id) {
this.action = 'delete';
await this.$api.deptOperate({ _id, action: this.action });
ElMessage({
message: '删除成功',
type: 'success',
})
this.getDeptList();
},
- 删除方法需要传入一份id,然后对action进行赋值为delete,
- 调用方法跟新增同一个接口,类型为post请求,需要传入一份id和action就行,然后重新刷新列表
3.编辑(未完成)
- 需要将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对象
- 首先先创建
mongoo
对象 - 然后通过
mongoose
定义schema
对象 - 声明
deptSchema
- 通过
mongoose
指定表,进行输出,通过mongoose.model
声明一个模型 - 第一个表名称为
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");
// 第一个是自己取的名字,二是定义好的模型,三表集合名称
- 在数据库中创建集合
depts
- 在routes文件夹中创建
depts.js
const router = require("koa-router")();
const util = require("./../utils/util");
const Dept = require('./../models/deptSchems');
router.prefix('/dept');
module.exports = router;
- 在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)
}
})
- 通过前端传入的数据,结构出,_id,action,params,数据,然后定义一个res,和返回的类型定义
- 通过判断action的值,来判定该接口实现的是什么功能,
- 新增通过
.create()
来新增一条数据 - 编辑 通过
.findByIdAndUpdate(_id,params)
方法来根据 ID 来修改编辑一条数据 - 删除 通过
.findByIdAndRemove(_id)
根据ID来删除一条数据, - 删除所有的含有父ID的元素,
.deleteMany({ parentId: { $all: [_id] } })
,parentId 是父元素包含子元素定义的标签
3. 关联用户列表
- 通过
find({}, "userId userName userEmail")
进行查询,只返回,userId,userName,userEmail这三个字段 - 通过 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.理论
- 权限 RBAC(Rile Based Access Conteol)
用户 角色 权限
菜单权限 按钮权限 数据权限
公司现状
- 一个系统一套权限
- 不同系统权限各不相同
- 很多系统前端是同一个团队,后端不同权限
大厂做法
- 通一各个系统权限
- 搭建权限中心,用户系统,实现单点登录,通一权限分配
- 业务图对只负责业务开发
工作流
什么是工作流?
部分或整体业务实现计算机环境下的自动化
那些场景或系统会使用工作流?
OA HR ERP CRM
加班,报销,出差,采购,报价,培训,考核,付款
工作流七要素
角色 场景 节点 环节 必要信息 通知 操作
角色:发起人,审批人
场景:请假,出差
节点:审批单节点,多节点
环节:审批单环节,多环节
必要信息:申请理由,申请时长
通知:申请人,审批人
操作:未审批,已驳回,已审批
2. 根据角色获取用户动态菜单
1.获取用户对应的权限菜单
- 在后端
user.js
方法中新建getPermissionList()
方法,想解出token中的信息,首先需要获取到authorization(不区分大小写),得到含有Bearer token 一段字符串 - 在util.js中公共方法中定义
decoded()
方法,以便复用,接收一个authorization,判断是否存在,如果存在,通过split()
利用空格进行分割,分割完成后取第一个字符串就是token,然后利用verify()
进行解密,imooc
就是秘钥,有值的话则进行返回,如果没有,则返回一个空字符串
decoded(authorization) {
if (authorization) {
let token = authorization.split(' ')[1];
return jwt.verify(token, 'imooc')
} else {
return '';
}
},
- 返回之后得到的是token中含有的信息,得到其中的内容data,
- 创建一个
getMenuList()
方法,判断是否是管理员,0是管理员,1是普通用户,调用getMenuList()
方法时,需要传入role (0是管理员,1是普通用户),和roleList
权限列表,在getMenuList()
中判断,如果是管理员,则在menu
集合中,查找所有菜单 - 然后通过调用公共方法中的,
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.封装公共的递归拼接树形菜单方法
- 在
menu.js
中,将getTreeMenu()
方法进行提取到util.js文件中,则在下面的递归中,再次进行getTreeMenu()
调用时,需要指用this.getTreeMenu()
进行调用 - 而在
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, [])
}
- 根据用户对应的角色有哪些,通过
find({ _id: { $in: roleKeys } })
查找role表中全部与登录用户相等的用户角色_id,然后声明一个新的数据,存放用户菜单。 - 将获得的用户角色进行一个循环,得到其中的checkedKeys,checkedKeys
- 通过
concat
连接 ,形成一个新的数组, - 通过
new Set(permissionList)
去重 - 然后通过id查找符合角色相应的菜单
3.按钮权限
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
- 首先在mutations.js中定义两个方法,分别是saveUserMenu,saveUserAction
saveUserMenu(state, menuList) {
state.menuList = menuList;
storage.setItem("menuList", menuList);
},
saveUserAction(state, actionList) {
state.actionList = actionList;
storage.setItem("actionList", actionList);
},
- 在store/index.js文件中,也是定义两个menuList,和actionList
const state = {
userInfo: storage.getItem("userInfo") || {}, //获取用户信息
menuList: storage.getItem("menuList") || [],
actionList: storage.getItem("actionList") || []
};
- 而在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.判断按钮权限 动态指令
- 在前端页面中,在main.js中定义一个全局指令,
- 第一个是指令名称,可以随便改
- 第二个可以定义指令相关的钩子,
- 通过
storage.getItem("actionList");
获取缓存到的actionList ,也就是权限列表 - 然后判断是否含有权限,如果没有就进行隐藏,通过DOM节点进行删除
- 而在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 路由守卫
- 创建一个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>
- 在前端index.js文件中,定义导航守卫
- 通过router.beforeEach((to,from,next)=>()} 来定义路由守卫而第一个参数是去哪,from是哪去,next是执行
- 定义
checkPermission
判断当期路由是否在路由当中,to.path
则代表当前页面路由,传到函数中,进行filter,与当前所有路由进行对比,看是否存在,如果存在则代表是,不存在则返回false - 通过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) {
}
}
}
总结
内容介绍
- 权限&工作流知识介绍
- 动态菜单渲染
- 按钮权限控制
- 导航守卫、权限拦截、动态路由
接口调用:权限列表: /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.表单校验
- 参考上述代码
: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']
}
]
})
- 表单提交之前需进行表单校验,通过,
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.休假时间的计算
- 休假时间的计算使用了
<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>
- 后在方法中定义方法了
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.对象解构 数据字典
- 通过对查看绑定点击事件,通过
scope.row
点击查看,但是传过去的值是一个对象形式,
<el-button @click="handleDetail(scope.row)">查看</el-button>
<el-button type="danger" @click="handleDelete(scope.row._id)" >作废</el-button>
- 对上述传过来的数据进行处理
- 通过
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文件
- 在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文件
- 在routes文件夹中创建
leave.js
文件
const leave = require('./routes/leave');
router.use(leave.routes(), leave.allowedMethods());
- 在app.js中引入
const leave = require('./routes/leave');
router.use(leave.routes(), leave.allowedMethods());
3.编写接口
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
更多推荐
所有评论(0)