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

后台管理系统负责园区内各项数据的管理,前台可视化项目负责重点园区数据的展现。
二、技术栈
后台管理平台
Vue 2.6
Vuex
ElementUI
我们是基于 vue-admin。vue-admin 是基于 vue-admin-template 做了一些升级和改版之后的后台管理系统脚手架,内置了必要的安装包、目录结构划分、路由表设计等等,方便做==二次开发==,我们需要做的大部分是==填空题== ,架子搭建部分工作通常由团队 Leader 来做。
js-cookie:一个简单轻量的 JavaScript API,用于处理浏览器 Cookie。
day.js
前台可视化
- Vue 3
- Echarts
- Spline
- VScaleScreen
微前端接入
- 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
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 历史版本。
四、接口文档
第二章:项目搭建
一、克隆项目结构
# 克隆项目
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 属性指定要使用的校验规则。
上面的实现后,只能做到提示。如果用户依然点击提交按钮,依旧会提交给服务器,怎么能强制要求用户按规则填好后才能提交?
<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
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
<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 中的登录方法中添加记住我的逻辑。
// remeber逻辑
if (this.remember) {
localStorage.setItem(FORMDATA_KEY, JSON.stringify(this.form))
} else {
localStorage.removeItem(FORMDATA_KEY)
}在 created 生命周期中,每次加载页面,都先看 localStorage 是否存了用户账号、密码信息,有就给 data 选项中添数据。
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 中添加清除方法。
mutations: {
removeUserToken (state) {
state.userToken = ''
removeToken()
}
},2)给 @/layout/components/Navbar.vue 绑定单击事件,调用上面的 removeUserToken。
<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?其实在后端处理了,前端就不用处理了。
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 文件。
// 权限控制
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 引入生效
// 在router引入之后引入
import './permission'3. 接口错误统一处理 & Token 失效处理
需求:接口报错的时候提示用户到底是哪里错误。
接口数量很多,需要实现统一管控,不管哪个接口报错了,都能监控到,而且给出提示。
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
点我查看代码
<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
export function getCardListAPI(params) {
return request({
url: '/parking/card/list',
method: 'get',
params
})
}2)组件中调用 api
@views/Car/CarCard/index.vue
// 渲染方法
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)添加列表组件
<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 是方法,作用是把原始的值在方法里操作后,返回值会显示在单元格中。
<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. 序号处理
<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)代码实现
<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 如何给用户呈现,以及如何搜集此类表单数据。
<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. 查询按钮绑定回调
<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
点我查看代码
<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. 配置路由完成跳转
{
path: '/car/addMonthCard',
component: () => import('@/views/car/CarCard/addMonthCard')
}<el-button type="primary" @click="$router.push('/car/addMonthCard')">添加月卡</el-button>3. 表单校验
1)添加校验规则
较复杂的校验可以通过设置一个校验函数来做,给校验选项添加一个 validator 选项,值为校验函数,在校验函数中编写校验规则。
<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)统一校验俩个表单
<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
/**
* 新增月卡
*/
export function createCardAPI(data) {
return request({
url: '/parking/card',
method: 'POST',
data
})
}5. 处理表单数据提交
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. 重置表单
重置表单主要做两件事:① 清空输入数据;② 清空校验错误。
// 重置表单
resetForm() {
this.$refs.feeForm.resetFields()
this.$refs.carInfoForm.resetFields()
}五、编辑月卡
1. 跳转到修改页,且携带 ID
<el-button size="mini" type="text" @click="editCard(scope.row.id)">编辑</el-button>
editCard(id) {
this.$router.push({
path: '/cardAdd',
query: {
id
}
})
}2. 回填数据
1)封装请求接口
// 获取月卡详情
export function getCardDetailAPI(id) {
return request({
url: `/parking/card/detail/${id}`
})
}2)在组件中使用
// 获取详情
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
// 删除月卡
export function delCardAPI(id) {
return request({
url: `/parking/card/${id}`,
method: 'DELETE'
})
}2)组件中使用
// 绑定事件
<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 的形式显示出来。
<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)处理数据调用接口
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
点我查看代码
<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
import request from '@/utils/request'
// 获取企业列表
export function getEnterpriseListAPI(params) {
return request({
url: '/park/enterprise',
params
})
}2)组件中获取数据
<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 属性指定当前列要渲染的字段名称。
<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)渲染分页
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)点击实现分页切换
<el-pagination
@current-change="pageChange"
/>
pageChange(page) {
// 更改参数
this.params.page = page
// 重新获取数据渲染
this.getExterpriseList()
}3. 序号处理
<el-table-column type="index" label="序号" :index="indexMethod" />
indexMethod(row) {
return (this.params.page - 1) * this.params.pageSize + 1 + row
},三、查询搜索
需求:
① 用户输入查询内容之后,点击查询按钮以当前输入关键词做为参数获取数据。
② 点击清空按钮时复原初始数据。
1. 搜集用户输入的数据
// 增加新的name查询字段
data() {
return {
params: {
page: 1,
pageSize: 10,
name: '' // 增加字段name
}
}
}2. 查询按钮绑定回调
clear 事件是 element ui 组件库自带的。在点击由 clearable 属性生成的清空按钮时触发。
// 绑定查询回调
<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
点我查看代码
<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)绑定路由(一级路由)
{
path: '/enterpriseAdd',
component: () => import('@/views/park/enterprise/Add')
}3)路由跳转
<el-button type="primary" @click="$router.push('/enterpriseAdd')">添加企业</el-button>2. 行业字段渲染
1)在 @\api\enterprise.js 中封装 API
// 获取行业列表
export function getIndustryListAPI() {
return request({
url: '/park/industry'
})
}2)获取数据
<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)绑定下拉框
<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
// 上传合同
export function uploadAPI(data) {
return request({
url: '/upload',
method: 'POST',
data
})
}2)准备上传组件
<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)调用接口完成上传
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)上传前验证文件
// 绑定上传前回调
<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)表单基础校验
// 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)正则校验手机号
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)统一校验
<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 表单校验系统不能得到上传之后的通知。
解决办法:在上传完毕之后手动校验营业执照字段。
async uploadRequest(data) {
// 上传逻辑...
// 单独校验表单,清除错误信息
this.$refs.ruleForm.validateField('businessLicenseId')
}5. 删除图片,清空表单中的数据
<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 封装新增接口
// 创建公司
export function createExterpriseAPI(data) {
return request({
url: '/park/enterprise',
method: 'POST',
data
})
}2)点击提交
@\views\park\enterprise\add.vue
confirmSubmit() {
this.$refs.ruleForm.validate(async valid => {
if (!valid) return
// 1. 调用接口
await createExterpriseAPI(this.addForm)
// 2. 返回列表页
this.$router.back()
})
}五、编辑企业
1. 携带参数跳转编辑页
<el-button
size="mini"
type="text"
@click="editRent(scope.row.id)"
>编辑</el-button>
editRent(id) {
this.$router.push({
path: '/exterpriseAdd',
query: {
id
}
})
}2. 根据 id 适配文案显示
<el-page-header
:content="`${id?'编辑企业':'添加企业'}`"
@back="$router.back()" />
computed: {
id() {
return this.$route.query.id
}
}3. 回填企业数据

