Skip to content

智慧园区 - 后台管理系统

第一章:项目介绍

一、简介

智慧园区是一个数字化园区管理项目,包括后台管理和可视化大屏两个部分,实现对园区内的楼宇、企业、员工、车辆和一体杆等进行数字化管理,以及通过园区 3D 模型实时展示园区概况。

后台管理系统负责园区内各项数据的管理,前台可视化项目负责重点园区数据的展现。

二、技术栈

后台管理平台

  1. Vue 2.6

  2. Vuex

  3. ElementUI

  4. vue-admin-template

    我们是基于 vue-admin。vue-admin 是基于 vue-admin-template 做了一些升级和改版之后的后台管理系统脚手架,内置了必要的安装包、目录结构划分、路由表设计等等,方便做==二次开发==,我们需要做的大部分是==填空题== ,架子搭建部分工作通常由团队 Leader 来做。

  5. js-cookie:一个简单轻量的 JavaScript API,用于处理浏览器 Cookie。

  6. day.js

前台可视化

  1. Vue 3
  2. Echarts
  3. Spline
  4. VScaleScreen

微前端接入

  1. qiankun

三、开发环境

node 版本不能低于 8.9、不能高于 17。

可以使用 n 模块进行 node 版本切换。

  • 全局安装 n:npm install -g n
  • 查看服务器上可用的版本:n ls-remote --all
  • 安装最新版 node:n latest
  • 安装某个具体版本:n 16.18.0
  • 查看已经安装过的 node 版本:n ls
  • 删除 14.13.1 版本:n rm 14.13.1
  • 切换版本:n

Windows 推荐 nvm,下载地址:coreybutler/nvm-windows

bash
nvm list [available]
nvm install <version> [arch]
nvm uninstall <version>
nvm use <version> [arch]
nvm current

因使用的 Node.js 版本过旧,nvm 也无法支持。所以直接下载安装包 release/v16.18.0。其他 Node.js 历史版本

四、接口文档

智慧园区接口文档

第二章:项目搭建

一、克隆项目结构

bash
# 克隆项目
git clone http://git.itcast.cn/heimaqianduan/vue-admin.git

# 安装依赖
npm i

# 启动项目
npm run start

二、熟悉目录结构

src 目录

  • assets - 静态资源(图片)
  • constants - 常量(不需要变动的数据)
  • directive - 全局指令
  • icons - 图标(png、svg、字体图标)
  • layout - 搭建项目的架子
  • views - 页面级组件、路由级别组件、业务组件

三、重要代码分析

1. 入口文件

2. 路由配置

3. 路由和菜单的关系

结论:路由表是菜单的数据支撑。

@/layout/components/Sidebar/index.vue

路由对象 meta 属性中的 icon 决定了显示的图标,title 决定了要显示的标题。

第三章:登录和 Token 管理

一、登录

1. 表单校验

使用 Element-UI 自带的表单校验。基本规则:

  • el-form 组件绑定表单对象(model)和规则对象(rules)。
  • 按照业务要求编写校验规则对象(rules)。
  • el-form-item 组件通过 prop 属性指定要使用的校验规则。

上面的实现后,只能做到提示。如果用户依然点击提交按钮,依旧会提交给服务器,怎么能强制要求用户按规则填好后才能提交?

vue
<template>
  <el-form ref="form">
    <el-form-item>
      <el-button type="primary" class="login_btn" @click="doLogin()">登录</el-button>
    </el-form-item>
  </el-form>
</template>

<script>
export default {
  name: 'Login',
  methods: {
    doLogin() {
      this.$refs.form.validate((valid) => {
        if (valid) {
          console.log('发起登录网络请求')
        }
      })
    }
  }
}
</script>

2. 实现登录

1)实现思路
思路:
1. 创建一个 user 的 store 模块,在 actions 里发起登录网络请求
2. 获取到 token,调用 mutations 的存储方法。同时也到 cookie 里存一份
3. 优化性能

1. 组件中给登录按钮绑定单击事件
2. 用户点击登录按钮后,调用 actions 中的方法
3. 记住我逻辑实现【TODO】
4. 清除 vue data 中搜集的用户数据
5. 跳转到主页
6. 提示登录成功
2)代码实现

@/store/modules/user.js

javascript
import { loginAPI } from '@/api/user'
import { setToken, getToken, removeToken } from '@/utils/auth'

export default {
  state: {
    userToken: getToken() || '' // 优化性能,其他页面也需要用到这个,不从浏览器 cookie (磁盘) 里取,直接从内存里取
  },
  mutations: {
    setUserToken(state, value) {
      state.userToken = value
      setToken(value)
    },
    removeUserToken(state) {
      state.userToken = ''
      removeToken()
    }
  },
  actions: {
    // 登录
    async doLogin(store, value) {
      const res = await loginAPI(value)
      // 在模块里调用本模块 mutations 方法,可以省略模块名
      store.commit('setUserToken', res.data.token)
    }
  }
}

@/views/Login/index.vue

vue
<el-button
  type="primary"
  class="login_btn"
  @click="submitForm('form')"
>
  登录
</el-button>

<script>
// submitForm 方法在 methods 选项中
submitForm (formName) {
  this.$refs[formName].validate(valid => {
    if (valid) {
      console.log('校验成功!')
      this.$store.dispatch('doLogin', this.form)
      // remeber逻辑
      if (this.remember) {
        localStorage.setItem(FORMDATA_KEY, JSON.stringify(this.form))
      } else {
        localStorage.removeItem(FORMDATA_KEY)
      }
      this.form = {
        username: '',
        password: ''
      }
      // 实现哪个页面退出的,登录时继续跳转到对应的页面
      if (this.$route.query.redirect) {
        this.$router.push(this.$route.query.redirect)
      } else {
        this.$router.push('/')
      }
    } else {
      console.log('校验失败!')
      return false
    }
  })
}
</script>

二、记住我

交互表现

1)如果当前用户选中了checkbox,点击登录之后,再次回到登录,应该把之前输入的用户名和密码回填到输入框里面。

2)如果当前用户取消了 checkbox,点击登录之后,再次回到登录,应该把之前存到本地的数据清除掉。

实现思路

1)完成选择框的双向绑定,得到一个 true 或者 false 的选中状态。

2)如果当前为 true,点击登录时,表示要记住,把当前的用户名和密码存入本地。

3)组件初始化的时候,从本地取账号和密码,把账号密码存入用来双向绑定的 form 身上。

4)如果当前用户没有记住,状态为 false,点击登录的时候要把之前的数据清空。

具体代码

@/views/Login/index.vue 中的登录方法中添加记住我的逻辑。

javascript
// remeber逻辑
if (this.remember) {
  localStorage.setItem(FORMDATA_KEY, JSON.stringify(this.form))
} else {
  localStorage.removeItem(FORMDATA_KEY)
}

在 created 生命周期中,每次加载页面,都先看 localStorage 是否存了用户账号、密码信息,有就给 data 选项中添数据。

javascript
created() {
  const userString = localStorage.getItem(FORMDATA_KEY)
  if (!userString) return
  const { username, password } = JSON.parse(userString)
  this.form.username = username
  this.form.password = password
}

三、退出登录

1)在 @/store/modules/user.js 中添加清除方法。

javascript
mutations: {
  removeUserToken (state) {
    state.userToken = ''
    removeToken()
  }
},

2)给 @/layout/components/Navbar.vue 绑定单击事件,调用上面的 removeUserToken

vue
<template>
  <!-- 省略... -->
  <el-dropdown-item divided @click.native="logout">
    <span style="display: block">退出登录</span>
  </el-dropdown-item>
  <!-- 省略... -->
</template>

<script>
// 退出登录
logout() {
  this.$store.commit('removeUserToken')
  this.$router.push(`/login?redirect=${this.$route.fullPath}`)
}
</script>

四、细节优化

1. axios 拦截器添加 token

为什么这里没有加 Bearer?其实在后端处理了,前端就不用处理了。

javascript
import store from '@/store'

// 请求拦截器
service.interceptors.request.use(
  config => {
    const token = store.state.user.userToken
    if (token) {
      config.headers.Authorization = token
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

2. Token 控制路由跳转

分析

目前是只要知道路由,就能跳过登录,怎么办?

效果:如果用户没有登录,不让用户进入到页面中。

解决方案:使用路由守卫,通过 token 的有无来控制路由的跳转。

具体代码

1)新建 @/permission.js 文件。

javascript
// 权限控制
import router from '@/router'
import store from '@/store'

// 路由白名单
const WHITE_LIST = ['/login', '/404']

router.beforeEach((to, from, next) => {
  const token = store.state.user.userToken
  if (token) {
    // 有token
    if (to.path === '/login') {
      next('/')
    } else {
      next()
    }
  } else {
    // 没有token
    if (WHITE_LIST.includes(to.path)) {
      next()
    } else {
      next('/login')
    }
  }
})

2)在入口文件 @/main.js 引入生效

javascript
// 在router引入之后引入
import './permission'

3. 接口错误统一处理 & Token 失效处理

需求:接口报错的时候提示用户到底是哪里错误。

            接口数量很多,需要实现统一管控,不管哪个接口报错了,都能监控到,而且给出提示。

javascript
import { Message } from 'element-ui'

// 响应拦截器
service.interceptors.response.use(
  response => {
    return response.data
  },
  error => {
    // Token 401处理
    if (error.response.status === 401) {
      // 清空用户数据
      store.commit('user/removeUserToken')
      // 跳转到登录
      router.push('/login')
    }
    // 错误统一处理
    Message.error(error.response.data.msg)
    return Promise.reject(error)
  }
)

export default service

第四章:行车管理 - 月卡管理

一、页面布局

@/views/Car/CarCard/index.vue

点我查看代码
vue
<template>
  <div class="card-container">
    <!-- 搜索区域 -->
    <div class="search-container">
      <span class="search-label">车牌号码:</span>
      <el-input clearable placeholder="请输入内容" class="search-main" />
      <span class="search-label">车主姓名:</span>
      <el-input clearable placeholder="请输入内容" class="search-main" />
      <span class="search-label">状态:</span>
      <el-select>
        <el-option v-for="item in []" :key="item.id" />
      </el-select>
      <el-button type="primary" class="search-btn">查询</el-button>
    </div>
    <!-- 新增删除操作区域 -->
    <div class="create-container">
      <el-button type="primary">添加月卡</el-button>
      <el-button>批量删除</el-button>
    </div>
    <!-- 表格区域 -->
    <div class="table">
      <el-table style="width: 100%" :data="[]">
        <el-table-column type="index" label="序号" />
        <el-table-column label="车主名称" />
        <el-table-column label="联系方式" />
        <el-table-column label="车牌号码" />
        <el-table-column label="车辆品牌" />
        <el-table-column label="剩余有效天数" />
        <el-table-column label="操作" fixed="right" width="180">
          <template #default="scope">
            <el-button size="mini" type="text">续费</el-button>
            <el-button size="mini" type="text">查看</el-button>
            <el-button size="mini" type="text">编辑</el-button>
            <el-button size="mini" type="text">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <div class="page-container">
      <el-pagination layout="total, prev, pager, next" :total="0" />
    </div>
    <!-- 添加楼宇 -->
    <el-dialog title="添加楼宇" width="580px">
      <!-- 表单接口 -->
      <div class="form-container">
        <!-- <el-form ref="addForm" :model="addForm" :rules="addFormRules">
          <el-form-item label="楼宇名称" prop="name">
            <el-input v-model="addForm.name" />
          </el-form-item>
          <el-form-item label="楼宇层数" prop="floors">
            <el-input v-model="addForm.floors" />
          </el-form-item>
          <el-form-item label="在管面积" prop="area">
            <el-input v-model="addForm.area" />
          </el-form-item>
          <el-form-item label="物业费" prop="propertyFeePrice">
            <el-input v-model="addForm.propertyFeePrice" />
          </el-form-item>
        </el-form> -->
      </div>
      <template #footer>
        <el-button size="mini">取 消</el-button>
        <el-button size="mini" type="primary">确 定</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script>
export default {}
</script>

<style lang="scss" scoped>
.card-container {
  padding: 20px;
  background-color: #fff;
}

.search-container {
  display: flex;
  align-items: center;
  border-bottom: 1px solid rgb(237, 237, 237, 0.9);
  padding-bottom: 20px;
  .search-main {
    width: 220px;
    margin-right: 10px;
  }
  .search-btn {
    margin-left: 20px;
  }
}
.create-container {
  margin: 10px 0px;
}
.page-container {
  padding: 4px 0px;
  text-align: right;
}
.form-container {
  padding: 0px 80px;
}
</style>

二、数据展示

1. 列表展示

1)封装 api

@/api/car.js

javascript
export function getCardListAPI(params) {
  return request({
    url: '/parking/card/list',
    method: 'get',
    params
  })
}
2)组件中调用 api

@views/Car/CarCard/index.vue

javascript
// 渲染方法
async getCarCardList() {
  const { data } = await getCardListAPI({
    page: this.page,
    pageSize: this.pageSize,
    carNumber: this.carNumber,
    personName: this.personName,
    cardStatus: this.cardStatus
  })
  this.tableData = data.rows
  this.total = data.total
},

// 在 created 生命周期中调用
created() {
  this.getCarCardList()
},
3)添加列表组件
vue
<el-table style="width:100%;" :data="tableData">
  <el-table-column type="index" label="序号" :index="indexMethod" />
  <el-table-column label="车主名称" prop="personName" />
  <el-table-column label="联系方式" prop="phoneNumber" />
  <el-table-column label="车牌号码" prop="carNumber" />
  <el-table-column label="车辆品牌" prop="carBrand" />
  <el-table-column label="剩余有效天数" prop="totalEffectiveDate" />
  <el-table-column
    label="状态"
    prop="cardStatus"
  />
  <el-table-column label="操作" fixed="right" width="180">
    <template #default="scoped">
      <el-button size="mini" type="text">续费</el-button>
      <el-button size="mini" type="text">查看</el-button>
      <el-button size="mini" type="text">编辑</el-button>
      <el-button size="mini" type="text">删除</el-button>
    </template>
  </el-table-column>
