|
|
|
|
@@ -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>
|
|
|
|
|
|