mirror of
https://github.com/chansee97/nova-admin.git
synced 2025-09-17 11:19:58 +08:00
feat: 角色权限和数据权限配置
This commit is contained in:
parent
0f4325fd68
commit
4687b32dc8
6
src/typings/entities/role.d.ts
vendored
6
src/typings/entities/role.d.ts
vendored
@ -30,9 +30,9 @@ namespace Entity {
|
||||
*/
|
||||
status?: number
|
||||
/**
|
||||
* 显示顺序
|
||||
* 数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限 5:仅本人数据权限)
|
||||
*/
|
||||
sort: number
|
||||
dataScope: number
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@ -41,6 +41,8 @@ namespace Entity {
|
||||
* 更新时间
|
||||
*/
|
||||
updateTime?: string
|
||||
menus?: Entity.Menu[]
|
||||
depts?: Entity.Dept[]
|
||||
[property: string]: any
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { NButton, NPopconfirm, NSpace, NSwitch } from 'naive-ui'
|
||||
import type { ProSearchFormColumns } from 'pro-naive-ui'
|
||||
import { renderProCopyableText, renderProDateText } from 'pro-naive-ui'
|
||||
import { renderProCopyableText, renderProTags } from 'pro-naive-ui'
|
||||
|
||||
export const searchColumns: ProSearchFormColumns<Entity.Role> = [
|
||||
{
|
||||
@ -51,11 +51,25 @@ export function createRoleColumns(actions: RoleColumnActions): DataTableColumns<
|
||||
key: 'roleKey',
|
||||
render: row => renderProCopyableText(row.roleKey),
|
||||
},
|
||||
{
|
||||
title: '数据范围',
|
||||
align: 'center',
|
||||
key: 'dataScope',
|
||||
render: (row) => {
|
||||
const dataScopeMap: Record<number, string> = {
|
||||
1: '全部数据权限',
|
||||
2: '自定数据权限',
|
||||
3: '本部门数据权限',
|
||||
4: '本部门及以下数据权限',
|
||||
5: '仅本人数据权限',
|
||||
}
|
||||
return renderProTags(dataScopeMap[row.dataScope])
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
align: 'center',
|
||||
key: 'remark',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
@ -80,9 +94,6 @@ export function createRoleColumns(actions: RoleColumnActions): DataTableColumns<
|
||||
align: 'center',
|
||||
key: 'createTime',
|
||||
width: 200,
|
||||
render: row => renderProDateText(row.createTime, {
|
||||
pattern: 'datetime',
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
|
@ -2,6 +2,8 @@
|
||||
import { useBoolean } from '@/hooks'
|
||||
import { createRole, getRoleById, updateRole } from '@/api'
|
||||
import { createProModalForm } from 'pro-naive-ui'
|
||||
import TreeDeptSelect from './TreeDeptSelect.vue'
|
||||
import TreeMenuSelect from './TreeMenuSelect.vue'
|
||||
|
||||
interface Props {
|
||||
modalName?: string
|
||||
@ -17,16 +19,18 @@ const emit = defineEmits<{
|
||||
|
||||
const { bool: submitLoading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
|
||||
|
||||
const modalForm = createProModalForm<Partial<Entity.Role>>({
|
||||
const modalForm = createProModalForm<Partial<Entity.Role> & { deptIds?: number[], menuIds?: number[] }>({
|
||||
omitEmptyString: false,
|
||||
initialValues: {
|
||||
status: 0,
|
||||
dataScope: 1,
|
||||
},
|
||||
onSubmit: submitModal,
|
||||
})
|
||||
|
||||
type ModalType = 'add' | 'edit'
|
||||
const modalType = shallowRef<ModalType>('add')
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
const titleMap: Record<ModalType, string> = {
|
||||
add: '添加',
|
||||
@ -37,11 +41,10 @@ const modalTitle = computed(() => {
|
||||
|
||||
async function openModal(type: ModalType = 'add', data?: Partial<Entity.Role>) {
|
||||
modalType.value = type
|
||||
|
||||
modalForm.open()
|
||||
|
||||
const handlers = {
|
||||
async add() {
|
||||
// 使用默认值
|
||||
},
|
||||
async edit() {
|
||||
if (!data)
|
||||
@ -49,6 +52,9 @@ async function openModal(type: ModalType = 'add', data?: Partial<Entity.Role>) {
|
||||
|
||||
const { data: role } = await getRoleById(data.id!)
|
||||
modalForm.values.value = role
|
||||
// 设置已选中的菜单和部门
|
||||
modalForm.values.value.menuIds = role.menus?.map(menu => menu.id) || []
|
||||
modalForm.values.value.deptIds = role.depts?.map(dept => dept.id) || []
|
||||
},
|
||||
}
|
||||
await handlers[type]()
|
||||
@ -99,7 +105,7 @@ defineExpose({
|
||||
:title="modalTitle"
|
||||
:form="modalForm"
|
||||
:loading="submitLoading"
|
||||
width="600px"
|
||||
width="800px"
|
||||
>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<pro-input
|
||||
@ -117,6 +123,46 @@ defineExpose({
|
||||
path="status"
|
||||
:field-props="{ checkedValue: 0, uncheckedValue: 1 }"
|
||||
/>
|
||||
<pro-select
|
||||
title="数据范围"
|
||||
path="dataScope"
|
||||
:field-props="{
|
||||
options: [
|
||||
{ label: '全部数据权限', value: 1 },
|
||||
{ label: '自定数据权限', value: 2 },
|
||||
{ label: '本部门数据权限', value: 3 },
|
||||
{ label: '本部门及以下数据权限', value: 4 },
|
||||
{ label: '仅本人数据权限', value: 5 },
|
||||
],
|
||||
}"
|
||||
/>
|
||||
<pro-field
|
||||
class="col-span-2"
|
||||
title="菜单权限"
|
||||
path="menuIds"
|
||||
>
|
||||
<template #input="{ inputProps }">
|
||||
<TreeMenuSelect
|
||||
:value="inputProps.value"
|
||||
@update:value="inputProps.onUpdateValue"
|
||||
/>
|
||||
</template>
|
||||
</pro-field>
|
||||
|
||||
<pro-field
|
||||
v-if="modalForm.values.value.dataScope === 2"
|
||||
class="col-span-2"
|
||||
title="数据权限"
|
||||
path="deptIds"
|
||||
>
|
||||
<template #input="{ inputProps }">
|
||||
<TreeDeptSelect
|
||||
:value="inputProps.value"
|
||||
@update:value="inputProps.onUpdateValue"
|
||||
/>
|
||||
</template>
|
||||
</pro-field>
|
||||
|
||||
<pro-textarea
|
||||
class="col-span-2"
|
||||
title="备注"
|
||||
|
87
src/views/system/role/components/TreeDeptSelect.vue
Normal file
87
src/views/system/role/components/TreeDeptSelect.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { getDeptOptions } from '@/api'
|
||||
|
||||
const deptOptions = ref<any[]>([])
|
||||
|
||||
const checkedKeys = defineModel<number[]>('value', { default: () => [] })
|
||||
const expandedKeys = ref<number[]>([])
|
||||
const cascade = ref(true)
|
||||
const isExpanded = ref(false)
|
||||
const isAllSelected = ref(false)
|
||||
|
||||
// 获取所有节点keys的辅助函数
|
||||
function getAllKeys(nodes: any[]): number[] {
|
||||
let keys: number[] = []
|
||||
nodes.forEach((node) => {
|
||||
keys.push(node.value)
|
||||
if (node.children && node.children.length > 0) {
|
||||
keys = keys.concat(getAllKeys(node.children))
|
||||
}
|
||||
})
|
||||
return keys
|
||||
}
|
||||
|
||||
// checkbox更新回调函数
|
||||
function handleExpandedChange(expanded: boolean) {
|
||||
if (expanded) {
|
||||
expandedKeys.value = getAllKeys(deptOptions.value)
|
||||
}
|
||||
else {
|
||||
expandedKeys.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function handleAllSelectedChange(selected: boolean) {
|
||||
if (selected) {
|
||||
checkedKeys.value = getAllKeys(deptOptions.value)
|
||||
}
|
||||
else {
|
||||
checkedKeys.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 加载部门数据
|
||||
async function loadDeptOptions() {
|
||||
try {
|
||||
const { data } = await getDeptOptions()
|
||||
deptOptions.value = data
|
||||
}
|
||||
catch (error) {
|
||||
console.error('加载部门数据失败:', error)
|
||||
deptOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadDeptOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="mb-2 flex gap-4 justify-end">
|
||||
<n-checkbox v-model:checked="isExpanded" @update:checked="handleExpandedChange">
|
||||
展开全部
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="isAllSelected" @update:checked="handleAllSelectedChange">
|
||||
全选
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="cascade">
|
||||
父子联动
|
||||
</n-checkbox>
|
||||
</div>
|
||||
<n-scrollbar class="border border-gray-200 rounded p-2" style="max-height: 240px">
|
||||
<n-tree
|
||||
v-model:checked-keys="checkedKeys"
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
block-line
|
||||
show-line
|
||||
checkable
|
||||
:cascade="cascade"
|
||||
:data="deptOptions"
|
||||
key-field="value"
|
||||
/>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
</template>
|
87
src/views/system/role/components/TreeMenuSelect.vue
Normal file
87
src/views/system/role/components/TreeMenuSelect.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { getMenuOptions } from '@/api'
|
||||
|
||||
const menuOptions = ref<any[]>([])
|
||||
|
||||
const checkedKeys = defineModel<number[]>('value', { default: () => [] })
|
||||
const expandedKeys = ref<number[]>([])
|
||||
const cascade = ref(true)
|
||||
const isExpanded = ref(false)
|
||||
const isAllSelected = ref(false)
|
||||
|
||||
// 获取所有节点keys的辅助函数
|
||||
function getAllKeys(nodes: any[]): number[] {
|
||||
let keys: number[] = []
|
||||
nodes.forEach((node) => {
|
||||
keys.push(node.value)
|
||||
if (node.children && node.children.length > 0) {
|
||||
keys = keys.concat(getAllKeys(node.children))
|
||||
}
|
||||
})
|
||||
return keys
|
||||
}
|
||||
|
||||
// checkbox更新回调函数
|
||||
function handleExpandedChange(expanded: boolean) {
|
||||
if (expanded) {
|
||||
expandedKeys.value = getAllKeys(menuOptions.value)
|
||||
}
|
||||
else {
|
||||
expandedKeys.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function handleAllSelectedChange(selected: boolean) {
|
||||
if (selected) {
|
||||
checkedKeys.value = getAllKeys(menuOptions.value)
|
||||
}
|
||||
else {
|
||||
checkedKeys.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 加载菜单数据
|
||||
async function loadMenuOptions() {
|
||||
try {
|
||||
const { data } = await getMenuOptions()
|
||||
menuOptions.value = data
|
||||
}
|
||||
catch (error) {
|
||||
console.error('加载菜单数据失败:', error)
|
||||
menuOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadMenuOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="mb-2 flex gap-4 justify-end">
|
||||
<n-checkbox v-model:checked="isExpanded" @update:checked="handleExpandedChange">
|
||||
展开全部
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="isAllSelected" @update:checked="handleAllSelectedChange">
|
||||
全选
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="cascade">
|
||||
父子联动
|
||||
</n-checkbox>
|
||||
</div>
|
||||
<n-scrollbar class="border border-gray-200 rounded p-2" style="max-height: 240px">
|
||||
<n-tree
|
||||
v-model:checked-keys="checkedKeys"
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
block-line
|
||||
show-line
|
||||
checkable
|
||||
:cascade="cascade"
|
||||
:data="menuOptions"
|
||||
key-field="value"
|
||||
/>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
</template>
|
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useBoolean } from '@/hooks'
|
||||
import { createUser, getRoleOptions, getUserById, updateUser } from '@/api'
|
||||
import { createUser, getDeptOptions, getRoleOptions, getUserById, updateUser } from '@/api'
|
||||
import { createProModalForm } from 'pro-naive-ui'
|
||||
import { Regex } from '@/constants'
|
||||
|
||||
@ -37,12 +37,16 @@ const modalTitle = computed(() => {
|
||||
return `${titleMap[modalType.value]}${modalName}`
|
||||
})
|
||||
const roleOptions = ref<Entity.TreeNode[]>([])
|
||||
const deptOptions = ref<any[]>([])
|
||||
|
||||
async function openModal(type: ModalType = 'add', data?: Partial<Entity.User>) {
|
||||
modalType.value = type
|
||||
getRoleOptions().then((res) => {
|
||||
roleOptions.value = res.data
|
||||
})
|
||||
getDeptOptions().then((res) => {
|
||||
deptOptions.value = res.data
|
||||
})
|
||||
modalForm.open()
|
||||
const handlers = {
|
||||
async add() {
|
||||
@ -132,9 +136,14 @@ defineExpose({
|
||||
],
|
||||
}"
|
||||
/>
|
||||
<pro-digit
|
||||
title="部门ID"
|
||||
<pro-tree-select
|
||||
title="部门"
|
||||
path="deptId"
|
||||
:field-props="{
|
||||
options: deptOptions,
|
||||
keyField: 'value',
|
||||
clearable: true,
|
||||
}"
|
||||
/>
|
||||
<pro-input
|
||||
title="邮箱"
|
||||
|
@ -113,6 +113,7 @@ onMounted(() => {
|
||||
</template>
|
||||
<n-tree
|
||||
block-line
|
||||
show-line
|
||||
:data="treeData"
|
||||
key-field="value"
|
||||
:pattern="deptPattern"
|
||||
|
Loading…
x
Reference in New Issue
Block a user