</el-table>

:data="tableData" 是要展示的数据。数据格式 [{}, {}, {} ...]

prop="personName":tableData 数据中每一项中的 property。

2. 状态显示

添加 formatter 属性,formatStatus 是方法,作用是把原始的值在方法里操作后,返回值会显示在单元格中。

vue
<el-table-column label="状态" prop="cardStatus" :formatter="formatStatus" />

<script>
formatStatus(row, column, cellValue, index) {
  const MAP = {
    0: '可用',
    1: '已过期'
  }
  return MAP[row.cardStatus] // 简写: MAP[cellValue]
}
</script>

3. 序号处理

Element 表格自定义索引

vue
<el-table
  :data="tableData"
  style="width: 100%">
  <el-table-column
    type="index"
    :index="indexMethod">
  <!-- ... -->
</el-table>

<script>
methods: {
  indexMethod(index) {
    return (当前页数 - 1) * 每页显示的条数 + index + 1;
  }
}
</script>

4. 分页展示

1)实现思路
思路:
1. 引入分页组件
2. 分页组件中所需数据与 data 关联
3. @size-change 和 @current-change 做好修改当前所在页数或每页显示数量、重新渲染页面列表数据

核心:分页组件也是调用渲染数据方法,只是部分请求参数改变。
2)代码实现

Element 分页

vue
<el-pagination
  :current-page="page"
  :page-sizes="[10, 15, 20, 30]"
  :page-size="pageSize"
  layout="total, sizes, prev, pager, next, jumper"
  :total="total"
  @size-change="handleSizeChange"
  @current-change="handleCurrentChange"
/>

<script>
handleSizeChange(val) {
  this.pageSize = val
  this.page = 1
  this.getCarCardList()
},
handleCurrentChange(val) {
  this.page = val
  this.getCarCardList()
},
</script>

三、搜索功能实现

1. 搜集用户输入的数据

这里主要说下 el-select 如何给用户呈现,以及如何搜集此类表单数据。

Element 选择器

vue
<el-select v-model="cardStatus">
  <el-option
    v-for="item in cardStatusList"
    :key="item.value"
    :label="item.label"
    :value="item.value"
  />
</el-select>

<script>
data () {
  return {
    // ...
    carNumber: '',
    personName: '',
    cardStatus: '',
    cardStatusList: [
      { label: '全部', value: '' },
      { label: '正常', value: 0 },
      { label: '过期', value: 1 }
    ]
  }
}
</script>

2. 查询按钮绑定回调

vue
<el-button type="primary" class="search-btn" @click="doSearch">查询</el-button>

<script>
doSearch() {
  this.page = 1
  this.getCarCardList()
}
</script>

四、新增月卡实现

1. 编写页面

@/views/Car/CarCard/addMonthCard.vue

点我查看代码
vue
<template>
  <div class="add-card">
    <header class="add-header">
      <el-page-header content="增加月卡" @back="$router.back()" />
    </header>
    <main class="add-main">
      <div class="form-container">
        <div class="title">车辆信息</div>
        <div class="form">
          <el-form label-width="100px">
            <el-form-item label="车主姓名">
              <el-input />
            </el-form-item>
            <el-form-item label="联系方式">
              <el-input />
            </el-form-item>
            <el-form-item label="车辆号码">
              <el-input />
            </el-form-item>
            <el-form-item label="车辆品牌">
              <el-input />
            </el-form-item>
          </el-form>
        </div>
      </div>
      <div class="form-container">
        <div class="title">最新一次月卡缴费信息</div>
        <div class="form">
          <el-form label-width="100px">
            <el-form-item label="有效日期">
              <el-input />
            </el-form-item>
            <el-form-item label="支付金额">
              <el-input />
            </el-form-item>
            <el-form-item label="支付方式">
              <el-select>
                <el-option
                  v-for="item in [{}]"
                  :key="item.industryCode"
                  :value="item.industryCode"
                  :label="item.industryName"
                />
              </el-select>
            </el-form-item>
          </el-form>
        </div>
      </div>
    </main>
    <footer class="add-footer">
      <div class="btn-container">
        <el-button>重置</el-button>
        <el-button type="primary">确定</el-button>
      </div>
    </footer>
  </div>
</template>

<script>
export default {}
</script>

<style scoped lang="scss">
.add-card {
  background-color: #f4f6f8;
  height: 100vh;

  .add-header {
    display: flex;
    align-items: center;
    padding: 0 20px;
    height: 64px;
    background-color: #fff;

    .left {
      span {
        margin-right: 4px;
      }
      .arrow {
        cursor: pointer;
      }
    }

    .right {
      text-align: right;
    }
  }

  .add-main {
    background: #f4f6f8;
    padding: 20px 130px;

    .form-container {
      background-color: #fff;

      .title {
        height: 60px;
        line-height: 60px;
        padding-left: 20px;
      }

      .form {
        margin-bottom: 20px;
        padding: 20px 65px 24px;

        .el-form {
          display: flex;
          flex-wrap: wrap;

          .el-form-item {
            width: 50%;
          }
        }
      }
    }
    .preview {
      img {
        width: 100px;
      }
    }
  }

  .add-footer {
    position: fixed;
    bottom: 0;
    width: 100%;
    padding: 24px 50px;
    color: #000000d9;
    font-size: 14px;
    background: #fff;
    text-align: center;
  }
}
</style>

2. 配置路由完成跳转

javascript
{
  path: '/car/addMonthCard',
  component: () => import('@/views/car/CarCard/addMonthCard')
}
vue
<el-button type="primary" @click="$router.push('/car/addMonthCard')">添加月卡</el-button>

3. 表单校验

1)添加校验规则

较复杂的校验可以通过设置一个校验函数来做,给校验选项添加一个 validator 选项,值为校验函数,在校验函数中编写校验规则。

vue
<script>
export default {
  data() {
    return {
      carInfoRules: {
        carNumber: [
          {
            required: true, message: '请输入车辆号码', trigger: 'blur'
          },
          {
            validator: validaeCarNumber, trigger: 'blur'
          }
        ]
      }
    }
  },
  method: {
    validaeCarNumber(rule, value, callback) {
      const plateNumberRegex = /^[\u4E00-\u9FA5][\da-zA-Z]{6}$/
      if (plateNumberRegex.test(value)) {
        callback()
      } else {
        callback(new Error('请输入正确的车牌号'))
      }
    }
  }
}
</script>
2)统一校验俩个表单
vue
<el-form ref="carInfoForm"></el-form>
<el-form ref="feeForm"></el-form>


methods: {
  confirmAdd() {
    this.$refs.carInfoForm.validate(valid => {
      if (valid) {
        this.$refs.feeForm.validate(valid => {
          if (valid) {
            // 全部校验通过 TODO
          }
        })
      }
    })
  }
}

4. 封装接口

@/api/car.js

javascript
/**
 * 新增月卡
 */
export function createCardAPI(data) {
  return request({
    url: '/parking/card',
    method: 'POST',
    data
  })
}

5. 处理表单数据提交

javascript
methods: {
  confirmAdd() {
    this.$refs.carInfoForm.validate(valid => {
      if (valid) {
        this.$refs.feeForm.validate(valid => {
          if (valid) {
            // 全部校验通过
             // 参数处理
            const payload = {
              ...this.feeForm,
              ...this.carInfoForm,
              // 单独处理时间
              cardStartDate: this.feeForm.payTime[0],
              cardEndDate: this.feeForm.payTime[1]
            }
            // 删掉多余字段
            delete payload.payTime
            await createCardAPI(payload)
            this.$message.success('新增成功')
            this.$router.back()
          }
        })
      }
    })
  }
}

6. 重置表单

重置表单主要做两件事:① 清空输入数据;② 清空校验错误。

javascript
// 重置表单
resetForm() {
  this.$refs.feeForm.resetFields()
  this.$refs.carInfoForm.resetFields()
}

五、编辑月卡

1. 跳转到修改页,且携带 ID

vue
<el-button size="mini" type="text" @click="editCard(scope.row.id)">编辑</el-button>
  
editCard(id) {
  this.$router.push({
    path: '/cardAdd',
    query: {
      id
    }
  })
}

2. 回填数据

1)封装请求接口
javascript
// 获取月卡详情
export function getCardDetailAPI(id) {
  return request({
    url: `/parking/card/detail/${id}`
  })
}
2)在组件中使用
javascript
// 获取详情
async getDetail() {
  const res = await getCardDetailAPI(this.id)
  // 回填车辆信息表单
  const { carInfoId, personName, phoneNumber, carNumber, carBrand } = res.data
  this.carInfoForm = {
    personName, phoneNumber, carNumber, carBrand, carInfoId
  }
  // 回填缴费信息表单
  const { rechargeId, cardStartDate, cardEndDate, paymentAmount, paymentMethod } = res.data
  this.feeForm = {
    rechargeId,
    paymentAmount,
    paymentMethod,
    payTime: [cardStartDate, cardEndDate]
  }
}

created() {
  if (this.id) {
    this.getDetail()
  }
}

六、删除功能

1. 单个删除

思路:
1. 绑定单击事件,需要传过来一个 ID 实参
2. 询问用户是否删除
3. 根据传过来的 ID 调用删除 API
4. 检查当前页面是否只有一条数据,如果是,就 currentPage--
5. 重新渲染页面列表数据
6. 提示用户删除成功
1)封装 api
javascript
// 删除月卡
export function delCardAPI(id) {
  return request({
    url: `/parking/card/${id}`,
    method: 'DELETE'
  })
}
2)组件中使用
vue
// 绑定事件
<el-button size="mini" type="text" @click="delCard(scope.row.id)">删除</el-button>

// 导入接口
import { delCardAPI } from '@/apis/card'

// 删除逻辑
delCard(id) {
  this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(async() => {
    await delCardAPI(id)
    if(this.cardList.length===1 && this.params.page>1){
      this.params.page--
    }
    this.getCardList()
    this.$message({
      type: 'success',
      message: '删除成功!'
    })
  }).catch((error) => {
    // 用户点击取消了
  })
}

2. 批量删除

思路:
1. 查找 element 文档,给页面 table 添加多选框
   https://element.eleme.cn/#/zh-CN/component/table#duo-xuan
2. element table 有一个 selection-change 属性,当用户点击多选框时,会自动触发这个事件,传递一个实参为用户此时选中的行数据,为数组
3. 在 selection-change 回调里修改选中的 state
4. 当用户点击多选删除按钮时
   1. 询问用户是否删除
   2. 根据选中的 state 调用删除 API
   3. 重新渲染页面列表数据
   4. 提示用户删除成功
1)修改页面

实现多选:手动添加一个 el-table-column,设 type 属性为 selection 即可;默认情况下若内容过多会折行显示,若需要单行显示可以使用 show-overflow-tooltip 属性,它接受一个 Boolean,为 true 时多余的内容会在 hover 时以 tooltip 的形式显示出来。

vue
<el-table style="width:100%;"
  :data="cardList"
  @selection-change="handleSelectionChange">
    <el-table-column
      type="selection"
      width="55"
    />
    <!-- 省略 -->
</el-table>

data() {
  return {
    // 已选择列表
    selectedCarList: []
  }
}

methods:{
  handleSelectionChange(rowList) {
    this.selectedCarList = rowList
  }
}
2)处理数据调用接口
javascript
delCartList() {
  if (this.selectedCarList.length <= 0) {
    this.$message.warning('还未选中要删除的数据')
    return
  }
  this.$confirm('此操作将永久删除选择的月卡, 是否继续?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(async() => {
    // 处理id
    const idList = this.selectedCarList.map(item => item.id)
    await delCardListAPI(idList.join(','))
    this.getCardList()
    this.$message({
      type: 'success',
      message: '删除成功!'
    })
  }).catch((error) => {
    // 点击了取消按钮
  })
}

第五章:园区管理 - 企业管理

一、页面布局

@\views\Park\Enterprise\index.vue

点我查看代码
vue
<template>
  <div class="building-container">
    <!-- 搜索区域 -->
    <div class="search-container">
      <div class="search-label">企业名称:</div>
      <el-input clearable placeholder="请输入内容" class="search-main"/>
      <el-button type="primary">查询</el-button>
    </div>
    <div class="create-container">
      <el-button type="primary" >添加企业</el-button>
    </div>
    <!-- 表格区域 -->
    <div class="table">
      <el-table style="width: 100%" :data="[]">
        <el-table-column type="index" label="序号" />
        <el-table-column label="企业名称" width="320" prop="name" />
        <el-table-column label="联系人" prop="contact" />
        <el-table-column
          label="联系电话"
          prop="contactNumber"
        />
        <el-table-column label="操作">
          <template #default="scope">
            <el-button size="mini" type="text">添加合同</el-button>
            <el-button size="mini" type="text">查看</el-button>
            <el-button size="mini" type="text">编辑</el-button>
            <el-button size="mini" type="text">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <div class="page-container">
      <el-pagination
        layout="total, prev, pager, next"
      />
    </div>
  </div>
</template>

<style lang="scss" scoped>
.department-container {
  padding: 10px;
}

.search-container {
  display: flex;
  align-items: center;
  border-bottom: 1px solid rgb(237, 237, 237, .9);
  ;
  padding-bottom: 20px;

  .search-label {
    width: 100px;
  }

  .search-main {
    width: 220px;
    margin-right: 10px;
  }
}
.create-container{
  margin: 10px 0px;
}
.page-container{
  padding:4px 0px;
  text-align: right;
}
.form-container{
  padding:0px 80px;
}
</style>

二、数据展示

1. 基本的展示

1)在 @\api\enterprise.js 中封装 API

js
import request from '@/utils/request'

// 获取企业列表
export function getEnterpriseListAPI(params) {
  return request({
    url: '/park/enterprise',
    params
  })
}

2)组件中获取数据

vue
<script>
import { getEnterpriseListAPI } from '@/api/enterprise'

