赞
踩
必须在管理员的命令下进行安装
npm install @vue/cli -g
安装完成后使用
vue --version
检查安装版本
yarn的安装并查看版本
npm install -g yarn
yarn --version
npm install create-vite-app -g
采用vite创建vue项目
yarn create vite manager{项目名}
一直Enter
会有两个选项
直到这样项目创建成功
yarn dev
# 安装项目生产依赖
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
vitejs
端口号修改成功后,需重新启动端口号
简称路由的封装在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");
在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;
作用: 主要是用于缓存的,用"命名空间"
在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],
};
首先在src/assets
创建一个style
文件夹,并在其下创建index.scss
公共样式文件,和页面cssreset.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
*{ 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>
<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>
<script setup>
import { useRouter } from 'vue-router'
let router = useRouter()
const goHome = ()=>{
router.push('/welcome')
}
</script>
使用管理员权限进入cmd,然后进入安装目录
使用命令
npm install -g koa-generator
进行安装
使用koa-generator生成koa2项目,输入命令:
koa2 manager-server
manager-server是项目名称
创建项目成功之后进入到mangager-server目录下,安装项目依赖
npm install
安装完成之后启动项目
npm start
将项目启动,默认端口号http://localhost:3000/
安装完毕后如果发现不能使用koa2命令,需配置环境变量
将找到koa-generator
文件夹下的bin文件夹目录下的koa2添加到环境变量添加到环境变量path
中
D:\software\Yarn\Data\global\node_modules\koa-generator\bin
使用命令安装
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");
在MongoDB官网进行下载,
无脑安装
1.安装完毕后需在安装目录下的bin目录下添加到全局的环境变量path中
然后在
然后安装后直接点击进行,然后在桌面会直接建立一个连接,点击进去之后直接连接就行
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
在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
在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;
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>
封装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)
在页面中使用
页面中获取成功之后,将返回值存储,并跳转到首页
npm install mongoose
进行安装
创建文件夹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
连接数据库
在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
新建一个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");
在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
<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>
在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() {},
};
location.hash.slice(1)
<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>
resolve: {
alias:{
'@': path.resolve( __dirname, './src' )
}
}
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()], });
css: {
preprocessorOptions: {
scss: {
additionalData: `@import '@/assets/style/base.scss';`
}
}
}
在这个项目中,使用jwt是使用jwt的插件来使用jwt
jsonwebtoken
插件地址
manager-server
中使用命令yarn add jsonwebtoken -S
// 用户管理模块 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;
在前端文件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 ”+ tokenapp.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’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;
getCurrentInstance
, onMounted
, reactive
, ref
,setup()
函数中引入ctx
,才能在后续操作中才能使用ctx.
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>
ctx.$api
有时候进行保存,无法找到函数,就得首先进行api引入,然后进行api的局部声明import api from './../api'
ctx.$api = api
<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() // 重新获取用户列表
}
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
});
},
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] } }, ])
ctx.$nextTick(() => { }
控制在DOM渲染完毕之后再把数据渲染给控件Object.assign(userForm, row);
使用的是浅拷贝
,将点击事件获取到的数据渲染给userForm
表格 // 用户编辑
const handleEdit = (row) => {
action.value = 'edit'; //控制是编辑还是新增
showModal.value = true; //打开弹窗
ctx.$nextTick(() => {
Object.assign(userForm, row);
})
}
// 用户管理模块 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;
/** * 工具函数封装 */ 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 } }
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)) } },
yarn add md5 -D
const md5 = require('md5')
userPwd: md5('123456'),
/** * 用户管理模块 */ 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;
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 })
await Menu.deleteMany({ parentId: { $all: [_id] } }) //删除保护的id
_id
父菜单,parentId
子菜单idMenu.findByIdAndUpdate(_id, params)
Menu.findByIdAndRemove(_id)
Menu.deleteMany({ parentId: { $all: [_id] } })
$all
指的是表中某一列包含[id]的数据,例如:parentId:[1,3,5] 包含 [3]
$in
指的是表中某一列在[id]这个数组中,例如:_id:3 在[1,3,5]中
resetFields
resetFields
,而使用resetFields
属性时 ,需要对方法传入一个ref属性ref="form"
,然后对表单中提交的方法传入这个form对象(hangleReset('form')")
// 重置表单
hangleReset(form) {
this.$nextTick(() => {
this.$refs[form].resetFields();
})
},
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(); }) },
...
进行结构,传回到后端,然后对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
});
}
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(); //调用新增,编辑,删除接口
},
<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();
},
后端所有接口的实现
/** * 用户管理模块 */ 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;
角色列表: /roles/list
菜单列表: /menu/list
角色操作: /roles/operate
权限设置: /roles/update/permission
所有角色列表: /roles/allList
注意事项:
{ ...this.queryForm, ...this.pager, }
actionMap
nextTick
checkedKeys
和 halfCheckedKeys
RBAC模型:
Role-Base-Access-Control
用户 分配角色 -> 角色 分配权限 -> 权限 对应菜单、按钮
用户登录以后,根据对应角色,拉取用户的所有权限列表,对菜单、按钮进行动态渲染。
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(); } }
resetFields()
方法生效,必须定义prop
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))
}
<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}); },
<!-- 弹窗 --> <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' } ], }
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中delete
即可删除post
,需要传入params
// 删除
async handleDel(_id) {
this.action = 'delete';
await this.$api.deptOperate({ _id, action: this.action });
ElMessage({
message: '删除成功',
type: 'success',
})
this.getDeptList();
},
$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();
},
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
depts.js
const router = require("koa-router")();
const util = require("./../utils/util");
const Dept = require('./../models/deptSchems');
router.prefix('/dept');
module.exports = router;
router
进行挂载const depts = require('./routes/depts');
router.use(depts.routes(), depts.allowedMethods());
// 部门操作/创建/删除 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) } })
.create()
来新增一条数据.findByIdAndUpdate(_id,params)
方法来根据 ID 来修改编辑一条数据.findByIdAndRemove(_id)
根据ID来删除一条数据,.deleteMany({ parentId: { $all: [_id] } })
,parentId 是父元素包含子元素定义的标签find({}, "userId userName userEmail")
进行查询,只返回,userId,userName,userEmail这三个字段//获取全量用户列表
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);
}
})
// 部门树形列表 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; }
公司现状
大厂做法
工作流
什么是工作流?
部分或整体业务实现计算机环境下的自动化
那些场景或系统会使用工作流?
OA HR ERP CRM
加班,报销,出差,采购,报价,培训,考核,付款
工作流七要素
角色 场景 节点 环节 必要信息 通知 操作
角色:发起人,审批人
场景:请假,出差
节点:审批单节点,多节点
环节:审批单环节,多环节
必要信息:申请理由,申请时长
通知:申请人,审批人
操作:未审批,已驳回,已审批
user.js
方法中新建getPermissionList()
方法,想解出token中的信息,首先需要获取到authorization(不区分大小写),得到含有Bearer token 一段字符串decoded()
方法,以便复用,接收一个authorization,判断是否存在,如果存在,通过split()
利用空格进行分割,分割完成后取第一个字符串就是token,然后利用verify()
进行解密,imooc
就是秘钥,有值的话则进行返回,如果没有,则返回一个空字符串decoded(authorization) {
if (authorization) {
let token = authorization.split(' ')[1];
return jwt.verify(token, 'imooc')
} else {
return '';
}
},
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, []) }
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; }
如果是管理员则返回所有的菜单
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,然后声明一个新的数据,存放用户菜单。concat
连接 ,形成一个新的数组,new Set(permissionList)
去重 首先需要拉取到用户完整的菜单权限,才能知道用户有哪些按钮
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 }
saveUserMenu(state, menuList) {
state.menuList = menuList;
storage.setItem("menuList", menuList);
},
saveUserAction(state, actionList) {
state.actionList = actionList;
storage.setItem("actionList", actionList);
},
const state = {
userInfo: storage.getItem("userInfo") || {}, //获取用户信息
menuList: storage.getItem("menuList") || [],
actionList: storage.getItem("actionList") || []
};
this.$store.commit()
进行缓存const { menuList, actionList } = await this.$api.getPermissionList()
this.userMenu = menuList
this.$store.commit("saveUserMenu", menuList)
this.$store.commit("saveUserAction",actionList)
storage.getItem("actionList");
获取缓存到的actionList ,也就是权限列表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>
这样则完成了权限按钮控制
<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>
checkPermission
判断当期路由是否在路由当中,to.path
则代表当前页面路由,传到函数中,进行filter,与当前所有路由进行对比,看是否存在,如果存在则代表是,不存在则返回false// 判断当前地址是否可以访问 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'); } })
在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
用户菜单权限:
用户登录 -> 获取用户身份(管理员和普通用户) -> 调用 权限列表 接口 -> 递归生成菜单和按钮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)
<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>
:rules="rules"
和 prop="applyType"
都是用于表单校验的 表格前面的 * 则是用 required
定义// 定义表单校验规格 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); } }) }
<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 + '天';
}
}
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]
是传过来的字段
: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>
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");
leave.js
文件const leave = require('./routes/leave');
router.use(leave.routes(), leave.allowedMethods());
const leave = require('./routes/leave');
router.use(leave.routes(), leave.allowedMethods());
/** * 休假申请模块 */ 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;
// 申请表单 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 .4已结束
c1c302e8baed9894c48c17e4738c092e
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。