1)封装接口
// 获取合同详情
export function getEnterpriseDetailAPI(id) {
return request({
url: `/park/enterprise/${id}`
})
}2)调用接口,回填数据
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)回显营业执照
<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)封装编辑更新接口
// 更新企业
export function updateExterpriseAPI(data) {
return request({
url: '/park/enterprise',
method: 'PUT',
data
})
}2)区分状态接口提交
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)封装接口
// 删除企业
export function delEnterpriseAPI(id) {
return request({
url: `/park/enterprise/${id}`,
method: 'DELETE'
})
}2)绑定事件执行删除
<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)控制弹框打开关闭
<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)准备表单
点我查看代码
<!-- 表单模版 -->
<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)封装获取楼宇列表接口
// 获取楼宇列表
export function getRentBuildListAPI() {
return request({
url: '/park/rent/building'
})
}2)打开弹框时调用接口
import { getRentBuildListAPI } from '@/apis/enterprise'
// 打开弹框
async addRent(enterpriseId) {
this.rentDialogVisible = true
// 获取楼宇下拉列表
const res = await getRentBuildListAPI()
this.buildList = res.data
}3)渲染下拉列表视图模版
<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)调用上传接口
<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)上传前校验
<!--
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)提交前的统一校验
<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)处理移除文件时,继续显示校验
<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)封装接口
// 创建合同
export function createRentAPI(data) {
return request({
url: '/park/enterprise/rent',
method: 'POST',
data
})
}2)补充企业 id 参数
<el-button size="mini" type="text" @click="addRent(row.id)">添加合同</el-button>
// 添加合同
addRent(id) {
this.dialogVisible = true
// 把企业id存入表单对象
this.rentForm.enterpriseId = id
}3)处理参数提交表单
// 确认提交
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 静态结构
<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
// 获取合同列表
export function getRentListAPI(id) {
return request({
url: `/park/enterprise/rent/${id}`
})
}2)展开时获取合同数据
// 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. 适配合同状态
// 格式化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)封装接口
// 退租
export function outRentAPI(rentId) {
return request({
url: `/park/enterprise/rent/${rentId}`,
method: 'PUT'
})
}2)修改状态
<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)适配视图状态
退租按钮逻辑:如果当前是退租状态 ==> 禁用;如果不是退租状态 ==> 启用。
删除按钮逻辑:只有已退租的时候,删除才是启用的,否则就是禁用的。
<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)封装接口
// 删除租赁合同
export function delRentAPI(rentId) {
return request({
url: `/park/enterprise/rent/${rentId}`,
method: 'DELETE'
})
}2)调用接口
<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
点我查看代码
<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
{
path: '/enterpriseDetail',
component: () => import('@/views/park/enterprise/Detail')
}2. 渲染列表
1)封装接口(已有)
// 获取详情
export function getEnterpriseDetailAPI(id) {
return request({
url: `/park/enterprise/${id}`,
method: 'GET'
})
}2)渲染数据
点我查看代码
<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. 预览功能实现
预览地址拼接上合同文件链接地址在新窗口打开。
<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. 下载功能
<el-table-column
prop="address"
label="操作"
>
<template #default="{row}">
<el-button type="text"><a :href="row.contractUrl">合同下载</a></el-button>
</template>
</el-table-column>第六章:行车管理 - 计费规则管理

