云办公开发笔记(上)

185 阅读4分钟

云办公

一、项目简介

本项目为在线办公系统,主要用于管理日常的办公事务。它包含了:日常的各种流程审批、新闻、通知、公告、文件信息、财务、人事、费用、资产、行政、项目、移动办公等;

image.png

二、环境搭建

  1. 安装Node

  2. 安装@vue/cli

npm install @vue/cli
  1. 查看版本

image.png

  1. 创建项目
vue create cloudoffice
  1. 启动项目
npm run serve
  1. 代码管理
git init
touch README.md
git add .
git commit -m '初始化项目'
git remote add origin git@gitee.com:harrylee13/cloud-office.git
git push -u origin "master"

三、实现登录

1. 引入ElementUI库

# 方式一:npm安装
npm i element-ui -S

# 方式二:CDN引入
# 引入样式
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> 
# 引入组件库
<script src="https://unpkg.com/element-ui/lib/index.js"></script>

2. 完整引入

import Vue from 'vue'; 
import ElementUI from 'element-ui'; 
import 'element-ui/lib/theme-chalk/index.css'; 
import App from './App.vue'; 
Vue.use(ElementUI); 
new Vue({ 
    el: '#app', 
    render: h => h(App) 
});

3.编写登录页面

<template>
  <div>
    <el-form :rules="rules" ref="loginForm" :model="loginForm" class="loginContainer">
      <h3 class="loginTitle">登录云办公</h3>
      <el-form-item prop="username">
        <el-input type="text" v-model="loginForm.username" placeholder="请输入用户名" auto-complete="off"></el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input type="password" v-model="loginForm.password" placeholder="请输入密码" auto-complete="off"></el-input>
      </el-form-item>
      <el-form-item prop="code">
        <el-input type="text" v-model="loginForm.code" placeholder="点击图片更换验证码" auto-complete="off"
        style="width: 250px;margin-right: 5px"></el-input>
        <img :src="captchaUrl" @click="updateCaptcha">
      </el-form-item>
      <el-checkbox v-model="checked" class="loginRememberMe">记住我</el-checkbox>
      <el-button type="primary" style="width: 100%;" @click="submitLogin">登录</el-button>
    </el-form>
  </div>
</template>

<script>
  export default {
    name: 'Login',
    data() {
      return {
        loginForm: {
          username: 'admin',
          password: '123',
          code: ''
        },
        checked: true,
        rules: {
          username: [{required: true, message:'请输入用户名', trigger: 'blur'}],
          password: [{required: true, message:'请输入密码', trigger: 'blur'}],
          code: [{required: true, message:'请输入验证码', trigger: 'blur'}]
        },
        captchaUrl: '/captcha/getCaptcha?time=' + new Date()
      }
    },
    methods: {
      updateCaptcha(){
        this.captchaUrl = '/captcha/getCaptcha?time=' + new Date()
      },
      submitLogin(){
        this.$refs.loginForm.validate((valid) => {
          if (valid) {
            alert('submit!');
          } else {
            this.$message.error('请输入所有字段!');
            return false;
          }
        });
      }
    }
  }
</script>
<style>
  .loginContainer{
    border-radius: 15px;
    background-clip: padding-box;
    margin: 300px auto;
    width: 350px;
    padding: 15px 35px;
    background: #fff;
    border: 1px solid #eaeaea;
    box-shadow: 0 0 25px #cac6c6;
  }

  .loginTitle{
    margin: 0px auto 40px auto;
    text-align: center;
  }

  .loginRememberMe{
    text-align: left;
    margin: 0px 0px 20px 0px;
  }

  .el-form-item__content{
    display: flex;
    align-items: center;
  }
</style>

image.png

4. 配置登录请求

① 安装axios

npm i axios

② 封装api.js

// 封装axios
import axios from 'axios'
import { Message } from 'element-ui'
import router from '../router'

axios.interceptors.response.use(success=>{
  if(success.status && success.status == 200){
    if(success.data.code==500 || success.data.code==401 || success.data.code==403){
      // 业务逻辑错误
      //500:服务器内部错误,无法完成请求
      //401:请求要求用户的身份认证
      //403:服务器理解请求客户端的请求,但是拒绝执行此请求
      Message.error({message: success.data.message})
      return;
    }
    if(success.data.message){
      Message.success({message: success.data.message})
    }
  }
  return success.data;
}, error=>{
  if(error.response.code==504 || error.response.code==404){
    Message.error({message: '服务器被吃了o(╥﹏╥)o'})
  }else if(error.response.code==403){
    Message.error({message: '权限不足,请联系管理员'})
  }else if(error.response.code==401){
    Message.error({message: '尚未登录,请登录'})
    router.replace('/') //跳转登录页面
  }else{
    if(error.response.data.message){
      Message.error({message: error.response.data.message})
    }else{
      Message.error({message: '未知错误'})
    }
  }
  return;
})

③ 配置POST请求

// api.js
...
let baseUrl = '';
// 传送json格式的post请求
export const postRequest = (url, params)=>{
  return axios({
    method: 'POST',
    url: `${baseUrl}${url}`,
    data: params
  })
}

④ 配置请求转发解决跨域

  • 上边的POST会存在请求跨域问题,所以需要通过Vue的代理,将8080端口的请求转发到8081端口!
// vue.config.js
...
// 请求经过nodejs时候,会通过这个代理对象,转发到8081端口
let proxyObj = {}
proxyObj['/'] = { // 所有要代理的路径是/
  // websocket
  ws: false,
  // 代理到哪里去,目标地址
  target: 'http://localhost:8081',
  // 表示发生请求头host会被设置为target
  changeOrigin: true,
  // 假如后端有前端路径,这里会不重写请求路径
  pathReWrite: {
    '^/': '/'
  }
}

module.exports = defineConfig({
  ...
  // 转发到8081端口
  devServer: {
    host: 'localhost',
    port: 8080,
    proxy: proxyObj
  }
})
  • 登录点击跳转
    1. 通过监听登录按钮的点击事件,发送api.js文件中的postRequest请求
    2. 请求后接收响应体中的token,将它存储在sessionStorage中
    3. 设置请求拦截器,判断sessionStorage中是否存在token,有则携带这个token再发送请求
// Login.vue
...
submitLogin(){
    this.$refs.loginForm.validate((valid) => {
      if (valid) {
        this.loading = true;
        this.postRequest('/login/doLogin', this.loginForm).then(resp => {
          // alert(JSON.stringify(resp));
          if(resp){
            // 存储token
            const tokenStr = resp.obj.tokenHead+resp.obj.token;
            window.sessionStorage.setItem('tokenStr', tokenStr);
            // 跳转首页
            this.$router.replace('/home')
          }
          // 重置数据
          this.loading = false;
          this.updateCaptcha();
          this.loginForm.code = ''
        })
      } else {
        this.$message.error('请输入所有字段!');
        return false;
      }
    });
    }
// api.js
...

