feat: 增加分类权限控制
- 新增角色分类授权模型与超级管理员配置接口 - 接入助手、插件、工作流、知识库、素材的分类可见性过滤 - 增加角色页分类权限树与插件多分类可见性支持
This commit is contained in:
@@ -9,10 +9,15 @@ import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
// 直接传入树数据
|
||||
data: {
|
||||
type: Array<any>,
|
||||
default: undefined,
|
||||
},
|
||||
// 获取树数据的URL
|
||||
dataUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
// 已选择的节点数组(支持双向绑定)
|
||||
modelValue: {
|
||||
@@ -47,6 +52,10 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: 200,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 是否显示子节点数量
|
||||
showCount: {
|
||||
type: Boolean,
|
||||
@@ -117,27 +126,38 @@ const buildNodeMap = (nodes: any) => {
|
||||
});
|
||||
};
|
||||
|
||||
function applyTreeData(data: any[]) {
|
||||
treeData.value = Array.isArray(data) ? data : [];
|
||||
nodeMap.value.clear();
|
||||
buildNodeMap(treeData.value);
|
||||
|
||||
if (props.modelValue && props.modelValue.length > 0) {
|
||||
nextTick(() => {
|
||||
if (treeRef.value) {
|
||||
treeRef.value.setCheckedKeys(props.modelValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(newData) => {
|
||||
if (newData !== undefined) {
|
||||
applyTreeData(newData || []);
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
|
||||
// 获取树数据
|
||||
const fetchTreeData = async () => {
|
||||
if (!props.dataUrl) return;
|
||||
if (props.data !== undefined || !props.dataUrl) return;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await api.get(props.dataUrl);
|
||||
|
||||
treeData.value = res.data;
|
||||
// 构建节点映射
|
||||
nodeMap.value.clear();
|
||||
buildNodeMap(res.data);
|
||||
|
||||
// 数据加载完成后,如果有选中值则设置
|
||||
if (props.modelValue && props.modelValue.length > 0) {
|
||||
nextTick(() => {
|
||||
if (treeRef.value) {
|
||||
treeRef.value.setCheckedKeys(props.modelValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
applyTreeData(res.data);
|
||||
} catch (error) {
|
||||
console.error('get data error:', error);
|
||||
ElMessage.error($t('message.getDataError'));
|
||||
@@ -189,13 +209,14 @@ defineExpose({
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchTreeData();
|
||||
if (props.data === undefined) {
|
||||
fetchTreeData();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tree-select">
|
||||
<div class="tree-header"></div>
|
||||
<div class="tree-select" :class="{ 'tree-select--disabled': disabled }">
|
||||
<div class="tree-wrapper">
|
||||
<ElTreeV2
|
||||
ref="treeRef"
|
||||
@@ -231,32 +252,129 @@ onMounted(() => {
|
||||
<style scoped>
|
||||
.tree-select {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.tree-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--el-color-primary-light-9) 32%, var(--el-bg-color)) 0%,
|
||||
var(--el-bg-color) 72%
|
||||
);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 14px;
|
||||
box-shadow: var(--el-box-shadow-lighter);
|
||||
}
|
||||
|
||||
.tree-wrapper {
|
||||
padding: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.tree-select--disabled {
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
var(--el-fill-color-extra-light) 0%,
|
||||
var(--el-fill-color-blank) 72%
|
||||
);
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
overflow: hidden;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.node-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 22px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1;
|
||||
background: var(--el-fill-color);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
:deep(.el-tree) {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__content) {
|
||||
height: 38px;
|
||||
margin-bottom: 4px;
|
||||
padding: 0 10px;
|
||||
border-radius: 10px;
|
||||
transition:
|
||||
background-color 0.18s ease,
|
||||
color 0.18s ease;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__content:hover) {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
:deep(.el-tree-node:focus > .el-tree-node__content) {
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
:deep(.el-tree-node.is-current > .el-tree-node__content) {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--el-color-primary-light-8) 70%,
|
||||
var(--el-bg-color)
|
||||
);
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__expand-icon) {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
:deep(.el-checkbox) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__children) {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__children .el-tree-node__content) {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-scrollbar__bar.is-vertical) {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-scrollbar__thumb) {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--el-text-color-secondary) 22%,
|
||||
transparent
|
||||
);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.tree-select--disabled :deep(.el-tree-node__content) {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.tree-select--disabled :deep(.el-checkbox),
|
||||
.tree-select--disabled :deep(.el-tree-node__expand-icon),
|
||||
.tree-select--disabled :deep(.el-tree-node__content) {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
"isDeleted": "IsDeleted",
|
||||
"menuPermission": "MenuPermission",
|
||||
"dataPermission": "DataPermission",
|
||||
"categoryPermission": "CategoryPermission",
|
||||
"categoryPermissionHint": "Deny by default. Applies to resource visibility and selectors/binders only.",
|
||||
"categoryScopeAll": "AllCategories",
|
||||
"categoryScopeCustom": "CustomCategories",
|
||||
"categoryScopePlaceholder": "Select categories",
|
||||
"checkStrictlyTrue": "Linked",
|
||||
"checkStrictlyFalse": "NotLinked"
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
"isDeleted": "删除标识",
|
||||
"menuPermission": "菜单权限",
|
||||
"dataPermission": "数据权限",
|
||||
"categoryPermission": "分类权限",
|
||||
"categoryPermissionHint": "未配置即拒绝,仅控制资源可见性与选择器/绑定器。",
|
||||
"categoryScopeAll": "全部分类",
|
||||
"categoryScopeCustom": "自定义分类",
|
||||
"categoryScopePlaceholder": "请选择分类",
|
||||
"checkStrictlyTrue": "联动",
|
||||
"checkStrictlyFalse": "不联动"
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ function handleSubmit() {
|
||||
});
|
||||
}
|
||||
const getSideList = async () => {
|
||||
const [, res] = await tryit(api.get)('/api/v1/botCategory/list', {
|
||||
const [, res] = await tryit(api.get)('/api/v1/botCategory/visibleList', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
});
|
||||
|
||||
|
||||
@@ -41,10 +41,10 @@ const authTypeList = ref<headersType[]>([
|
||||
},
|
||||
]);
|
||||
onMounted(() => {
|
||||
api.get('/api/v1/model/list?supportEmbed=true').then((res) => {
|
||||
api.get('/api/v1/plugin/modelList?supportEmbed=true').then((res) => {
|
||||
embeddingLlmList.value = res.data;
|
||||
});
|
||||
api.get('/api/v1/model/list?supportRerankerLlmList=true').then((res) => {
|
||||
api.get('/api/v1/plugin/modelList?supportRerankerLlmList=true').then((res) => {
|
||||
rerankerLlmList.value = res.data;
|
||||
});
|
||||
api.get('/api/v1/pluginCategory/list').then((res) => {
|
||||
|
||||
@@ -124,7 +124,7 @@ const footerButton = {
|
||||
},
|
||||
};
|
||||
const getPluginCategoryList = async () => {
|
||||
return api.get('/api/v1/pluginCategory/list').then((res) => {
|
||||
return api.get('/api/v1/pluginCategory/visibleList').then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
const serverCategories = Array.isArray(res.data)
|
||||
? (res.data as PluginCategory[])
|
||||
|
||||
@@ -230,7 +230,7 @@ function handleSideSubmit() {
|
||||
});
|
||||
}
|
||||
const getSideList = async () => {
|
||||
const [, res] = await tryit(api.get)('/api/v1/resourceCategory/list', {
|
||||
const [, res] = await tryit(api.get)('/api/v1/resourceCategory/visibleList', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
|
||||
import { ElForm, ElFormItem, ElInput, ElMessage, ElSwitch } from 'element-plus';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
@@ -13,20 +18,57 @@ import Tree from '#/components/tree/Tree.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
// vue
|
||||
onMounted(() => {});
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
|
||||
type ResourceType = 'BOT' | 'KNOWLEDGE' | 'PLUGIN' | 'RESOURCE' | 'WORKFLOW';
|
||||
|
||||
interface CategoryScopeItem {
|
||||
categoryIds: Array<number | string>;
|
||||
resourceType: ResourceType;
|
||||
scopeMode: 'ALL' | 'CUSTOM';
|
||||
}
|
||||
|
||||
interface CategoryOption {
|
||||
id: number | string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface CategoryPermissionTreeNode {
|
||||
children?: CategoryPermissionTreeNode[];
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const RESOURCE_SCOPE_GROUPS: Array<{ label: string; resourceType: ResourceType }> =
|
||||
[
|
||||
{ resourceType: 'BOT', label: $t('bot.chatAssistant') },
|
||||
{ resourceType: 'PLUGIN', label: $t('menus.ai.plugin') },
|
||||
{ resourceType: 'WORKFLOW', label: $t('menus.ai.workflow') },
|
||||
{ resourceType: 'KNOWLEDGE', label: $t('menus.ai.documentCollection') },
|
||||
{ resourceType: 'RESOURCE', label: $t('menus.ai.resources') },
|
||||
];
|
||||
|
||||
const saveForm = ref<FormInstance>();
|
||||
// variables
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const entity = ref<any>({
|
||||
roleName: '',
|
||||
roleKey: '',
|
||||
status: '',
|
||||
remark: '',
|
||||
const entity = ref<any>(buildDefaultEntity());
|
||||
const categoryScopeLoaded = ref(false);
|
||||
const categoryScopeEditable = ref(false);
|
||||
const categoryOptions = ref<Record<ResourceType, CategoryOption[]>>({
|
||||
BOT: [],
|
||||
KNOWLEDGE: [],
|
||||
PLUGIN: [],
|
||||
RESOURCE: [],
|
||||
WORKFLOW: [],
|
||||
});
|
||||
const categoryTreeCheckedKeys = ref<string[]>([]);
|
||||
const categoryScopeDetail = ref<{
|
||||
roleId?: number | string;
|
||||
scopes: CategoryScopeItem[];
|
||||
}>({
|
||||
scopes: buildDefaultScopes(),
|
||||
});
|
||||
const btnLoading = ref(false);
|
||||
const rules = ref({
|
||||
@@ -40,50 +82,229 @@ const rules = ref({
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
});
|
||||
// functions
|
||||
function openDialog(row: any) {
|
||||
const categoryPermissionTreeData = computed<CategoryPermissionTreeNode[]>(() =>
|
||||
RESOURCE_SCOPE_GROUPS.map(({ label, resourceType }) => ({
|
||||
id: buildGroupNodeKey(resourceType),
|
||||
label,
|
||||
children: categoryOptions.value[resourceType].map((option) => ({
|
||||
id: buildCategoryNodeKey(resourceType, option.id),
|
||||
label: option.label,
|
||||
})),
|
||||
})),
|
||||
);
|
||||
|
||||
function buildDefaultEntity() {
|
||||
return {
|
||||
menuCheckStrictly: true,
|
||||
menuIds: [],
|
||||
remark: '',
|
||||
roleKey: '',
|
||||
roleName: '',
|
||||
status: '',
|
||||
};
|
||||
}
|
||||
|
||||
function buildDefaultScopes(): CategoryScopeItem[] {
|
||||
return RESOURCE_SCOPE_GROUPS.map(({ resourceType }) => ({
|
||||
categoryIds: [],
|
||||
resourceType,
|
||||
scopeMode: 'CUSTOM',
|
||||
}));
|
||||
}
|
||||
|
||||
function openDialog(row: any = {}) {
|
||||
entity.value = {
|
||||
...buildDefaultEntity(),
|
||||
...row,
|
||||
menuCheckStrictly: true,
|
||||
};
|
||||
categoryScopeDetail.value = {
|
||||
roleId: row.id,
|
||||
scopes: buildDefaultScopes(),
|
||||
};
|
||||
categoryScopeEditable.value = false;
|
||||
syncCategoryTreeCheckedKeys(buildDefaultScopes());
|
||||
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
getMenuIds(row.id);
|
||||
getDeptIds(row.id);
|
||||
}
|
||||
entity.value = row;
|
||||
getCategoryScopeDetail(row.id);
|
||||
void ensureCategoryOptions();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
saveForm.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
btnLoading.value = true;
|
||||
api
|
||||
.post('/api/v1/sysRole/saveRole', entity.value)
|
||||
.then((res) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btnLoading.value = false;
|
||||
try {
|
||||
const res = await api.post('/api/v1/sysRole/saveRole', {
|
||||
...entity.value,
|
||||
menuCheckStrictly: true,
|
||||
});
|
||||
if (res.errorCode !== 0) {
|
||||
btnLoading.value = false;
|
||||
return;
|
||||
}
|
||||
const roleId = res.data || entity.value.id;
|
||||
if (roleId && categoryScopeEditable.value) {
|
||||
const scopeRes = await saveCategoryScope(roleId);
|
||||
if (scopeRes.errorCode !== 0) {
|
||||
btnLoading.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
btnLoading.value = false;
|
||||
ElMessage.success(res.message);
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
} catch {
|
||||
btnLoading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
saveForm.value?.resetFields();
|
||||
isAdd.value = true;
|
||||
entity.value = {};
|
||||
entity.value = buildDefaultEntity();
|
||||
categoryScopeDetail.value = {
|
||||
scopes: buildDefaultScopes(),
|
||||
};
|
||||
categoryScopeEditable.value = false;
|
||||
syncCategoryTreeCheckedKeys(buildDefaultScopes());
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
|
||||
function getMenuIds(roleId: any) {
|
||||
api.get(`/api/v1/sysRole/getRoleMenuIds?roleId=${roleId}`).then((res) => {
|
||||
entity.value.menuIds = res.data;
|
||||
});
|
||||
}
|
||||
function getDeptIds(roleId: any) {
|
||||
api.get(`/api/v1/sysRole/getRoleDeptIds?roleId=${roleId}`).then((res) => {
|
||||
entity.value.deptIds = res.data;
|
||||
|
||||
async function ensureCategoryOptions() {
|
||||
if (categoryScopeLoaded.value) {
|
||||
return;
|
||||
}
|
||||
const requests = [
|
||||
api.get('/api/v1/botCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
api.get('/api/v1/pluginCategory/list'),
|
||||
api.get('/api/v1/workflowCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
api.get('/api/v1/documentCollectionCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
api.get('/api/v1/resourceCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
];
|
||||
|
||||
const [botRes, pluginRes, workflowRes, knowledgeRes, resourceRes] =
|
||||
await Promise.all(requests);
|
||||
categoryOptions.value = {
|
||||
BOT: normalizeCategoryOptions(botRes.data, 'categoryName'),
|
||||
KNOWLEDGE: normalizeCategoryOptions(knowledgeRes.data, 'categoryName'),
|
||||
PLUGIN: normalizeCategoryOptions(pluginRes.data, 'name'),
|
||||
RESOURCE: normalizeCategoryOptions(resourceRes.data, 'categoryName'),
|
||||
WORKFLOW: normalizeCategoryOptions(workflowRes.data, 'categoryName'),
|
||||
};
|
||||
categoryScopeLoaded.value = true;
|
||||
}
|
||||
|
||||
function normalizeCategoryOptions(data: any[] = [], labelKey: string) {
|
||||
return (Array.isArray(data) ? data : []).map((item) => ({
|
||||
id: item.id,
|
||||
label: item[labelKey],
|
||||
}));
|
||||
}
|
||||
|
||||
function buildGroupNodeKey(resourceType: ResourceType) {
|
||||
return `group:${resourceType}`;
|
||||
}
|
||||
|
||||
function buildCategoryNodeKey(
|
||||
resourceType: ResourceType,
|
||||
categoryId: number | string,
|
||||
) {
|
||||
return `category:${resourceType}:${String(categoryId)}`;
|
||||
}
|
||||
|
||||
function syncCategoryTreeCheckedKeys(scopes: CategoryScopeItem[]) {
|
||||
categoryTreeCheckedKeys.value = scopes.flatMap((scope) => {
|
||||
if (scope.scopeMode === 'ALL') {
|
||||
return [buildGroupNodeKey(scope.resourceType)];
|
||||
}
|
||||
return scope.categoryIds.map((categoryId) =>
|
||||
buildCategoryNodeKey(scope.resourceType, categoryId),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function buildScopeItemsFromTree(): CategoryScopeItem[] {
|
||||
const checkedKeySet = new Set(categoryTreeCheckedKeys.value);
|
||||
return RESOURCE_SCOPE_GROUPS.map(({ resourceType }) => {
|
||||
const groupNodeKey = buildGroupNodeKey(resourceType);
|
||||
const grantedCategoryIds = categoryOptions.value[resourceType]
|
||||
.filter((option) =>
|
||||
checkedKeySet.has(buildCategoryNodeKey(resourceType, option.id)),
|
||||
)
|
||||
.map((option) => option.id);
|
||||
const allCategoriesSelected =
|
||||
categoryOptions.value[resourceType].length > 0 &&
|
||||
grantedCategoryIds.length === categoryOptions.value[resourceType].length;
|
||||
const scopeMode: CategoryScopeItem['scopeMode'] =
|
||||
checkedKeySet.has(groupNodeKey) || allCategoriesSelected
|
||||
? 'ALL'
|
||||
: 'CUSTOM';
|
||||
return {
|
||||
categoryIds: scopeMode === 'ALL' ? [] : grantedCategoryIds,
|
||||
resourceType,
|
||||
scopeMode,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getCategoryScopeDetail(roleId: number | string) {
|
||||
api.get('/api/v1/sysRoleCategoryScope/detail', {
|
||||
params: { roleId },
|
||||
}).then((res) => {
|
||||
if (res.errorCode !== 0) {
|
||||
categoryScopeEditable.value = false;
|
||||
categoryScopeDetail.value = {
|
||||
roleId,
|
||||
scopes: buildDefaultScopes(),
|
||||
};
|
||||
syncCategoryTreeCheckedKeys(categoryScopeDetail.value.scopes);
|
||||
return;
|
||||
}
|
||||
categoryScopeEditable.value = !!res.data?.editable;
|
||||
categoryScopeDetail.value = {
|
||||
roleId,
|
||||
scopes: (res.data?.scopes || buildDefaultScopes()).map((item: any) => ({
|
||||
categoryIds: item.categoryIds || [],
|
||||
resourceType: item.resourceType,
|
||||
scopeMode: item.scopeMode || 'CUSTOM',
|
||||
})),
|
||||
};
|
||||
syncCategoryTreeCheckedKeys(categoryScopeDetail.value.scopes);
|
||||
});
|
||||
}
|
||||
|
||||
async function saveCategoryScope(roleId: number | string) {
|
||||
await ensureCategoryOptions();
|
||||
const scopes = buildScopeItemsFromTree();
|
||||
categoryScopeDetail.value = {
|
||||
roleId,
|
||||
scopes,
|
||||
};
|
||||
return api.post('/api/v1/sysRoleCategoryScope/save', {
|
||||
roleId,
|
||||
scopes,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -120,11 +341,6 @@ function getDeptIds(roleId: any) {
|
||||
<ElInput v-model.trim="entity.remark" />
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('sysRole.menuPermission')">
|
||||
<ElSwitch
|
||||
v-model="entity.menuCheckStrictly"
|
||||
:active-text="$t('sysRole.checkStrictlyTrue')"
|
||||
:inactive-text="$t('sysRole.checkStrictlyFalse')"
|
||||
/>
|
||||
<Tree
|
||||
data-url="/api/v1/sysMenu/list?asTree=true"
|
||||
v-model="entity.menuIds"
|
||||
@@ -132,25 +348,23 @@ function getDeptIds(roleId: any) {
|
||||
label: 'menuTitle',
|
||||
children: 'children',
|
||||
}"
|
||||
:check-strictly="!entity.menuCheckStrictly"
|
||||
:check-strictly="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('sysRole.dataPermission')">
|
||||
<DictSelect v-model="entity.dataScope" dict-code="dataScope" />
|
||||
<div v-if="entity.dataScope === 5" style="width: 100%">
|
||||
<ElSwitch
|
||||
v-model="entity.deptCheckStrictly"
|
||||
:active-text="$t('sysRole.checkStrictlyTrue')"
|
||||
:inactive-text="$t('sysRole.checkStrictlyFalse')"
|
||||
/>
|
||||
<ElFormItem :label="$t('sysRole.categoryPermission')">
|
||||
<div class="role-category-scope-panel">
|
||||
<Tree
|
||||
data-url="/api/v1/sysDept/list?asTree=true"
|
||||
v-model="entity.deptIds"
|
||||
:data="categoryPermissionTreeData"
|
||||
v-model="categoryTreeCheckedKeys"
|
||||
:default-props="{
|
||||
label: 'deptName',
|
||||
label: 'label',
|
||||
children: 'children',
|
||||
}"
|
||||
:check-strictly="!entity.deptCheckStrictly"
|
||||
node-key="id"
|
||||
:default-expand-all="true"
|
||||
:check-strictly="false"
|
||||
:height="280"
|
||||
:disabled="!categoryScopeEditable"
|
||||
/>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
@@ -158,4 +372,34 @@ function getDeptIds(roleId: any) {
|
||||
</EasyFlowFormModal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.role-category-scope-panel {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.role-category-scope-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
background: var(--el-fill-color-extra-light);
|
||||
}
|
||||
|
||||
.role-category-scope-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.role-category-scope-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user