export default {
  name: 'Building',
  data() {
    return {
      exterpriseList: [],
      params: {
        page: 1,
        pageSize: 10
      }
    }
  },
  mounted() {
    this.getExterpriseList()
  },
  methods: {
    async getExterpriseList() {
      const res = await getEnterpriseListAPI(this.params)
      this.exterpriseList = res.data.rows
    }
  }
}
</script>

3)绑定模版

核心思路:① 通过 data 属性给 el-table 组件绑定数据列表。② 通过 prop 属性指定当前列要渲染的字段名称。

vue
<template>
  <div class="building-container">
    <!-- 表格区域 -->
    <div class="table">
      <el-table style="width: 100%" :data="exterpriseList">
        <el-table-column type="index" label="序号" />
        <el-table-column label="企业名称" width="320" prop="name" />
        <el-table-column label="联系人" prop="contact" />
        <el-table-column
          label="联系电话"
          prop="contactNumber"
        />
        <el-table-column label="操作">
          <template #default="scope">
            <el-button size="mini" type="text">添加合同</el-button>
            <el-button size="mini" type="text">查看</el-button>
            <el-button size="mini" type="text">编辑</el-button>
            <el-button size="mini" type="text">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>

2. 分页管理

1)渲染分页

jsx
data() {
  return {
    total: 0
  }
}


async getExterpriseList() {
  const res = await getEnterpriseListAPI(this.params)
  this.exterpriseList = res.data.rows
  this.total = res.data.total
}


<el-pagination
  layout="total, prev, pager, next"
  :total="total"
  :page-size="params.pageSize"
/>

2)点击实现分页切换

jsx
<el-pagination
  @current-change="pageChange"
/>

pageChange(page) {
  // 更改参数
  this.params.page = page
  // 重新获取数据渲染
  this.getExterpriseList()
}

3. 序号处理

js
<el-table-column type="index" label="序号" :index="indexMethod" />

 indexMethod(row) {
    return (this.params.page - 1) * this.params.pageSize + 1 + row
},

三、查询搜索

需求:

① 用户输入查询内容之后,点击查询按钮以当前输入关键词做为参数获取数据。

② 点击清空按钮时复原初始数据。

1. 搜集用户输入的数据

js
// 增加新的name查询字段
data() {
  return {
    params: {
      page: 1,
      pageSize: 10,
      name: '' // 增加字段name
    }
  }
}

2. 查询按钮绑定回调

clear 事件是 element ui 组件库自带的。在点击由 clearable 属性生成的清空按钮时触发。

js
// 绑定查询回调
<div class="search-container">
  <div class="search-label">企业名称:</div>
  <el-input v-model="params.name" clearable @clear="doSearch" placeholder="请输入内容" class="search-main" />
  <el-button type="primary" @click="doSearch">查询</el-button>
</div>

// 准备查询后调方法
doSearch() {
  this.params.page = 1
  this.getExterpriseList()
}

四、添加企业

1. 页面布局

1)准备组件

@\views\park\enterprise\Add.vue

点我查看代码
vue
<template>
  <div class="add-enterprise">
    <header class="add-header">
      <div class="left">
        <span class="arrow" @click="$router.back()"><i class="el-icon-arrow-left" />返回</span>
        <span>|</span>
        <span>添加企业</span>
      </div>
      <div class="right">
        系统管理员
      </div>
    </header>
    <main class="add-main">
      <div class="form-container">
        <div class="title">企业信息</div>
        <div class="form">
          <el-form ref="ruleForm" label-width="100px">
            <el-form-item label="企业名称" prop="name">
              <el-input v-model="addForm.name" />
            </el-form-item>
            <el-form-item label="法人" prop="name">
              <el-input v-model="addForm.legalPerson" />
            </el-form-item>
            <el-form-item label="注册地址" prop="name">
              <el-input v-model="addForm.registeredAddress" />
            </el-form-item>
            <el-form-item label="所在行业" prop="name">
              <el-select v-model="addForm.industryCode" />
            </el-form-item>
            <el-form-item label="企业联系人" prop="name">
              <el-input v-model="addForm.contact" />
            </el-form-item>
            <el-form-item label="联系电话" prop="name">
              <el-input v-model="addForm.contactNumber" />
            </el-form-item>
            <el-form-item label="营业执照" prop="name" />
          </el-form>
        </div>
      </div>
    </main>
    <footer class="add-footer">
      <div class="btn-container">
        <el-button>重置</el-button>
        <el-button type="primary">确定</el-button>
      </div>
    </footer>
  </div>
</template>

<script>
export default {
  data() {
    return {
      addForm: {
        name: '', // 企业名称
        legalPerson: '', // 法人
        registeredAddress: '', // 注册地址
        industryCode: '', // 所在行业
        contact: '', // 企业联系人
        contactNumber: '', // 联系人电话
        businessLicenseUrl: '', // 营业执照url
        businessLicenseId: '' // 营业执照id
      }
    }
  }
}
</script>

<style scoped lang="scss">
.add-enterprise {
  background-color: #f4f6f8;
  height: 100vh;

  .add-header {
    display: flex;
    justify-content: space-between;
    padding: 0 20px;
    height: 64px;
    line-height: 64px;
    font-size: 16px;
    font-weight: bold;
    background-color: #fff;

    .left {
      span {
        margin-right: 4px;
      }
      .arrow{
        cursor: pointer;
      }
    }

    .right {
      text-align: right;
    }
  }

  .add-main {
    background: #f4f6f8;
    padding: 20px 130px;

    .form-container {
      background-color: #fff;
      .title {
        height: 60px;
        line-height: 60px;
        padding-left:20px;
      }
      .form {
        margin-bottom: 20px;
        padding: 20px 65px 24px;
        .el-form{
          display: flex;
          flex-wrap: wrap;
          .el-form-item{
            width: 50%;
          }
        }
      }
    }
  }

  .add-footer {
    position: fixed;
    bottom: 0;
    width: 100%;
    padding: 24px 50px;
    color: #000000d9;
    font-size: 14px;
    background: #fff;
    text-align: center;
  }
}
</style>

2)绑定路由(一级路由)

js
{
    path: '/enterpriseAdd',
    component: () => import('@/views/park/enterprise/Add')
}

3)路由跳转

vue
<el-button type="primary" @click="$router.push('/enterpriseAdd')">添加企业</el-button>

2. 行业字段渲染

1)在 @\api\enterprise.js 中封装 API

js
// 获取行业列表
export function getIndustryListAPI() {
  return request({
    url: '/park/industry'
  })
}

2)获取数据

vue
<script>
import { getIndustryListAPI } from '@/api/enterprise'
export default {
  data() {
    return {
      industryList: [] // 可选行业列表
    }
  },
  mounted() {
    this.getIndustryList()
  },
  methods: {
    async getIndustryList() {
      const res = await getIndustryListAPI()
      this.industryList = res.data
    }
  }
}
</script>

3)绑定下拉框

vue
<el-form-item label="所在行业" prop="name">
  <el-select v-model="addForm.industryCode">
    <el-option
      v-for="item in industryList"
      :key="item.industryCode"
      :value="item.industryCode"
      :label="item.industryName"
    />
  </el-select>
</el-form-item>

3. 营业执照上传

1)封装上传接口

@\api\enterprise.js

js
// 上传合同
export function uploadAPI(data) {
  return request({
    url: '/upload',
    method: 'POST',
    data
  })
}

2)准备上传组件

vue
<el-form-item label="营业执照">
    <el-upload
      action="#"
      :http-request="uploadRequest"
    >
      <el-button size="small" type="primary">点击上传</el-button>
      <div slot="tip" class="el-upload__tip">只能上传 jpg/png 文件,且不超过 500kb</div>
    </el-upload>
</el-form-item>

3)调用接口完成上传

js
import { getIndustryListAPI, uploadAPI } from '@/api/enterprise'

async uploadRequest(data) {
  const file = data.file

  // 处理formData类型参数
  const formData = new FormData()
  formData.append('file', file)
  formData.append('type', 'businessLicense')
  const res = await uploadAPI(formData)

  // 赋值表单数据
  this.addForm.businessLicenseId = res.data.id
  this.addForm.businessLicenseUrl = res.data.url
}

4)上传前验证文件

jsx
// 绑定上传前回调
<el-upload
  action="#"
  :http-request="uploadRequest"
  :before-upload="beforeUpload"
  >
  <el-button size="small" type="primary">点击上传</el-button>
  <div slot="tip" class="el-upload__tip">支持扩展名:.png .jpg .jpeg,文件大小不得超过5M</div>
</el-upload>


// 编写校验逻辑
beforeUpload(file) {
  const allowImgType = ['image/jpeg', 'image/png', 'image/jpg'].includes(file.type)
  const isLt5M = file.size / 1024 / 1024 < 5

  if (!allowImgType) {
    this.$message.error('上传合同图片只能是 PNG/JPG/JPEG 格式!')
  }
  if (!isLt5M) {
    this.$message.error('上传合同图片大小不能超过 5MB!')
  }
  return allowImgType && isLt5M
}

4. 表单校验

表单字段可以分为两类:① 直接通过 element 默认配置即可解决。② 需要单独校验的字段。

1)表单基础校验
jsx
// 1.创建表单规则
data() {
  return {
    addRules: {
      name: [
        { required: true, message: '企业名称为必填', trigger: 'blur' }
      ],
      legalPerson: [
        { required: true, message: '法人为必填', trigger: 'blur' }
      ],
      registeredAddress: [
        { required: true, message: '注册地址为必填', trigger: 'blur' }
      ],
      industryCode: [
        { required: true, message: '所在行业为必填', trigger: 'change' }
      ],
      contact: [
        { required: true, message: '企业联系人为必填', trigger: 'blur' }
      ],
      contactNumber: [
        { required: true, message: '企业联系人电话为必填', trigger: 'blur' }
      ],
      businessLicenseId: [
        { required: true, message: '请上传营业执照', trigger: 'blur' }
      ]
    }
  }
}

// 2.绑定表单规则
<el-form :model="addForm" :rules="addRules" label-width="100px">
  <el-form-item label="企业名称" prop="name">
    <el-input v-model="addForm.name" />
  </el-form-item>
  <el-form-item label="法人" prop="legalPerson">
    <el-input v-model="addForm.legalPerson" />
  </el-form-item>
  <el-form-item label="注册地址" prop="registeredAddress">
    <el-input v-model="addForm.registeredAddress" />
  </el-form-item>
  <el-form-item label="所在行业" prop="industryCode">
    <el-select v-model="addForm.industryCode">
      <el-option
        v-for="item in industryList"
        :key="item.industryCode"
        :value="item.industryCode"
        :label="item.industryName"
      />
    </el-select>
  </el-form-item>
  <el-form-item label="企业联系人" prop="contact">
    <el-input v-model="addForm.contact" />
  </el-form-item>
  <el-form-item label="联系电话" prop="contactNumber">
    <el-input v-model="addForm.contactNumber" />
  </el-form-item>
  <el-form-item label="营业执照" prop="businessLicenseId">
    <el-upload
      action="#"
      :http-request="uploadRequest"
      :before-upload="beforeUpload"
    >
      <el-button size="small" type="primary">点击上传</el-button>
      <div slot="tip" class="el-upload__tip">
        支持扩展名:.png .jpg .jpeg,文件大小不得超过5M
      </div>
    </el-upload>
  </el-form-item>
</el-form>
2)正则校验手机号
jsx
const validatePhone = (rule, value, callback) => {
  if (/^1[3-9]\d{9}$/.test(value)) {
    callback()
  } else {
    callback(new Error('请输入正常的手机号'))
  }
}

contactNumber: [
    { required: true, message: '企业联系人电话为必填', trigger: 'blur' },
    // 方法一
    { validator: validatePhone, trigger: 'blur' }
    // 方法二
    // { pattern: /^1[3-9]\d{9}$/, message: '电话格式不正确', trigger: 'blur' }
]
3)统一校验
jsx
<el-form ref="ruleForm"></el-form>

<el-button type="primary" @click="confirmSubmit">确定</el-button>

confirmSubmit() {
  this.$refs.ruleForm.validate(valid => {
    console.log(valid)
  })
}
4)上传完毕单独校验营业执照字段

问题:上传营业执照完毕之后并不能让校验痕迹消失掉,原因是 el-form 表单校验系统不能得到上传之后的通知。

解决办法:在上传完毕之后手动校验营业执照字段。

js
async uploadRequest(data) {
    // 上传逻辑...

    // 单独校验表单,清除错误信息
    this.$refs.ruleForm.validateField('businessLicenseId')
}

5. 删除图片,清空表单中的数据

jsx
<el-upload
  action="#"
  class="upload-demo"
  :http-request="uploadImage"
  :before-upload="beforeUpload"
  :on-remove="onRemove"
>
  <el-button size="small" type="primary">点击上传</el-button>
  <div slot="tip" class="el-upload__tip">只能上传.png .jpg .jpeg文件,且不超过5M</div>
</el-upload>

onRemove() {
    this.addForm.businessLicenseId = ''
    this.addForm.businessLicenseUrl = ''
    this.$refs.ruleForm.validateField('businessLicenseId')
},

6. 提交表单

1)在 @\api\enterprise.js 封装新增接口

js
// 创建公司
export function createExterpriseAPI(data) {
  return request({
    url: '/park/enterprise',
    method: 'POST',
    data
  })
}

2)点击提交

@\views\park\enterprise\add.vue

js
confirmSubmit() {
  this.$refs.ruleForm.validate(async valid => {
    if (!valid) return
    // 1. 调用接口
    await createExterpriseAPI(this.addForm)
    // 2. 返回列表页
    this.$router.back()
  })
}

五、编辑企业

1. 携带参数跳转编辑页

jsx
<el-button 
  size="mini" 
  type="text" 
  @click="editRent(scope.row.id)"
  >编辑</el-button>
  
editRent(id) {
  this.$router.push({
    path: '/exterpriseAdd',
    query: {
      id
    }
  })
}

2. 根据 id 适配文案显示

jsx
<el-page-header 
  :content="`${id?'编辑企业':'添加企业'}`" 
  @back="$router.back()" />


computed: {
  id() {
    return this.$route.query.id
  }
}

3. 回填企业数据

1)封装接口