// 请求拦截器
axios.interceptors.request.use(config=>{
  // 如果存在token,请求携带这个token
  if(window.sessionStorage.getItem('tokenStr')){
    config.headers['Authorization'] = window.sessionStorage.getItem('tokenStr');
  }
  return config;
}, error=>{
  console.log(error)
})
...
  • 全局引入api
    • 如果每个组件都去引入api,显得有点麻烦,这里直接在main.js中全局引入
    • 以后发送请求时,使用this.postRequest形式即可
...
// 引入接口
import { postRequest } from './utils/api';
import { putRequest } from './utils/api';
import { getRequest } from './utils/api';
import { deleteRequest } from './utils/api';
...
// 添加到原型中
Vue.prototype.postRequest = postRequest;
Vue.prototype.putRequest = putRequest;
Vue.prototype.getRequest = getRequest;
Vue.prototype.deleteRequest = deleteRequest;
...

四、导航开发

1. 导航菜单

① 使用ElementUI模板

<!-- Home.vue -->
<template>
  <div>
    <el-container>
      <el-header>Header</el-header>
      <el-container>
        <el-aside width="200px">
          <el-menu>
            <el-submenu index="1">
              <template slot="title"><i class="el-icon-location"></i>导航一</template>
              <el-menu-item index="/test1">选项一</el-menu-item>
              <el-menu-item index="/test2">选项二</el-menu-item>
            </el-submenu>
          </el-menu>
        </el-aside>
        <el-main>main</el-main>
      </el-container>
    </el-container>
  </div>
</template>
<script>
export default {
  name: "Home",
};
</script>
<style scoped>
</style>

② 开启router模式

<el-menu router>
    ...
</el-menu>
<el-main>
    <!-- 这个是对应子路由的,因为它的外层APP.js也有<router-view/> -->
    <router-view/>
</el-main>

③ 编写路由

// index.js
...
const routes = [
    ...
    {
        path: '/home',
        name: '导航一',
        component: Home,
        children: [
          {
            path: '/test1',
            name: '选项一',
            component: Test1
          },
          {
            path: '/test2',
            name: '选项二',
            component: Test2
          }
        ]
    }
]
...

④ 修改模板

  • 通过v-for遍历路由,实现导航栏与路由一一对应
  • 因为/login这个路由不需要展示,通过给它添加hidden属性实现隐藏
<!-- Home.vue -->
<template>
  <div>
    <el-container>
      <el-header>Header</el-header>
      <el-container>
        <el-aside width="200px">
          <el-menu router>
            <el-submenu index="1"  v-for="(item, index) in this.$router.options.routes" 
                                   :key="index" v-show="!item.hidden">
              <template slot="title">
                  <i class="el-icon-location"></i>{{item.name}}
              </template>
              <el-menu-item :index="children.path" 
                            v-for="(children, indexj) in item.children" 
                            :key="indexj">{{children.name}}
              </el-menu-item>
            </el-submenu>
          </el-menu>
        </el-aside>
        <el-main>
          <router-view></router-view>
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>
...

⑤ 效果展示

image.png

2. 动态导航栏

因为直接写死路由的话,将来后端的菜单数据改变时,就需要重新更改路由,工作量将不可估量;这里通过Vuex将后端返回的JSON数据动态的添加到路由中,之所以存在Vuex中是因为其中可能涉及隐私,存在Vuex就相当于存入这个网页应用的内存里;

① 安装Vuex

npm install vuex@3.6.2 --save

② 编写index.js

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    routes: []
  },

  mutations: {
    initRoutes(state, payload){
      state.routes = payload;
    }
  },

  actions: {}
})

③ 全局引入store

// main.js
...
import store from './store';
...
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

④ 封装菜单请求类

思路分析:因为后端返回的是字符串,通过将它们格式化为一个“路由数组”,并存入vuex中实现状态管理

// utils/menu.js
import { getRequest } from "./api";

export default initMenu = (router, store)=>{
  // 判断store.routes是否有值
  if(store.state.routes.length > 0){
    return;
  }
  // 发送请求并处理
  getRequest('/system/cfg/menu').then(data=>{
    if(data){
      // 格式化路由
      let fmtRoutes = formatRoutes(data.obj);
      // 添加路由
      router.addRoutes(fmtRoutes);
      // 将数据存入vuex
      store.commit('initRoutes', fmtRoutes);
    }
  })
}

// 格式化路由的函数
export const formatRoutes = (routes)=>{
  let fmtRoutes = [];
  // 将routes转换为真正的数组
  routes = Array.from(routes);
  routes.forEach(router=>{
    let {
      path,
      component,
      name,
      iconCls,
      children
    } = router;
    if(children && children instanceof Array){
      children = formatRoutes(children)
    }
    // 格式化好后的对象
    let fmRouter = {
      path: path,
      name: name,
      iconCls: iconCls,
      children: children,
      // 路由组件懒加载ES5
      component(resolve){
        // 判断开头并修改路径
        if(component.startsWith('Home')){
          require(['../views/'+component+'.vue'], resolve)
        }else if(component.startsWith('Emp')){
          require(['../views/emp/'+component+'.vue'], resolve)
        }else if(component.startsWith('Per')){
          require(['../views/per/'+component+'.vue'], resolve)
        }else if(component.startsWith('Sal')){
          require(['../views/sal'+component+'.vue'], resolve)
        }else if(component.startsWith('Sta')){
          require(['../views/sta'+component+'.vue'], resolve)
        }else if(component.startsWith('Sys')){
          require(['../views/sys'+component+'.vue'], resolve)
        }
      }
      // ES6
      // component: () => import ('../views/'+component+'.vue')
    }
    // 存入数组并返回
    fmtRoutes.push(fmRouter)
  });
  return fmtRoutes;
}

⑤ 路由导航守卫

封装好菜单请求类后,这时候就需要思考什么时候去调用这个初始化方法?登录时?不,如果在登录时初始化,那么页面一刷新,state里的routes就会被清空,那么导航栏就会被清空!这里通过路由导航守卫实现。

// main.js
// 全局前置导航守卫
router.beforeEach((to, from, next) => {
  // 判断用户是否登录
  if(window.sessionStorage.getItem('tokenStr')){
    // 初始化导航栏
    initMenu(router, store);
    next();
  }else if(to.path == '/'){
    next()
  }
})

⑥ 修改图标

  1. 安装font-awesome
npm install font-awesome
  1. 导入CSS样式
// main.js
import 'font-awesome/css/font-awesome.css'
  1. 修改模板
<i :class="item.iconCls"></i>

⑦ 修改模板

因为需要动态读取store.state中的routes,所以不再遍历router.routes,而是通过计算属性的方式,读取store.state中的routes;需要注意的是,el-submenu中的index属性需要的是字符串类型

<template>
  <div>
    <el-container>
      <el-container>
        ...
        <el-aside width="200px">
          <el-menu router unique-opened>
            <el-submenu :index="index+''"  v-for="(item, index) in routes" :key="index" v-show="!item.hidden">
              <template slot="title"><i class="el-icon-location"></i>{{item.name}}</template>
              <el-menu-item :index="children.path" v-for="(children, indexj) in item.children" :key="indexj">{{children.name}}</el-menu-item>
            </el-submenu>
          </el-menu>
        </el-aside>
        ...
      </el-container>
    </el-container>
  </div>
