feat: 增加工作流和知识库三级权限
- 抽取统一资源访问骨架与部门可见范围判断 - 接入工作流和知识库的 READ/MANAGE 权限校验 - 增加可见范围配置与只读态前端交互
This commit is contained in:
@@ -1,37 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import type {FormInstance} from 'element-plus';
|
||||
import {ElForm, ElFormItem, ElInput, ElInputNumber, ElMessage, ElMessageBox,} from 'element-plus';
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import type {ActionButton, CardPrimaryAction,} from '#/components/page/CardList.vue';
|
||||
import CardPage from '#/components/page/CardList.vue';
|
||||
import type {
|
||||
ActionButton,
|
||||
CardPrimaryAction,
|
||||
} from '#/components/page/CardList.vue';
|
||||
|
||||
import {computed, onMounted, ref} from 'vue';
|
||||
import {useRouter} from 'vue-router';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import {EasyFlowFormModal} from '@easyflow/common-ui';
|
||||
import {$t} from '@easyflow/locales';
|
||||
import { useAccess } from '@easyflow/access';
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
import { $t } from '@easyflow/locales';
|
||||
import { useUserStore } from '@easyflow/stores';
|
||||
|
||||
import {Delete, Edit, Notebook, Plus, Search} from '@element-plus/icons-vue';
|
||||
import {tryit} from 'radash';
|
||||
import {
|
||||
Check,
|
||||
Delete,
|
||||
Edit,
|
||||
Lock,
|
||||
Notebook,
|
||||
OfficeBuilding,
|
||||
Plus,
|
||||
Promotion,
|
||||
Search,
|
||||
} from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElPopover,
|
||||
} from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import {api} from '#/api/request';
|
||||
import { api } from '#/api/request';
|
||||
import defaultIcon from '#/assets/ai/knowledge/book.svg';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import CardPage from '#/components/page/CardList.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
import DocumentCollectionModal from '#/views/ai/documentCollection/DocumentCollectionModal.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
const collectionTypeLabelMap = {
|
||||
DOCUMENT: $t('documentCollection.collectionTypeDocument'),
|
||||
FAQ: $t('documentCollection.collectionTypeFaq'),
|
||||
};
|
||||
type VisibilityScope = 'DEPT' | 'PRIVATE' | 'PUBLIC';
|
||||
|
||||
const canManageKnowledgePermission = computed(() =>
|
||||
hasAccessByCodes(['/api/v1/documentCollection/save']),
|
||||
);
|
||||
const updatingScopeId = ref<null | number | string>(null);
|
||||
const visibilityScopePopoverRefs = ref<Record<string, any>>({});
|
||||
const visibilityScopeMeta = computed(() => ({
|
||||
PRIVATE: {
|
||||
label: $t('documentCollection.visibilityScopePrivate'),
|
||||
description: $t('documentCollection.visibilityScopePrivateDesc'),
|
||||
icon: Lock,
|
||||
tone: 'private',
|
||||
},
|
||||
DEPT: {
|
||||
label: $t('documentCollection.visibilityScopeDept'),
|
||||
description: $t('documentCollection.visibilityScopeDeptDesc'),
|
||||
icon: OfficeBuilding,
|
||||
tone: 'dept',
|
||||
},
|
||||
PUBLIC: {
|
||||
label: $t('documentCollection.visibilityScopePublic'),
|
||||
description: $t('documentCollection.visibilityScopePublicDesc'),
|
||||
icon: Promotion,
|
||||
tone: 'public',
|
||||
},
|
||||
}));
|
||||
const visibilityScopeOptions = computed(() =>
|
||||
(['PRIVATE', 'DEPT', 'PUBLIC'] as VisibilityScope[]).map((value) => ({
|
||||
value,
|
||||
...visibilityScopeMeta.value[value],
|
||||
})),
|
||||
);
|
||||
|
||||
function isSuperAdmin() {
|
||||
return (
|
||||
String(userStore.userInfo?.id || '') === '1' ||
|
||||
(userStore.userRoles || []).includes('super_admin')
|
||||
);
|
||||
}
|
||||
|
||||
function canManageKnowledgeItem(row: Record<string, any>) {
|
||||
if (!canManageKnowledgePermission.value) {
|
||||
return false;
|
||||
}
|
||||
const currentUserId = String(userStore.userInfo?.id || '');
|
||||
return isSuperAdmin() || currentUserId === String(row?.createdBy || '');
|
||||
}
|
||||
|
||||
function ensureManageKnowledgeItem(row: Record<string, any>) {
|
||||
if (canManageKnowledgeItem(row)) {
|
||||
return true;
|
||||
}
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return false;
|
||||
}
|
||||
|
||||
function resolveNavTitle(row: Record<string, any>) {
|
||||
return row?.title || row?.name || '';
|
||||
}
|
||||
|
||||
function openKnowledgeDetail(row: { id: string; name?: string; title?: string }) {
|
||||
function openKnowledgeDetail(row: {
|
||||
id: string;
|
||||
name?: string;
|
||||
title?: string;
|
||||
}) {
|
||||
router.push({
|
||||
path: '/ai/documentCollection/document',
|
||||
query: {
|
||||
@@ -56,7 +142,7 @@ interface FieldDefinition {
|
||||
const primaryAction: CardPrimaryAction = {
|
||||
icon: Notebook,
|
||||
text: $t('documentCollection.actions.knowledge'),
|
||||
permission: '/api/v1/documentCollection/save',
|
||||
permission: '/api/v1/documentCollection/query',
|
||||
onClick(row) {
|
||||
openKnowledgeDetail(row);
|
||||
},
|
||||
@@ -69,6 +155,9 @@ const actions: ActionButton[] = [
|
||||
permission: '/api/v1/documentCollection/save',
|
||||
placement: 'inline',
|
||||
onClick(row) {
|
||||
if (!ensureManageKnowledgeItem(row)) {
|
||||
return;
|
||||
}
|
||||
aiKnowledgeModalRef.value.openDialog(row);
|
||||
},
|
||||
},
|
||||
@@ -95,6 +184,9 @@ const actions: ActionButton[] = [
|
||||
permission: '/api/v1/documentCollection/remove',
|
||||
placement: 'inline',
|
||||
onClick(row) {
|
||||
if (!ensureManageKnowledgeItem(row)) {
|
||||
return;
|
||||
}
|
||||
handleDelete(row);
|
||||
},
|
||||
},
|
||||
@@ -178,6 +270,9 @@ const formRules = computed(() => {
|
||||
const handleSearch = (params: any) => {
|
||||
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
|
||||
};
|
||||
const reloadKnowledgeList = () => {
|
||||
pageDataRef.value?.reload?.();
|
||||
};
|
||||
const formData = ref<any>({});
|
||||
const dialogVisible = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
@@ -189,7 +284,7 @@ function showControlDialog(item: any) {
|
||||
const categoryList = ref<any[]>([]);
|
||||
const getCategoryList = async () => {
|
||||
const [, res] = await tryit(api.get)(
|
||||
'/api/v1/documentCollectionCategory/list',
|
||||
'/api/v1/documentCollectionCategory/visibleList',
|
||||
{
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
},
|
||||
@@ -258,6 +353,51 @@ const footerButton = {
|
||||
},
|
||||
};
|
||||
const saveLoading = ref(false);
|
||||
function resolveVisibilityScopeMeta(scope?: string) {
|
||||
return (
|
||||
visibilityScopeMeta.value[(scope || 'PRIVATE') as VisibilityScope] ||
|
||||
visibilityScopeMeta.value.PRIVATE
|
||||
);
|
||||
}
|
||||
function setVisibilityScopePopoverRef(id: number | string, el: any) {
|
||||
const cacheKey = String(id);
|
||||
if (el) {
|
||||
visibilityScopePopoverRefs.value[cacheKey] = el;
|
||||
return;
|
||||
}
|
||||
delete visibilityScopePopoverRefs.value[cacheKey];
|
||||
}
|
||||
function closeVisibilityScopePopover(id: number | string) {
|
||||
visibilityScopePopoverRefs.value[String(id)]?.hide?.();
|
||||
}
|
||||
async function updateVisibilityScope(
|
||||
row: any,
|
||||
visibilityScope: VisibilityScope,
|
||||
) {
|
||||
if (
|
||||
!canManageKnowledgeItem(row) ||
|
||||
!row?.id ||
|
||||
updatingScopeId.value === row.id ||
|
||||
row.visibilityScope === visibilityScope
|
||||
) {
|
||||
closeVisibilityScopePopover(row.id);
|
||||
return;
|
||||
}
|
||||
updatingScopeId.value = row.id;
|
||||
try {
|
||||
const res = await api.post('/api/v1/documentCollection/update', {
|
||||
id: row.id,
|
||||
visibilityScope,
|
||||
});
|
||||
if (res.errorCode === 0) {
|
||||
row.visibilityScope = visibilityScope;
|
||||
ElMessage.success($t('message.updateOkMessage'));
|
||||
closeVisibilityScopePopover(row.id);
|
||||
}
|
||||
} finally {
|
||||
updatingScopeId.value = null;
|
||||
}
|
||||
}
|
||||
function handleSubmit() {
|
||||
formRef.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
@@ -318,7 +458,97 @@ function changeCategory(category: any) {
|
||||
:actions="actions"
|
||||
tag-field="collectionType"
|
||||
:tag-map="collectionTypeLabelMap"
|
||||
/>
|
||||
>
|
||||
<template #corner="{ item }">
|
||||
<ElPopover
|
||||
v-if="canManageKnowledgeItem(item)"
|
||||
:ref="(el) => setVisibilityScopePopoverRef(item.id, el)"
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
:width="208"
|
||||
popper-class="knowledge-visibility-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<button
|
||||
type="button"
|
||||
class="knowledge-scope-chip"
|
||||
:class="`knowledge-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
|
||||
:disabled="updatingScopeId === item.id"
|
||||
@click.stop
|
||||
>
|
||||
<ElIcon class="knowledge-scope-chip__icon">
|
||||
<component
|
||||
:is="
|
||||
resolveVisibilityScopeMeta(item.visibilityScope)
|
||||
.icon
|
||||
"
|
||||
/>
|
||||
</ElIcon>
|
||||
<span class="knowledge-scope-chip__label">
|
||||
{{
|
||||
resolveVisibilityScopeMeta(item.visibilityScope).label
|
||||
}}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<div class="knowledge-scope-panel" @click.stop>
|
||||
<button
|
||||
v-for="option in visibilityScopeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="knowledge-scope-option"
|
||||
:class="[
|
||||
`knowledge-scope-option--${option.tone}`,
|
||||
{
|
||||
'knowledge-scope-option--active':
|
||||
item.visibilityScope === option.value,
|
||||
},
|
||||
]"
|
||||
:disabled="updatingScopeId === item.id"
|
||||
@click.stop="updateVisibilityScope(item, option.value)"
|
||||
>
|
||||
<span class="knowledge-scope-option__leading">
|
||||
<span class="knowledge-scope-option__icon-wrap">
|
||||
<ElIcon class="knowledge-scope-option__icon">
|
||||
<component :is="option.icon" />
|
||||
</ElIcon>
|
||||
</span>
|
||||
<span class="knowledge-scope-option__text">
|
||||
<span class="knowledge-scope-option__label">
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<span class="knowledge-scope-option__desc">
|
||||
{{ option.description }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<ElIcon
|
||||
v-if="item.visibilityScope === option.value"
|
||||
class="knowledge-scope-option__check"
|
||||
>
|
||||
<Check />
|
||||
</ElIcon>
|
||||
</button>
|
||||
</div>
|
||||
</ElPopover>
|
||||
<div
|
||||
v-else
|
||||
class="knowledge-scope-chip knowledge-scope-chip--readonly"
|
||||
:class="`knowledge-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
|
||||
>
|
||||
<ElIcon class="knowledge-scope-chip__icon">
|
||||
<component
|
||||
:is="
|
||||
resolveVisibilityScopeMeta(item.visibilityScope).icon
|
||||
"
|
||||
/>
|
||||
</ElIcon>
|
||||
<span class="knowledge-scope-chip__label">
|
||||
{{ resolveVisibilityScopeMeta(item.visibilityScope).label }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</CardPage>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
@@ -362,7 +592,10 @@ function changeCategory(category: any) {
|
||||
</EasyFlowFormModal>
|
||||
|
||||
<!-- 新增知识库模态框-->
|
||||
<DocumentCollectionModal ref="aiKnowledgeModalRef" @reload="handleSearch" />
|
||||
<DocumentCollectionModal
|
||||
ref="aiKnowledgeModalRef"
|
||||
@reload="reloadKnowledgeList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -372,4 +605,206 @@ h1 {
|
||||
margin-bottom: 30px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.knowledge-scope-chip {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-height: 30px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: hsl(var(--text-strong));
|
||||
background: hsl(var(--surface-subtle) / 0.92);
|
||||
border: 1px solid hsl(var(--line-subtle));
|
||||
border-radius: 999px;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
background-color 0.18s ease,
|
||||
color 0.18s ease,
|
||||
transform 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
button.knowledge-scope-chip {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.knowledge-scope-chip:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 22px -18px hsl(var(--foreground) / 0.32);
|
||||
}
|
||||
|
||||
button.knowledge-scope-chip:focus-visible {
|
||||
outline: none;
|
||||
box-shadow:
|
||||
0 0 0 4px hsl(var(--primary) / 0.12),
|
||||
0 10px 22px -18px hsl(var(--foreground) / 0.32);
|
||||
}
|
||||
|
||||
button.knowledge-scope-chip:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.knowledge-scope-chip--readonly {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.knowledge-scope-chip__icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.knowledge-scope-chip__label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.knowledge-scope-chip--private {
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 0.09);
|
||||
border-color: hsl(var(--primary) / 0.2);
|
||||
}
|
||||
|
||||
.knowledge-scope-chip--dept {
|
||||
color: hsl(var(--warning));
|
||||
background: hsl(var(--warning) / 0.12);
|
||||
border-color: hsl(var(--warning) / 0.2);
|
||||
}
|
||||
|
||||
.knowledge-scope-chip--public {
|
||||
color: hsl(var(--success));
|
||||
background: hsl(var(--success) / 0.12);
|
||||
border-color: hsl(var(--success) / 0.2);
|
||||
}
|
||||
|
||||
.knowledge-scope-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.knowledge-scope-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 10px 8px;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
transition:
|
||||
background-color 0.18s ease,
|
||||
transform 0.18s ease,
|
||||
color 0.18s ease;
|
||||
}
|
||||
|
||||
.knowledge-scope-option:hover {
|
||||
background: hsl(var(--foreground) / 0.04);
|
||||
}
|
||||
|
||||
.knowledge-scope-option:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 4px hsl(var(--primary) / 0.12);
|
||||
}
|
||||
|
||||
.knowledge-scope-option:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.knowledge-scope-option__leading {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.knowledge-scope-option__icon-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.knowledge-scope-option__icon {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.knowledge-scope-option__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.knowledge-scope-option__label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
color: hsl(var(--text-strong));
|
||||
}
|
||||
|
||||
.knowledge-scope-option__desc {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.knowledge-scope-option__check {
|
||||
font-size: 16px;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.knowledge-scope-option--private .knowledge-scope-option__icon-wrap {
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
}
|
||||
|
||||
.knowledge-scope-option--dept .knowledge-scope-option__icon-wrap {
|
||||
color: hsl(var(--warning));
|
||||
background: hsl(var(--warning) / 0.12);
|
||||
}
|
||||
|
||||
.knowledge-scope-option--public .knowledge-scope-option__icon-wrap {
|
||||
color: hsl(var(--success));
|
||||
background: hsl(var(--success) / 0.12);
|
||||
}
|
||||
|
||||
.knowledge-scope-option--private.knowledge-scope-option--active {
|
||||
background: hsl(var(--primary) / 0.08);
|
||||
}
|
||||
|
||||
.knowledge-scope-option--dept.knowledge-scope-option--active {
|
||||
background: hsl(var(--warning) / 0.08);
|
||||
}
|
||||
|
||||
.knowledge-scope-option--public.knowledge-scope-option--active {
|
||||
background: hsl(var(--success) / 0.08);
|
||||
}
|
||||
|
||||
.knowledge-scope-option--private .knowledge-scope-option__check {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.knowledge-scope-option--dept .knowledge-scope-option__check {
|
||||
color: hsl(var(--warning));
|
||||
}
|
||||
|
||||
.knowledge-scope-option--public .knowledge-scope-option__check {
|
||||
color: hsl(var(--success));
|
||||
}
|
||||
|
||||
:global(.knowledge-visibility-popover.el-popover.el-popper) {
|
||||
padding: 8px;
|
||||
border-radius: 16px;
|
||||
border-color: hsl(var(--line-subtle));
|
||||
box-shadow: 0 18px 34px -28px hsl(var(--foreground) / 0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user