js
// 获取合同详情
export function getEnterpriseDetailAPI(id) {
  return request({
    url: `/park/enterprise/${id}`
  })
}

2)调用接口,回填数据

js
async getEnterpriseDetail() {
  const res = await getEnterpriseDetailAPI(this.rentId)
  const { businessLicenseId, businessLicenseUrl, contact, contactNumber, industryCode, legalPerson, name, registeredAddress } = res.data
  this.addForm = { businessLicenseId, businessLicenseUrl, contact, contactNumber, industryCode, legalPerson, name, registeredAddress }
}

mounted() {
  // 有合同 id,调用详情接口
  if (this.Id) {
    this.getEnterpriseDetail()
  }
}

3)回显营业执照

vue
<el-form-item label="营业执照" prop="businessLicenseId">
  <el-upload
    action="#"
    :http-request="uploadRequest"
    :before-upload="beforeUpload"
  >
    <el-button size="small" type="primary">点击上传</el-button>
    <div slot="tip" class="el-upload__tip">
      只能上传jpg/png文件,且不超过500kb
    </div>
  </el-upload>
  <img v-if="id" :src="addForm.businessLicenseUrl" style="width:60px">
</el-form-item>

4. 编辑确认修改

1)封装编辑更新接口

js
// 更新企业
export function updateExterpriseAPI(data) {
  return request({
    url: '/park/enterprise',
    method: 'PUT',
    data
  })
}

2)区分状态接口提交

js
confirmSubmit() {
  this.$refs.ruleForm.validate(async valid => {
    console.log(valid)
    if (this.id) {
       delete this.addForm.businessLicenseName
       delete this.addForm.rent
       delete this.addForm.industryName
      // 编辑
      await updateExterpriseAPI(this.addForm)
    } else {
      // 新增
      await createExterpriseAPI(this.addForm)
    }
    this.$router.back()
  })
}

六、删除功能实现

1)封装接口

js
// 删除企业
export function delEnterpriseAPI(id) {
  return request({
    url: `/park/enterprise/${id}`,
    method: 'DELETE'
  })
}

2)绑定事件执行删除

jsx
<el-table-column label="操作">
  <template #default="scope">
    <el-button size="mini" type="text">添加合同</el-button>
    <el-button size="mini" type="text">查看</el-button>
    <el-button size="mini" type="text">编辑</el-button>
    <el-button size="mini" type="text" @click="delEnterprise(scope.row.id)">删除</el-button>
  </template>
</el-table-column>

delEnterprise(id) {
  this.$confirm('确认删除该企业吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(async() => {
    await delEnterpriseAPI(id)
    if (this.list.length === 1 && this.params.page > 1) {
        this.params.page--
    }
    this.getEnterpriseList()
    this.$message({
      type: 'success',
      message: '删除成功!'
    })
  })
}

七、创建合同

1. 页面布局

1)控制弹框打开关闭
jsx
<el-button size="mini" type="text" @click="addRent>添加合同</el-button>

<el-dialog
  title="添加合同"
  :visible="rentDialogVisible"
  width="580px"
  @close="closeDialog"
  >
  <!-- 表单区域 -->
  <div class="form-container">表单区域</div>
  <template #footer>
    <el-button size="mini" @click="closeDialog">取 消</el-button>
    <el-button size="mini" type="primary">确 定</el-button>
  </template>
</el-dialog>


data(){
  return {
    rentDialogVisible: false
  }
},
methods:{
  // 打开
  addRent(){
    this.rentDialogVisible = true
  },
  // 关闭
  closeDialog(){
    this.rentDialogVisible = false
  }
}
2)准备表单
点我查看代码
vue
<!-- 表单模版 -->
<div class="form-container">
  <el-form ref="addForm" :model="rentForm" :rules="rentRules" label-position="top">
    <el-form-item label="租赁楼宇" prop="buildingId">
      <el-select v-model="rentForm.buildingId" placeholder="请选择">
        <el-option
          v-for="item in []"
          :key="item.id"
          :label="item.name"
          :value="item.id"
        />
      </el-select>
    </el-form-item>
    <el-form-item label="租赁起止日期" prop="rentTime">
      <el-date-picker
        v-model="rentForm.rentTime"
        type="daterange"
        range-separator="至"
        start-placeholder="开始日期"
        end-placeholder="结束日期"
        value-format="yyyy-MM-dd"
      />
    </el-form-item>
    <el-form-item label="租赁合同" prop="contractId">
      <el-upload
        action="#"
      >
        <el-button size="small" type="primary" plain>上传合同文件</el-button>
        <div slot="tip" class="el-upload__tip">支持扩展名:.doc .docx .pdf, 文件大小不超过5M</div>
      </el-upload>
    </el-form-item>
  </el-form>
</div>

data(){
  return {
    rentForm: {
        buildingId: null, // 楼宇id
        contractId: null, // 合同id
        contractUrl: '', // 合同Url
        enterpriseId: null, // 企业名称
        type: 0, // 合同类型
        rentTime: [] // 合同时间
    },
    rentRules: {
        buildingId: [
          { required: true, message: '请选择楼宇', trigger: 'change' }
        ],
        rentTime: [
          { required: true, message: '请选择租赁日期', trigger: 'change' }
        ],
        contractId: [
          { required: true, message: '请上传合同文件', trigger: 'change' }
        ]
    }
  }
}

2. 获取楼宇下拉框数据

1)封装获取楼宇列表接口

js
// 获取楼宇列表
export function getRentBuildListAPI() {
  return request({
    url: '/park/rent/building'
  })
}

2)打开弹框时调用接口

js
import { getRentBuildListAPI } from '@/apis/enterprise'

// 打开弹框
async addRent(enterpriseId) {
  this.rentDialogVisible = true
  // 获取楼宇下拉列表
  const res = await getRentBuildListAPI()
  this.buildList = res.data
}

3)渲染下拉列表视图模版

vue
<el-form-item label="租赁楼宇" prop="buildingId">
  <el-select v-model="rentForm.buildingId" placeholder="请选择">
    <el-option
      v-for="item in buildList"
      :key="item.id"
      :label="item.name"
      :value="item.id"
    />
  </el-select>
</el-form-item>

3. 上传合同

1)调用上传接口

jsx
<el-form-item label="租赁合同" prop="contractId">
  <el-upload
    action="#"
    :http-request="uploadHandle"
  >
    <el-button size="small" type="primary" plain>上传合同文件</el-button>
    <div slot="tip" class="el-upload__tip">支持扩展名:.doc .docx .pdf, 文件大小不超过5M</div>
  </el-upload>
</el-form-item


// 上传合同
async uploadHandle(fileData) {
  // 1. 处理FormData
  const { file } = fileData
  const formData = new FormData()
  formData.append('file', file)
  formData.append('type', 'contract')
  // 2. 上传并赋值
  const res = await uploadAPI(formData)
  const { id, url } = res.data
  this.rentForm.contractId = id
  this.rentForm.contractUrl = url  
}

2)上传前校验

jsx
<!--  
  before-upload:上传前的校验
  limit:限制文件的上传个数
  on-exceed:上传文件超出限制时,执行的钩子函数
  -->
<el-upload
  action="#"
  :http-request="uploadHandle"
  :before-upload="beforeUpload"
  :limit="1"
  :on-exceed="onExceed"
>
  <el-button size="small" type="primary" plain>上传合同文件</el-button>
  <div slot="tip" class="el-upload__tip">支持扩展名:.doc .docx .pdf, 文件大小不超过5M</div>
</el-upload>


methods:{
  onExceed() {
      this.$message.warning('文件上传个数超出限制')
    },
    // 上传前的校验
  beforeUpload(file) {
    const typeList = [
      'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
      'application/msword',
      'application/pdf']
    const fileType = typeList.includes(file.type)
    const isLt5M = file.size / 1024 / 1024 < 5
    if (fileType && isLt5M) {
      return true
    } else {
      this.$message.warning('上传文件不符合要求,请重新上传')
      return false
    }
  }
}

4. 表单校验

1)提交前的统一校验

jsx
<el-button size="mini" type="primary" @click="confirmAdd">确 定</el-button>

confirmAdd() {
  this.$refs.addForm.validate(flag => {
    if (!flag) return
    console.log('可以提交了')
  })
},

// 单独校验上传文件
async uploadHandle({ file })  
  .....
  this.$refs.addForm.validateField('contractId')
},

2)处理移除文件时,继续显示校验

jsx
 <el-upload
      action="#"
      :http-request="uploadHandle"
      :before-upload="beforeUpload"
      :limit="1"
      :on-exceed="onExceed"
      :on-remove="onRemove"
    >
      <el-button size="small" type="primary" plain>上传合同文件</el-button>
      <div slot="tip" class="el-upload__tip">支持扩展名:.doc .docx .pdf, 文件大小不超过5M</div>
    </el-upload>

        
onRemove() {
  this.rentForm.contractId = ''
  this.rentForm.contractUrl = ''
  this.$refs.addForm.validateField('contractId')
}

5. 提交表单创建合同

1)封装接口

js
// 创建合同
export function createRentAPI(data) {
  return request({
    url: '/park/enterprise/rent',
    method: 'POST',
    data
  })
}

2)补充企业 id 参数

jsx
<el-button size="mini" type="text" @click="addRent(row.id)">添加合同</el-button>
  
// 添加合同
addRent(id) {
  this.dialogVisible = true
  // 把企业id存入表单对象
  this.rentForm.enterpriseId = id
}

3)处理参数提交表单

js
 // 确认提交
confirmAdd() {
  this.$refs.addForm.validate(async valid => {
    if (valid) {
      const { buildingId, contractId, contractUrl, enterpriseId, type } = this.rentForm
      await createRentAPI({
        buildingId, contractId, contractUrl, enterpriseId, type,
        startTime: this.rentForm.rentTime[0],
        endTime: this.rentForm.rentTime[1]
      })
      // 更新列表
      this.getExterpriseList()
       // 关闭弹框
      this.closeDialog()
    }
  })
},

closeDialog() {
  this.rentDialogVisible = false
  this.$refs.addForm.resetFields()
  this.rentForm = {
    buildingId: null, // 楼宇id
    contractId: null, // 合同id
    contractUrl: '', // 合同Url
    enterpriseId: null, // 企业名称
    type: 0, // 合同类型
    rentTime: [] // 合同时间
  }
  //清空上传后的文件
  this.$refs.upload.clearFiles()
},

八、显示合同列表

1. 准备 Table 静态结构

表格组件 - 展开行

vue
<el-table-column type="expand">
  <template #default>
    <el-table>
      <el-table-column label="租赁楼宇" width="320" prop="buildingName" />
      <el-table-column label="租赁起始时间" prop="startTime" />
      <el-table-column label="合同状态" prop="status" />
      <el-table-column label="操作" width="180">
        <template #default="scope">
          <el-button size="mini" type="text">退租</el-button>
          <el-button size="mini" type="text">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
  </template>
</el-table-column>

2. 获取当前合同列表

1)封装接口函数 @\api/enterprise.js

js
// 获取合同列表
export function getRentListAPI(id) {
  return request({
    url: `/park/enterprise/rent/${id}`
  })
}

2)展开时获取合同数据

jsx
// 1. 绑定事件 & 绑定数据
<el-table style="width: 100%" :data="exterpriseList"
  @expand-change="expandHandle">
<el-table-column type="expand">
  <template #default="{row}">
    <el-table :data="row.rentList">
      <el-table-column label="租赁楼宇" width="320" prop="buildingName" />
      <el-table-column label="租赁起始时间" prop="startTime" />
      <el-table-column label="合同状态" prop="status" />
      <el-table-column label="操作" width="180">
        <template #default="scope">
          <el-button size="mini" type="text">续租</el-button>
          <el-button size="mini" type="text">退租</el-button>
          <el-button size="mini" type="text">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
  </template>
</el-table-column>


// 2. 初始化时增加合同数据默认列表   
async getExterpriseList() {
  const res = await getEnterpriseListAPI(this.params)
  this.exterpriseList = res.data.rows.map((item) => {
    return {
      ...item,
      rentList: [] // 每一行补充存放合同的列表
    }
  })
  this.total = res.data.total
}
    

// 3. 只有展开时获取数据并绑定
async expandHandle(row, rows) {
  const isExpend = rows.find(item => item.id === row.id)
  if (!isExpend) return
  const res = await getRentListAPI(row.id)
  // eslint-disable-next-line require-atomic-updates
  row.rentList = res.data
}

3. 适配合同状态

jsx
// 格式化tag类型
formatInfoType(status) {
  const MAP = {
    0: 'warning',
    1: 'success',
    2: 'info',
    3: 'danger'
  }
  // return 格式化之后的中文显示
  return MAP[status]
}

// 格式化status
formartStatus(type) {
  const TYPEMAP = {
    0: '待生效',
    1: '生效中',
    2: '已到期',
    3: '已退租'
  }
  return TYPEMAP[type]
}

<el-table-column label="合同状态">
  <template #default="scope">
    <el-tag :type="formatInfoType(scope.row.status)"> 
      {{ formatStatus(scope.row.status) }}
    </el-tag>
  </template>
</el-table-column>

九、合同退租状态修改

1)封装接口

js
// 退租
export function outRentAPI(rentId) {
  return request({
    url: `/park/enterprise/rent/${rentId}`,
    method: 'PUT'
  })
}

2)修改状态

jsx
<el-table-column label="操作" width="180" fixed="right">
  <template #default="scope">
    <el-button size="mini" type="text" @click="outRent(scope.row.id)">退租</el-button>
    <el-button size="mini" type="text">删除</el-button>
  </template>
</el-table-column>


outRent(rentId) {
  this.$confirm('确认退租吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(async() => {
    // 1.调用接口
    await outRentAPI(rentId)
    // 2.重新拉取列表
    this.getEnterpriseList()
    this.$message({
      type: 'success',
      message: '退租成功!'
    })
  }).catch(() => {
    this.$message({
      type: 'info',
      message: '取消退租'
    })
  })
}

3)适配视图状态

退租按钮逻辑:如果当前是退租状态 ==> 禁用;如果不是退租状态 ==> 启用。

