feat 权限管理

This commit is contained in:
xxm
2022-11-07 16:55:03 +08:00
parent eedf2e648e
commit 60e922f431
10 changed files with 420 additions and 29 deletions

View File

@@ -18,7 +18,9 @@
<div class="mt-10 font-medium text-white -enter-x">
<span class="inline-block mt-4 text-3xl"> 开箱即用的中后台管理系统</span>
</div>
<div class="mt-5 font-normal text-white dark:text-gray-500 -enter-x"> 输入您的个人详细信息开始使用 </div>
<div class="mt-5 font-normal text-white dark:text-gray-500 -enter-x"
>基于Vite+Vue3打造支持支付收单工作流(Flowable)三方对接等模块
</div>
</div>
</div>
<div class="flex w-full h-full py-5 xl:h-auto xl:py-0 xl:my-0 xl:w-6/12">

View File

@@ -78,6 +78,17 @@ export const existsByPermCodeNotId = (permCode, id) => {
})
}
/**
* 菜单列表
* @param clientCode
*/
export function allMenuTree(clientCode) {
return defHttp.get<Result<MenuTree[]>>({
url: '/perm/menu/allTree',
params: { clientCode },
})
}
/**
* 权限_菜单
*/
@@ -85,7 +96,7 @@ export interface Menu extends BaseEntity {
// 终端code
clientCode?: string
// 父id
parentId: number | null
parentId: number | null | string
// 菜单名称
title: string
// 路由名称
@@ -120,6 +131,13 @@ export interface Menu extends BaseEntity {
remark?: string
}
/**
* 菜单树
*/
export interface MenuTree extends Menu {
children: []
}
/**
* 权限_资源(码)
*/

View File

@@ -5,7 +5,7 @@
<a-row :gutter="10">
<a-col :md="6" :sm="24">
<a-form-item label="终端">
<a-select v-model:value="clientCode" @change="init" :default-value="clientCode" style="width: 100%">
<a-select v-model:value="clientCode" @change="queryPage" :default-value="clientCode" style="width: 100%">
<a-select-option v-for="o in clients" :key="o.code">{{ o.name }}</a-select-option>
</a-select>
</a-form-item>
@@ -25,7 +25,7 @@
</a-form>
</div>
<div class="m-3 p-3 bg-white">
<vxe-toolbar ref="xToolbar" custom zoom :refresh="{ query: init }">
<vxe-toolbar ref="xToolbar" custom zoom :refresh="{ query: queryPage }">
<template #buttons>
<a-button type="primary" @click="add()"> 新建 </a-button>
<a-button style="margin-left: 8px" @click="allTreeExpand(true)">展开所有</a-button>
@@ -90,7 +90,7 @@
</template>
</vxe-column>
</vxe-table>
<menu-edit ref="menuEdit" @ok="init" />
<menu-edit ref="menuEdit" @ok="queryPage" />
<resource-list ref="resourceList" />
</div>
</div>
@@ -127,7 +127,7 @@
onMounted(() => {
vxeBind()
initClients()
init()
queryPage()
})
function vxeBind() {
@@ -139,7 +139,7 @@
clients = data
}
async function init() {
async function queryPage() {
loading = true
menuTree(clientCode).then((res) => {
remoteTableData = res.data
@@ -175,7 +175,7 @@
onOk: () => {
del(record.id).then(() => {
notification.success({ message: '删除成功' })
init()
queryPage()
})
},
})

View File

@@ -1,10 +1,18 @@
<template>
<basic-modal :loading="confirmLoading" v-bind="$attrs" :title="title" :visible="visible" :mask-closable="showable" @cancel="handleCancel">
<a-form class="small-from-item" :model="form" ref="formRef" :rules="rules" :label-col="labelCol" :wrapper-col="wrapperCol">
<a-form
class="small-from-item"
:model="form"
ref="formRef"
:rules="rules"
:validate-trigger="['blur', 'change']"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<a-form-item label="主键" :hidden="true">
<a-input v-model:value="form.id" :disabled="showable" />
</a-form-item>
<a-form-item label="编码" name="permCode">
<a-form-item validate-first label="编码" name="permCode">
<a-input v-model:value="form.permCode" :disabled="showable" placeholder="请输入编码" />
</a-form-item>
<a-form-item label="名称" name="title">
@@ -30,10 +38,12 @@
import { FormInstance, Rule } from 'ant-design-vue/lib/form'
import { $ref } from 'vue/macros'
import { nextTick } from 'vue'
import { add, get, Resource, update } from './Menu.api'
import { add, existsByPermCode, existsByPermCodeNotId, get, Resource, update } from './Menu.api'
import { BasicModal } from '/@/components/Modal/'
import { useValidate } from '/@/hooks/bootx/useValidate'
const emits = defineEmits(['ok'])
const { initFormModel, handleCancel, search, labelCol, wrapperCol, title, confirmLoading, visible, editable, showable, formEditType } =
useFormEdit()
const { existsByServer } = useValidate()
@@ -49,19 +59,21 @@
remark: '',
} as Resource)
const rules = {
title: [{ required: true, message: '请输入权限名称', trigger: ['blur', 'change'] }],
permCode: [{ required: true, message: '请输入权限编码', trigger: ['blur', 'change'] }],
effect: [{ required: true, message: '' }],
title: [{ required: true, message: '请输入权限名称' }],
permCode: [
{ required: true, message: '请输入权限编码' },
{ validator: validateCode, trigger: 'blur' },
],
} as Record<string, Rule[]>
const formRef: FormInstance = $ref()
// 事件
const emits = defineEmits(['ok'])
// 入口
function init(id, editType: FormEditType, clientCode) {
function init(id, editType: FormEditType, clientCode, parentId) {
initFormModel(id, editType)
resetForm()
form.clientCode = clientCode
console.log(parentId)
form.parentId = parentId
getInfo(id, editType)
}
@@ -97,6 +109,12 @@
formRef.resetFields()
})
}
// 校验
function validateCode() {
const { permCode, id } = form
return existsByServer(permCode, id, formEditType, existsByPermCode, existsByPermCodeNotId)
}
defineExpose({
init,
})

