mirror of
https://github.com/chansee97/nova-admin.git
synced 2025-09-19 04:49: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
|
status?: number
|
||||||
/**
|
/**
|
||||||
* 显示顺序
|
* 数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限 5:仅本人数据权限)
|
||||||
*/
|
*/
|
||||||
sort: number
|
dataScope: number
|
||||||
/**
|
/**
|
||||||
* 创建时间
|
* 创建时间
|
||||||
*/
|
*/
|
||||||
@ -41,6 +41,8 @@ namespace Entity {
|
|||||||
* 更新时间
|
* 更新时间
|
||||||
*/
|
*/
|
||||||
updateTime?: string
|
updateTime?: string
|
||||||
|
menus?: Entity.Menu[]
|
||||||
|
depts?: Entity.Dept[]
|
||||||
[property: string]: any
|
[property: string]: any
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { DataTableColumns } from 'naive-ui'
|
import type { DataTableColumns } from 'naive-ui'
|
||||||
import { NButton, NPopconfirm, NSpace, NSwitch } from 'naive-ui'
|
import { NButton, NPopconfirm, NSpace, NSwitch } from 'naive-ui'
|
||||||
import type { ProSearchFormColumns } from 'pro-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> = [
|
export const searchColumns: ProSearchFormColumns<Entity.Role> = [
|
||||||
{
|
{
|
||||||
@ -51,11 +51,25 @@ export function createRoleColumns(actions: RoleColumnActions): DataTableColumns<
|
|||||||
key: 'roleKey',
|
key: 'roleKey',
|
||||||
render: row => renderProCopyableText(row.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: '备注',
|
title: '备注',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
key: 'remark',
|
key: 'remark',
|
||||||
width: 200,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
@ -80,9 +94,6 @@ export function createRoleColumns(actions: RoleColumnActions): DataTableColumns<
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
key: 'createTime',
|
key: 'createTime',
|
||||||
width: 200,
|
width: 200,
|
||||||
render: row => renderProDateText(row.createTime, {
|
|
||||||
pattern: 'datetime',
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
import { useBoolean } from '@/hooks'
|
import { useBoolean } from '@/hooks'
|
||||||
import { createRole, getRoleById, updateRole } from '@/api'
|
import { createRole, getRoleById, updateRole } from '@/api'
|
||||||
import { createProModalForm } from 'pro-naive-ui'
|
import { createProModalForm } from 'pro-naive-ui'
|
||||||
|
import TreeDeptSelect from './TreeDeptSelect.vue'
|
||||||
|
import TreeMenuSelect from './TreeMenuSelect.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modalName?: string
|
modalName?: string
|
||||||
@ -17,16 +19,18 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const { bool: submitLoading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
|
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,
|
omitEmptyString: false,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
status: 0,
|
status: 0,
|
||||||
|
dataScope: 1,
|
||||||
},
|
},
|
||||||
onSubmit: submitModal,
|
onSubmit: submitModal,
|
||||||
})
|
})
|
||||||
|
|
||||||
type ModalType = 'add' | 'edit'
|
type ModalType = 'add' | 'edit'
|
||||||
const modalType = shallowRef<ModalType>('add')
|
const modalType = shallowRef<ModalType>('add')
|
||||||
|
|
||||||
const modalTitle = computed(() => {
|
const modalTitle = computed(() => {
|
||||||
const titleMap: Record<ModalType, string> = {
|
const titleMap: Record<ModalType, string> = {
|
||||||
add: '添加',
|
add: '添加',
|
||||||
@ -37,11 +41,10 @@ const modalTitle = computed(() => {
|
|||||||
|
|
||||||
async function openModal(type: ModalType = 'add', data?: Partial<Entity.Role>) {
|
async function openModal(type: ModalType = 'add', data?: Partial<Entity.Role>) {
|
||||||
modalType.value = type
|
modalType.value = type
|
||||||
|
|
||||||
modalForm.open()
|
modalForm.open()
|
||||||
|
|
||||||
const handlers = {
|
const handlers = {
|
||||||
async add() {
|
async add() {
|
||||||
// 使用默认值
|
|
||||||
},
|
},
|
||||||
async edit() {
|
async edit() {
|
||||||
if (!data)
|
if (!data)
|
||||||
@ -49,6 +52,9 @@ async function openModal(type: ModalType = 'add', data?: Partial<Entity.Role>) {
|
|||||||
|
|
||||||
const { data: role } = await getRoleById(data.id!)
|
const { data: role } = await getRoleById(data.id!)
|
||||||
modalForm.values.value = role
|
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]()
|
await handlers[type]()
|
||||||
@ -99,7 +105,7 @@ defineExpose({
|
|||||||
:title="modalTitle"
|
:title="modalTitle"
|
||||||
:form="modalForm"
|
:form="modalForm"
|
||||||
:loading="submitLoading"
|
:loading="submitLoading"
|
||||||
width="600px"
|
width="800px"
|
||||||
>
|
>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<pro-input
|
<pro-input
|
||||||
@ -117,6 +123,46 @@ defineExpose({
|
|||||||
path="status"
|
path="status"
|
||||||
:field-props="{ checkedValue: 0, uncheckedValue: 1 }"
|
: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
|
<pro-textarea
|
||||||
class="col-span-2"
|
class="col-span-2"
|
||||||
title="备注"
|
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">
|
<script setup lang="ts">
|
||||||
import { useBoolean } from '@/hooks'
|
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 { createProModalForm } from 'pro-naive-ui'
|
||||||
import { Regex } from '@/constants'
|
import { Regex } from '@/constants'
|
||||||
|
|
||||||
@ -37,12 +37,16 @@ const modalTitle = computed(() => {
|
|||||||
return `${titleMap[modalType.value]}${modalName}`
|
return `${titleMap[modalType.value]}${modalName}`
|
||||||
})
|
})
|
||||||
const roleOptions = ref<Entity.TreeNode[]>([])
|
const roleOptions = ref<Entity.TreeNode[]>([])
|
||||||
|
const deptOptions = ref<any[]>([])
|
||||||
|
|
||||||
async function openModal(type: ModalType = 'add', data?: Partial<Entity.User>) {
|
async function openModal(type: ModalType = 'add', data?: Partial<Entity.User>) {
|
||||||
modalType.value = type
|
modalType.value = type
|
||||||
getRoleOptions().then((res) => {
|
getRoleOptions().then((res) => {
|
||||||
roleOptions.value = res.data
|
roleOptions.value = res.data
|
||||||
})
|
})
|
||||||
|
getDeptOptions().then((res) => {
|
||||||
|
deptOptions.value = res.data
|
||||||
|
})
|
||||||
modalForm.open()
|
modalForm.open()
|
||||||
const handlers = {
|
const handlers = {
|
||||||
async add() {
|
async add() {
|
||||||
@ -132,9 +136,14 @@ defineExpose({
|
|||||||
],
|
],
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<pro-digit
|
<pro-tree-select
|
||||||
title="部门ID"
|
title="部门"
|
||||||
path="deptId"
|
path="deptId"
|
||||||
|
:field-props="{
|
||||||
|
options: deptOptions,
|
||||||
|
keyField: 'value',
|
||||||
|
clearable: true,
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
<pro-input
|
<pro-input
|
||||||
title="邮箱"
|
title="邮箱"
|
||||||
|
@ -113,6 +113,7 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
<n-tree
|
<n-tree
|
||||||
block-line
|
block-line
|
||||||
|
show-line
|
||||||
:data="treeData"
|
:data="treeData"
|
||||||
key-field="value"
|
key-field="value"
|
||||||
:pattern="deptPattern"
|
:pattern="deptPattern"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user