</template>
<script>
export default {
  name: "Home",
  computed: {
    routes(){
      return this.$store.state.routes
    }
  }
};
</script>
...

⑧ 创建组件

image.png

⑨ 效果展示

image.png

五、用户信息开发

1. 首页用户信息

① 获取用户信息

// main.js
// 全局前置导航守卫
router.beforeEach((to, from, next) => {
  // 判断用户是否登录
  if(window.sessionStorage.getItem('tokenStr')){
    // 初始化导航栏
    initMenu(router, store);
    // 获取用户信息
    if(!window.sessionStorage.getItem('user')){
      // 判断用户信息是否存在
      return getRequest('/login/getUserInfo').then(resp=>{
        if(resp){
          window.sessionStorage.setItem('user', JSON.stringify(resp.obj))
          next()
        }
      })
    }
    next();
  }else{
    next()
  }
})

② 修改模板

<template>
  <div>
    <el-container>
      <el-header class="homeHeader">
        <div class="title">
          <i class="el-icon-cloudy"></i>
          云办工
        </div>
        <el-dropdown class="userInfo">
          <span class="el-dropdown-link">
            {{user.name}}<img :src="user.userFace">
          </span>
          <el-dropdown-menu slot="dropdown">
            <el-dropdown-item>个人中心</el-dropdown-item>
            <el-dropdown-item>设置</el-dropdown-item>
            <el-dropdown-item>注销登录</el-dropdown-item>
          </el-dropdown-menu>
        </el-dropdown>
      </el-header>
      ...
  </div>
</template>

<script>
export default {
  name: "Home",
  data(){
    return {
      user: JSON.parse(window.sessionStorage.getItem('user'))
    }
  },
  computed: {
    routes(){
      return this.$store.state.routes
    }
  }
};
</script>
<style scoped>
  .homeHeader{
    background: #409eff;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 15px;
    box-sizing: border-box;
  }

  .homeHeader .title{
    font-size: 30px;
    font-family: 楷体;
    color: #fff;
    font-weight: 500;
  }

  .userInfo{
    cursor: pointer;
    color: #fff;
    text-align: center;
  }

  .el-dropdown-link img{
    background: #fff;
    width: 24px;
    height: 24px;
    border-radius: 24px;
    margin-left: 10px;
  }
</style>

④ 效果展示

image.png

2. 注销登录

① 监听点击事件

因为使用了ElementUI的下拉框,这个下拉框可以通过@command="方法"去监听组件中的item项的command属性,以此来监听点击事件对象并做相应处理;在此期间,添加了确认弹出框;退出后,需要清空sessionStorage中的tokenStr和user、vuex中的state.routes

<template>
  <div>
    <el-container>
      <el-header class="homeHeader">
        ...
        <el-dropdown class="userInfo" @command="handleCommand">
          <span class="el-dropdown-link">
            {{user.name}}
            <img :src="user.userFace">
          </span>
          <el-dropdown-menu slot="dropdown">
            <el-dropdown-item command="userinfo">个人中心</el-dropdown-item>
            <el-dropdown-item command="setting">设置</el-dropdown-item>
            <el-dropdown-item command="logout">注销登录</el-dropdown-item>
          </el-dropdown-menu>
        </el-dropdown>
      </el-header>
      ...
  </div>
</template>
<script>
export default {
  ...
  methods: {
    handleCommand(command){
      if(command == 'logout'){
          // 确认框
          this.$confirm('此操作将注销登录, 是否继续?', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
          }).then(() => {
          // 发送退出登录请求
          this.postRequest('/login/dologout');
          // 清空用户信息
          window.sessionStorage.removeItem('tokenStr');
          window.sessionStorage.removeItem('user');
          // 清空vuex中的菜单
          this.$store.commit('initRoutes',[]);
          // 跳转登陆页
          this.$router.replace('/');
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消操作'
          });          
        });
        
      }
    }
  }
};
</script>
<style scoped>
...
</style>

六、首页补充

1. 面包屑

...
<el-main>
  <el-breadcrumb v-if="this.$route.path!='/home'">
    <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
    <el-breadcrumb-item>{{this.$route.name}}</el-breadcrumb-item>
  </el-breadcrumb>
  <div class="homeWelcome" v-else>
    欢迎来到云办工系统
  </div>
  <router-view></router-view>
</el-main>
...

2. 直接输入页面跳转

情况解析:当用户直接访问登录后的页面,但是未登录,通过下面的处理,实现登录后直接跳转到用户所输入的页面;

// main.js
...
// 全局前置导航守卫
router.beforeEach((to, from, next) => {
  // 判断用户是否登录
  if(window.sessionStorage.getItem('tokenStr')){
    // 初始化导航栏
    initMenu(router, store);
    if(!window.sessionStorage.getItem('user')){
      // 判断用户信息是否存在
      return getRequest('/login/getUserInfo').then(resp=>{
        if(resp){
          window.sessionStorage.setItem('user', JSON.stringify(resp.obj))
          next()
        }
      })
    }
    next();
  }else{
    // 判断是否属于直接输入网址
    //   - 是,则将网址保存为参数,等登录成功后直接跳转到相应页面
    //   - 否,则常规跳转
    if(to.path == '/'){
      next()
    }else{
      next('/?redirect='+to.path)
    }
  }
})
...
// Login.vue/methods
submitLogin(){
    this.$refs.loginForm.validate((valid) => {
      if (valid) {
        ...
        this.postRequest('/login/doLogin', this.loginForm).then(resp => {
          if(resp){
            // 存储token
            ...
            // 跳转首页
            // this.$router.replace('/home')
            // 直接输入页面地址,登录后跳转到相应页面
            let path = this.$route.query.redirect;
            this.$router.replace( (path=='/' || path == undefined) ? '/home' : path )
          }
          // 重置数据
          ...
        })
      } else {
        ...
      }
  });
}
...

image.png image.png

image.png image.png

七、基础信息设置

1. 标签页

① 创建组件

image.png

② 使用ElementUI的Tags

<!-- SysBasic.vue -->
<template>
  <div>
    <el-tabs v-model="activeName" type="card">
        <el-tab-pane label="部门管理" name="DepMana">
          <DepMana></DepMana>
        </el-tab-pane>
        <el-tab-pane label="职位管理" name="PosMana">
          <PosMana></PosMana>
        </el-tab-pane>
        <el-tab-pane label="职称管理" name="JoblevelMana">
          <JoblevelMana></JoblevelMana>
        </el-tab-pane>
        <el-tab-pane label="奖惩规则" name="EcMana">
          <EcMana></EcMana>
        </el-tab-pane>
        <el-tab-pane label="权限组" name="PermissMana">
          <PermissMana></PermissMana>
        </el-tab-pane>
    </el-tabs>
  </div>
</template>

③ 引入组件