一、页面布局
点我查看代码
<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
import request from '@/utils/request'
// 获取规则列表
export function getRuleListAPI(params) {
return request({
url: 'parking/rule/list',
params
})
}2)组件中获取数据
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)绑定模版
<el-table :data="ruleList" style="width: 100%">2. 分页管理
<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. 序号处理
<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)调用插件方法导出
代码实现
npm i --save https://cdn.sheetjs.com/xlsx-0.20.0/xlsx-0.20.0.tgzimport { 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. 按照业务数据导出
// 导出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')
}四、适配付费状态
// 适配收费状态
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 文件
点我查看代码
<!-- 弹框 -->
<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)父组件中调用子组件
<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)页面布局
点我查看代码
<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
// 获取角色列表
export function getRoleListAPI() {
return request({
url: '/park/sys/role'
})
}3)编写渲染列表
<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)点击激活高亮
<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)页面布局
<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
// 获取tree权限列表
export function getTreeListAPI() {
return request({
url: '/park/sys/permision/all/tree'
})
}3)编写渲染逻辑
// 组件中获取功能权限列表
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. 每组权限列表渲染
1)树形控件绑定数据
<!-- 组件中渲染模版 -->
<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)配置展示的字段
<el-tree
:props="{
label: 'title',
children: 'children'
}"
/>3)定制 tree 样式
show-checkbox:定制选择框。
default-expand-all:定制默认展开所有。
<!-- 树形结构 -->
<el-tree
:data="item.children"
:props="{
label: 'title',
children: 'children'
}"
show-checkbox
:default-expand-all="true"
/>4)禁用功能实现
实现方式一:
// 递归处理函数
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 为不可选状态。
<!-- 树形结构 -->
<el-tree
:data="item.children"
:props="{
label: 'title',
children: 'children',
disabled: () => true
}"
show-checkbox
:default-expand-all="true"
/>三、高亮权限点
1. 点击左侧列表获取 roleId
// 切换角色是获取roleId
changeRole(idx) {
this.activeIndex = idx
// 获取当前角色下的权限点数据列表
const roleId = this.roleList[idx].roleId
console.log(roleId)
}2. 封装请求接口
封装 API 到 @\api\system.js
// 获取当前角色权限点列表
export function getRoleDetailAPI(roleId) {
return request({
url: `/park/sys/role/${roleId}`
})
}3. 根据当前 roleId 获取权限点列表
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 节点
<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. 初始化时获取当前角色权限点
// 生命周期里先以第一项roleId获取
async mounted() {
// 先获取角色列表和可选权限列表
await this.getRoleList()
await this.getTreeList()
// 再获取当前角色下的权限列表
this.getRoleDetail(this.roleList[0].roleId)
}注意:确保 roleList 和 TreeList 完成之后,再请求 perms。
四、角色成员列表渲染
1. 页面布局
点我查看代码
<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
// 获取角色成员列表
export function getRoleUserAPI(roleId) {
return request({
url: `/park/sys/roleUser/${roleId}`
})
}3. 组件逻辑编写
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
点我查看代码
<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 中添加一级路由
{
path: '/roleAdd',
component: () => import('@/views/System/role/AddRole')
}3)@\views\System\Role\index.vue 给按钮绑定单击事件
<el-button class="addBtn" size="mini" @click="$router.push('/roleAdd')">
添加角色
</el-button>2. 角色信息表单校验
<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 列表
<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)如果长度不为零,代表选中了,直接进入下一步。
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. 检查并完成 - 回填数据
<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
// 创建角色
export function createRoleUserAPI(data) {
return request({
url: `/park/sys/role`,
method: 'POST',
data
})
}2)给确认添加按钮绑定单击事件
async confirmAdd() {
await createRoleUserAPI(this.roleForm)
this.$message({
type: 'success',
message: '添加角色成功'
})
this.$router.back()
}六、编辑角色
1. 页面布局
在 @\views\System\Role\index.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
<el-dropdown-item
@click.native="$router.push(`/addRole?id=${item.roleId}`)">
编辑角色
</el-dropdown-item>3. 修改页面标题
// 缓存roleId
computed: {
roleId() {
return this.$route.query.id
}
}
<span>{{ roleId ? "修改角色": "添加角色" }}</span>4. 回填当前角色信息
// 回填数据
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
// 修改角色
export function updateRoleAPI(data) {
return request({
url: `/park/sys/role`,
method: 'PUT',
data
})
}2)给按钮添加单击事件
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
// 删除角色
export function delRoleUserAPI(roleId) {
return request({
url: `/park/sys/role/${roleId}`,
method: 'DELETE'
})
}2)给删除按钮添加单击事件,并且发送网络请求
<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
// 获取用户信息
export function getProfileAPI() {
return request({
url: '/park/user/profile',
method: 'GET'
})
}2)Vuex 逻辑编写
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
// 权限控制
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. 筛选一级与二级路由权限标识
// 权限控制
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
点我查看代码
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: '员工管理' }
}]
}
]初始化时,只处理静态路由表。
点我查看代码
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 router4. 根据菜单标识过滤动态路由表
使用一级权限点过滤一级路由,使用二级权限点过滤二级路由。
1)在 permission.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. 动态添加路由表
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 逻辑
import { contantsRoutes } from '@/router'
export default {
namespaced: true,
state: () => {
return {
menuList: []
}
},
mutations: {
setMenuList(state, filterRoutes) {
state.menuList = [...contantsRoutes, ...filterRoutes]
}
}
}2)触发 mutation
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)改写组件中的菜单渲染数据
computed: {
routes() {
return this.$router.options.routes
return this.$store.state.menu.menuList
}
}4)处理管理员的权限
从小号退出后,再登录管理员账号,并不能正确的显示路由菜单。针对管理员账号添加对应逻辑。
// 根据权限标识过滤路由表
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 中保存的路由信息,以及路由对象的信息。
clearMenuList(state) {
state.menuList = []
resetRouter()
}2)在退出的时候调用此方法。
二、按钮权限