View File

@@ -8,7 +8,7 @@
:visible="visible"
@close="visible = false"
>
<vxe-toolbar ref="xToolbar" custom zoom :refresh="{ query: init }">
<vxe-toolbar ref="xToolbar" custom zoom :refresh="{ query: queryPage }">
<template #buttons>
<a-button type="primary" pre-icon="ant-design:plus-outlined" @click="add">新建</a-button>
</template>
@@ -30,7 +30,7 @@
<a href="javascript:" @click="show(row)">查看</a>
</span>
<a-divider type="vertical" />
<a href="javascript:" :disabled="row.admin" @click="edit(row)">编辑</a>
<a href="javascript:" @click="edit(row)">编辑</a>
<a-divider type="vertical" />
<a-popconfirm title="是否删除该项" @confirm="remove(row)" okText="是" cancelText="否">
<a href="javascript:" v-if="row.admin" disabled>删除</a>
@@ -76,13 +76,14 @@
// 分页查询
function queryPage() {
loading.value = true
resourceList(menuInfo.id).then(({ data }) => {
tableData = data
loading.value = false
})
}
function add() {
resourceEdit.init(null, FormEditType.Add, menuInfo.clientCode)
resourceEdit.init(null, FormEditType.Add, menuInfo.clientCode, menuInfo.id)
}
function edit(record: Menu) {
resourceEdit.init(record.id, FormEditType.Edit, menuInfo.clientCode)

View File

@@ -32,6 +32,26 @@ export const add = (obj: Role) => {
})
}
/**
* 保存 角色分配菜单关系
*/
export function saveRoleMenu(obj) {
return defHttp.post({
url: `/role/menu/save`,
data: obj,
})
}
/**
* 保存 角色请求权限关系
*/
export function saveRolePath(obj) {
return defHttp.post({
url: `/role/path/save`,
data: obj,
})
}
/**
* 更新
*/
@@ -95,6 +115,35 @@ export const findAll = () => {
})
}
/**
* 根据用户获取拥有的权限
*/
export function findPermissionIdsByRole(roleId, clientCode) {
return defHttp.get<Result<string[]>>({
url: `/role/menu/findPermissionIdsByRole`,
params: { roleId, clientCode },
})
}
/**
* 根据用户id获取角色授权(请求权限列表)
*/
export function findPathsByUser() {
return defHttp.get<Result<string[]>>({
url: `/role/path/findPathsByUser`,
})
}
/**
* 根据角色id获取关联请求权限id
*/
export function findPathIdsByRole(roleId) {
return defHttp.get<Result>({
url: `/role/path/findIdsByRole`,
params: { roleId },
})
}
/**
* 角色
*/