<!-- SysBasic.vue -->
<script>
  import DepMana from '@/components/sys/basic/DepMana.vue'
  import PosMana from '@/components/sys/basic/PosMana.vue'
  import JoblevelMana from '@/components/sys/basic/JoblevelMana.vue'
  import EcMana from '@/components/sys/basic/EcMana.vue'
  import PermissMana from '@/components/sys/basic/PermissMana.vue'
  export default {
    name: 'SysBasic',
    data(){
      return {
        activeName: 'DepMana'
      }
    },
    components: {
      DepMana,
      PosMana,
      JoblevelMana,
      EcMana,
      PermissMana
    }
  }
</script>
<style scoped>
</style>

④ 效果展示

image.png

2. 职位管理

① 页面设计

<template>
  <div>
    <div>
      <el-input
        class="addPosInput"
        size="small"
        placeholder="添加职位..."
        suffix-icon="el-icon-plus"
        v-model="pos.name">
      </el-input>
      <el-button type="primary" size="small">添加</el-button>
    </div>
    <div class="posManaMain">
      <el-table :data="positions" stripe border style="width: 70%">
        <el-table-column type="selection" width="55"></el-table-column>
        <el-table-column
          prop="id"
          label="编号"
          width="55">
        </el-table-column>
        <el-table-column
          prop="name"
          label="职位"
          width="120">
        </el-table-column>
        <el-table-column
          prop="createDate"
          label="创建时间"
          width="200">
        </el-table-column>
        <el-table-column
          label="操作">
          <template slot-scope="scope">
            <el-button
              size="mini"
              @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
            <el-button
              size="mini"
              type="danger"
              @click="handleDelete(scope.$index, scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'PosMana',
    data(){
      return {
        pos: {
          name: ''
        },
        positions:[]
      }
    },
    methods: {
      handleEdit(index, data){},
      handleDelete(index, data){}
    }
  }
</script>
<style scoped>
  .addPosInput{
    width: 300px;
    margin-right: 10px;
  }
  .posManaMain{
    margin-top: 10px;
  }
</style>

image.png

② 接口调用

...
<script>
  export default {
    name: 'PosMana',
    data(){
      return {
        ...
        positions:[]
      }
    },
    methods: {
      initPositions(){
        this.getRequest('/system/basic/pos/getAllPositions').then(resp=>{
          if(resp){
            // console.log(resp);
            this.positions = resp.obj;
          }
        })
      },
      ...
    },
    mounted(){
      this.initPositions()
    }
  }
</script>
...

③ 添加职位

<template>
  <div>
    <div>
      <el-input
        ...
        @keydown.enter.native="addPosition"
        v-model="pos.name">
      </el-input>
      <el-button type="primary" size="small" @click="addPosition">添加</el-button>
    </div>
    ...
  </div>
</template>
<script>
  export default {
    name: 'PosMana',
    data(){
      return {
        pos: {
          name: ''
        },
        ...
      }
    },
    methods: {
      addPosition(){
        if(this.pos.name){
          this.postRequest('/system/basic/pos/', this.pos).then(resp=>{
            if(resp){
              // 刷新列表
              this.initPositions()
              // 输入框置空
              this.pos.name = ''
            }
          })
        }else{
          this.$message.error('职位名称不能为空')
        }
      },
      ...
    },
    ...
  }
</script>

image.png image.png

④ 删除职位

<template>
  <div>
    ...
    <div class="posManaMain">
      <el-table :data="positions" stripe border style="width: 70%">
        ...
        <el-table-column
          label="操作">
          <template slot-scope="scope">
            ...
            <el-button
              size="mini"
              type="danger"
              @click="handleDelete(scope.$index, scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>
<script>
  export default {
    ...
    methods: {
      ...
      handleDelete(index, data){
        this.$confirm('此操作将永久删除[ '+data.name+' ]职位,是否继续', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          this.deleteRequest('/system/basic/pos/'+data.id).then(resp=>{
            if(resp){
              // 刷新列表
              this.initPositions()
            }
          })
          this.$message({
            type: 'success',
            message: '删除成功!'
          });
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消删除'
          });          
        });
      }
    },
    ...
  }
</script>

image.png

⑤ 编辑职位

<template>
  <div>
    ...
    <el-dialog
      title="提示"
      :visible.sync="dialogVisible"
      width="25%">
      <el-tag>职位名称</el-tag>
      <el-input size="small" class="updatePosInput" v-model="updatePos.name"></el-input>
      <span slot="footer" class="dialog-footer">
        <el-button size="small" @click="dialogVisible = false">取 消</el-button>
        <el-button size="small" type="primary" @click="doUpdatePos">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script>
  export default {
    name: 'PosMana',
    data(){
      return {
        ...
        dialogVisible: false,
        updatePos: { //编辑职位的变量
          name: ''
        }
      }
    },
    methods: {
      ...
      // 编辑职位
      handleEdit(index, data){
        Object.assign(this.updatePos, data); //直接赋值会造成未确定修改,列表中的值就改变
        this.updatePos.createDate = ''; //后端不允许修改创建时间
        this.dialogVisible = true;
      },
      doUpdatePos(){
        this.putRequest('/system/basic/pos/',this.updatePos).then(resp=>{
          if(resp){
            this.initPositions();
            this.dialogVisible = false;
          }
        })
      },
      ...
    },
    ...
  }
</script>
<style scoped>
  ...
  .updatePosInput{
    width: 200px;
    margin-left: 8px;
  }
</style>

image.png

⑥ 批量删除

思路分析:通过给el-table绑定监听checkbox修改的事件@selection-change="handleSelectionChange",修改数组multipleSelection;通过判断这个数组的长度决定是否启用批量删除按钮,通过给按钮添加点击事件,发送删除请求进行批量删除。

<template>
  <div>
    ...
    <div class="posManaMain">
      <el-table :data="positions" stripe border style="width: 70%" 
                @selection-change="handleSelectionChange">
        ...
      </el-table>
    </div>
    <el-button size="small" style="margin-top: 8px;" type="danger" 
               :disabled="this.multipleSelection.length == 0"
               @click="deleteMany">批量删除
    </el-button>
    ...
  </div>
</template>

<script>
  export default {
    name: 'PosMana',
    data(){
      return {
        ...
        multipleSelection: [] //批量删除数组
      }
    },
    methods: {
      ...
      // 批量删除
      handleSelectionChange(val){
        this.multipleSelection = val;
      },
      deleteMany(){
        this.$confirm('此操作将永久删除[ '+this.multipleSelection.length+' ]条职位,是否继续?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          let ids = '?'
          this.multipleSelection.forEach(item=>{
            ids += 'ids='+item.id+'&';
          })
          this.deleteRequest('/system/basic/pos/'+ids).then(resp=>{
            if(resp){
              // 刷新列表
              this.initPositions()
            }
          })
          this.$message({
            type: 'success',
            message: '删除成功!'
          });
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消删除'
          });          
        });
      }
    },
    ...
  }
</script>
...

image.png

3. 职称管理

① 页面设计

