手把手教你实现一个vue3+ts+nodeJS后台管理系统(二十一)
前言
已经实现了登录模块(图形验证码、获取token...)和layout模块,vuex(持久化用户信息)和router(根据权限获取路由列表)模块也已经封装完毕。接下来就可以开始页面组件的开发,主要有用户管理、角色管理和菜单管理这三个后台管理系统中通用的模块,其余的模块大家感兴趣可以自行扩展。
后端获取分页列表的bug
在调试的时候发现,发现后端分页的时候count数目错误。大概就是sequelize
在findAndCountAll
带include配置项的时候会加上include所带模型的count,也就是它查了一个表的数量又带上另一个表的数量。解决方法就是在有findAndCountAll
方法并且带include
配置项中加入distinct:true
去重
用户管理
用户管理页面主要分以下几个功能
- 分页获取用户列表
- 对用户进行增删改查
- 控制不同角色的权限按钮的显隐
控制不同角色的权限按钮的显隐
我们先来完成第三个功能,要完成按钮的显隐我们第一个想到的就是在button标签中加入v-show
或者v-if
,但这两个指令没有办法很容易对用户的角色是否拥有此按钮进行判断,所以我们自己定义一个指令来完成这个工作。
我们先在src目录下建立一个directive目录来存放我们的自定义指令,然后建立文件夹permission来存放我们的控制权限按钮显隐的指令,再建立一个主文件导出所有指令方法,目录结构如下。
我们首先思考若有指令我们传的应该是权限标识字段(按钮的唯一标识),形如v-hasPerm="system:user:Add"
,这样指令方法接收到权限标识到用户的权限按钮列表中遍历查找,若有返回true按钮显示,若无则移除按钮,我们来实现此方法。
directive/permission/index.ts
import { store } from '@/store';
import { Directive, DirectiveBinding } from 'vue';
/**
* 按钮权限校验
*/
export const hasPerm: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
// 「超级管理员」拥有所有的按钮权限
const roles = store.state.user.roles;
if (roles.includes('管理员')) {
return true;
}
// 「其他角色」按钮权限校验
const { value } = binding;
if (value) {
const requiredPerms = value; // DOM绑定需要的按钮权限标识,例如system:user:add
// 判断vuex的buttons中是否存在传入的权限标识
const hasPerm = store.state.user.buttons?.some((button: string) => {
return requiredPerms.includes(button);
});
// 不存在,从DOM结构中移除这个按钮
if (!hasPerm) {
el.parentNode && el.parentNode.removeChild(el);
}
} else {
throw new Error('需要权限!"');
}
}
};
directive/index.ts
export { hasPerm } from './permission';
然后在main.ts
中注册指令
main.ts
import { createApp, Directive } from 'vue';
...
// 导入自定义指令
import * as directive from '@/directive';
Object.keys(directive).forEach((key) => {
app.directive(key, (directive as { [key: string]: Directive })[key]);
});
但暂时我们还没有添加权限按钮,我们稍后添加后再来实际使用
分页获取用户列表
分页列表用的是element-plus
的组件el-pagination
,给它传递两个值pageSize
(每页数量)、currentPage
(当前页)。再传递两个方法,一是当每页数量变化的时候的方法,这时我们将上面传递的pageSize
传给此方法,然后重新获取列表。而是当前页变化时候的方法,与第一个方法同样做法。
views/system/user.vue
<el-pagination v-model:currentPage="queryParams.currentPage" v-model:page-size="queryParams.pageSize"
:page-sizes="[1, 3, 5, 10]" layout="total, sizes, prev, pager, next, jumper" :total="userTotal"
@size-change="handleSizeChange" @current-change="handleCurrentChange" style="margin-top: 20px;" />
...
<script lang="ts" setup>
// 分页参数
const queryParams = reactive<userQueryParams>({
currentPage: 1, // 当前页
pageSize: 10, // 每页条数
username: undefined,
status: undefined
})
const queryFormRef = ref<FormInstance>()
let userTotal = ref(0) // 数据总条数
// 按照分页显示数据的函数
const getListUser = () => {
loading.value = true
// 发送ajax请求 把分页数据发送给后端
listUser(queryParams)
.then(res => {
console.log(res);
// 接收后端返回的数据总条数 total 和 对应页码的数据 data
let { count, rows } = res.data;
// 赋值给对应的变量即可
userTotal.value = count;
tableData.data = rows;
// 如果当前页没有数据 且 排除第一页
if (!rows.length && queryParams.currentPage !== 1) {
// 页码减去 1
queryParams.currentPage -= 1;
// 再调用自己
getListUser();
}
})
.finally(() => {
loading.value = false
})
}
// 每页显示条数改变 就会触发这个函数
const handleSizeChange = (val: number) => {
// 保存每页显示的条数
queryParams.pageSize = val;
queryParams.currentPage = 1;
// 调用分页函数
getListUser();
}
// 当前页码改变 就会触发这个函数
const handleCurrentChange = (val: number) => {
// 保存当前页码
queryParams.currentPage = val;
// 调用分页函数
getListUser();
}
</script>
显示效果
对用户进行增删改查
增改的功能需要弹窗与表单进行操作。而查只需要传递对应查询参数给分页函数即可,删只需要传递删除id或数组去调用接口方法,在此不再多做赘述。
增加和修改我们放在同一个弹窗当中,但点击新增(不带用户id) 时我们展示空表单并在点击提交时调用新增用户的方法。而点击修改或重置密码我们传递一个用户id,然后通过action字段来区分操作,然后传id时获取该id的用户数据传递给表单对象渲染上去,用户就可对其进行修改,并提交调用修改用户方法。有action字段例如为edit-pwd
就将表单显示为重置密码表单。
新增弹窗