删除按钮逻辑:只有已退租的时候,删除才是启用的,否则就是禁用的。

vue
<el-table-column label="操作" width="180" fixed="right">
  <template #default="scope">
    <el-button size="mini" type="text" :disabled="scope.row.status === 3" @click="outRent(scope.row.id)">退租</el-button>
    <el-button size="mini" type="text" :disabled="scope.row.status !== 3">删除</el-button>
  </template>
</el-table-column>

十、租赁合同删除

1)封装接口

js
// 删除租赁合同
export function delRentAPI(rentId) {
  return request({
    url: `/park/enterprise/rent/${rentId}`,
    method: 'DELETE'
  })
}

2)调用接口

jsx
<el-button size="mini" type="text" :disabled="scope.row.status !== 3" @click="delRent(scope.row.id)">删除</el-button>

// 删除合同
delRent(id) {
  this.$confirm('确认删除此合同吗吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(async() => {
    // 1. 调用接口
    await delRentAPI(id)
    // 2. 重新拉取列表
    this.getEnterpriseList()
    this.$message({
      type: 'success',
      message: '删除成功!'
    })
  }).catch(() => {
    this.$message({
      type: 'info',
      message: '取消删除'
    })
  })
}

十一、查看企业

1. 页面布局

@\views\Park\Enterprise\EnterpriseDetail.vue

点我查看代码
vue
<template>
  <div class="detail-enterprise">
    <header class="add-header">
      <el-page-header content="查看企业" @back="$router.back()" />
    </header>
    <main class="add-main">
      <div class="form-container">
        <div class="title">租赁记录</div>
        <div class="table-container">
          <el-table
            :data="[]"
            style="width: 100%"
            border
          >
            <el-table-column
              prop="index"
              label="序号"
              width="50"
              type="index"
            />
            <el-table-column
              prop="name"
              label="租赁楼宇"
              width="180"
            />
            <el-table-column
              label="租赁起止时间"
              width="280"
            >
              <template #default="{row}">
                {{ row.startTime }} - {{ row.endTime }}
              </template>
            </el-table-column>
            <el-table-column
              label="租赁合同(点击预览)"
            >
              <template #default="{row}">
                <el-button type="text">
                  {{ row.contractName }}
                </el-button>
              </template>
            </el-table-column>
            <el-table-column
              prop="createTime"
              label="录入时间"
            />
            <el-table-column
              prop="address"
              label="操作"
            >
              <template #default="{row}">
                <el-button type="text">合同下载</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>

      </div>
    </main>

  </div>
</template>

<script>
export default {
  data() {
    return {
    }
  }
}
</script>

<style scoped lang="scss">
.detail-enterprise {
  background-color: #f4f6f8;
  height: 100vh;

  .add-header {
    display: flex;
    align-items: center;
    padding: 0 20px;
    height: 64px;
    background-color: #fff;

    .left {
      span {
        margin-right: 4px;
      }
      .arrow{
        cursor: pointer;
      }
    }

    .right {
      text-align: right;
    }
  }

  .add-main {
    background: #f4f6f8;
    padding: 20px 130px;

    .form-container {
      background-color: #fff;
      margin-bottom: 20px;
      .title {
        height: 60px;
        line-height: 60px;
        padding-left: 20px;
      }
    }
    .table-container{
      padding:20px;
    }
    .preview{
      img{
        width: 100px;
      }
    }
  }
}
</style>

2)配置路由(一级路由)

@\router\index.js

js
{
    path: '/enterpriseDetail',
    component: () => import('@/views/park/enterprise/Detail')
}

2. 渲染列表

1)封装接口(已有)

js
// 获取详情
export function getEnterpriseDetailAPI(id) {
  return request({
    url: `/park/enterprise/${id}`,
    method: 'GET'
  })
}

2)渲染数据

点我查看代码
vue
<template>
  <div class="detail-enterprise">
    <header class="add-header">
      <el-page-header content="查看企业" @back="$router.back()" />
    </header>
    <main class="add-main">
      <div class="form-container">
        <div class="title">租赁记录</div>
        <div class="table-container">
          <el-table
            :data="form.rent"
            style="width: 100%"
            border
          >
            <el-table-column
              prop="index"
              label="序号"
              width="50"
              type="index"
            />
            <el-table-column
              prop="name"
              label="租赁楼宇"
              width="180"
            />
            <el-table-column
              label="租赁起止时间"
              width="280"
            >
              <template #default="{row}">
                {{ row.startTime }} - {{ row.endTime }}
              </template>
            </el-table-column>
            <el-table-column
              label="租赁合同(点击预览)"
            >
              <template #default="{row}">
                <el-button type="text">
                  {{ row.contractName }}
                </el-button>
              </template>
            </el-table-column>
            <el-table-column
              prop="createTime"
              label="录入时间"
            />
            <el-table-column
              prop="address"
              label="操作"
            >
              <template #default="{row}">
                <el-button type="text">合同下载</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </div>
    </main>
  </div>
</template>

<script>
import { getEnterpriseDetailAPI } from '@/api/enterprise'
export default {
  data() {
    return {
      rentList: []
    }
  },
  mounted() {
    this.getDetail()
  },
  methods: {
    async getDetail() {
      const res = await getEnterpriseDetailAPI(this.$route.query.id)
      this.rentList = res.data.rent
    }
  }
}
</script>

3. 预览功能实现

预览地址拼接上合同文件链接地址在新窗口打开。

jsx
<el-button type="text">
  <a :href="`${previewURL}${row.contractUrl}`" target="_blank">
    {{ row.contractName }}
  </a>
</el-button>

data() {
  return {
    previewURL: 'https://view.officeapps.live.com/op/view.aspx?src=' // 预览地址
  }
}

4. 下载功能

vue
<el-table-column
  prop="address"
  label="操作"
>
  <template #default="{row}">
    <el-button type="text"><a :href="row.contractUrl">合同下载</a></el-button>
  </template>
</el-table-column>

第六章:行车管理 - 计费规则管理

一、页面布局

点我查看代码
vue
<template>
  <div class="rule-container">
    <div class="create-container">
      <el-button type="primary">增加停车计费规则</el-button>
      <el-button>导出Excel</el-button>
    </div>
    <!-- 表格区域 -->
    <div class="table">
      <el-table :data="ruleList" style="width: 100%">
        <el-table-column  label="序号" />
        <el-table-column label="计费规则编号" />
        <el-table-column label="计费规则名称"  />
        <el-table-column label="免费时长(分钟)" />
        <el-table-column label="收费上线(元)" />
        <el-table-column label="计费方式"/> 
        <el-table-column label="计费规则"/>
        <el-table-column label="操作" fixed="right" width="120">
          <template #default="scope">
            <el-button size="mini" type="text">编辑</el-button>
            <el-button size="mini" type="text">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Building',
  data() {
    return {
     
    }
  }
}
</script>

<style lang="scss" scoped>
.rule-container {
  padding: 20px;
  background-color: #fff;
}

.search-container {
  display: flex;
  align-items: center;
  border-bottom: 1px solid rgb(237, 237, 237, .9);
  padding-bottom: 20px;

  .search-label {
    width: 100px;
  }

  .search-main {
    width: 220px;
    margin-right: 10px;
  }
}
.create-container{
  margin: 10px 0px;
}
.page-container{
  padding:4px 0px;
  text-align: right;
}
</style>

二、数据展示

1. 基本的展示

1)在 @\api\car.js 中封装 API

js
import request from '@/utils/request'

// 获取规则列表
export function getRuleListAPI(params) {
  return request({
    url: 'parking/rule/list',
    params
  })
}

2)组件中获取数据

js
import { getRuleListAPI } from '@/apis/car'

data(){
  return {
    ruleList:[],
    params: {
      page: 1,
      pageSize: 10
    }
  }
},
mounted() {
  this.getRuleList()
},
methods: {
  // 获取规则列表
  async getRuleList() {
    const res = await getRuleListAPI(this.params)
    this.ruleList = res.data.rows
  }
}

3)绑定模版

vue
<el-table :data="ruleList" style="width: 100%">

2. 分页管理

vue
<div style="margin-top:10px;float:right">
    <el-pagination
        layout="prev, pager, next"
        :total="total"
        :page-size="params.pageSize"
        @current-change="currentChange" />
</div>

<script>
  data() {
    return {
      ruleList: [],
      params: {
        page: 1,
        pageSize: 10
      },
      total: 0,
    }
  },
  methods:{
    currentChange(val) {
      this.params.page = val
      this.getRuleList()
    }
  }
</script>

3. 序号处理

vue
<el-table-column type="index" label="序号" 
  :index="indexMethod" />


methods:{
   indexMethod(row) {
      return (this.params.page - 1) * this.params.pageSize + row + 1
    }
}

三、导出 excel

后端主导实现

​ 流程:前端调用到导出excel接口 ==> 后端返回 excel 文件流 ==> 浏览器会识别并自动下载

​ 场景:大量数据的场景都由后端来做

前端主导实现

​ 流程:前端获取要导出的数据 ==> 把常规数据处理成一个excel文件 ==> 浏览器识别下载

​ 场景:少数据量的导出

1. 实现基础导出

Tutorial | SheetJS Community Edition

插件导出流程

1)创建一个工作簿

2)创建一个工作表

3)把工作表加入到工作簿中

4)调用插件方法导出

代码实现

bash
npm i --save https://cdn.sheetjs.com/xlsx-0.20.0/xlsx-0.20.0.tgz
js
import { utils, writeFileXLSX } from 'xlsx'

exportToExcel() {
  // 创建一个新的工作簿
  const workbook = utils.book_new()
  // 创建一个工作表 要求一个对象数组格式
  const worksheet = utils.json_to_sheet(
    [
      { name: '张三', age: 18 },
      { name: '李四', age: 28 }
    ]
  )
 
  // 把工作表添加到工作簿  Data为工作表名称
  utils.book_append_sheet(workbook, worksheet, 'Data')
  // 改写表头
  utils.sheet_add_aoa(worksheet, [['姓名', '年龄']], { origin: 'A1' })
  // 导出方法进行导出
  writeFileXLSX(workbook, 'SheetJSVueAoO.xlsx')
}

2. 按照业务数据导出

js
// 导出excel
async exportToExcel() {
  // 获取要导出的业务数据
  const res = await getRuleListAPI(this.params)
  // 表头英文字段key
  const tableHeaderKeys = ['ruleNumber', 'ruleName', 'freeDuration', 'chargeCeiling', 'chargeType', 'ruleNameView']
  // 表头中文字段value
  const tableHeaderValues = ['计费规则编号', '计费规则名称', '免费时长(分钟)', '收费上线(元)', '计费方式', '计费规则']
  // 以excel表格的顺序调整后端数据
  const sheetData = res.data.rows.map((item) => {
    const obj = {}
    tableHeaderKeys.forEach(key => {
      obj[key] = item[key]
    })
    return obj
  })
  // 创建一个工作表
  const worksheet = utils.json_to_sheet(sheetData)
  // 创建一个新的工作簿
  const workbook = utils.book_new()
  // 把工作表添加到工作簿
  utils.book_append_sheet(workbook, worksheet, 'Data')
  // 改写表头
  utils.sheet_add_aoa(worksheet, [tableHeaderValues], { origin: 'A1' })
  writeFileXLSX(workbook, 'SheetJSVueAoO.xlsx')
}

四、适配付费状态

jsx
// 适配收费状态
formatChargeType(type) {
  const TYPEMAP = {
    'duration': '按时长收费',
    'turn': '按次收费',
    'partition': '分段收费'
  }
  return TYPEMAP[type]
}

// 适配table
<el-table-column label="计费方式">
  <template #default="scope">
    {{ formatChargeType(scope.row.chargeType) }}
  </template>
</el-table-column>


// 适配excel表格
const sheetData = res.data.rows.map(item => {
  const _obj = {}
  headerKeys.forEach(key => {
    // 赋值
    // 针对计费规则做处理
    if (key === 'chargeType') {
      _obj[key] = this.formatChargeType(item[key])
    } else {
      _obj[key] = item[key]
    }
  })
  return _obj
})

五、添加计费规则

1. 页面布局

1)新建 @\components\AddRule.vue 文件

点我查看代码
vue
<!-- 弹框 -->
<el-dialog :visible="dialogVisible" width="680px" title="新增规则">
  <!-- 表单接口 -->
  <div class="form-container">
    <el-form ref="addForm" :model="addForm" :rules="addFormRules" label-position="top">
      <el-form-item label="计费规则编号" prop="ruleNumber">
        <el-input v-model="addForm.ruleNumber" />
      </el-form-item>
      <el-form-item label="计费规则名称" prop="ruleName">
        <el-input v-model="addForm.ruleName" />
      </el-form-item>
      <el-form-item label="计费方式" prop="chargeType">
        <el-radio-group v-model="addForm.chargeType" size="small">
          <el-radio label="duration" border>时长收费</el-radio>
          <el-radio label="turn" border>按次收费</el-radio>
          <el-radio label="partition" border>分段收费</el-radio>
        </el-radio-group>
      </el-form-item>
      <div class="flex-container" style="display:flex;justify-content:space-between">
        <el-form-item label="免费时长">
          <el-input v-model="addForm.freeDuration" />
        </el-form-item>
        <el-form-item label="收费上限">
          <el-input v-model="addForm.chargeCeiling" />
        </el-form-item>
      </div>
     <el-form-item label="计费规则">
    <!-- 按时长收费区域 -->
    <div class="duration" style="display:flex">
      每 <el-input v-model="addForm.durationTime" class="input-box" />
      小时
      <el-input v-model="addForm.durationPrice" class="input-box" /> 元
    </div>
    <!-- 按次收费区域 -->
    <div class="turn" style="display:flex">
      每次<el-input v-model="addForm.turnPrice" class="input-box" style="width:150px" /> 元
    </div>
    <!-- 按分段收费区域 -->
    <div class="partition">
      <div class="item" style="display:flex">
        <el-input v-model="addForm.partitionFrameTime" class="input-box" style="width:100px" />
        小时内,每小时收费
        <el-input v-model="addForm.partitionFramePrice" class="input-box" style="width:100px" />

      </div>
      <div class="item" style="display:flex;margin:30px auto 0">
        每增加
        <el-input v-model="addForm.partitionIncreaseTime" class="input-box" style="width:120px;" />
        小时,增加
        <el-input v-model="addForm.partitionIncreasePrice" class="input-box" style="width:120px;" />

      </div>
    </div>
  </el-form-item>
  </el-form>
