feat: 增加工作流和知识库三级权限
- 抽取统一资源访问骨架与部门可见范围判断 - 接入工作流和知识库的 READ/MANAGE 权限校验 - 增加可见范围配置与只读态前端交互
This commit is contained in:
@@ -46,6 +46,9 @@ export interface CardListProps {
|
||||
titleField?: string;
|
||||
descField?: string;
|
||||
actions?: ActionButton[];
|
||||
cornerTagField?: string;
|
||||
cornerTagMap?: Record<string, string>;
|
||||
cornerTagTypeMap?: Record<string, string>;
|
||||
defaultIcon: any;
|
||||
data: any[];
|
||||
primaryAction?: CardPrimaryAction;
|
||||
@@ -58,6 +61,9 @@ const props = withDefaults(defineProps<CardListProps>(), {
|
||||
titleField: 'title',
|
||||
descField: 'description',
|
||||
actions: () => [],
|
||||
cornerTagField: '',
|
||||
cornerTagMap: () => ({}),
|
||||
cornerTagTypeMap: () => ({}),
|
||||
primaryAction: undefined,
|
||||
tagField: '',
|
||||
tagMap: () => ({}),
|
||||
@@ -154,6 +160,23 @@ function handleActionClick(event: Event, action: ActionButton, item: any) {
|
||||
{{ item[descField] }}
|
||||
</ElText>
|
||||
</div>
|
||||
<div
|
||||
v-if="$slots.corner || (cornerTagField && item[cornerTagField])"
|
||||
class="card-corner-tag"
|
||||
>
|
||||
<slot name="corner" :item="item">
|
||||
<ElTag
|
||||
size="small"
|
||||
effect="plain"
|
||||
:type="cornerTagTypeMap[item[cornerTagField]] || 'info'"
|
||||
round
|
||||
>
|
||||
{{
|
||||
cornerTagMap[item[cornerTagField]] || item[cornerTagField]
|
||||
}}
|
||||
</ElTag>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -343,6 +366,26 @@ function handleActionClick(event: Event, action: ActionButton, item: any) {
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.card-corner-tag {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
min-height: 28px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.card-corner-tag :deep(.el-tag) {
|
||||
--el-tag-border-radius: 999px;
|
||||
--el-tag-font-size: 12px;
|
||||
--el-tag-border-color: transparent;
|
||||
|
||||
padding: 0 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
"englishName": "EnglishName",
|
||||
"status": "ShowInUserCenter",
|
||||
"categoryId": "Category",
|
||||
"visibilityScope": "Visibility Scope",
|
||||
"visibilityScopePrivate": "Personal",
|
||||
"visibilityScopePrivateDesc": "Only the creator can access it",
|
||||
"visibilityScopeDept": "Dept",
|
||||
"visibilityScopeDeptDesc": "Available to the dept and descendants",
|
||||
"visibilityScopePublic": "Public",
|
||||
"visibilityScopePublicDesc": "Available to internal users matched by category",
|
||||
"params": "Params",
|
||||
"steps": "Steps",
|
||||
"result": "Result",
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
"collectionTypeDocument": "Document",
|
||||
"collectionTypeFaq": "FAQ",
|
||||
"description": "Description",
|
||||
"visibilityScope": "Visibility Scope",
|
||||
"visibilityScopePrivate": "Personal",
|
||||
"visibilityScopePrivateDesc": "Only the creator can access it",
|
||||
"visibilityScopeDept": "Dept",
|
||||
"visibilityScopeDeptDesc": "Available to the dept and descendants",
|
||||
"visibilityScopePublic": "Public",
|
||||
"visibilityScopePublicDesc": "Available to internal users matched by category",
|
||||
"slug": "Slug",
|
||||
"vectorStoreEnable": "VectorStoreEnable",
|
||||
"vectorStoreType": "VectorStoreType",
|
||||
@@ -46,12 +53,24 @@
|
||||
},
|
||||
"importDoc": {
|
||||
"fileUpload": "File upload",
|
||||
"parameterSettings": "ParameterSettings",
|
||||
"parameterSettings": "Parameter settings",
|
||||
"strategyAnalysis": "Strategy analysis",
|
||||
"segmentedPreview": "SegmentedPreview",
|
||||
"confirmImport": "ConfirmImport",
|
||||
"fileName": "File Name",
|
||||
"progressUpload": "Progress of file upload",
|
||||
"fileSize": "File size"
|
||||
"fileSize": "File size",
|
||||
"analysisTip": "The system analyzes multilingual structure first and recommends a splitting strategy. You can still adjust each file manually.",
|
||||
"confidence": "Confidence",
|
||||
"recommendReason": "Reasons",
|
||||
"candidateStrategies": "Candidates",
|
||||
"strategySelection": "Strategy",
|
||||
"previewTip": "The preview result is the final import basis. Confirm it before committing.",
|
||||
"previewEmpty": "No preview data",
|
||||
"warningCount": "Warnings",
|
||||
"chunkCount": "Chunks",
|
||||
"resultEmpty": "No import result",
|
||||
"importFailed": "Import failed"
|
||||
},
|
||||
"splitterDoc": {
|
||||
"fileType": "FileType",
|
||||
@@ -64,6 +83,12 @@
|
||||
"simpleTokenizeSplitter": "SimpleTokenizeSplitter",
|
||||
"regexDocumentSplitter": "RegexDocumentSplitter",
|
||||
"markdownHeaderSplitter": "MarkdownHeaderSplitter",
|
||||
"autoStrategy": "Auto recommendation",
|
||||
"markdownSection": "Markdown headings",
|
||||
"outlineSection": "Outline sections",
|
||||
"qaPair": "Q&A pairs",
|
||||
"paragraphLength": "Paragraph length",
|
||||
"customRegex": "Custom regex",
|
||||
"mdSplitterLevel": "MarkdownSplitterLevel",
|
||||
"uploadStatus": "UploadStatus",
|
||||
"pendingUpload": "PendingUpload",
|
||||
@@ -139,5 +164,6 @@
|
||||
"tencentCloud": "tencentCloud",
|
||||
"vectorEmbedModelTips": "After successful vector data, it is not allowed to modify the vector model",
|
||||
"dimensionOfVectorModelTips": "After successful vector data, it is not allowed to modify the dimensions of the vector model",
|
||||
"dimensionOfVectorModel": "Dimension of vector model"
|
||||
"dimensionOfVectorModel": "Dimension of vector model",
|
||||
"managePermissionHint": "Only the creator or super admin can modify this knowledge base"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
"englishName": "英文名称",
|
||||
"status": "在用户中心显示",
|
||||
"categoryId": "分类",
|
||||
"visibilityScope": "可见范围",
|
||||
"visibilityScopePrivate": "个人",
|
||||
"visibilityScopePrivateDesc": "仅创建者可访问",
|
||||
"visibilityScopeDept": "部门",
|
||||
"visibilityScopeDeptDesc": "本部门及下级部门可访问",
|
||||
"visibilityScopePublic": "公开",
|
||||
"visibilityScopePublicDesc": "分类命中的内部用户可访问",
|
||||
"params": "执行参数",
|
||||
"steps": "执行步骤",
|
||||
"result": "执行结果",
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
"collectionTypeDocument": "文档",
|
||||
"collectionTypeFaq": "FAQ",
|
||||
"description": "描述",
|
||||
"visibilityScope": "可见范围",
|
||||
"visibilityScopePrivate": "个人",
|
||||
"visibilityScopePrivateDesc": "仅创建者可访问",
|
||||
"visibilityScopeDept": "部门",
|
||||
"visibilityScopeDeptDesc": "本部门及下级部门可访问",
|
||||
"visibilityScopePublic": "公开",
|
||||
"visibilityScopePublicDesc": "分类命中的内部用户可访问",
|
||||
"slug": "URL 别名",
|
||||
"vectorStoreEnable": "是否启用向量数据库",
|
||||
"vectorStoreType": "向量数据库类型",
|
||||
@@ -47,11 +54,23 @@
|
||||
"importDoc": {
|
||||
"fileUpload": "文件上传",
|
||||
"parameterSettings": "参数设置",
|
||||
"strategyAnalysis": "策略分析",
|
||||
"segmentedPreview": "分段预览",
|
||||
"confirmImport": "确认导入",
|
||||
"fileName": "文件名称",
|
||||
"progressUpload": "文件上传进度",
|
||||
"fileSize": "文件大小"
|
||||
"fileSize": "文件大小",
|
||||
"analysisTip": "系统会先基于文档结构做中英文规则分析,再推荐拆分策略,你也可以逐个文件手动调整。",
|
||||
"confidence": "置信度",
|
||||
"recommendReason": "推荐理由",
|
||||
"candidateStrategies": "备选策略",
|
||||
"strategySelection": "拆分策略",
|
||||
"previewTip": "预览结果就是最终入库依据,确认无误后再执行导入。",
|
||||
"previewEmpty": "暂无可预览内容",
|
||||
"warningCount": "警告数",
|
||||
"chunkCount": "分块数",
|
||||
"resultEmpty": "暂无导入结果",
|
||||
"importFailed": "导入失败"
|
||||
},
|
||||
"splitterDoc": {
|
||||
"fileType": "文件类型",
|
||||
@@ -64,6 +83,12 @@
|
||||
"simpleTokenizeSplitter": "简单分词器",
|
||||
"regexDocumentSplitter": "正则文档分割器",
|
||||
"markdownHeaderSplitter": "Markdown标题层级拆分器",
|
||||
"autoStrategy": "自动推荐",
|
||||
"markdownSection": "Markdown 标题拆分",
|
||||
"outlineSection": "章节标题拆分",
|
||||
"qaPair": "问答对拆分",
|
||||
"paragraphLength": "自然段长度拆分",
|
||||
"customRegex": "自定义正则拆分",
|
||||
"mdSplitterLevel": "Markdown标题等级",
|
||||
"uploadStatus": "上传状态",
|
||||
"pendingUpload": "待上传",
|
||||
@@ -139,5 +164,6 @@
|
||||
"tencentCloud": "腾讯云",
|
||||
"vectorEmbedModelTips": "成功向量数据之后不允许修改向量模型",
|
||||
"dimensionOfVectorModelTips": "成功向量数据之后不允许修改向量模型维度",
|
||||
"dimensionOfVectorModel": "向量模型维度"
|
||||
"dimensionOfVectorModel": "向量模型维度",
|
||||
"managePermissionHint": "仅创建者或超级管理员可修改当前知识库"
|
||||
}
|
||||
|
||||
@@ -27,14 +27,26 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
manageable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
const dialogVisible = ref(false);
|
||||
const pageDataRef = ref();
|
||||
const handleEdit = (row: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
form.value = { id: row.id, content: row.content };
|
||||
openDialog();
|
||||
};
|
||||
const handleDelete = (row: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
@@ -68,6 +80,10 @@ const queryParams = ref({
|
||||
sortType: 'asc',
|
||||
});
|
||||
const save = () => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
btnLoading.value = true;
|
||||
api.post('/api/v1/documentChunk/update', form.value).then((res: any) => {
|
||||
btnLoading.value = false;
|
||||
@@ -103,7 +119,12 @@ const form = ref({
|
||||
:label="$t('documentCollection.content')"
|
||||
min-width="240"
|
||||
/>
|
||||
<ElTableColumn :label="$t('common.handle')" width="100" align="right">
|
||||
<ElTableColumn
|
||||
v-if="props.manageable"
|
||||
:label="$t('common.handle')"
|
||||
width="100"
|
||||
align="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-3">
|
||||
<ElButton link type="primary" @click="handleEdit(row)">
|
||||
@@ -130,6 +151,7 @@ const form = ref({
|
||||
</template>
|
||||
</PageData>
|
||||
<EasyFlowFormModal
|
||||
v-if="props.manageable"
|
||||
v-model:open="dialogVisible"
|
||||
:closable="!btnLoading"
|
||||
:title="$t('button.edit')"
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { useAccess } from '@easyflow/access';
|
||||
import { $t } from '@easyflow/locales';
|
||||
import { useUserStore } from '@easyflow/stores';
|
||||
|
||||
import { ArrowLeft, Plus } from '@element-plus/icons-vue';
|
||||
import { ElIcon, ElImage } from 'element-plus';
|
||||
@@ -20,11 +22,34 @@ import KnowledgeSearchConfig from '#/views/ai/documentCollection/KnowledgeSearch
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
const knowledgeId = ref<string>((route.query.id as string) || '');
|
||||
const activeMenu = ref<string>((route.query.activeMenu as string) || '');
|
||||
const knowledgeInfo = ref<any>({});
|
||||
const selectedCategory = ref('');
|
||||
const canManageKnowledgePermission = computed(() =>
|
||||
hasAccessByCodes(['/api/v1/documentCollection/save']),
|
||||
);
|
||||
|
||||
const isSuperAdmin = computed(() => {
|
||||
return (
|
||||
String(userStore.userInfo?.id || '') === '1' ||
|
||||
(userStore.userRoles || []).includes('super_admin')
|
||||
);
|
||||
});
|
||||
|
||||
const canManageCurrentKnowledge = computed(() => {
|
||||
if (!knowledgeInfo.value?.id || !canManageKnowledgePermission.value) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
isSuperAdmin.value ||
|
||||
String(userStore.userInfo?.id || '') ===
|
||||
String(knowledgeInfo.value.createdBy || '')
|
||||
);
|
||||
});
|
||||
|
||||
const syncNavTitle = (title: string) => {
|
||||
if (!title) {
|
||||
@@ -163,6 +188,9 @@ const backDoc = () => {
|
||||
<div class="description">
|
||||
{{ knowledgeInfo.description || '' }}
|
||||
</div>
|
||||
<div v-if="!canManageCurrentKnowledge" class="permission-tip">
|
||||
{{ $t('documentCollection.managePermissionHint') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-top-menu">
|
||||
<button
|
||||
@@ -184,7 +212,7 @@ const backDoc = () => {
|
||||
<div class="doc-header" v-if="!viewDocVisible">
|
||||
<HeaderSearch
|
||||
v-if="!isFaqCollection"
|
||||
:buttons="headerButtons"
|
||||
:buttons="canManageCurrentKnowledge ? headerButtons : []"
|
||||
@search="handleSearch"
|
||||
@button-click="handleButtonClick"
|
||||
/>
|
||||
@@ -192,6 +220,7 @@ const backDoc = () => {
|
||||
<DocumentTable
|
||||
ref="documentTableRef"
|
||||
:knowledge-id="knowledgeId"
|
||||
:manageable="canManageCurrentKnowledge"
|
||||
@view-doc="viewDoc"
|
||||
v-if="!viewDocVisible"
|
||||
/>
|
||||
@@ -199,24 +228,32 @@ const backDoc = () => {
|
||||
<ChunkDocumentTable
|
||||
v-else
|
||||
:document-id="documentId"
|
||||
:manageable="canManageCurrentKnowledge"
|
||||
:default-summary-prompt="knowledgeInfo.summaryPrompt"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="selectedCategory === 'faqList'" class="doc-table">
|
||||
<FaqTable :knowledge-id="knowledgeId" />
|
||||
<FaqTable
|
||||
:knowledge-id="knowledgeId"
|
||||
:manageable="canManageCurrentKnowledge"
|
||||
/>
|
||||
</div>
|
||||
<!--知识检索-->
|
||||
<div
|
||||
v-if="selectedCategory === 'knowledgeSearch'"
|
||||
class="doc-search-container"
|
||||
>
|
||||
<KnowledgeSearchConfig :document-collection-id="knowledgeId" />
|
||||
<KnowledgeSearchConfig
|
||||
:document-collection-id="knowledgeId"
|
||||
:manageable="canManageCurrentKnowledge"
|
||||
/>
|
||||
<KnowledgeSearch :knowledge-id="knowledgeId" />
|
||||
</div>
|
||||
<!--配置-->
|
||||
<div v-if="selectedCategory === 'config'">
|
||||
<DocumentCollectionDataConfig
|
||||
:detail-data="knowledgeInfo"
|
||||
:manageable="canManageCurrentKnowledge"
|
||||
@reload="getKnowledge"
|
||||
/>
|
||||
</div>
|
||||
@@ -306,6 +343,13 @@ const backDoc = () => {
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.permission-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.doc-table {
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type {FormInstance} from 'element-plus';
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElForm,
|
||||
@@ -13,13 +17,9 @@ import {
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import {computed, onMounted, ref, watch} from 'vue';
|
||||
|
||||
import {InfoFilled} from '@element-plus/icons-vue';
|
||||
|
||||
import {api} from '#/api/request';
|
||||
import { api } from '#/api/request';
|
||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||
import {$t} from '#/locales';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const props = defineProps({
|
||||
detailData: {
|
||||
@@ -45,7 +45,10 @@ const props = defineProps({
|
||||
searchEngineEnable: false,
|
||||
englishName: '',
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
manageable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -54,7 +57,7 @@ const emit = defineEmits(['reload']);
|
||||
const normalizeEntity = (raw: any) => {
|
||||
const options = {
|
||||
canUpdateEmbeddingModel: true,
|
||||
...(raw?.options || {}),
|
||||
...raw?.options,
|
||||
};
|
||||
if (options.rerankEnable === undefined || options.rerankEnable === null) {
|
||||
options.rerankEnable = !!raw?.rerankModelId;
|
||||
@@ -102,7 +105,7 @@ const vectorStoreConfigPlaceholder = computed(() => {
|
||||
|
||||
const getEmbeddingLlmListData = async () => {
|
||||
try {
|
||||
const url = `/api/v1/model/list?modelType=embeddingModel`;
|
||||
const url = `/api/v1/documentCollection/modelList?modelType=embeddingModel`;
|
||||
const res = await api.get(url, {});
|
||||
if (res.errorCode === 0) {
|
||||
embeddingLlmList.value = res.data;
|
||||
@@ -115,7 +118,9 @@ const getEmbeddingLlmListData = async () => {
|
||||
|
||||
const getRerankerLlmListData = async () => {
|
||||
try {
|
||||
const res = await api.get('/api/v1/model/list?modelType=rerankModel');
|
||||
const res = await api.get(
|
||||
'/api/v1/documentCollection/modelList?modelType=rerankModel',
|
||||
);
|
||||
rerankerLlmList.value = res.data;
|
||||
} catch (error) {
|
||||
ElMessage.error($t('message.apiError'));
|
||||
@@ -156,6 +161,10 @@ const rules = ref({
|
||||
});
|
||||
|
||||
async function save() {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const valid = await saveForm.value?.validate();
|
||||
if (!valid) return;
|
||||
@@ -190,9 +199,13 @@ async function save() {
|
||||
label-width="150px"
|
||||
ref="saveForm"
|
||||
:model="entity"
|
||||
:disabled="!props.manageable"
|
||||
status-icon
|
||||
:rules="rules"
|
||||
>
|
||||
<div v-if="!props.manageable" class="config-readonly-tip">
|
||||
{{ $t('documentCollection.managePermissionHint') }}
|
||||
</div>
|
||||
<ElFormItem
|
||||
prop="icon"
|
||||
:label="$t('documentCollection.icon')"
|
||||
@@ -371,7 +384,7 @@ async function save() {
|
||||
type="primary"
|
||||
@click="save"
|
||||
:loading="btnLoading"
|
||||
:disabled="btnLoading"
|
||||
:disabled="btnLoading || !props.manageable"
|
||||
>
|
||||
{{ $t('button.save') }}
|
||||
</ElButton>
|
||||
@@ -385,4 +398,11 @@ async function save() {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.config-readonly-tip {
|
||||
margin-bottom: 16px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
|
||||
@@ -29,7 +29,7 @@ const rerankerLlmList = ref<any>([]);
|
||||
|
||||
const getEmbeddingLlmListData = async () => {
|
||||
try {
|
||||
const url = `/api/v1/model/list?modelType=embeddingModel`;
|
||||
const url = `/api/v1/documentCollection/modelList?modelType=embeddingModel`;
|
||||
const res = await api.get(url, {});
|
||||
if (res.errorCode === 0) {
|
||||
embeddingLlmList.value = res.data;
|
||||
@@ -42,7 +42,9 @@ const getEmbeddingLlmListData = async () => {
|
||||
|
||||
const getRerankerLlmListData = async () => {
|
||||
try {
|
||||
const res = await api.get('/api/v1/model/list?modelType=rerankModel');
|
||||
const res = await api.get(
|
||||
'/api/v1/documentCollection/modelList?modelType=rerankModel',
|
||||
);
|
||||
rerankerLlmList.value = res.data;
|
||||
} catch (error) {
|
||||
ElMessage.error($t('message.apiError'));
|
||||
@@ -87,8 +89,9 @@ const defaultEntity = {
|
||||
rerankEnable: false,
|
||||
},
|
||||
rerankModelId: '',
|
||||
searchEngineEnable: '',
|
||||
searchEngineEnable: false,
|
||||
englishName: '',
|
||||
visibilityScope: 'PRIVATE',
|
||||
};
|
||||
const normalizeEntity = (raw: any = {}) => {
|
||||
const options = {
|
||||
@@ -135,6 +138,20 @@ const rules = ref({
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
});
|
||||
const visibilityScopeOptions = computed(() => [
|
||||
{
|
||||
label: $t('documentCollection.visibilityScopePrivate'),
|
||||
value: 'PRIVATE',
|
||||
},
|
||||
{
|
||||
label: $t('documentCollection.visibilityScopeDept'),
|
||||
value: 'DEPT',
|
||||
},
|
||||
{
|
||||
label: $t('documentCollection.visibilityScopePublic'),
|
||||
value: 'PUBLIC',
|
||||
},
|
||||
]);
|
||||
const collectionTypeList = [
|
||||
{
|
||||
label: $t('documentCollection.collectionTypeDocument'),
|
||||
@@ -257,6 +274,19 @@ defineExpose({
|
||||
dict-code="aiDocumentCollectionCategory"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="visibilityScope"
|
||||
:label="$t('documentCollection.visibilityScope')"
|
||||
>
|
||||
<ElSelect v-model="entity.visibilityScope" class="w-full">
|
||||
<ElOption
|
||||
v-for="item in visibilityScopeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="alias" :label="$t('documentCollection.alias')">
|
||||
<ElInput v-model.trim="entity.alias" />
|
||||
</ElFormItem>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
import { downloadFileFromBlob } from '@easyflow/utils';
|
||||
|
||||
import { Delete, Download, MoreFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
@@ -25,6 +26,10 @@ const props = defineProps({
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
manageable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['viewDoc']);
|
||||
defineExpose({
|
||||
@@ -38,10 +43,20 @@ const pageDataRef = ref();
|
||||
const handleView = (row: any) => {
|
||||
emits('viewDoc', row.id);
|
||||
};
|
||||
const handleDownload = (row: any) => {
|
||||
window.open(row.documentPath, '_blank');
|
||||
const handleDownload = async (row: any) => {
|
||||
const blob = await api.download(
|
||||
`/api/v1/document/download?documentId=${row.id}`,
|
||||
);
|
||||
downloadFileFromBlob({
|
||||
fileName: row.title || 'document',
|
||||
source: blob,
|
||||
});
|
||||
};
|
||||
const handleDelete = (row: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
@@ -122,7 +137,10 @@ const handleDelete = (row: any) => {
|
||||
{{ $t('button.download') }}
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem @click="handleDelete(row)">
|
||||
<ElDropdownItem
|
||||
v-if="props.manageable"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<ElButton link :icon="Delete" type="danger">
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
|
||||
@@ -23,9 +23,9 @@ import {
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElInput,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTree,
|
||||
@@ -43,6 +43,10 @@ const props = defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
manageable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const pageDataRef = ref();
|
||||
@@ -143,6 +147,10 @@ const handleResetSearch = () => {
|
||||
};
|
||||
|
||||
const openAddDialog = () => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
editData.value = {
|
||||
collectionId: props.knowledgeId,
|
||||
categoryId:
|
||||
@@ -189,12 +197,17 @@ const exportFaqExcel = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportSuccess = () => {
|
||||
const handleImportSuccess = async () => {
|
||||
await reloadCategoryTree();
|
||||
refreshList();
|
||||
};
|
||||
|
||||
const handleMoreActionCommand = (command: string) => {
|
||||
if (command === 'import') {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
importDialogVisible.value = true;
|
||||
return;
|
||||
}
|
||||
@@ -208,6 +221,10 @@ const handleMoreActionCommand = (command: string) => {
|
||||
};
|
||||
|
||||
const openEditDialog = (row: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
editData.value = {
|
||||
id: row.id,
|
||||
collectionId: row.collectionId,
|
||||
@@ -223,6 +240,10 @@ const openEditDialog = (row: any) => {
|
||||
};
|
||||
|
||||
const saveFaq = async (payload: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
const url = payload.id ? '/api/v1/faqItem/update' : '/api/v1/faqItem/save';
|
||||
const res = await api.post(url, payload);
|
||||
if (res.errorCode === 0) {
|
||||
@@ -237,6 +258,10 @@ const saveFaq = async (payload: any) => {
|
||||
};
|
||||
|
||||
const removeFaq = (row: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
@@ -259,6 +284,10 @@ const handleCategoryClick = (data: any) => {
|
||||
};
|
||||
|
||||
const openAddRootCategory = () => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
categoryDialogTitle.value = $t('documentCollection.faq.addCategory');
|
||||
categoryDialogDisableParent.value = false;
|
||||
categoryEditData.value = {
|
||||
@@ -271,6 +300,10 @@ const openAddRootCategory = () => {
|
||||
};
|
||||
|
||||
const openAddSiblingCategory = (node: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
categoryDialogTitle.value = $t('documentCollection.faq.addSiblingCategory');
|
||||
categoryDialogDisableParent.value = false;
|
||||
categoryEditData.value = {
|
||||
@@ -286,6 +319,10 @@ const openAddSiblingCategory = (node: any) => {
|
||||
};
|
||||
|
||||
const openAddChildCategory = (node: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
if (node.isDefault) {
|
||||
ElMessage.warning(
|
||||
$t('documentCollection.faq.defaultCategoryChildForbidden'),
|
||||
@@ -308,6 +345,10 @@ const openAddChildCategory = (node: any) => {
|
||||
};
|
||||
|
||||
const openEditCategory = (node: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
categoryDialogTitle.value = $t('documentCollection.faq.editCategory');
|
||||
categoryDialogDisableParent.value = !!node.isDefault;
|
||||
categoryEditData.value = {
|
||||
@@ -324,6 +365,10 @@ const openEditCategory = (node: any) => {
|
||||
};
|
||||
|
||||
const removeCategory = (node: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
if (node.isDefault) {
|
||||
ElMessage.warning(
|
||||
$t('documentCollection.faq.defaultCategoryDeleteForbidden'),
|
||||
@@ -612,6 +657,10 @@ const demoteCategory = async (node: any) => {
|
||||
};
|
||||
|
||||
const saveCategory = async (payload: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
const url = payload.id
|
||||
? '/api/v1/faqCategory/update'
|
||||
: '/api/v1/faqCategory/save';
|
||||
@@ -692,6 +741,7 @@ onMounted(() => {
|
||||
<div class="faq-category-header">
|
||||
<span>{{ $t('documentCollection.faq.categoryTree') }}</span>
|
||||
<ElButton
|
||||
v-if="props.manageable"
|
||||
link
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@@ -719,7 +769,7 @@ onMounted(() => {
|
||||
data.categoryName
|
||||
}}</span>
|
||||
<div
|
||||
v-if="!data.isVirtual && !data.isDefault"
|
||||
v-if="props.manageable && !data.isVirtual && !data.isDefault"
|
||||
class="faq-category-node-actions"
|
||||
@click.stop
|
||||
>
|
||||
@@ -770,7 +820,10 @@ onMounted(() => {
|
||||
>
|
||||
{{ $t('documentCollection.faq.addChildCategory') }}
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem :icon="Edit" @click="openEditCategory(data)">
|
||||
<ElDropdownItem
|
||||
:icon="Edit"
|
||||
@click="openEditCategory(data)"
|
||||
>
|
||||
{{ $t('button.edit') }}
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem
|
||||
@@ -791,6 +844,9 @@ onMounted(() => {
|
||||
|
||||
<div class="faq-content-pane">
|
||||
<div class="faq-header">
|
||||
<div v-if="!props.manageable" class="faq-readonly-tip">
|
||||
{{ $t('documentCollection.managePermissionHint') }}
|
||||
</div>
|
||||
<div class="faq-toolbar">
|
||||
<div class="faq-search-actions">
|
||||
<ElInput
|
||||
@@ -809,19 +865,25 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div class="faq-primary-actions">
|
||||
<ElButton type="primary" :icon="Plus" @click="openAddDialog">
|
||||
<ElButton
|
||||
v-if="props.manageable"
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="openAddDialog"
|
||||
>
|
||||
{{ $t('button.add') }}
|
||||
</ElButton>
|
||||
<ElDropdown
|
||||
trigger="click"
|
||||
@command="handleMoreActionCommand"
|
||||
>
|
||||
<ElDropdown trigger="click" @command="handleMoreActionCommand">
|
||||
<ElButton :icon="MoreFilled">
|
||||
{{ $t('documentCollection.faq.import.moreActions') }}
|
||||
</ElButton>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem command="import" :icon="Upload">
|
||||
<ElDropdownItem
|
||||
v-if="props.manageable"
|
||||
command="import"
|
||||
:icon="Upload"
|
||||
>
|
||||
{{ $t('button.import') }}
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem
|
||||
@@ -871,6 +933,7 @@ onMounted(() => {
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<ElTableColumn
|
||||
v-if="props.manageable"
|
||||
:label="$t('common.handle')"
|
||||
width="170"
|
||||
align="right"
|
||||
@@ -1002,6 +1065,13 @@ onMounted(() => {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.faq-readonly-tip {
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.faq-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1062,8 +1132,11 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
:deep(
|
||||
.faq-category-tree > .el-tree-node > .el-tree-node__content > .el-tree-node__expand-icon
|
||||
) {
|
||||
.faq-category-tree
|
||||
> .el-tree-node
|
||||
> .el-tree-node__content
|
||||
> .el-tree-node__expand-icon
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1077,7 +1150,9 @@ onMounted(() => {
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
:deep(.faq-category-tree .el-tree-node__content:hover .faq-category-node-actions) {
|
||||
:deep(
|
||||
.faq-category-tree .el-tree-node__content:hover .faq-category-node-actions
|
||||
) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -1086,7 +1161,9 @@ onMounted(() => {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
:deep(.el-tree-node.is-current > .el-tree-node__content .faq-category-node-actions) {
|
||||
:deep(
|
||||
.el-tree-node.is-current > .el-tree-node__content .faq-category-node-actions
|
||||
) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, reactive, ref} from 'vue';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import {$t} from '@easyflow/locales';
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import {InfoFilled} from '@element-plus/icons-vue';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElForm,
|
||||
@@ -16,13 +16,17 @@ import {
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import {api} from '#/api/request';
|
||||
import { api } from '#/api/request';
|
||||
|
||||
const props = defineProps({
|
||||
documentCollectionId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
manageable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
@@ -55,6 +59,10 @@ const searchConfig = reactive({
|
||||
});
|
||||
|
||||
const submitConfig = () => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
const submitData = {
|
||||
id: props.documentCollectionId,
|
||||
options: {
|
||||
@@ -89,6 +97,9 @@ const searchEngineOptions = [
|
||||
},
|
||||
];
|
||||
const handleSearchEngineEnableChange = () => {
|
||||
if (!props.manageable) {
|
||||
return;
|
||||
}
|
||||
api.post('/api/v1/documentCollection/update', {
|
||||
id: props.documentCollectionId,
|
||||
searchEngineEnable: searchEngineEnable.value,
|
||||
@@ -100,11 +111,15 @@ const handleSearchEngineEnableChange = () => {
|
||||
<div class="search-config-sidebar">
|
||||
<div class="config-header">
|
||||
<h3>{{ $t('documentCollectionSearch.title') }}</h3>
|
||||
<div v-if="!props.manageable" class="config-readonly-tip">
|
||||
{{ $t('documentCollection.managePermissionHint') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElForm
|
||||
class="config-form"
|
||||
:model="searchConfig"
|
||||
:disabled="!props.manageable"
|
||||
label-width="100%"
|
||||
size="small"
|
||||
>
|
||||
@@ -227,7 +242,12 @@ const handleSearchEngineEnableChange = () => {
|
||||
</ElForm>
|
||||
|
||||
<div class="config-footer">
|
||||
<ElButton type="primary" @click="submitConfig" class="submit-btn">
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="submitConfig"
|
||||
class="submit-btn"
|
||||
:disabled="!props.manageable"
|
||||
>
|
||||
{{ $t('documentCollectionSearch.button.save') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
@@ -256,6 +276,13 @@ const handleSearchEngineEnableChange = () => {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.config-readonly-tip {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.config-form {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import type {FormInstance} from 'element-plus';
|
||||
import {ElForm, ElFormItem, ElInput, ElInputNumber, ElMessage, ElMessageBox,} from 'element-plus';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElPopover,
|
||||
} from 'element-plus';
|
||||
|
||||
import type {ActionButton, CardPrimaryAction,} from '#/components/page/CardList.vue';
|
||||
import CardList from '#/components/page/CardList.vue';
|
||||
|
||||
import {computed, markRaw, onMounted, ref} from 'vue';
|
||||
|
||||
import {useAccess} from '@easyflow/access';
|
||||
import {EasyFlowFormModal} from '@easyflow/common-ui';
|
||||
|
||||
import {
|
||||
Check,
|
||||
CopyDocument,
|
||||
Delete,
|
||||
Download,
|
||||
Edit,
|
||||
Lock,
|
||||
OfficeBuilding,
|
||||
Plus,
|
||||
Promotion,
|
||||
Tickets,
|
||||
Upload,
|
||||
VideoPlay,
|
||||
@@ -47,6 +61,8 @@ interface FieldDefinition {
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
type VisibilityScope = 'PRIVATE' | 'DEPT' | 'PUBLIC';
|
||||
|
||||
const primaryAction: CardPrimaryAction = {
|
||||
icon: DesignIcon,
|
||||
text: $t('button.design'),
|
||||
@@ -56,6 +72,39 @@ const primaryAction: CardPrimaryAction = {
|
||||
},
|
||||
};
|
||||
|
||||
const {hasAccessByCodes} = useAccess();
|
||||
const canManageWorkflow = computed(() =>
|
||||
hasAccessByCodes(['/api/v1/workflow/save']),
|
||||
);
|
||||
const updatingScopeId = ref<string | number | null>(null);
|
||||
const visibilityScopePopoverRefs = ref<Record<string, any>>({});
|
||||
const visibilityScopeMeta = computed(() => ({
|
||||
PRIVATE: {
|
||||
label: $t('aiWorkflow.visibilityScopePrivate'),
|
||||
description: $t('aiWorkflow.visibilityScopePrivateDesc'),
|
||||
icon: Lock,
|
||||
tone: 'private',
|
||||
},
|
||||
DEPT: {
|
||||
label: $t('aiWorkflow.visibilityScopeDept'),
|
||||
description: $t('aiWorkflow.visibilityScopeDeptDesc'),
|
||||
icon: OfficeBuilding,
|
||||
tone: 'dept',
|
||||
},
|
||||
PUBLIC: {
|
||||
label: $t('aiWorkflow.visibilityScopePublic'),
|
||||
description: $t('aiWorkflow.visibilityScopePublicDesc'),
|
||||
icon: Promotion,
|
||||
tone: 'public',
|
||||
},
|
||||
}));
|
||||
const visibilityScopeOptions = computed(() => {
|
||||
return (['PRIVATE', 'DEPT', 'PUBLIC'] as VisibilityScope[]).map((value) => ({
|
||||
value,
|
||||
...visibilityScopeMeta.value[value],
|
||||
}));
|
||||
});
|
||||
|
||||
const actions: ActionButton[] = [
|
||||
{
|
||||
icon: Edit,
|
||||
@@ -96,6 +145,7 @@ const actions: ActionButton[] = [
|
||||
{
|
||||
icon: Download,
|
||||
text: $t('button.export'),
|
||||
permission: '/api/v1/workflow/save',
|
||||
placement: 'menu',
|
||||
onClick: (row: any) => {
|
||||
exportJson(row);
|
||||
@@ -104,6 +154,7 @@ const actions: ActionButton[] = [
|
||||
{
|
||||
icon: CopyDocument,
|
||||
text: $t('button.copy'),
|
||||
permission: '/api/v1/workflow/save',
|
||||
placement: 'menu',
|
||||
onClick: (row: any) => {
|
||||
showDialog({
|
||||
@@ -151,6 +202,50 @@ const headerButtons = [
|
||||
function initDict() {
|
||||
dictStore.fetchDictionary('dataStatus');
|
||||
}
|
||||
function resolveVisibilityScopeMeta(scope?: string) {
|
||||
return visibilityScopeMeta.value[(scope || 'PRIVATE') as VisibilityScope] ||
|
||||
visibilityScopeMeta.value.PRIVATE;
|
||||
}
|
||||
function setVisibilityScopePopoverRef(id: string | number, el: any) {
|
||||
const cacheKey = String(id);
|
||||
if (el) {
|
||||
visibilityScopePopoverRefs.value[cacheKey] = el;
|
||||
return;
|
||||
}
|
||||
delete visibilityScopePopoverRefs.value[cacheKey];
|
||||
}
|
||||
function closeVisibilityScopePopover(id: string | number) {
|
||||
visibilityScopePopoverRefs.value[String(id)]?.hide?.();
|
||||
}
|
||||
async function updateVisibilityScope(
|
||||
row: any,
|
||||
visibilityScope: VisibilityScope,
|
||||
) {
|
||||
if (
|
||||
!canManageWorkflow.value ||
|
||||
!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/workflow/update', {
|
||||
id: row.id,
|
||||
visibilityScope,
|
||||
});
|
||||
if (res.errorCode === 0) {
|
||||
row.visibilityScope = visibilityScope;
|
||||
ElMessage.success(res.message);
|
||||
closeVisibilityScopePopover(row.id);
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
} finally {
|
||||
updatingScopeId.value = null;
|
||||
}
|
||||
}
|
||||
const handleSearch = (params: string) => {
|
||||
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
|
||||
};
|
||||
@@ -341,7 +436,7 @@ function handleSubmit() {
|
||||
});
|
||||
}
|
||||
const getSideList = async () => {
|
||||
const [, res] = await tryit(api.get)('/api/v1/workflowCategory/list', {
|
||||
const [, res] = await tryit(api.get)('/api/v1/workflowCategory/visibleList', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
});
|
||||
|
||||
@@ -394,7 +489,90 @@ function handleHeaderButtonClick(data: any) {
|
||||
:data="pageList"
|
||||
:primary-action="primaryAction"
|
||||
:actions="actions"
|
||||
/>
|
||||
>
|
||||
<template #corner="{ item }">
|
||||
<ElPopover
|
||||
v-if="canManageWorkflow"
|
||||
:ref="(el) => setVisibilityScopePopoverRef(item.id, el)"
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
:width="208"
|
||||
popper-class="workflow-visibility-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<button
|
||||
type="button"
|
||||
class="workflow-scope-chip"
|
||||
:class="`workflow-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
|
||||
:disabled="updatingScopeId === item.id"
|
||||
@click.stop
|
||||
>
|
||||
<ElIcon class="workflow-scope-chip__icon">
|
||||
<component
|
||||
:is="resolveVisibilityScopeMeta(item.visibilityScope).icon"
|
||||
/>
|
||||
</ElIcon>
|
||||
<span class="workflow-scope-chip__label">
|
||||
{{ resolveVisibilityScopeMeta(item.visibilityScope).label }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<div class="workflow-scope-panel" @click.stop>
|
||||
<button
|
||||
v-for="option in visibilityScopeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="workflow-scope-option"
|
||||
:class="[
|
||||
`workflow-scope-option--${option.tone}`,
|
||||
{
|
||||
'workflow-scope-option--active':
|
||||
item.visibilityScope === option.value,
|
||||
},
|
||||
]"
|
||||
:disabled="updatingScopeId === item.id"
|
||||
@click.stop="updateVisibilityScope(item, option.value)"
|
||||
>
|
||||
<span class="workflow-scope-option__leading">
|
||||
<span class="workflow-scope-option__icon-wrap">
|
||||
<ElIcon class="workflow-scope-option__icon">
|
||||
<component :is="option.icon" />
|
||||
</ElIcon>
|
||||
</span>
|
||||
<span class="workflow-scope-option__text">
|
||||
<span class="workflow-scope-option__label">
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<span class="workflow-scope-option__desc">
|
||||
{{ option.description }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<ElIcon
|
||||
v-if="item.visibilityScope === option.value"
|
||||
class="workflow-scope-option__check"
|
||||
>
|
||||
<Check />
|
||||
</ElIcon>
|
||||
</button>
|
||||
</div>
|
||||
</ElPopover>
|
||||
<div
|
||||
v-else
|
||||
class="workflow-scope-chip workflow-scope-chip--readonly"
|
||||
:class="`workflow-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
|
||||
>
|
||||
<ElIcon class="workflow-scope-chip__icon">
|
||||
<component
|
||||
:is="resolveVisibilityScopeMeta(item.visibilityScope).icon"
|
||||
/>
|
||||
</ElIcon>
|
||||
<span class="workflow-scope-chip__label">
|
||||
{{ resolveVisibilityScopeMeta(item.visibilityScope).label }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</CardList>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
@@ -440,4 +618,205 @@ function handleHeaderButtonClick(data: any) {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.workflow-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.workflow-scope-chip {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.workflow-scope-chip:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 22px -18px hsl(var(--foreground) / 0.32);
|
||||
}
|
||||
|
||||
button.workflow-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.workflow-scope-chip:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.workflow-scope-chip--readonly {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.workflow-scope-chip__icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.workflow-scope-chip__label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workflow-scope-chip--private {
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 0.09);
|
||||
border-color: hsl(var(--primary) / 0.2);
|
||||
}
|
||||
|
||||
.workflow-scope-chip--dept {
|
||||
color: hsl(var(--warning));
|
||||
background: hsl(var(--warning) / 0.12);
|
||||
border-color: hsl(var(--warning) / 0.2);
|
||||
}
|
||||
|
||||
.workflow-scope-chip--public {
|
||||
color: hsl(var(--success));
|
||||
background: hsl(var(--success) / 0.12);
|
||||
border-color: hsl(var(--success) / 0.2);
|
||||
}
|
||||
|
||||
.workflow-scope-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.workflow-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;
|
||||
}
|
||||
|
||||
.workflow-scope-option:hover {
|
||||
background: hsl(var(--foreground) / 0.04);
|
||||
}
|
||||
|
||||
.workflow-scope-option:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 4px hsl(var(--primary) / 0.12);
|
||||
}
|
||||
|
||||
.workflow-scope-option:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.workflow-scope-option__leading {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.workflow-scope-option__icon-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.workflow-scope-option__icon {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.workflow-scope-option__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.workflow-scope-option__label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--text-strong));
|
||||
}
|
||||
|
||||
.workflow-scope-option__desc {
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.workflow-scope-option__check {
|
||||
flex-shrink: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.workflow-scope-option--private .workflow-scope-option__icon-wrap {
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
}
|
||||
|
||||
.workflow-scope-option--dept .workflow-scope-option__icon-wrap {
|
||||
color: hsl(var(--warning));
|
||||
background: hsl(var(--warning) / 0.12);
|
||||
}
|
||||
|
||||
.workflow-scope-option--public .workflow-scope-option__icon-wrap {
|
||||
color: hsl(var(--success));
|
||||
background: hsl(var(--success) / 0.12);
|
||||
}
|
||||
|
||||
.workflow-scope-option--private.workflow-scope-option--active {
|
||||
background: hsl(var(--primary) / 0.08);
|
||||
}
|
||||
|
||||
.workflow-scope-option--dept.workflow-scope-option--active {
|
||||
background: hsl(var(--warning) / 0.08);
|
||||
}
|
||||
|
||||
.workflow-scope-option--public.workflow-scope-option--active {
|
||||
background: hsl(var(--success) / 0.08);
|
||||
}
|
||||
|
||||
.workflow-scope-option--private .workflow-scope-option__check {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.workflow-scope-option--dept .workflow-scope-option__check {
|
||||
color: hsl(var(--warning));
|
||||
}
|
||||
|
||||
.workflow-scope-option--public .workflow-scope-option__check {
|
||||
color: hsl(var(--success));
|
||||
}
|
||||
|
||||
:global(.workflow-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>
|
||||
|
||||
@@ -5,7 +5,15 @@ import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
|
||||
import { ElForm, ElFormItem, ElInput, ElMessage, ElUpload } from 'element-plus';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElUpload,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
@@ -28,7 +36,7 @@ const isImport = ref(false);
|
||||
const jsonFile = ref<any>(null);
|
||||
const uploadFileList = ref<any[]>([]);
|
||||
const uploadRef = ref<UploadInstance>();
|
||||
const entity = ref<any>({
|
||||
const createDefaultEntity = () => ({
|
||||
alias: '',
|
||||
deptId: '',
|
||||
title: '',
|
||||
@@ -36,8 +44,24 @@ const entity = ref<any>({
|
||||
icon: '',
|
||||
content: '',
|
||||
englishName: '',
|
||||
visibilityScope: 'PRIVATE',
|
||||
});
|
||||
const entity = ref<any>(createDefaultEntity());
|
||||
const btnLoading = ref(false);
|
||||
const visibilityScopeOptions = computed(() => [
|
||||
{
|
||||
label: $t('aiWorkflow.visibilityScopePrivate'),
|
||||
value: 'PRIVATE',
|
||||
},
|
||||
{
|
||||
label: $t('aiWorkflow.visibilityScopeDept'),
|
||||
value: 'DEPT',
|
||||
},
|
||||
{
|
||||
label: $t('aiWorkflow.visibilityScopePublic'),
|
||||
value: 'PUBLIC',
|
||||
},
|
||||
]);
|
||||
const jsonFileModel = computed({
|
||||
get: () => (uploadFileList.value.length > 0 ? uploadFileList.value[0] : null),
|
||||
set: (value: any) => {
|
||||
@@ -57,10 +81,11 @@ const rules = computed(() => ({
|
||||
// functions
|
||||
function openDialog(row: any, importMode = false) {
|
||||
isImport.value = importMode;
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
}
|
||||
entity.value = row;
|
||||
isAdd.value = !row?.id;
|
||||
entity.value = {
|
||||
...createDefaultEntity(),
|
||||
...(row || {}),
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
@@ -137,7 +162,7 @@ function closeDialog() {
|
||||
jsonFile.value = null;
|
||||
isAdd.value = true;
|
||||
isImport.value = false;
|
||||
entity.value = {};
|
||||
entity.value = createDefaultEntity();
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
</script>
|
||||
@@ -198,6 +223,19 @@ function closeDialog() {
|
||||
dict-code="aiWorkFlowCategory"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="visibilityScope"
|
||||
:label="$t('aiWorkflow.visibilityScope')"
|
||||
>
|
||||
<ElSelect v-model="entity.visibilityScope" class="w-full">
|
||||
<ElOption
|
||||
v-for="item in visibilityScopeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="alias" :label="$t('aiWorkflow.alias')">
|
||||
<ElInput v-model.trim="entity.alias" />
|
||||
</ElFormItem>
|
||||
|
||||
Reference in New Issue
Block a user