<template>
  <div>
    <div>
      <el-input class="addPosInput" size="small" placeholder="添加职称..."
        prefix-icon="el-icon-plus" @keydown.enter.native="addJobLevel"
        v-model="jl.name" style="width: 300px;">
      </el-input>
      <el-select size="small" v-model="jl.titleLevel" placeholder="职称等级" 
        style="margin-left: 8px;margin-right: 8px;">
        <el-option v-for="item in titleLevels" :key="item" 
                   :label="item" :value="item"
        ></el-option>
      </el-select>
      <el-button type="primary" size="small" @click="addJobLevel">添加</el-button>
    </div>
    <div style="margin-top: 10px">
      <el-table :data="jls" stripe border style="width: 70%" 
                @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="55"></el-table-column>
        <el-table-column
          prop="id"
          label="编号"
          width="55">
        </el-table-column>
        <el-table-column
          prop="name"
          label="职称名称"
          width="150">
        </el-table-column>
        <el-table-column
          prop="titleLevel"
          label="职称等级"
          width="150">
        </el-table-column>
        <el-table-column
          prop="createDate"
          label="创建时间"
          width="150">
        </el-table-column>
        <el-table-column
          prop="enabled"
          label="是否启用"
          width="150">
        </el-table-column>
        <el-table-column
          label="操作">
          <template slot-scope="scope">
            <el-button
              size="mini"
              @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
            <el-button
              size="mini"
              type="danger"
              @click="handleDelete(scope.$index, scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>
<script>
  export default {
    name: 'JoblevelMana',
    data() {
      return {
        jl: {
          name: '',
          titleLevel: ''
        },
        titleLevels: ['正高级', '副高级', '中级', '初级', '员级'],
        jls: []
      }
    },
    ...
  }
</script>
...

image.png

② 接口调用

...
<script>
  export default {
    ...
    methods: {
      // 初始化列表数据
      initJls(){
        this.getRequest('/system/basic/joblevel/getAllJoblevels').then(resp=>{
          if(resp){
            this.jls = resp.obj;
            // 数据置空
            this.jl.name = '';
            this.jl.titleLevel = '';
          }
        })
      },
      ...
    },
    mounted(){
      // 模板挂载后再初始化列表
      this.initJls();
    }
  }
</script>
...

③ 添加职称

...
<script>
  export default {
    ...
    data() {
      return {
        jl: {
          name: '',
          titleLevel: ''
        },
        ...
      }
    },
    methods: {
      ...
      ,
      ...
    },
    ...
  }
</script>
...

image.png

④ 删除职称

<template>
  <div>
    ...
    <div style="margin-top: 10px">
      <el-table :data="jls" stripe border style="width: 75%" @selection-change="handleSelectionChange">
        ...
        <el-table-column label="操作">
          <template slot-scope="scope">
            ...
            <el-button size="mini" type="danger"
              @click="handleDelete(scope.$index, scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>
<script>
  export default {
    ...
    methods: {
      ...
      handleDelete(index, data){
        this.$confirm('此操作将永久删除[ '+data.name+' ]职称,是否继续?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          this.deleteRequest('/system/basic/joblevel/'+data.id).then(resp=>{
            if(resp){
              // 刷新列表
              this.initJls()
            }
          })
          this.$message({
            type: 'success',
            message: '删除成功!'
          });
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消删除'
          });          
        });
      },
      ...
    },
    ...
  }
</script>
...

image.png

⑤ 修改职称

<template>
  <div>
    ...
    <div style="margin-top: 10px">
      <el-table :data="jls" stripe border style="width: 75%" @selection-change="handleSelectionChange">
        ...
        <el-table-column label="操作">
          <template slot-scope="scope">
            <el-button size="mini"
              @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
            ...
          </template>
        </el-table-column>
      </el-table>
    </div>
    <el-dialog
      title="提示"
      :visible.sync="dialogVisible"
      width="25%">
      <table>
        <tr>
          <td><el-tag>职称名称</el-tag></td>
          <td><el-input size="small" v-model="updateJl.name" 
                  style="margin-left: 8px;">
              </el-input>
          </td>
        </tr>
        <tr>
          <td><el-tag >职称等级</el-tag></td>
          <td>
            <el-select size="small" v-model="updateJl.titleLevel" 
              style="margin-left: 8px;">
              <el-option v-for="item in titleLevels" :key="item"
                :label="item" :value="item">
              </el-option>
            </el-select>
          </td>
        </tr>
        <tr>
          <td><el-tag>是否启用</el-tag></td>
          <td>
            <el-switch 
              v-model="updateJl.enabled"  active-color='#13ce66'
              inactive-color="#ff4949" active-text="已启用"
              inactive-text="未启用" style="margin-left: 6px">
            </el-switch>
          </td>
        </tr>
      </table>
      <span slot="footer" class="dialog-footer">
        <el-button size="small" @click="dialogVisible = false">取 消</el-button>
        <el-button size="small" type="primary" @click="doUpdate">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>
<script>
  export default {
    name: 'JoblevelMana',
    data() {
      return {
        ...
        dialogVisible: false,
        updateJl: {
          name: '',
          titleLevel: '',
          enabled: ''
        }
      }
    },
    methods: {
      ...
      handleEdit(index, data){
        Object.assign(this.updateJl, data);
        this.updateJl.createDate = ''; //后端不允许修改创建时间
        this.dialogVisible = true;
      },
      doUpdate(){
        this.putRequest('/system/basic/joblevel/', this.updateJl).then(resp=>{
          if(resp){
            this.initJls();
            this.dialogVisible = false;
          }
        })
      }
    },
    ...
  }
</script>
...

image.png

⑥ 批量删除

<template>
  <div>
    ...
    <div style="margin-top: 10px">
      <el-table :data="jls" stripe border style="width: 75%" @selection-change="handleSelectionChange">
        ...
      </el-table>
    </div>
    <el-button size="small" style="margin-top: 8px;" type="danger" 
               :disabled="this.multipleSelection.length == 0"
               @click="deleteMany">批量删除
    </el-button>
    ...
  </div>
</template>
<script>
  export default {
    name: 'JoblevelMana',
    data() {
      return {
        ...
        multipleSelection: []
      }
    },
    methods: {
      ...
      handleSelectionChange(val){
        this.multipleSelection = val;
      },
      deleteMany(){
        this.$confirm('此操作将永久删除[ '+this.multipleSelection.length+' ]条职称,是否继续?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          let ids = '?'
          this.multipleSelection.forEach(item=>{
            ids += 'ids='+item.id+'&';
          })
          this.deleteRequest('/system/basic/joblevel/'+ids).then(resp=>{
            if(resp){
              // 刷新列表
              this.initJls()
            }
          })
          this.$message({
            type: 'success',
            message: '删除成功!'
          });
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消删除'
          });          
        });
      }
    },
    ...
  }
</script>
...

image.png

4. 权限组管理

① 页面设计