</div>
<template #footer>
  <el-button size="mini" @click="dialogVisible = false">取 消</el-button>
  <el-button size="mini" type="primary">确 定</el-button>
</template>
</el-dialog>


<script>
data() {
  return {
    addForm: {
      ruleNumber: '', // 计费规则编号
      ruleName: '', // 计费规则名称
      chargeType: 'duration', // 计费规则类型 duration / turn / partition
      chargeCeiling: null,
      freeDuration: null,
      // 时长计费独有字段
      durationTime: null, // 时长计费单位时间
      durationPrice: null, // 时长计费单位价格
      durationType: 'hour',
      // 按次收费独有字段
      turnPrice: null,
      // 分段计费独有字段
      partitionFrameTime: null, // 段内时间
      partitionFramePrice: null, // 段内费用
      partitionIncreaseTime: null, // 超出时间
      partitionIncreasePrice: null // 超出费为收费价钱

    }, // 计费规则表单对象
    addFormRules: {
      ruleNumber: [{ required: true, message: '请输入规则编号', trigger: 'blur' }],
      ruleName: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
      chargeType: [{ required: true, message: '请选择收费类型', trigger: 'blur' }],
    } // 计费规则校验规则对象
  }
},
//控制弹层的隐藏和显示
 props: {
    dialogVisible: {
      type: Boolean,
      default: false
    }
  }
</script>

<style lang="scss" scoped>
.form-container{
  padding:0px 80px;
}
</style>

2)父组件中调用子组件

vue
<template>
  <div class="rule-container">
    ...
    <AddRule :dialog-visible.sync="dialogVisible"  />
	</div>
</template>

<script>
import AddRule from './components/AddRule.vue'
data(){
  return {
    dialogVisible:false
  }
},
components: {
  AddRule
}
</script>

第七章:系统管理 - 角色管理

一、角色列表渲染

1)页面布局

点我查看代码
vue
<template>
  <div class="role-container">
    <div class="left-wrapper">
      <div v-for="item in []" :key="item.roleId" class="role-item">
        <div class="role-info">
          <svg-icon icon-class="user" class="icon" />
          管理员
        </div>
        <div class="more">
          <svg-icon icon-class="more" />
        </div>
      </div>
      <el-button class="addBtn" size="mini">添加角色</el-button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Role'
}
</script>

<style scoped lang="scss">
.role-container {
  display: flex;
  font-size: 14px;
  background-color: #fff;
  padding: 20px;
  .left-wrapper {
    width: 200px;
    border-right: 1px solid #e4e7ec;
    padding: 4px;
    text-align: center;

    .role-item {
      position: relative;
      height: 56px;
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 0 20px;
      cursor: pointer;

      &.active {
        background: #edf5ff;
        color: #4770ff;
      }
    }

    .role-info {
      display: flex;
      align-items: center;

      .icon {
        margin-right: 10px;
      }
    }

    .more {
      display: flex;
      align-items: center;
    }
    .addBtn {
      width: 100%;
      margin-top: 20px;
    }
  }

  .right-wrapper {
    flex: 1;

    .tree-wrapper {
      display: flex;
      justify-content: space-between;

      .tree-item {
        flex: 1;
        border-right: 1px solid #e4e7ec;
        padding: 0px 4px;
        text-align: center;
        .tree-title {
          background: #f5f7fa;
          text-align: center;
          padding: 10px 0;
          margin-bottom: 12px;
        }
      }
    }

    ::v-deep .el-tabs__nav-wrap {
      padding-left: 20px;
    }

    .user-wrapper {
      padding: 20px;
    }
  }
}
</style>

2)封装 API 到 @\api\system.js

js
// 获取角色列表
export function getRoleListAPI() {
  return request({
    url: '/park/sys/role'
  })
}

3)编写渲染列表

vue
<template>
  <div class="role-container">
    <div class="left-wrapper">
      <div v-for="item in roleList" :key="item.roleId" class="role-item">
        <div class="role-info">
          <svg-icon icon-class="user" class="icon" />
          {{ item.roleName }}
        </div>
        <div class="more">
          <svg-icon icon-class="more" />
        </div>
      </div>
      <el-button class="addBtn" size="mini">添加角色</el-button>
    </div>
  </div>
</template>

<script>
import { getRoleListAPI } from '@/api/system'

export default {
  name: 'Role',
  data() {
    return {
      roleList: []
    }
  },
  methods: {
    async getRoleList() {
      const res = await getRoleListAPI()
      this.roleList = res.data
    }
  },
  created() {
    this.getRoleList()
  }
}
</script>

4)点击激活高亮

vue
<template>
  <div class="role-container">
    <div class="left-wrapper">
      <div
        v-for="(item, index) in roleList"
        :key="item.roleId"
        @click="changeRole(index)"
        :class="{ active: index === activeIndex }"
        class="role-item"
      >
        <div class="role-info">
          <svg-icon :icon-class="index === activeIndex ? 'user-active' : 'user'" class="icon" />
          {{ item.roleName }}
        </div>
        <div class="more">
          <svg-icon icon-class="more" />
        </div>
      </div>
      <el-button class="addBtn" size="mini">添加角色</el-button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Role',
  data() {
    return {
      activeIndex: 0
    }
  },
  methods: {
    // 切换角色
    changeRole (index) {
      this.activeIndex = index
    }
  }
}
</script>

二、权限列表渲染

1. 分组标题

1)页面布局

html
<div class="left-wrapper"> 省略代码 </div>

<div class="right-wrapper">
  <div class="tree-wrapper">
    <div v-for="item in []" :key="item.id" class="tree-item">
      <div class="tree-title"> 楼宇管理 </div>
      <el-tree
        :data="[]"
        />
    </div>
  </div>
</div>

2)封装 API 到 @\api\system.js

js
// 获取tree权限列表
export function getTreeListAPI() {
  return request({
    url: '/park/sys/permision/all/tree'
  })
}

3)编写渲染逻辑

jsx
// 组件中获取功能权限列表
data() {
  return {
    treeList: [], // 权限树形列表
  }
},
methods:{
  async getTreeList() {
    const res = await getTreeListAPI()
    this.treeList = res.data
  }
},
mounted() {
  this.getTreeList()
}


// 组件中渲染模版
<div v-for="item in treeList" :key="item.id" class="tree-item">
  <div class="tree-title">{{ item.title }}</div>
  <el-tree
    :data="[]"
  />
</div>

2. 每组权限列表渲染

Element 树形控件

1)树形控件绑定数据

vue
<!-- 组件中渲染模版 -->
<div v-for="item in treeList" :key="item.id" class="tree-item">
  <div class="tree-title">{{ item.title }}</div>
  <el-tree
    :data="item.children"
  />
</div>

2)配置展示的字段

vue
<el-tree
  :props="{
    label: 'title',
    children: 'children'
  }"
/>

3)定制 tree 样式

show-checkbox:定制选择框。

default-expand-all:定制默认展开所有。

vue
<!-- 树形结构 -->
<el-tree
  :data="item.children"
  :props="{
    label: 'title',
    children: 'children'
  }"
  show-checkbox
  :default-expand-all="true"
/>

4)禁用功能实现

实现方式一:

js
// 递归处理函数
function addDisabled(treeList) {
  treeList.forEach(item => {
    item.disabled = true
    // 递归处理
    if (item.children && item.children.length) {
      addDisabled(item.children)
    }
  })
}

// 获取tree数据
async getTreeList() {
  const res = await getTreeListAPI()
  this.treeList = res.data
  // 禁用
  // 目标:treeList里面的每一项以及嵌套的子项都添加一个disabled = true
  addDisabled(this.treeList)
}

实现方式二:

在 tree 组件上有一个 disabled 属性,值为函数,返回 true 则会禁用该组件里所有 children 为不可选状态。

vue
<!-- 树形结构 -->
<el-tree
  :data="item.children"
  :props="{
    label: 'title',
    children: 'children',
    disabled: () => true
  }"
  show-checkbox
  :default-expand-all="true"
/>

三、高亮权限点

1. 点击左侧列表获取 roleId

js
// 切换角色是获取roleId
changeRole(idx) {
  this.activeIndex = idx
  // 获取当前角色下的权限点数据列表
  const roleId = this.roleList[idx].roleId
  console.log(roleId)
}

2. 封装请求接口

封装 API 到 @\api\system.js

js
// 获取当前角色权限点列表
export function getRoleDetailAPI(roleId) {
  return request({
    url: `/park/sys/role/${roleId}`
  })
}

3. 根据当前 roleId 获取权限点列表

js
data() {
  return {
    perms: [] // 当前角色权限点列表
  }
}

// 封装请求方法
async getRoleDetail(roleId) {
  const res = await getRoleDetailAPI(roleId)
  this.perms = res.data.perms
}

// 切换角色
changeRole(idx) {
  this.activeIndex = idx
  const roleId = this.roleList[idx].roleId
  // 获取当前角色下的权限点数据列表
  this.getRoleDetail(roleId)
}

4. 高亮 Tree 节点

jsx
<el-tree ref="tree"/>

// 获取当前角色详情
async getRoleDetail(roleId) {
  const res = await getRoleDetailAPI(roleId)
  this.perms = res.data.perms
  // 高亮权限列表
  const treeComponentList = this.$refs.tree
  // 调用setCheckedKeys方法
  treeComponentList.forEach((tree, index) => {
    tree.setCheckedKeys(this.perms[index])
  })
}

5. 初始化时获取当前角色权限点

js
// 生命周期里先以第一项roleId获取
async mounted() {
  // 先获取角色列表和可选权限列表
  await this.getRoleList()
  await this.getTreeList()
  // 再获取当前角色下的权限列表
  this.getRoleDetail(this.roleList[0].roleId)   
}

注意:确保 roleList 和 TreeList 完成之后,再请求 perms。

四、角色成员列表渲染

1. 页面布局

点我查看代码
jsx
<el-tabs v-model="activeName">
  <el-tab-pane label="功能权限" name="tree">
    <div class="tree-wrapper">
      <div v-for="item in treeList" :key="item.id" class="tree-item">
        <!-- title -->
        <div class="tree-title">{{ item.title }}</div>
        <!-- 树形结构 -->
        <el-tree
          ref="tree"
          :props="{label:'title'}"
          :data="item.children"
          show-checkbox
          :default-expand-all="true"
          node-key="id"
        />
      </div>
    </div>
  </el-tab-pane>
  <el-tab-pane label="成员列表(100)" name="member">成员管理</el-tab-pane>
</el-tabs>

2. 封装请求接口

封装 API 到 @\api\system.js

js
// 获取角色成员列表
export function getRoleUserAPI(roleId) {
  return request({
    url: `/park/sys/roleUser/${roleId}`
  })
}

3. 组件逻辑编写

jsx
data() {
    return {
      roleUserList: [], // 当前角色下的成员列表
      total:0
    }
}

// 封装获取当前角色下的成员的方法
async getRoleUserList(roleId) {
  const res = await getRoleUserAPI(roleId)
  this.roleUserList = res.data.rows
  this.total = res.data.total
}

async mounted() {
  // 获取角色列表(必须加await)
  await this.getRoleList()
  await this.getTreeList()
  this.changeRole(0)
}

// 切换角色
changeRole(idx) {
    this.activeIndex = idx
    // 1. 获取当前角色下的权限点数据列表
    const roleId = this.roleList[idx].roleId
    this.getRoleDetail(roleId)
    // 2. 获取当前角色下的成员数据列表
    this.getRoleUserList(roleId)
}


// 模版渲染
<el-tab-pane :label="`成员(${total})`" name="user">
  <div class="user-wrapper">
    <el-table
      :data="roleUserList"
      style="width: 100%"
    >
      <el-table-column
        type="index"
        width="250"
        label="序号"
      />
      <el-table-column
        prop="name"
        label="员工姓名"
      />
      <el-table-column
        prop="userName"
        label="登录账号"
      />
    </el-table>
  </div>
</el-tab-pane>

五、新增角色

1. 准备路由

1)@\views\System\Role\AddRole.vue

点我查看代码
vue
<template>
  <div class="add-role">
    <header class="add-header">
      <div class="left">
        <span class="arrow" @click="$router.back()"
          ><i class="el-icon-arrow-left" />返回</span
        >
        <span>|</span>
        <span>添加角色</span>
      </div>
      <div class="right">系统管理员</div>
    </header>
    <main class="add-main">
      <div class="step-container">
        <el-steps direction="vertical" :active="1">
          <el-step title="角色信息" />
          <el-step title="权限信息" />
          <el-step title="检查并完成" />
        </el-steps>
      </div>
      <div class="form-container">
        <div class="title">角色信息</div>
        <div class="form">角色信息内容</div>
      </div>
      <div class="form-container">
        <div class="title">权限配置</div>
        <div class="form">权限配置内容</div>
      </div>
      <div class="form-container">
        <div class="title">检查并完成</div>
        <div class="form">检查并完成内容</div>
      </div>
    </main>
    <footer class="add-footer">
      <div class="btn-container">
        <el-button>上一步</el-button>
        <el-button type="primary">下一步</el-button>
      </div>
    </footer>
  </div>
</template>

<script>
export default {}
</script>