View File

@@ -36,10 +36,10 @@
<template #overlay>
<a-menu>
<a-menu-item>
<a href="javascript:" @click="handleRoleMenu(row)">菜单授权</a>
<a href="javascript:" @click="handleRoleMenu(row)">菜单和权限码</a>
</a-menu-item>
<a-menu-item>
<a href="javascript:" @click="handleRolePath(row)">请求授权</a>
<a href="javascript:" @click="handleRolePath(row)">请求资源</a>
</a-menu-item>
</a-menu>
</template>
@@ -56,6 +56,8 @@
@page-change="handleTableChange"
/>
<role-edit ref="roleEdit" @ok="queryPage" />
<role-menu-modal ref="roleMenuModal" />
<role-path-modal ref="rolePathModal" />
</div>
</div>
</template>
@@ -72,12 +74,16 @@
import { $ref } from 'vue/macros'
import { QueryField, STRING } from '/@/components/Bootx/Query/Query'
import Icon from '/@/components/Icon/src/Icon.vue'
import RoleMenuModal from "./RoleMenuModal.vue";
import RolePathModal from "./RolePathModal.vue";
// 使用hooks
const { handleTableChange, pageQueryResHandel, resetQueryParams, pagination, pages, model, loading } = useTablePage(queryPage)
const { createMessage } = useMessage()
const roleEdit = ref()
const roleEdit = $ref<any>()
const roleMenuModal = $ref<any>()
const rolePathModal = $ref<any>()
// 查询条件
const fields = [
{ field: 'code', type: STRING, name: '角色编号', placeholder: '请输入角色编码' },
@@ -106,20 +112,24 @@
}
// 新增
function add() {
roleEdit.value.init(null, FormEditType.Add)
roleEdit.init(null, FormEditType.Add)
}
// 查看
function edit(record) {
roleEdit.value.init(record.id, FormEditType.Edit)
roleEdit.init(record.id, FormEditType.Edit)
}
// 查看
function show(record) {
roleEdit.value.init(record.id, FormEditType.Show)
roleEdit.init(record.id, FormEditType.Show)
}
// 菜单授权处理
function handleRoleMenu(record) {}
function handleRoleMenu(record) {
roleMenuModal.init(record.id)
}
// 请求授权处理
function handleRolePath(record) {}
function handleRolePath(record) {
rolePathModal.init(record.id)
}
// 删除
function remove(record) {
del(record.id).then(() => {

View File

@@ -0,0 +1,205 @@
<template>
<basic-drawer showFooter v-bind="$attrs" title="角色菜单和权限码配置" width="40%" :visible="visible" @close="handleCancel">
<a-spin :spinning="loading">
<a-input style="margin-bottom: 8px" placeholder="筛选" allowClear v-model:value="searchName" @change="search" />
<a-tree
:checkable="true"
v-model:checkedKeys="checkedKeys"
v-model:expandedKeys="expandedKeys"
:checkStrictly="checkStrictly"
:auto-expand-parent="autoExpandParent"
:tree-data="treeData"
@check="onCheck"
@expand="onExpand"
>
<template #title="{ title }">
<span v-if="title.toLowerCase().indexOf(searchName.toLowerCase()) > -1">
{{ searchRenderStart(title, searchName) }}
<span style="color: #f50">
{{ searchRenderMiddle(title, searchName) }}
</span>
{{ searchRenderEnd(title, searchName) }}
</span>
<span v-else>{{ title }}</span>
</template>
</a-tree>
</a-spin>
<template #footer>
<a-select style="min-width: 100px" @change="clientSwitch" v-model:value="clientCode">
<a-select-option v-for="o in clients" :key="o.code">{{ o.name }}</a-select-option>
</a-select>
<a-dropdown style="margin-left: 5px" :trigger="['click']" placement="topCenter">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="checkALL">全部勾选</a-menu-item>
<a-menu-item key="2" @click="cancelCheckALL">取消全选</a-menu-item>
<a-menu-item key="3" @click="expandAll">展开所有</a-menu-item>
<a-menu-item key="4" @click="closeAll">合并所有</a-menu-item>
<a-menu-item key="5" @click="switchCheckStrictly(false)">父子关联</a-menu-item>
<a-menu-item key="6" @click="switchCheckStrictly(true)">取消关联</a-menu-item>
</a-menu>
</template>
<a-button post-icon="ant-design:up-outlined"> 操作 </a-button>
</a-dropdown>
<a-button @click="visible = false" style="margin-right: 0.8rem">取消</a-button>
<a-button @click="handleSubmit()" type="primary" :loading="loading" style="margin-right: 0.8rem">保存</a-button>
</template>
</basic-drawer>
</template>
<script lang="ts" setup>
import { BasicDrawer } from '/@/components/Drawer'
import { $ref } from 'vue/macros'
import { findAll as findClients, Client } from '/@/views/modules/system/client/Client.api'
import { getAppEnvConfig } from '/@/utils/env'
import { findPermissionIdsByRole, saveRoleMenu } from '/@/views/modules/system/role/Role.api'
import { treeDataTranslate } from '/@/utils/dataUtil'
import XEUtils from 'xe-utils'
import { allMenuTree, MenuTree } from '/@/views/modules/system/menu/Menu.api'
const { VITE_GLOB_APP_CLIENT } = getAppEnvConfig()
let loading = $ref(false)
let roleId = $ref('')
let visible = $ref(false)
// 终端列表
let clients = $ref([] as Client[])
let clientCode = $ref(VITE_GLOB_APP_CLIENT)
let searchName = $ref('')
// 父子选项默认不受控
let checkStrictly = $ref(true)
// 所有的key
let allTreeKeys = $ref<string[]>([])
// 展开的key
let expandedKeys = $ref<string[]>([])
// 被选中的key
let checkedKeys = $ref<string[]>([])
let autoExpandParent = $ref(false)
//菜单树信息
let treeData = $ref<MenuTree[]>([])
let treeList = $ref<MenuTree[]>([])
function init(id) {
roleId = id
initData()
initAssign()
}
function initData() {
findClients().then(({ data }) => {
clients = data
})
}
// 初始化菜单分配信息
async function initAssign() {
visible = true
loading = true
searchName = ''
expandedKeys = []
// 权限树
await allMenuTree(clientCode).then((res) => {
treeData = treeDataTranslate(res.data, 'id', 'title') as MenuTree[]
generateTreeList(res.data)
})
// 当前角色已经选择的
await findPermissionIdsByRole(roleId, clientCode).then((res) => {
checkedKeys = res.data
})
// 所有的key值
allTreeKeys = treeList.map((item) => item.id) as string[]
loading = false
}
// 保存
function handleSubmit() {
loading = true
saveRoleMenu({
roleId,
clientCode,
permissionIds: checkedKeys,
}).then(() => {
handleCancel()
})
}
// 取消
function handleCancel() {
visible = false
}
// 树数据铺平
function generateTreeList(treeData) {
for (let i = 0; i < treeData.length; i++) {
const node = treeData[i]
treeList.push(node)
if (node.children) {
generateTreeList(node.children)
}
}
}
// 搜索
function search() {
const value = XEUtils.toValueString(searchName).toLowerCase()
expandedKeys = treeList
.map((node) => {
if (searchName && node.parentId && XEUtils.toValueString(node.title).toLowerCase().indexOf(value) > -1) {
return node.parentId
}
})
.filter((item, i, self) => item && self.indexOf(item) === i) as string[]
}
// 渲染搜索项目数据开始段
function searchRenderStart(title, searchName) {
return title.substring(0, title.toLowerCase().indexOf(searchName.toLowerCase()))
}
// 渲染搜索项目数据中间段
function searchRenderMiddle(title, searchName) {
return title.substring(
title.toLowerCase().indexOf(searchName.toLowerCase()),
title.toLowerCase().indexOf(searchName.toLowerCase()) + searchName.length,
)
}
// 渲染搜索项目数据结束段
function searchRenderEnd(title, searchName) {
return title.substring(title.toLowerCase().indexOf(searchName.toLowerCase()) + searchName.length)
}
// 展开/收起节点时触发
function onExpand(keys) {
expandedKeys = keys
autoExpandParent = false
}
// 点击复选框触发
function onCheck(key) {
if (checkStrictly) {
checkedKeys = key.checked
} else {
checkedKeys = key
}
}
// 展开全部
function expandAll() {
expandedKeys = allTreeKeys
}
// 合并全部
function closeAll() {
expandedKeys = []
}
// 全选
function checkALL() {
checkedKeys = allTreeKeys
}
// 全不选
function cancelCheckALL() {
checkedKeys = []
}
// 切换父子受控状态
function switchCheckStrictly(v) {
checkStrictly = v
}
// 终端切换
function clientSwitch(item) {
clientCode = item
initAssign()
}
defineExpose({ init })
</script>
<style scoped></style>

View File

@@ -0,0 +1,88 @@
<template>
<basic-drawer forceRender showFooter v-bind="$attrs" title="请求资源权限配置" width="40%" :visible="visible" @close="handleCancel">
<a-spin :spinning="loading">
<vxe-table
row-id="id"
border="none"
ref="xTable"
size="small"
:loading="loading"
:data="tableData"
:showHeader="true"
:tree-config="{ children: 'children' }"
:checkbox-config="{ labelField: 'name', checkRowKeys: checkedKeys }"
>
<vxe-column type="checkbox" title="权限名称" tree-node />
<vxe-column field="code" title="权限代码" />
</vxe-table>
</a-spin>
<template #footer>
<a-space>
<a-button @click="handleCancel()">取消</a-button>
<a-button @click="handleSubmit()" type="primary" :loading="loading">保存</a-button>
</a-space>
</template>
</basic-drawer>
</template>
<script lang="ts" setup>
import BasicDrawer from '/@/components/Drawer/src/BasicDrawer.vue'
import { $ref } from 'vue/macros'
import { VxeTableInstance } from 'vxe-table'
import { findPathIdsByRole, findPathsByUser, saveRoleMenu, saveRolePath } from "./Role.api";
import XEUtils from 'xe-utils'
const xTable = $ref<VxeTableInstance>()
let loading = $ref(false)
let roleId = $ref('')
let visible = $ref(false)
let checkedKeys = $ref<string[]>([])
let tableData = $ref<string[]>([])
async function init(id) {
visible = true
loading = true
roleId = id
await findPathIdsByRole(roleId).then(({ data }) => {
checkedKeys = data
})
await findPathsByUser().then(({ data }) => {
const result = [] as any[]
// 对数据进行分组
XEUtils.each(XEUtils.groupBy(data, 'groupName'), (children, key) => {
result.push({
name: key,
children: children,
})
})
tableData = result
xTable.reloadData(tableData)
loading = false
})
}
// 保存
function handleSubmit() {
loading = true
const checkedKeys = xTable
.getCheckboxRecords()
.map((res) => res.id)
.filter((id) => id.indexOf('row_') === -1)
saveRolePath({
roleId,
permissionIds: checkedKeys,
}).then(() => {
handleCancel()
})
}
// 取消
function handleCancel() {
visible = false
}
defineExpose({ init })
</script>
<style scoped></style>

2
types/web.d.ts vendored
View File

@@ -35,5 +35,5 @@ export interface TablePageModel<T = any> {
* 基础实体对象
*/
export interface BaseEntity {
id?: number | null | undefined
id?: number | string | null | undefined
}