<template>
  <div>
    <div class="permissManaTool">
      <el-input size="small" placeholder="请输入角色英文名" v-model="role.name">
        <template slot="prepend">ROLE_</template>
      </el-input>
      <el-input size="small" placeholder="请输入角色中文名" v-model="role.nameZh"></el-input>
      <el-button size="small" type="primary" icon="el-icon-plus">添加角色</el-button>
    </div>
    <div class="permissManaMain">
      <el-collapse v-model="activeName" accordion>
        <el-collapse-item title="一致性 Consistency" name="1">
          <div>与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;</div>
          <div>在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。</div>
        </el-collapse-item>
        <el-collapse-item title="反馈 Feedback" name="2">
          <div>控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;</div>
          <div>页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。</div>
        </el-collapse-item>
        <el-collapse-item title="效率 Efficiency" name="3">
          <div>简化流程:设计简洁直观的操作流程;</div>
          <div>清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;</div>
          <div>帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。</div>
        </el-collapse-item>
        <el-collapse-item title="可控 Controllability" name="4">
          <div>用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;</div>
          <div>结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。</div>
        </el-collapse-item>
      </el-collapse>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'PermissMana',
    data(){
      return {
        role: {
          name: '',
          nameZh: ''
        },
        activeName: '1'
      }
    }
  }
</script>

<style scoped>
  .permissManaTool{
    display: flex;
    justify-content: flex-start;
  }
  .permissManaTool .el-input{
    width: 300px;
    margin-right: 8px;
  }
  .permissManaMain{
    margin-top: 10px;
    width: 700px;
  }
</style>

image.png

② 权限树

<template>
  <div>
    ...
    <div class="permissManaMain">
      <el-collapse accordion @change="change">
        <el-collapse-item :title="r.remark" :name="r.id" v-for="(r, index) in roles" :key="index">
          <el-card class="box-card">
            ...
            <div>
              <el-tree :data="allMenus" :props="defaultProps" show-checkbox></el-tree>
            </div>
          </el-card>
        </el-collapse-item>
      </el-collapse>
    </div>
  </div>
</template>
<script>
  export default {
    name: 'PermissMana',
    data(){
      return {
        ...
        roles: [],  //所有角色
        allMenus: [],  //权限树
        defaultProps: {  //决定子树、展示名称
          children: 'children',
          label: 'name'
        }
      }
    },
    methods: {
      initRoles(){
        this.getRequest('/system/basic/permiss/getAllRoles').then(resp=>{
          if(resp){
            this.roles = resp.obj;
          }
        })
      },
      initAllMenu(){
        this.getRequest('/system/basic/permiss/menus').then(resp=>{
          if(resp){
            this.allMenus = resp.obj
          }
        })
      },
      change(rid){  //监听是否打开折叠卡
        if(rid){
          this.initAllMenu()
        }
      }
    },
    ...
  }
</script>
...

image.png

③ 根据用户展示

思路分析:根据点击的用户的id去后端查询权限,将返回的数组赋值给selectedMenus,通过将它动态el-tree的default-checked-keys属性,实现根据用户id展示权限树。

<template>
    ...
    <div class="permissManaMain">
      <el-collapse accordion @change="change" v-model="acticvName">
        <el-collapse-item :title="r.remark" :name="r.id" v-for="(r, index) in roles" :key="index">
          <el-card class="box-card">
            ...
            <div>
              <el-tree :data="allMenus" :props="defaultProps" show-checkbox
                node-key="id" :default-checked-keys="selectedMenus"
                ref="tree">
              </el-tree>
            </div>
          </el-card>
        </el-collapse-item>
      </el-collapse>
    </div>
  </div>
</template>
<script>
  export default {
    name: 'PermissMana',
    data(){
      return {
        ...
        selectedMenus: []
      }
    },
    methods: {
      ...
      // 根据用户id查询角色权限
      initSelectedMenus(rid){
        this.getRequest('/system/basic/permiss/mid/'+rid).then(resp=>{
          if(resp){
            this.selectedMenus = resp.obj;
          }
        })
      }
    },
    ...
  }
</script>
...

image.png

④ 修改权限

思路分析:通过监听折叠选项卡的change事件,传递点击的选项卡索引,当点击确认修改时,发送修改请求到后端,后端响应后刷新菜单并关闭选项卡。

<template>
  <div>
    ...
    <div class="permissManaMain">
      <el-collapse accordion @change="change" v-model="acticvName">
        <el-collapse-item :title="r.remark" :name="r.id" v-for="(r, index) in roles" :key="index">
          <el-card class="box-card">
            ...
            <div>
              <el-tree :data="allMenus" :props="defaultProps" show-checkbox
                node-key="id" :default-checked-keys="selectedMenus"
                ref="tree">
              </el-tree>
              <div style="display: flex; justify-content: flex-end;">
                <el-button size="mini" @click="cancelUpdate">取消修改</el-button>
                <el-button size="mini" type="primary" @click="doUpdate(r.id, index)">确认修改</el-button>
              </div>
            </div>
          </el-card>
        </el-collapse-item>
      </el-collapse>
    </div>
  </div>
</template>
<script>
  export default {
    name: 'PermissMana',
    data(){
      return {
        ...
        acticvName: -1  //修改后关闭的变量
      }
    },
    methods: {
      ...
      change(rid){  //监听是否打开折叠卡
        if(rid){
          // console.log(rid);
          this.initAllMenu();
          this.initSelectedMenus(rid);
        }
      },
      // 修改权限
      doUpdate(rid, index){
        let tree = this.$refs.tree[index];
        let selectedKeys = tree.getCheckedKeys(true); //获取选中的节点
        console.log(selectedKeys);
        // 注意这里后端参数为rId、menuIds
        let url = '/system/basic/permiss/updateMenuRoles/?rId='+rid;
        selectedKeys.forEach(key=>{
          url += '&menuIds='+key;
        })
        this.putRequest(url).then(resp=>{
          if(resp){
            this.initRoles();
            this.acticvName = -1;
          }
        })
      },
      cancelUpdate(){
        this.acticvName = -1;
      }
    },
    ...
  }
</script>
...

image.png

⑤ 添加、删除角色

<template>
  <div>
    <div class="permissManaTool">
      ...
      <el-button size="small" type="primary" icon="el-icon-plus" @click="doAddRole">添加角色</el-button>
    </div>
    <div class="permissManaMain">
      <el-collapse accordion @change="change" v-model="acticvName">
        <el-collapse-item :title="r.remark" :name="r.id" 
                          v-for="(r, index) in roles" :key="index">
          <el-card class="box-card">
            <div slot="header" class="clearfix">
              ...
              <el-button style="float: right; padding: 3px 0; color: red;" type="text" 
                         icon="el-icon-delete" @click="doDeleteRole(r)">
              </el-button>
            </div>
            ...
          </el-card>
        </el-collapse-item>
      </el-collapse>
    </div>
  </div>
</template>
<script>
  export default {
    name: 'PermissMana',
    data(){
      return {
        role: {  //添加角色
          name: '',
          remark: ''
        },
        ...
      }
    },
    methods: {
      ...
      // 添加角色 
      doAddRole(){
        if(this.role.name && this.role.remark){
          this.postRequest('/system/basic/permiss/',this.role).then(resp=>{
            if(resp){
              this.initRoles();
              this.role.name = '';
              this.role.remark = '';
            }
          })
        }else{
          this.$message.error('所有字段不能为空 ')
        }
      },
      // 删除角色
      doDeleteRole(role){
        this.$confirm('此操作将永久删除[ '+role.remark+' ]角色,是否继续?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          this.deleteRequest('/system/basic/permiss/'+role.id).then(resp=>{
            if(resp){
              // 刷新列表
              this.initRoles();
            }
          })
          this.$message({
            type: 'success',
            message: '删除成功!'
          });
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消删除'
          });          
        });
      },
      ...
    },
    ...
  }