修改弹窗

重置密码弹窗

views/system/user.vue
<el-dialog v-model="dialogFormVisible" :title="title" width="25%">
<!-- 新增及编辑弹窗表单 -->
<el-form :model="form" ref="editFormRef" :rules="rules" label-width="6em">
<el-form-item v-if="form.action !== 'edit-pwd'" label="用户帐号" prop="username">
<el-input v-model="form.username" placeholder="请输入帐号" />
</el-form-item>
<el-form-item v-if="form.action === 'edit-pwd'" label="原密码" prop="old_password">
<el-input v-model="form.old_password" type="password" placeholder="请输入原用户密码" />
</el-form-item>
<el-form-item v-if="!form.user_id || form.action === 'edit-pwd'" label="用户密码" prop="password">
<el-input v-model="form.password" type="password" placeholder="请输入用户密码" />
</el-form-item>
<el-form-item v-if="!form.user_id || form.action === 'edit-pwd'" label="确认密码" prop="repassword">
<el-input v-model="form.repassword" type="password" placeholder="请再次输入用户密码" />
</el-form-item>
<el-form-item v-if="form.action !== 'edit-pwd'" label="角色" prop="role_ids">
<el-select v-model="form.role_ids" multiple placeholder="请选择角色">
<el-option v-for="item in roles" :key="item.role_id" :label="item.role_name" :value="item.role_id" />
</el-select>
</el-form-item>
<el-form-item v-if="form.action !== 'edit-pwd'" label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio :label="1">开启</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogClose(editFormRef)">取消</el-button>
<el-button type="primary" @click="submitForm(editFormRef)" :loading="buttonLoading">保存
</el-button>
</div>
</el-dialog>
<script lang="ts" setup>
/**
* 新增编辑及修改密码弹窗
*/
// 新增编辑及修改密码弹窗变量及方法
let title = ref('')
let dialogFormVisible = ref(false)
const editFormRef = ref<FormInstance>()
// 表单校验
// 验证密码
const validateOldPwd = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error('请输入原密码'))
} else if (value.length < 6) {
callback(new Error('密码长度不能小于6'))
} else {
callback()
}
}
// 验证密码
const validatePwd = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error('请输入密码'))
} else if (value.length < 6) {
callback(new Error('密码长度不能小于6'))
} else {
callback()
}
}
// 二次验证密码
const validateRePwd = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== form.password) {
callback(new Error('两次输入密码不一致!'))
} else {
callback()
}
}
const rules = reactive({
username: [
{ required: true, message: '帐号不能为空', trigger: 'blur' },
{ min: 3, max: 12, message: '帐号长度3-12之内', trigger: 'blur' },
{ pattern: /^[a-zA-Z0-9]+$/, message: '帐号只能字母数字组成', trigger: 'blur' }
],
role_ids: [
{ required: true, message: '请选择角色', trigger: 'change', type: 'array' }
],
old_password: [{ validator: validateOldPwd, trigger: 'blur' }],
password: [{ validator: validatePwd, trigger: 'blur' }],
repassword: [{ validator: validateRePwd, trigger: 'blur' }]
})
// 弹窗表单对象
const form = reactive<userEditForm>({
user_id: 0,
action: '',
username: '',
old_password: '',
password: '',
repassword: '',
status: 0,
role_ids: []
})
const roles = ref([] as RoleItem[])
const buttonLoading = ref(false)
const getRoles = () => {
allRole().then(res => {
roles.value = res.data
})
}
// 将form对象初始化
const reset = () => {
form.user_id = 0
form.action = ''
form.username = ''
form.old_password = ''
form.password = ''
form.repassword = ''
form.status = 0
form.role_ids = []
}
// 重置表单方法
const resetQuery = (formEl: FormInstance | undefined) => {
queryParams.username = undefined
queryParams.status = undefined
formEl?.resetFields()
getListUser();
}
const dialogClose = (formEl: FormInstance | undefined) => {
dialogFormVisible.value = false
formEl?.resetFields()
reset()
}
// 点击新增,重置表单并显示
const handleAdd = () => {
title.value = "添加用户"
reset()
dialogFormVisible.value = true
}
// 点击重置密码:根据id获取用户数据并传递action字段
const handleReset = (user_id: number) => {
loading.value = true
reset()
getUserInfoById(user_id).then(res => {
loading.value = false
form.user_id = res.data.user_id
form.action = "edit-pwd"
dialogFormVisible.value = true
title.value = "重置密码"
})
}
// 点击编辑按钮:清空表单,根据id获取用户数据
const handleEdit = (user_id: number) => {
loading.value = true
reset()
getUserInfoById(user_id).then(res => {
loading.value = false
form.user_id = res.data.user_id
form.username = res.data.username
form.status = res.data.status
form.role_ids = res.data.roles.map((item: RoleItem) => {
if (item.role_id) {
return item.role_id
}
})
dialogFormVisible.value = true
title.value = "编辑用户信息"
})
}
// 当用户修改自己的密码需要重新登录
const reLogin = () => {
ElMessageBox.confirm('修改成功,请重新登录', '重新登录', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).finally(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
// 提交表单,根据action字段和id字段调用对应方法
const submitForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.validate((valid, fields) => {
if (valid) {
buttonLoading.value = true
if (form.action === 'edit-pwd') {
updatePwd({ user_id: form.user_id, old_password: form.old_password, password: form.password, repassword: form.repassword }).then(res => {
console.log(form.user_id, store.state.user.user_id);
if (form.user_id === store.state.user.user_id) {
reLogin()
}
ElMessage.success("重置密码成功")
dialogClose(editFormRef.value)
// 重新获取表格数据
getListUser()
})
.finally(() => {
buttonLoading.value = false
})
return false;
}
if (form.user_id) {
updateUser(form)
.then((res) => {
ElMessage.success('修改用户成功')
// 关闭弹窗
dialogClose(editFormRef.value)
// 重新获取表格数据
getListUser()
})
.finally(() => {
buttonLoading.value = false
})
} else {
addUser(form)
.then((res) => {
ElMessage.success('新增用户成功')
// 关闭弹窗
dialogClose(editFormRef.value)
// 重新获取表格数据
getListUser()
})
.finally(() => {
buttonLoading.value = false
})
}
}
})
}
</script>
转载自:https://juejin.cn/post/7178757659565228091