snowy/snowy-admin-web/src/views/sys/role/grantPermissionForm.vue

581 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<xn-form-container
title="授权权限"
:width="drawerWidth"
:visible="visible"
:destroy-on-close="true"
:show-pagination="false"
@close="onClose"
>
<a-alert
message="注:此功能界面需要与代码查询条件配合使用,并非所有接口都需设置数据范围,多用于业务模块!"
type="warning"
closable
/>
<a-spin :spinning="spinningLoading">
<a-table
class="mt-4"
size="middle"
:columns="columns"
:data-source="tableLoadData"
:row-key="(record) => record.api"
:pagination="pagination"
@change="handleTableChange"
bordered
>
<template #headerCell="{ column }">
<template v-if="column.key === 'prefix'">
<a-checkbox :checked="allChecked" @update:checked="(val) => onCheckAllChange(val)">
{{ column.title }}
</a-checkbox>
</template>
<template v-if="column.key === 'suffix'">
<a-checkbox :checked="allChecked" @update:checked="(val) => onCheckAllChange(val)"> 接口 </a-checkbox>
</template>
<template v-if="column.key === 'dataScope'">
<span>{{ column.title }}</span>
<a-radio-group v-model:value="scopeRadioValue" @change="radioChange" style="padding: 0 10px">
<a-radio value="SCOPE_ALL"> 全部 </a-radio>
<a-radio value="SCOPE_SELF"> 仅自已 </a-radio>
<a-radio value="SCOPE_ORG"> 所属组织 </a-radio>
<a-radio value="SCOPE_ORG_CHILD"> 所属组织及以下 </a-radio>
</a-radio-group>
</template>
</template>
<template #customFilterDropdown="{ setSelectedKeys, selectedKeys, clearFilters, column }">
<div class="xn-pd8">
<a-input
ref="searchInput"
placeholder="请输入接口或接口名称"
:value="selectedKeys[0]"
class="xn-jk-line"
@change="(e) => setSelectedKeys(e.target.value ? [e.target.value] : [])"
@pressEnter="handleSearch(selectedKeys, column.dataIndex)"
/>
<a-button
type="primary"
size="small"
class="xn-wd90 xn-mr8"
@click="handleSearch(selectedKeys, column.dataIndex)"
>
<template #icon><SearchOutlined /></template>
搜索
</a-button>
<a-button size="small" class="xn-wd90" @click="handleReset(clearFilters)"> 重置 </a-button>
</div>
</template>
<template #customFilterIcon="{ filtered }">
<a-badge :dot="state.searchText?.trim().length > 0" status="processing">
<search-outlined :style="{ color: filtered ? '#108ee9' : undefined }" />
</a-badge>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'prefix'">
<a-checkbox :checked="record.parentCheck" @update:checked="(val) => changeParentApi(record, val)">
{{ record.prefix }}
</a-checkbox>
</template>
<template v-if="column.dataIndex === 'suffix'">
<a-checkbox :checked="record.check" @update:checked="(val) => changeApi(record, val)">
{{ record.suffix }}
</a-checkbox>
</template>
<template v-if="column.dataIndex === 'dataScope'">
<template v-if="record.dataScope.length > 0">
<template v-for="item in record.dataScope" :key="item.id + record.api">
<a-radio
v-model:checked="item.check"
:name="item.title"
@change="(evt) => changeDataScope(record, evt)"
>
<a-badge
v-if="
(item.value === 'SCOPE_ORG_DEFINE') &
record.dataScope[4].check &
(item.scopeDefineOrgIdList !== undefined)
"
:count="item.scopeDefineOrgIdList.length"
:number-style="{ backgroundColor: '#52c41a' }"
>
{{ item.title }}</a-badge
>
<div v-else>{{ item.title }}</div>
</a-radio>
</template>
<a-button v-if="record.dataScope[4].check" type="link" size="small" @click="handleDefineOrg(record)"
>选择组织</a-button
>
</template>
</template>
</template>
</a-table>
</a-spin>
<template #footer>
<a-button class="xn-mr8" @click="onClose">关闭</a-button>
<a-button type="primary" :loading="submitLoading" @click="onSubmit">保存</a-button>
</template>
<ScopeDefineOrg ref="scopeDefineOrgModal" @click="scopeDefineOrgClick" />
</xn-form-container>
</template>
<script setup name="grantResourceForm">
import { SearchOutlined } from '@ant-design/icons-vue'
import roleApi from '@/api/sys/roleApi'
import ScopeDefineOrg from './scopeDefineOrg.vue'
import { userStore } from '@/store/user'
import { cloneDeep } from 'lodash-es'
const visible = ref(false)
const spinningLoading = ref(false)
const scopeDefineOrgModal = ref(null)
const emit = defineEmits({ successful: null })
const submitLoading = ref(false)
const CustomValue = 'SCOPE_ORG_DEFINE'
// 抽屉的宽度
const drawerWidth = 1050
// 自动获取宽度默认获取浏览器的宽度的90%
//(window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth) * 0.9
let loadDatas = ref([])
// 标题接口列搜索, 数据范围默认
const scopeRadioValue = ref()
const state = reactive({
searchText: '',
searchedColumn: ''
})
const searchInput = ref()
// 分页
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
defaultPageSize: 10,
pageSizeOptions: ['10', '20', '50', '100'],
onChange: () => {
nextTick(() => {
searchFunc()
})
}
})
const firstShowMap = ref({})
// 全选
const allChecked = ref(false)
const tableLoadData = ref([])
const columns = [
{
key: 'prefix',
title: '接口前缀',
dataIndex: 'prefix',
width: 140,
customCell: (row, index) => {
const indexArr = firstShowMap.value[row.prefix]
if (index === indexArr[0]) {
return { rowSpan: indexArr.length }
}
return { rowSpan: 0 }
}
},
{
key: 'suffix',
title: '接口',
dataIndex: 'suffix',
width: 290,
customFilterDropdown: true,
onFilter: (value, record) => record.api.includes(value),
onFilterDropdownOpenChange: (visible) => {
if (visible) {
setTimeout(() => {
searchInput.value.focus()
}, 100)
}
}
},
{
key: 'dataScope',
title: '数据范围',
dataIndex: 'dataScope'
}
]
// 获取数据
const loadData = async () => {
spinningLoading.value = true
const res = await roleApi.rolePermissionTreeSelector()
// 获取他已有的权限
const param = {
id: grantPermissionParam.id
}
const resOwn = await roleApi.roleOwnPermission(param)
// 数据转换
loadDatas.value = echoModuleData(res, resOwn)
pagination.value.total = loadDatas.value.length
allChecked.value = loadDatas.value.every((item) => item.parentCheck)
spinningLoading.value = false
searchFunc()
}
// 数据搜索
const searchFunc = () => {
spinningLoading.value = true
const loadData = loadDatas.value.filter((item) => {
if (!state.searchText || state.searchText.trim().length === 0) {
return true
}
return item.api.toUpperCase().includes(state.searchText?.toUpperCase())
})
pagination.value.total = loadData.length
const start = (pagination.value.current - 1) * pagination.value.pageSize
const end = start + pagination.value.pageSize
const tableData = loadData.slice(start, end)
firstShowMap.value = {}
// 生成map
tableData?.forEach((item, index) => {
if (firstShowMap.value[item.prefix]) {
firstShowMap.value[item.prefix].push(index)
} else {
firstShowMap.value[item.prefix] = [index]
}
})
tableLoadData.value = tableData
spinningLoading.value = false
}
// table事件触发
const handleTableChange = (pageInfo) => {
Object.assign(pagination.value, pageInfo)
}
// 数据转换
const echoModuleData = (res, resOwn) => {
let list = []
res.forEach((api) => {
const apiArr = splitByThirdSlash(api)
const obj = {
api: api,
prefix: apiArr[0],
suffix: apiArr[1],
dataScope: datascope(api),
check: false,
parentCheck: false
}
if (resOwn.grantInfoList.length > 0) {
resOwn.grantInfoList.forEach((item) => {
if (item.apiUrl === subStrApi(api)) {
obj.check = true
// dataScopeInfo
obj.dataScope.forEach((o) => {
if (o.value === item.scopeCategory) {
o.check = true
// 如果是自定义
if (item.scopeCategory === 'SCOPE_ORG_DEFINE') {
o.scopeDefineOrgIdList = item.scopeDefineOrgIdList
}
}
})
}
})
}
list.push(obj)
})
// 设置父节点check状态
return setParentDataCheckedStatus(list)
}
const datascope = (id) => {
return [
{
id: `SCOPE_ALL_${id}`,
title: '全部',
value: 'SCOPE_ALL',
check: false
},
{
id: `SCOPE_SELF_${id}`,
title: '仅自己',
value: 'SCOPE_SELF',
check: false
},
{
id: `SCOPE_ORG_${id}`,
title: '所属组织',
value: 'SCOPE_ORG',
check: false
},
{
id: `SCOPE_ORG_CHILD_${id}`,
title: '所属组织及以下',
value: 'SCOPE_ORG_CHILD',
check: false
},
{
id: `SCOPE_ORG_DEFINE_${id}`,
title: '自定义',
value: 'SCOPE_ORG_DEFINE',
check: false
}
]
}
// 点击数据权限选择
const changeDataScope = (record, evt) => {
const name = evt.target.name
// 这里做互斥,每个
record.dataScope.forEach((item) => {
if (item.title !== name) {
item.check = false
}
})
changeChildCheckBox(record, evt)
}
// 处理自定义
const handleDefineOrg = (recordDataScope) => {
// 弹框选择子自定义
const data = recordDataScope.dataScope.find((f) => f.value === CustomValue)
// 选中了
if (data.check) {
// 获取到选中的key数组传过去让其那边回显
const checkKeysStr = recordDataScope.dataScope[4].scopeDefineOrgIdList
scopeDefineOrgModal.value.onOpen(data.id, checkKeysStr)
} else {
// 清理缓存中的结构,去掉就行
handleDatascope(false, data.id, null)
}
}
// 自定义数据弹窗回调
const scopeDefineOrgClick = (value) => {
handleDatascope(true, value.dataScopeId, value.defineOrgIdData.scopeDefineOrgIdList)
}
// 处理Datascope数据被选中自定义或取消自定义数据
const handleDatascope = (check, id, orgData) => {
loadDatas.value.forEach((item) => {
if (id === 'SCOPE_ORG_DEFINE_' + item.api) {
item.dataScope.forEach((items) => {
if (items.value === 'SCOPE_ORG_DEFINE') {
if (check) {
items.scopeDefineOrgIdList = orgData
} else {
items.scopeDefineOrgIdList = []
}
}
})
}
})
}
// 打开抽屉
const onOpen = (record) => {
grantPermissionParam.id = record.id
visible.value = true
firstShowMap.value = {}
pagination.value.current = 1
pagination.value.pageSize = 10
pagination.value.total = 0
allChecked.value = false
loadData()
}
// 关闭抽屉
const onClose = () => {
// 将这些缓存的给清空
loadDatas.value = []
scopeRadioValue.value = ''
visible.value = false
}
// 全选
const onCheckAllChange = (value) => {
allChecked.value = value
spinningLoading.value = true
loadDatas.value.forEach((data) => {
changeApi(data, value, false)
data.parentCheck = value
spinningLoading.value = false
})
}
// 选中接口前缀
const changeParentApi = (record, val) => {
loadDatas.value.forEach((data) => {
if (data.prefix === record.prefix) {
data.check = val
data.parentCheck = val
if (val) {
let isChecked = data.dataScope.some((item) => item.check)
if (!isChecked) {
data.dataScope[0].check = true
}
} else {
data.dataScope.forEach((item) => {
item.check = false
})
}
data.dataScope.forEach((item) => {
if (item.value === 'SCOPE_ORG_DEFINE') {
item.scopeDefineOrgIdList = []
}
})
}
})
allChecked.value = loadDatas.value.every((item) => item.parentCheck)
}
// 选中接口
const changeApi = (record, val, isLoadData = true) => {
record.check = val
if (val) {
let checkStatus = 0
for (let i = 0; i < record.dataScope.length; i++) {
if (record.dataScope[i].check) {
checkStatus++
}
}
if (checkStatus === 0) {
record.dataScope[0].check = true
}
} else {
// 去掉已选中的
record.dataScope.forEach((item) => {
item.check = false
if (item.value === 'SCOPE_ORG_DEFINE') {
item.scopeDefineOrgIdList = []
}
})
}
isLoadData && (loadDatas.value = setParentDataCheckedStatus(loadDatas.value))
}
// 设置选中状态
const changeChildCheckBox = (record, evt) => {
let checked = evt.target.checked
if (!checked) {
record.check = false
} else if (checked) {
record.check = checked
}
}
// 提交数据模型
let grantPermissionParam = {
// 角色id
id: '',
// 授权权限信息
grantInfoList: []
}
// 提交之前转换数据
const convertData = () => {
grantPermissionParam.grantInfoList = []
loadDatas.value.forEach((table) => {
if (table.check) {
table.dataScope.forEach((item) => {
if (item.check) {
const dataScopeInfo = {
apiUrl: subStrApi(table.api),
scopeCategory: item.value,
scopeDefineOrgIdList: item.scopeDefineOrgIdList === undefined ? [] : item.scopeDefineOrgIdList
}
grantPermissionParam.grantInfoList.push(dataScopeInfo)
}
})
}
})
return grantPermissionParam
}
// 截取api串中的中文及括号
const subStrApi = (api) => {
return api.substring(0, api.indexOf('['))
}
// 验证并提交数据
const onSubmit = () => {
const param = convertData()
submitLoading.value = true
roleApi
.roleGrantPermission(param)
.then(() => {
onClose()
emit('successful')
// 刷新权限
nextTick(() => {
userStore().refreshUserLoginUserInfo()
})
})
.finally(() => {
submitLoading.value = false
})
}
// 标题接口列搜索
const handleSearch = (selectedKeys, dataIndex) => {
state.searchText = selectedKeys[0]
state.searchedColumn = dataIndex
refreshSearch()
}
// 标题接口列搜索重置
const handleReset = (clearFilters) => {
clearFilters()
state.searchText = ''
state.searchedColumn = ''
refreshSearch()
}
// 重置搜索
const refreshSearch = () => {
pagination.value.current = 1
nextTick(() => {
searchFunc()
})
}
// 标题数据范围列radio-group事件
const radioChange = (obj) => {
loadDatas.value.forEach((data) => {
data.dataScope.forEach((data) => {
if (obj.target.value === data.value) {
data.check = true
} else {
data.check = false
}
})
})
}
// 设置父节点check状态
const setParentDataCheckedStatus = (records) => {
const cloneRecords = cloneDeep(records)
cloneRecords.forEach((item) => {
let childrenList = records.filter((f) => f.prefix === item.prefix)
item.parentCheck = childrenList.every((e) => e.check)
})
return cloneRecords
}
// 字符串分割
function splitByThirdSlash(str) {
const arr = str.split('/').filter(Boolean)
const leftPart = '/' + arr.slice(0, 2).join('/')
const rightPart = '/' + arr.slice(2).join('/')
return [leftPart, rightPart]
}
// 监听分页及数据变化
watch(
loadDatas,
(val) => {
const loadData = val.filter((item) => {
return item.api.toUpperCase().includes(state.searchText.toUpperCase())
})
pagination.value.total = loadData.length
const start = (pagination.value.current - 1) * pagination.value.pageSize
const end = start + pagination.value.pageSize
const tableData = loadData.slice(start, end)
firstShowMap.value = {}
// 生成map
tableData?.forEach((item, index) => {
if (firstShowMap.value[item.prefix]) {
firstShowMap.value[item.prefix].push(index)
} else {
firstShowMap.value[item.prefix] = [index]
}
})
tableLoadData.value = tableData
},
{ deep: true }
)
// 调用这个函数将子组件的一些数据和方法暴露出去
defineExpose({
onOpen
})
</script>
<style scoped>
/* 重写复选框的样式 */
.ant-checkbox-wrapper {
margin-left: 0px !important;
padding-top: 2px !important;
padding-bottom: 2px !important;
}
</style>