</script>
...

image.png image.png

5. 部门管理

① 基本展示

思路分析:通过监听搜索输入框的变化,触发节点树对节点的筛选。

<template>
  <div style="width: 500px;">
    <el-input
      placeholder="请输入部门名称进行搜索..."
      prefix-icon="el-icon-search"
      v-model="filterText">
    </el-input>
    <el-tree
      :data="deps"
      :props="defaultProps"
      :filter-node-method="filterNode"
      ref="tree">
    </el-tree>
  </div>
</template>

<script>
  export default {
    name: 'DepMana',
    data() {
      return {
        filterText: '', //输入框的值
        deps: [], //接口返回的数据,用于展示
        defaultProps: { //节点树中子节点的字段配置
          children: 'children',
          label: 'name'
        }
      }
    },
    watch: {
      filterText(val) { 
        // 通过监听输入框的改变,触发节点树的展示
        this.$refs.tree.filter(val);
      }
    },
    methods: {
      // 初始化部门菜单
      initDeps(){
        this.getRequest('/system/basic/department/').then(resp=>{
          if(resp){
            this.deps = resp.obj;
          }
        })
      },
      // 用于搜索过滤节点
      filterNode(value, data) {
        // val->value:搜索值  data->返回的搜索结果
        // 函数返回TRUE展示全部数据,否则展示搜索结果
        if (!value) return true; 
        return data.name.indexOf(value) !== -1;
      }
    },
    // 模板渲染完成后初始化菜单
    mounted(){
      this.initDeps();
    }
  }
</script>
<style scoped>
</style>

image.png

② 操作设计

<template>
  <div style="width: 500px;">
    ...
    <el-tree
      ...
      :expand-on-click-node="false"
      ...>
      <span class="custom-tree-node" slot-scope="{ node, data }" 
            style="display: flex; justify-content: space-between; width: 100%">
        <span>{{ node.label }}</span>
        <span>
          <el-button type="primary" size="mini" class="depBtn"
            @click="() => showAddDep(data)">
            添加部门
          </el-button>
          <el-button type="danger" size="mini" class="depBtn"
            @click="() => deleteDep(node, data)">
            删除部门
          </el-button>
        </span>
      </span>
    </el-tree>
  </div>
</template>
...
<style scoped>
  .depBtn{
    padding: 2px;
  }
</style>

image.png

③ 添加部门

思路分析:为了提高用户体验,这里采用了“伪动态”添加节点到节点树的操作,需要注意的是,当添加子节点时,后端自然会修改isParent,但是前端因为不刷新,所以需要手动修改;

<template>
  <div style="width: 500px;">
    ...
    <el-tree ...>
      <span ...>
        <span>{{ node.label }}</span>
        <span>
          <el-button type="primary" size="mini" class="depBtn"
            @click="() => showAddDep(data)">
            添加部门
          </el-button>
          <el-button type="danger" size="mini" class="depBtn"
            @click="() => deleteDep(node, data)">
            删除部门
          </el-button>
        </span>
      </span>
    </el-tree>
    <el-dialog title="添加部门" :visible.sync="dialogVisible" width="30%"
      :before-close="handleClose">
      <div>
        <table>
          <tr>
            <td><el-tag>上级部门</el-tag></td>
            <td>{{preDep}}</td>
          </tr>
          <tr>
            <td><el-tag>部门名称</el-tag></td>
            <td><el-input v-model="addDep.name" placeholder="请输入部门名称..."></el-input></td>
          </tr>
        </table>
      </div>
      <span slot="footer" class="dialog-footer">
        <el-button @click="cancelAddDep">取 消</el-button>
        <el-button type="primary" @click="doAddDep">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>
<script>
  export default {
    name: 'DepMana',
    data() {
      return {
        ...
        deps: [], //接口返回的数据,用于展示
        dialogVisible: false, //弹出框是否可见
        addDep: { //添加的部门对象
          name: '',
          parentId: ''
        },
        preDep: '' //添加时的上级部门名称
      }
    },
    ...
    methods: {
      ...
      // 添加部门
      doAddDep(){
        this.postRequest('/system/basic/department/', this.addDep).then(resp=>{
          if(resp){
            // 因为刷新整个页面会关闭部门树,所以这里手动添加到界面
            this.addDep2Deps(this.deps, resp.obj);
            this.dialogVisible = false;
            this.initAddDep()
          }
        })
      },
      // 初始化添加部门输入框
      initAddDep(){
        this.addDep = {
          name: '',
          parentId: -1
        }
        this.preDep = ''
      },
      // 手动添加新部门进部门树
      addDep2Deps(deps, dep){
        for(let i = 0; i < deps.length; i++){
          let d = deps[i];
          if(d.id == dep.parentId){
            d.children = d.children.concat(dep);
            // 修改isParent
            if(d.children.length > 0){
              d.isParent = true;
            }
            return;
          }else{
            this.addDep2Deps(d.children, dep);
          }
        }
      }
    },
    cancelAddDep(){
      this.dialogVisible = false;
      this.initAddDep();
    },
    ...
  }
</script>
...

image.png

④ 删除部门

思路分析:这里也采用了“伪动态”的删除方式,也需要注意isParent的修改。

<template>
  <div style="width: 500px;">
    ...
    <el-tree ...>
      <span ...>
        <span>{{ node.label }}</span>
        <span>
          ...
          <el-button type="danger" size="mini" class="depBtn"
            @click="() => deleteDep(data)">
            删除部门
          </el-button>
        </span>
      </span>
    </el-tree>
    ...
  </div>
</template>
<script>
  export default {
    ...
    methods: {
      ...
      // 删除部门
      deleteDep(data){
        if(data.isParent){
          this.$message.error('父部门删除失败!')
        }else{
          this.$confirm('此操作将永久删除[ '+data.name+' ]该部门,是否继续?', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
          }).then(() => {
            this.deleteRequest('/system/basic/department/'+data.id).then(resp=>{
              if(resp){
                // 手动删除列表节点
                this.removeDepFromDeps(null, this.deps, data.id)
              }
            })
            this.$message({
              type: 'success',
              message: '删除成功!'
            });
          }).catch(() => {
            this.$message({
              type: 'info',
              message: '已取消删除'
            });          
          });
        }
      },
      removeDepFromDeps(p, deps, id){
        for(let i = 0; i < deps.length; i++){
          let d = deps[i];
          if(d.id == id){
            deps.splice(i, 1);
            if(deps.length==0){
              p.isParent = false;
            }
            return;
          }else{
            this.removeDepFromDeps(d, d.children, id)
          }
        }
      }
    },
    ...
  }
</script>
...

八、操作员管理

1. 页面设计