1. 方案一
在用户请求权限数据时,响应的接口里就有权限数据列表。可以在每个按钮上添加 v-if="权限数据列表.includes('权限名')"。
2. 方案二:全局指令方案
1)@\directive\index.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)使用指令
<el-button
v-auth-btn="'park:building:add_edit'"
type="primary" @click="addBuilding">添加楼宇</el-button>附录
一、自动生成导航菜单
根据路由配置,智能地决定如何渲染侧边栏中的每一个菜单项。
@/layout/components/Sidebar/index.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
点我查看代码
<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结构的一致性。
3. 路径处理与链接生成 (Path Resolution & Link Generation)
- 路径拼接 (
resolvePath方法):由于菜单是嵌套的,子菜单的路径需要和父菜单的路径拼接起来才能得到完整的 URL。resolvePath方法负责处理这个拼接逻辑,并能正确处理绝对路径和相对路径。 - 链接类型 (
AppLink组件):为了同时支持内部路由跳转(用<router-link>)和外部网站链接(用<a>标签),它封装了一个AppLink组件。这个组件会检查路径是不是一个外部链接,然后动态地渲染成正确的标签。
4. 内容渲染 (Item 组件)
- 为了统一菜单项的图标和标题的渲染,它抽象出了一个
Item.vue函数式组件。 - 这个组件负责接收
icon和title,然后用render函数生成对应的 DOM,并处理了 Element UI 图标和 SVG 图标两种情况。