<style scoped lang="scss">
.add-role {
  background-color: #f4f6f8;
  height: 100vh;

  .add-header {
    display: flex;
    justify-content: space-between;
    padding: 0 20px;
    height: 64px;
    line-height: 64px;
    font-size: 16px;
    font-weight: bold;
    background-color: #fff;

    .left {
      span {
        margin-right: 4px;
      }

      .arrow {
        cursor: pointer;
      }
    }

    .right {
      text-align: right;
    }
  }

  .add-main {
    position: fixed;
    top: 64px;
    bottom: 88px;
    width: 100%;
    overflow-y: auto;
    background: #f4f6f8;
    padding: 20px 230px;
    display: flex;

    .tree-wrapper {
      display: flex;
      justify-content: start;
      flex-wrap: wrap;

      .tree-item {
        flex: 0 0 auto;
        width: 33.33333%;
        border-right: 1px solid #e4e7ec;
        padding: 0px 10px;
        margin-bottom: 10px;
        text-align: center;
        .tree-title {
          background: #f5f7fa;
          text-align: center;
          padding: 10px 0;
          margin-bottom: 12px;
        }
      }
    }

    .step-container {
      height: 400px;
      width: 200px;
    }
    .form-container {
      flex: 1;
      background-color: #fff;
      overflow-y: auto;
      .title {
        height: 60px;
        line-height: 60px;
        padding-left: 20px;
      }
      .form {
        margin-bottom: 20px;
        padding: 20px 65px 24px;
        .el-form {
          display: flex;
          flex-wrap: wrap;
        }
        .info {
          font-size: 14px;
          color: #666;
          .form-item {
            margin-bottom: 20px;
          }
        }
      }
      .form-box {
        width: 600px;
        display: flex;
        flex-direction: column;
      }
    }
  }

  .add-footer {
    position: fixed;
    bottom: 0;
    width: 100%;
    padding: 24px 50px;
    color: #000000d9;
    font-size: 14px;
    background: #fff;
    text-align: center;
    z-index: 10001;
  }
}
</style>

2)@\router\index.js 中添加一级路由

js
{
  path: '/roleAdd',
  component: () => import('@/views/System/role/AddRole')
}

3)@\views\System\Role\index.vue 给按钮绑定单击事件

vue
<el-button class="addBtn" size="mini" @click="$router.push('/roleAdd')">
  添加角色
</el-button>

2. 角色信息表单校验

jsx
<div class="form">
  <el-form ref="roleForm" class="form-box" :model="roleForm" :rules="roleRules">
    <el-form-item label="角色名称" prop="roleName">
      <el-input v-model="roleForm.roleName" />
    </el-form-item>
    <el-form-item label="角色描述" prop="remark">
      <el-input v-model="roleForm.remark" />
    </el-form-item>
  </el-form>
</div>


// data 选项中添加
roleForm: {
  roleName: '',
  remark: ''
},
roleRules: {
  roleName: [
    { required: true, message: '请输入角色名称', trigger: 'blur' }
  ],
  remark: [
    { required: true, message: '请输入描述名称', trigger: 'blur' }
  ]
}

// methods 选项
nextStep() {
  if (this.activeStep >= 2) {
    this.activeStep = 2
    return
  }
  if (this.activeStep === 0) {
    this.$refs.roleForm.validate(valid => {
      if (valid) {
        this.activeStep++
      }
    })
    return
  }
},

3. 渲染权限配置初始化 Tree 列表

jsx
<div class="form">
  <div class="tree-wrapper">
    <div v-for="item in treeList" :key="item.id" class="tree-item">
      <div class="tree-title">{{ item.title }}</div>
      <el-tree
        ref="tree"
        node-key="id"
        :data="item.children"
        :props="defaultProps"
        show-checkbox
        :default-expand-all="true"
      />
    </div>
  </div>
</div>

// data 选项中添加
treeList: [],
defaultProps: {
  children: 'children',
  label: 'title'
}
// created 选项中添加
created() {
  this.getTreeList()
},
// methods 选项中添加
async getTreeList() {
  const res = await getTreeListAPI()
  this.treeList = res.data
}

4. 检查权限配置是否至少选择一项

通过调用 Tree 实例方法,拿到选中的节点数组,检查数组长度是否为零。

1)如果长度为零,代表没有选中任何东西,提示用户,不进入下一步。

2)如果长度不为零,代表选中了,直接进入下一步。

js
data() {
  return {
    activeStep: 0,
    roleForm: {
      roleName: '',
      remark: '',
      perms: []
    }
    // ...
  }
}

// methods 选项中添加
nextStep() {
  if (this.activeStep >= 3) return
  if (this.activeStep === 1) {
    // ...
  } else if (this.activeStep === 2) {
    // 当前是权限信息状态
    this.roleForm.perms = []
    this.$refs.tree.forEach(tree => {
      this.roleForm.perms.push(tree.getCheckedKeys())
    })
    // 如果长度为零,则没有选中任何东西
    if (this.roleForm.perms.flat().length === 0) {
      this.$message({
        type: 'error',
        message: '请至少选择一个权限点!'
      })
    } else {
      // 进入到下一页
      this.activeStep++
    }
  }
}

5. 检查并完成 - 回填数据

jsx
<div class="form">
  <div class="info">
    <div class="form-item">角色名称:{{ roleForm.roleName }}</div>
    <div class="form-item">角色描述:{{ roleForm.remark }}</div>
    <div class="form-item">角色权限:</div>
    <div class="tree-wrapper">
      <div v-for="item in treeList" :key="item.id" class="tree-item">
        <div class="tree-title">{{ item.title }}</div>
        <el-tree
          ref="diabledTree"
          :data="item.children"
          show-checkbox
          check-strictly	
          default-expand-all
          node-key="id"
          :props="{ label: 'title'}"
        />
      </div>
    </div>
  </div>
</div>

// methods 选项中的 nextStep 方法添加
if (this.roleForm.perms.flat().length === 0) {
  this.$message({
    type: 'error',
    message: '请至少选择一个权限点!'
  })
} else {
  // 进入到下一页
  this.activeStep++
  // 回填已选择数据
  this.$refs.diabledTree.forEach((tree, index) => {
    tree.setCheckedKeys(this.roleForm.perms[index])
  })
}

6. 检查并完成 - 确认添加

1)在 @\api\system.js 中封装 API

js
// 创建角色
export function createRoleUserAPI(data) {
  return request({
    url: `/park/sys/role`,
    method: 'POST',
    data
  })
}

2)给确认添加按钮绑定单击事件

js
async confirmAdd() {
  await createRoleUserAPI(this.roleForm)
  this.$message({
    type: 'success',
    message: '添加角色成功'
  })
  this.$router.back()
}

六、编辑角色

1. 页面布局

@\views\System\Role\index.vue 中添加下拉菜单。

vue
<div class="more">
  <el-dropdown>
    <span class="el-dropdown-link">
      <svg-icon icon-class="more" />
    </span>
    <el-dropdown-menu slot="dropdown">
      <el-dropdown-item>编辑角色</el-dropdown-item>
      <el-dropdown-item>删除</el-dropdown-item>
    </el-dropdown-menu>
  </el-dropdown>
</div>

2. 跳转编辑携带 roleId

vue
 <el-dropdown-item 
   @click.native="$router.push(`/addRole?id=${item.roleId}`)">
   编辑角色
 </el-dropdown-item>

3. 修改页面标题

jsx
// 缓存roleId
computed: {
  roleId() {
    return this.$route.query.id
  }
}


<span>{{ roleId ? "修改角色": "添加角色" }}</span>

4. 回填当前角色信息

js
// 回填数据
async getRoleDetail() {
  const res = await getRoleDetailAPI(this.roleId)
  const { perms, remark, roleId, roleName } = res.data
  // 回填基础表单
  this.roleForm = { perms, remark, roleId, roleName }
  // 回填Tree
  const treeComponentList = this.$refs.tree
  treeComponentList.forEach((tree, index) => {
    tree.setCheckedKeys(this.roleForm.perms[index])
  })
}

mounted() {
  if (this.$route.query.id) {
    this.getRoleDetail()
  }
}

5. 确认修改

1)在 @\api\system.js 中封装 API

js
// 修改角色
export function updateRoleAPI(data) {
  return request({
    url: `/park/sys/role`,
    method: 'PUT',
    data
  })
}

2)给按钮添加单击事件

js
 async confirmAdd() {
  // 编辑
  if (this.roleId) {
    await updateRoleAPI(this.roleForm)
  } else {
    // 添加
    await createRoleUserAPI(
      this.roleForm
    )
  }
  this.$message({
    type: 'success',
    message: `${this.roleId ? '编辑' : '新增'}角色成功`
  })
  this.$router.back()
}

七、删除角色

1)在 @\api\system.js 中封装 API

js
// 删除角色
export function delRoleUserAPI(roleId) {
  return request({
    url: `/park/sys/role/${roleId}`,
    method: 'DELETE'
  })
}

2)给删除按钮添加单击事件,并且发送网络请求

jsx
<div class="more">
  <el-dropdown>
    <span class="el-dropdown-link">
      <svg-icon name="more" />
    </span>
    <el-dropdown-menu slot="dropdown">
      <el-dropdown-item>编辑角色</el-dropdown-item>
      <el-dropdown-item @click.native="delRole(item.roleId)">删除</el-dropdown-item>
    </el-dropdown-menu>
  </el-dropdown>
</div>


delRole(roleId) {
  this.$confirm('是否确认删除当前角色?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(async() => {
    await delRoleUserAPI(roleId)
    this.getRoleList()
    this.$message({
      type: 'success',
      message: '删除成功!'
    })
  })
}

第八章:前端权限控制 - 菜单路由权限

一、根据权限加载路由

1. 获取用户权限数据

权限数据也属于当前用户相关的信息,使用 Vuex 进行维护。

1)封装请求 API 到 @\api\user.js

js
// 获取用户信息
export function getProfileAPI() {
  return request({
    url: '/park/user/profile',
    method: 'GET'
  })
}

2)Vuex 逻辑编写

js
import { getProfileAPI } from '@/apis/user'
export default {
  namespaced: true,
  state: () => {
    return {
      profile: {}
    }
  },
  mutations: {
    setProfile(state, profile) {
      state.profile = profile
    }
  },
  actions: {
    async getProfile(ctx) {
      const res = await getProfileAPI()
      ctx.commit('setProfile', res.data)
      return res.data.permissions
    }
  }
}

3)permission 文件触发 action

js
// 权限控制
import router from './router'
import { getCookie } from './utils/auth'
import store from './store'

const WHITE_LIST = ['/login', '/404']

router.beforeEach(async(to, from, next) => {
  const token = getCookie('park_token')
  // 有token
  if (token) {
    next()
    // 获取用户信息
    if (!store.state.user.profile.id) {
      // 拿到用户的权限信息
      const permissions = await store.dispatch('user/getProfile')
      console.log(permissions)
      // 后续操作都在这里
    }
  } else {
    // 没有token
    if (WHITE_LIST.includes(to.path)) {
      next()
    } else {
      next('/login')
    }
  }
})

2. 筛选一级与二级路由权限标识

js
// 权限控制
import router from './router'
import { getCookie } from './utils/auth'
import store from './store'

const WHITE_LIST = ['/login', '/404']

// 获取一级权限的标识
function getFirstRoutePermission(permission) {
  const firstPermArr = permission.map(item => {
    return item.split(':')[0]
  })
  // 去重
  // Set 不能存重复的数据(去重)但是它不是一个真实的数组
  return Array.from(new Set(firstPermArr))
}
// 获取二级权限标识
function getSecondRoutePermission(permission) {
  const secondPermArr = permission.map(item => {
    const arr = item.split(':')
    return `${arr[0]}:${arr[1]}`
  })
  return Array.from(new Set(secondPermArr))
}

router.beforeEach(async(to, from, next) => {
  const token = getCookie('park_token')
  // 有token
  if (token) {
    next()
    // 获取用户信息
    if (!store.state.user.profile.id) {
      // 1.拿到用户的权限信息
      const permissions = await store.dispatch('user/getProfile')
      console.log(permissions)
      // 2.根据权限标识 筛选出对应的一级路由的标识
      const firstPermission = getFirstRoutePermission(permission)
      console.log('一级权限标识', firstPermission)
      // 3.根据权限标识 筛选出对应的二级路由的标识
      const secondPermission = getSecondRoutePermission(permission)
      console.log('二级权限标识', secondPermission)
    }
  } else {
    // 没有token
    if (WHITE_LIST.includes(to.path)) {
      next()
    } else {
      next('/login')
    }
  }
})

3. 拆分静态和动态路由表

动态路由:需要做权限控制。可以根据不同的权限进行路由数量上的变化。

静态路由:不需要做权限控制。每一个用户都可以看到。

@\router\asyncRoutes.js

点我查看代码
js
import Layout from '@/layout'

// 动态路由表
export const asyncRoutes = [
  {
    path: '/park',
    component: Layout,
    permission: 'park',
    meta: { title: '园区管理', icon: 'el-icon-office-building' },
    children: [{
      path: 'building',
      permission: 'park:building',
      meta: { title: '楼宇管理' },
      component: () => import('@/views/Park/Building/index')
    },
    {
      path: 'enterprise',
      permission: 'park:enterprise',
      meta: { title: '企业管理' },
      component: () => import('@/views/Park/Enterprise/index')
    }]
  },
  {
    path: '/parking',
    component: Layout,
    permission: 'parking',
    meta: { title: '行车管理', icon: 'el-icon-guide' },
    children: [{
      path: 'area',
      permission: 'parking:area',
      component: () => import('@/views/Car/CarArea'),
      meta: { title: '区域管理' }
    }, {
      path: 'card',
      permission: 'parking:card',
      component: () => import('@/views/Car/CarCard'),
      meta: { title: '月卡管理' }
    }, {
      path: 'pay',
      permission: 'parking:payment',
      component: () => import('@/views/Car/CarPay'),
      meta: { title: '停车缴费管理' }
    },
    {
      path: 'rule',
      permission: 'parking:rule',
      component: () => import('@/views/Car/CarRule'),
      meta: { title: '计费规则管理' }
    }]
  },
  {
    path: '/pole',
    component: Layout,
    permission: 'pole',
    meta: { title: '一体杆管理', icon: 'el-icon-refrigerator' },
    children: [{
      path: 'info',
      permission: 'pole:info',
      component: () => import('@/views/Rod/RodManage'),
      meta: { title: '一体杆管理' }
    }, {
      path: 'waring',
      permission: 'pole:warning',
      component: () => import('@/views/Rod/RodWarn'),
      meta: { title: '告警记录' }
    }]
  },
  {
    path: '/sys',
    component: Layout,
    permission: 'sys',
    meta: { title: '系统管理', icon: 'el-icon-setting' },
    children: [{
      path: 'role',
      permission: 'sys:role',
      component: () => import('@/views/System/Role/index'),
      meta: { title: '角色管理' }
    }, {
      path: 'user',
      permission: 'sys:user',
      component: () => import('@/views/System/Employee/index'),
      meta: { title: '员工管理' }
    }]
  }
]