<template>
  <div>
    <div class="search-container">
      <el-input placeholder="通过用户名搜索用户..." prefix-icon="el-icon-search" 
        style="width: 400px;margin-right: 10px" size="small"></el-input>
      <el-button type="primary" icon="el-icon-search" size="small">搜索</el-button>
    </div>
    <div class="admin-container">
      <el-card class="admin-card" v-for="(admin, index) in admins" :key="index">
        <div slot="header" class="clearfix">
          <span>{{admin.name}}</span>
          <el-button style="float: right; padding: 3px 0; color: red;" 
            type="text" icon="el-icon-delete"></el-button>
        </div>
        <div class="userface-container">
          <img :src="admin.userFace" :alt="admin.name" :title="admin.name" class="admin-userface">
        </div>
        <div class="userinfo-container">
          <div>用户名:{{admin.name}}</div>
          <div>手机号码:{{admin.phone}}</div>
          <div>电话号码:{{admin.telphone}}</div>
          <div>地址:{{admin.address}}</div>
          <div>用户状态:
            <el-switch v-model="admin.enabled" 
              active-color="#13ce66" inactive-color="#ff4949"
              active-text="启用" inactive-text="禁用">
            </el-switch>
          </div>
          <div>用户角色:
            <el-tag type="success" v-for="(role, indexj) in admin.roles" :key="indexj"
              style="margin-right: 5px;">{{role.remark}}</el-tag>
            <el-button type="text" icon="el-icon-more"></el-button>
          </div>
          <div>备注:
            {{admin.remark}}
          </div>
        </div>
      </el-card>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'SysAdmin',
    data() {
      return {
        admins: []
      }
    },
    methods: {
      initAdmins(){
        this.getRequest('/system/admin/').then(resp=>{
          if(resp){
            this.admins = resp.obj;
          }
        })
      }
    },
    mounted(){
      this.initAdmins()
    }
  }
</script>

<style scoped>
  .search-container{
    display: flex;
    justify-content: center;
    margin-top: 10px;
  }
  .admin-container{
    display: flex;
    justify-content: space-around;
    flex-wrap: wrap;
    margin-top: 10px;
  }
  .userface-container{
    display: flex;
    justify-content: center;
  }
  .admin-userface{
    width: 72px;
    height: 72px;
    border-radius: 72px;
  }
  .admin-card{
    width: 420px;
    margin-top: 10px;
  }
  .userinfo-container{
    margin-top: 10px;
    font-size: 12px;
  }
</style>

image.png

2. 搜索功能

思路分析:通过给搜索框和变量keywords进行双向绑定,监听搜索按钮的点击事件,触发初始化界面的请求,即可刷新用户列表;

<template>
  <div>
    <div class="search-container">
      <el-input v-model="keywords" ...></el-input>
      <el-button @click="doSearch" ...>搜索</el-button>
    </div>
    ...
  </div>
</template>
<script>
  export default {
    name: 'SysAdmin',
    data() {
      return {
        admins: [],
        keywords: ''
      }
    },
    methods: {
      initAdmins(){
        this.getRequest('/system/admin/?keyWords='+this.keywords).then(resp=>{
          if(resp){
            this.admins = resp.obj;
          }
        })
      },
      doSearch(){
        this.initAdmins()
      }
    },
    // watch:{
    //   keywords(){
    //     this.initAdmins()
    //   }
    // },
    mounted(){
      this.initAdmins()
    }
  }
</script>
...

image.png image.png

3. 删除操作

<template>
  <div>
    ...
    <div class="admin-container">
      <el-card v-for="(admin, index) in admins" ...>
        <div ...>
          ...
          <el-button @click="deleteAdmin(admin)" ...></el-button>
        </div>
        ...
      </el-card>
    </div>
  </div>
</template>
<script>
  export default {
    ...
    methods: {
      ...
      deleteAdmin(admin){
        this.$confirm('此操作将永久删除[ '+admin.name+' ]该角色,是否继续?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          this.deleteRequest('/system/admin/'+admin.id).then(resp=>{
            if(resp){
              // 刷新列表
              this.initAdmins();
            }
          })
          this.$message({
            type: 'success',
            message: '删除成功!'
          });
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消删除'
          });          
        });
      }
    },
    ...
  }
</script>
...

image.png

4. 修改角色

思路分析:将用户角色后边的三个小点按钮,嵌套进弹出框Popover中,当点击时,向后端发送get请求,获取所有角色展示在下拉框中;因为前边已经遍历了这个管理员,所以可以在弹出框的show的对应showPopover方法中传入admin这个参数,获取已取得的角色,存入selectedRoles中,该变量用于下拉框的选中展示;当弹出框隐藏时,触发hidePopover方法,比对admin.roles和selectedRoles,通过flag决定是否发送更新请求。

<template>
  <div>
    ...
    <div class="admin-container">
      <el-card class="admin-card" v-for="(admin, index) in admins" :key="index">
        ...
        <div class="userinfo-container">
          ...
          <div>用户角色:
            ...
            <el-popover placement="right" width="300" title="角色列表" 
              trigger="click" @show="showPopover(admin)" @hide="hidePopover(admin)">
              <el-select v-model="selectedRoles" multiple placeholder="请选择" 
                style="width: 100%">
                <el-option v-for="(r, index) in allRoles"
                  :key="index" :label="r.remark" :value="r.id">
                </el-option>
              </el-select>
              <el-button type="text" icon="el-icon-more" slot="reference"></el-button>
            </el-popover>
          </div>
          ...
        </div>
      </el-card>
    </div>
  </div>
</template>
<script>
  export default {
    name: 'SysAdmin',
    data() {
      return {
        ...
        allRoles: [],  //所有角色
        selectedRoles: []  //选中的角色
      }
    },
    methods: {
      ...
      initAllRoles(){ //获取所有角色
        this.getRequest('/system/admin/roles').then(resp=>{
          if(resp){
            this.allRoles = resp.obj;
          }
        })
      },
      showPopover(admin){  //弹出时,类似于编辑角色的选择框
        this.initAllRoles(); 
        let roles = admin.roles; //获取已经选中的角色,这是一个对象
        this.selectedRoles = [];
        roles.forEach(r => {
          this.selectedRoles.push(r.id);
        });
      },
      hidePopover(admin){
        let flag = false;
        let myroles = [];
        Object.assign(myroles, admin.roles);
        if(myroles.length != this.selectedRoles.length){
          flag = true;
        }else{
          for(let i=0; i<myroles.length; i++){
            let role = myroles[i];
            for(let j=0; j<this.selectedRoles.length; j++){
              let sr = this.selectedRoles[j];
              if(role.id == sr){
                myroles.splice(i, 1);
                i--; //因为删除,后边的下标会-1,所以这里i--,让它匹配所以
                break;
              }
            }
          }
          if(myroles.length != 0){
            flag = true;
          }
        }
        if(flag){
          let url = '/system/admin/updateAdminRoles?adminId=' + admin.id;
          this.selectedRoles.forEach(sr=>{
            url += '&rids=' + sr;
          })
          this.putRequest(url).then(resp => {
            if(resp){
              this.initAdmins();
            }
          })
        }
      },
      ...
    },
    ...
  }
</script>
...

image.png