初始化时,只处理静态路由表。

点我查看代码
js
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

/* Layout */
import Layout from '@/layout'

// 俩种路由

export const routes = [
  {
    path: '/login',
    component: () => import('@/views/Login/index'),
    hidden: true
  },
  {
    path: '/addCard',
    component: () => import('@/views/Car/CarCard/add-card'),
    hidden: true
  },
  {
    path: '/addEnterprise',
    component: () => import('@/views/Park/Enterprise/AddEnterprise'),
    hidden: true
  },
  {
    path: '/enterpriseDetail',
    component: () => import('@/views/Park/Enterprise/EnterpriseDetail'),
    hidden: true
  },
  {
    path: '/addRole',
    component: () => import('@/views/System/Role/AddRole'),
    hidden: true
  },
  {
    path: '/',
    component: Layout,
    redirect: '/workbench'
  },
  // 一级路由:负责把layout架子渲染出来
  // 二级路由:path为空,会作为默认的二级路由一上来就渲染出来
  {
    path: '/workbench',
    component: Layout,
    children: [{
      path: '',
      component: () => import('@/views/Workbench/index'),
      meta: { title: '工作台', icon: 'el-icon-data-board' }
    }]
  },
  {
    path: '/404',
    component: () => import('@/views/404'),
    hidden: true
  }
]

const createRouter = () => new Router({
  mode: 'history', // require service support
  scrollBehavior: () => ({ y: 0 }),
  routes: [...routes]
})

const router = createRouter()

// 重置路由方法
export function resetRouter() {
  // 得到一个全新的router实例对象
  const newRouter = createRouter()
  // 使用新的路由记录覆盖掉老的路由记录
  router.matcher = newRouter.matcher
}

export default router

4. 根据菜单标识过滤动态路由表

使用一级权限点过滤一级路由,使用二级权限点过滤二级路由。

1)在 permission.js 文件中编写处理函数

js
// 根据权限标识过滤路由表
function getRoutes(firstRoutePerms, secondRoutePerms, asyncRoutes) {
  // 根据一级和二级对动态路由表做过滤 return出去过滤之后的路由表
  // 1. 根据一级路由对动态路由表做过滤
  return asyncRoutes.filter(route => {
    return firstRoutePerms.includes(route.permission)
  }).map(item => {
    // 2. 对二级路由做过滤
    return {
      ...item,
      children: item.children.filter(item => secondRoutePerms.includes(item.permission))
    }
  })
}

router.beforeEach(async(to, from, next) => {
  const token = store.state.user.token
  if (token) {
    // 有token
    if (to.path === '/login') {
      next('/')
    } else {
      next()
      // 1.拿到用户的权限信息
      const permission = await store.dispatch('menu/getUserPermission')
      console.log('全部的权限标识', permission)
      // 2.根据权限标识 筛选出对应的一级路由的标识
      const firstPermission = getFirstRoutePermission(permission)
      console.log('一级权限标识', firstPermission)
      // 3.根据权限标识 筛选出对应的二级路由的标识
      const secondPermission = getSecondRoutePermission(permission)
      console.log('二级权限标识', secondPermission)
      // 4.根据一级权限标识和二级权限标识和动态路由进行筛选
      console.log('所有的动态路由', asyncRoutes)
      const routes = getRoutes(firstPermission, secondPermission, asyncRoutes)
      console.log('筛选之后的动态路由', routes)
    }
  } else {
    // 没有token
    if (whiteList.includes(to.path)) {
      next()
    } else {
      next('/login')
    }
  }
})

5. 动态添加路由表

js
router.beforeEach(async(to, from, next) => {
  const token = store.state.user.token
  if (token) {
    // 有token
    if (to.path === '/login') {
      next('/')
    } else {
      next()
      // 1.拿到用户的权限信息
      const permission = await store.dispatch('menu/getUserPermission')
      console.log('全部的权限标识', permission)
      // 2.根据权限标识 筛选出对应的一级路由的标识
      const firstPermission = getFirstRoutePermission(permission)
      console.log('一级权限标识', firstPermission)
      // 3.根据权限标识 筛选出对应的二级路由的标识
      const secondPermission = getSecondRoutePermission(permission)
      console.log('二级权限标识', secondPermission)
      // 4.根据一级权限标识和二级权限标识和动态路由进行筛选
      console.log('所有的动态路由', asyncRoutes)
      const routes = getRoutes(firstPermission, secondPermission, asyncRoutes)
      console.log('筛选之后的动态路由', routes)
      // 5.把筛选后的路由 展示在左侧
      // 5.1先把筛选之后的路由添加到路由对象中(跳转)
      routes.forEach(item => router.addRoute(item))
    }
  } else {
    // 没有token
    if (whiteList.includes(to.path)) {
      next()
    } else {
      next('/login')
    }
  }
})

6. 渲染左侧菜单

左侧的菜单应该和路由是同步的,使用的同一份数据,因为要动态变化渲染,所以可以通过 Vuex 进行维护。

1)vuex 新增一个模块,menu 模块,先以静态的路由表作为初始值。

2)在得到过滤之后的动态路由表之后,和之前的静态做一个结合。

3)在组件中结合 v-for 指令做使用 Vuex 中的数据做渲染。

1)编写 vuex 逻辑
js
import { contantsRoutes } from '@/router'
export default {
  namespaced: true,
  state: () => {
    return {
      menuList: []
    }
  },
  mutations: {
    setMenuList(state, filterRoutes) {
      state.menuList = [...contantsRoutes, ...filterRoutes]
    }
  }
}
2)触发 mutation
js
router.beforeEach(async(to, from, next) => {
  const token = getCookie('park_token')
  // 有token
  if (token) {
    next()
    // 获取用户信息
    if (!store.state.user.profile.id) {
      // 1.拿到用户的权限信息
      const permission = await store.dispatch('menu/getUserPermission')
      console.log('全部的权限标识', permission)
      // 2.根据权限标识 筛选出对应的一级路由的标识
      const firstPermission = getFirstRoutePermission(permission)
      console.log('一级权限标识', firstPermission)
      // 3.根据权限标识 筛选出对应的二级路由的标识
      const secondPermission = getSecondRoutePermission(permission)
      console.log('二级权限标识', secondPermission)
      // 4.根据一级权限标识和二级权限标识和动态路由进行筛选
      console.log('所有的动态路由', asyncRoutes)
      const routes = getRoutes(firstPermission, secondPermission, asyncRoutes)
      console.log('筛选之后的动态路由', routes)
      // 5.把筛选后的路由 展示在左侧
      // 5.1先把筛选之后的路由添加到路由对象中(跳转)
      routes.forEach(item => router.addRoute(item))
      // 5.2存入Vuex渲染左侧菜单
      store.commit('menu/setMenuList', routes)
    }
  } else {
    // 没有token
    if (WHITE_LIST.includes(to.path)) {
      next()
    } else {
      next('/login')
    }
  }
})
3)改写组件中的菜单渲染数据
vue
computed: {
  routes() {
    return this.$router.options.routes
    return this.$store.state.menu.menuList
  }
}
4)处理管理员的权限

从小号退出后,再登录管理员账号,并不能正确的显示路由菜单。针对管理员账号添加对应逻辑。

js
// 根据权限标识过滤路由表
function getRoutes(firstRoutePerms, secondRoutePerms, asyncRoutes) {
  if (firstRoutePerms.includes('*')) {
    // 管理员
    return asyncRoutes
  }
  // 根据一级和二级对动态路由表做过滤 return出去过滤之后的路由表
  // 1. 根据一级路由对动态路由表做过滤
  return asyncRoutes.filter(route => {
    return firstRoutePerms.includes(route.permission)
  }).map(item => {
    // 2. 对二级路由做过滤
    return {
      ...item,
      children: item.children.filter(item => secondRoutePerms.includes(item.permission))
    }
  })
}

7. 退出登录重置

1)在 @\store\modules\menu.js 的 mutations 中添加清除 vuex 中保存的路由信息,以及路由对象的信息。

js
clearMenuList(state) {
    state.menuList = []
    resetRouter()
}

2)在退出的时候调用此方法。

二、按钮权限

1. 方案一

在用户请求权限数据时,响应的接口里就有权限数据列表。可以在每个按钮上添加 v-if="权限数据列表.includes('权限名')"

2. 方案二:全局指令方案

1)@\directive\index.js

js
// 放置全局指令
import Vue from 'vue'
import store from '@/store'

// 管理员权限特殊处理
const adminPerms = '*:*:*'
Vue.directive('auth-btn', {
  inserted(el, data) {
    const perms = store.state.user.profile.permissions
    if (!perms.includes(data.value) && !perms.includes(adminPerms)) {
      el.remove()
    }
  }
})

2)使用指令

vue
<el-button 
  v-auth-btn="'park:building:add_edit'" 
  type="primary" @click="addBuilding">添加楼宇</el-button>

附录

一、自动生成导航菜单

根据路由配置,智能地决定如何渲染侧边栏中的每一个菜单项。

@/layout/components/Sidebar/index.vue

vue
<template>
  <!-- 左侧菜单组件 -->
  <el-menu
    :default-active="activeMenu"
    mode="vertical"
    :collapse-transition="false"  <!-- 是否开启折叠动画 -->
    :unique-opened="true"  <!-- 是否只保持一个子菜单的展开 -->
  >
    <!-- 菜单中的每一项 -->
    <sidebar-item
      v-for="route in routes"
      :key="route.path"
      :item="route"
      :base-path="route.path"
    />
  </el-menu>
<template>


<script>
export default {
  computed: {
    routes() {
      return this.$router.options.routes
    },
    activeMenu() {
      const route = this.$route
      const { meta, path } = route
      // if set path, the sidebar will highlight the path you set
      if (meta.activeMenu) {
        return meta.activeMenu
      }
      return path
    }
  }
}
</script>

@/layout/components/Sidebar/SidebarItem.vue

点我查看代码
vue
<template>
  <div v-if="!item.hidden">
    <template
      v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow"
    >
      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
          <item :icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" :title="onlyOneChild.meta.title" />
        </el-menu-item>
      </app-link>
    </template>

    <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
      <template slot="title">
        <item
          v-if="item.meta"
          :icon="item.meta && item.meta.icon"
          :title="item.meta.title"
        />
      </template>
      <sidebar-item
        v-for="child in item.children"
        :key="child.path"
        :is-nest="true"
        :item="child"
        :base-path="resolvePath(child.path)"
        class="nest-menu"
      />
    </el-submenu>
  </div>
</template>

<script>
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'

export default {
  name: 'SidebarItem',
  components: { Item, AppLink },
  props: {
    // route object
    item: {
      type: Object,
      required: true
    },
    isNest: {
      type: Boolean,
      default: false
    },
    basePath: {
      type: String,
      default: ''
    }
  },
  data() {
    this.onlyOneChild = null
    return {}
  },
  methods: {
    hasOneShowingChild(children = [], parent) {
      const showingChildren = children.filter(item => {
        if (item.hidden) {
          return false
        } else {
          // Temp set(will be used if only has one showing child)
          this.onlyOneChild = item
          return true
        }
      })

      // When there is only one child router, the child router is displayed by default
      if (showingChildren.length === 1 && showingChildren[0].path === '') {
        return true
      }

      // Show parent if there are no child router to display
      if (showingChildren.length === 0) {
        this.onlyOneChild = { ...parent, path: '', noShowingChildren: true }
        return true
      }

      return false
    },
    resolvePath(routePath) {
      if (isExternal(routePath)) {
        return routePath
      }
      if (isExternal(this.basePath)) {
        return this.basePath
      }
      return path.resolve(this.basePath, routePath)
    }
  }
}
</script>

它会根据以下三种情况,决定最终的显示形态:

  • 情况一:渲染为“单级菜单”如果满足以下任一条件,组件会把自己渲染成一个可以直接点击的、没有下拉箭头的菜单项 (<el-menu-item>):

    • 只有一个可见的子菜单:比如“系统管理”下只有一个“用户管理”,为了简化操作,就直接在侧边栏显示“用户管理”。
    • 没有任何可见的子菜单:比如“报表中心”下的所有子菜单都因权限被隐藏了,这时就把“报表中心”自身作为一个可点击的链接显示出来。
  • 情况二:渲染为“多级子菜单”如果不满足情况一,并且有多个可见的子菜单,组件就会把自己渲染成一个带下拉箭头的、可展开的父菜单 (<el-submenu>)。

  • 情况三:强制始终显示为“多级子菜单”** 有一个特殊例外:如果在路由配置中设置了 alwaysShow: true,那么无论它有几个子菜单(即使只有一个),它都会强制自己显示为可展开的父菜单 (<el-submenu>)。这通常用于保持UI结构的一致性。

  • 路径拼接 (resolvePath 方法):由于菜单是嵌套的,子菜单的路径需要和父菜单的路径拼接起来才能得到完整的 URL。resolvePath 方法负责处理这个拼接逻辑,并能正确处理绝对路径和相对路径。
  • 链接类型 (AppLink 组件):为了同时支持内部路由跳转(用 <router-link>)和外部网站链接(用 <a> 标签),它封装了一个 AppLink 组件。这个组件会检查路径是不是一个外部链接,然后动态地渲染成正确的标签。

4. 内容渲染 (Item 组件)

  • 为了统一菜单项的图标和标题的渲染,它抽象出了一个 Item.vue 函数式组件。
  • 这个组件负责接收 icontitle,然后用 render 函数生成对应的 DOM,并处理了 Element UI 图标和 SVG 图标两种情况。

Released under the MIT License.