1262 lines
35 KiB
Vue
1262 lines
35 KiB
Vue
<script setup lang="ts">
|
||
import type { FormInstance } from 'element-plus';
|
||
|
||
import type {
|
||
ActionButton,
|
||
CardPrimaryAction,
|
||
} from '#/components/page/CardList.vue';
|
||
import type { OfflineImpactCheck } from '#/views/ai/shared/offline-impact';
|
||
|
||
import { computed, onMounted, ref } from 'vue';
|
||
import { useRoute, useRouter } from 'vue-router';
|
||
|
||
import { useAccess } from '@easyflow/access';
|
||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||
import { $t } from '@easyflow/locales';
|
||
import { useUserStore } from '@easyflow/stores';
|
||
|
||
import {
|
||
Check,
|
||
Delete,
|
||
Edit,
|
||
Lock,
|
||
OfficeBuilding,
|
||
Plus,
|
||
Promotion,
|
||
Search,
|
||
Share,
|
||
} 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 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 {
|
||
buildListRouteFullPath,
|
||
mergeListRouteStateQuery,
|
||
parseListRouteState,
|
||
watchListRouteState,
|
||
} from '#/composables/useListRouteState';
|
||
import { withListReturnTo } from '#/router/list-return-context';
|
||
import { copyTextWithFeedback } from '#/utils/clipboard-feedback';
|
||
import { createLazyComponentController } from '#/utils/lazy-component';
|
||
import { buildAbsoluteAppRouteUrl } from '#/utils/share-route-context';
|
||
import { documentCollectionListRouteSchema } from '#/views/ai/documentCollection/document-collection-list-route-state';
|
||
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
|
||
import { confirmPublishSubmission } from '#/views/ai/shared/approval-application-reason';
|
||
import { buildOfflineImpactMessage } from '#/views/ai/shared/offline-impact';
|
||
import {
|
||
canAiResourceDelete,
|
||
canAiResourceOffline,
|
||
canAiResourcePublish,
|
||
canAiResourceRepublish,
|
||
isAiResourceApprovalPending,
|
||
resolveAiResourceDisplayStatus,
|
||
} from '#/views/ai/shared/publish-status';
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
const initialListState = parseListRouteState(
|
||
route.query,
|
||
documentCollectionListRouteSchema,
|
||
);
|
||
const selectedCategoryId = ref(initialListState.categoryId);
|
||
const searchKeyword = ref(initialListState.keyword);
|
||
const currentPageNumber = ref(initialListState.pageNumber);
|
||
const currentPageSize = ref(initialListState.pageSize);
|
||
let initialCategoryChangePending = Boolean(initialListState.categoryId);
|
||
watchListRouteState(route, documentCollectionListRouteSchema, (state) => {
|
||
if (state.categoryId !== selectedCategoryId.value) {
|
||
initialCategoryChangePending = Boolean(state.categoryId);
|
||
}
|
||
selectedCategoryId.value = state.categoryId;
|
||
searchKeyword.value = state.keyword;
|
||
currentPageNumber.value = state.pageNumber;
|
||
currentPageSize.value = state.pageSize;
|
||
pageDataRef.value?.restoreState?.({
|
||
pageNumber: state.pageNumber,
|
||
pageSize: state.pageSize,
|
||
queryParams: {
|
||
categoryId: state.categoryId || undefined,
|
||
keyword: state.keyword || undefined,
|
||
},
|
||
});
|
||
});
|
||
const userStore = useUserStore();
|
||
const { hasAccessByCodes } = useAccess();
|
||
const collectionTypeLabelMap = {
|
||
DOCUMENT: $t('documentCollection.collectionTypeDocument'),
|
||
FAQ: $t('documentCollection.collectionTypeFaq'),
|
||
};
|
||
type VisibilityScope = 'DEPT' | 'PRIVATE' | 'PUBLIC';
|
||
|
||
interface DocumentCollectionModalInstance {
|
||
openDialog: (row: Record<string, unknown>) => void;
|
||
}
|
||
|
||
const {
|
||
component: DocumentCollectionModalComponent,
|
||
componentRef: aiKnowledgeModalRef,
|
||
ensureInstance: ensureDocumentCollectionModal,
|
||
loading: knowledgeModalLoading,
|
||
} = createLazyComponentController<DocumentCollectionModalInstance>(
|
||
() => import('#/views/ai/documentCollection/DocumentCollectionModal.vue'),
|
||
);
|
||
|
||
async function openKnowledgeModal(row: Record<string, unknown>) {
|
||
try {
|
||
const modal = await ensureDocumentCollectionModal();
|
||
modal.openDialog(row);
|
||
} catch (error) {
|
||
console.error('Failed to load knowledge modal:', error);
|
||
ElMessage.error($t('message.getDataError'));
|
||
}
|
||
}
|
||
|
||
const canManageKnowledgePermission = computed(() =>
|
||
hasAccessByCodes(['/api/v1/documentCollection/save']),
|
||
);
|
||
const sharingKnowledgeId = ref<null | number | string>(null);
|
||
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 currentListFullPath() {
|
||
return buildListRouteFullPath(
|
||
router,
|
||
route.query,
|
||
{
|
||
categoryId: selectedCategoryId.value,
|
||
keyword: searchKeyword.value,
|
||
pageNumber: currentPageNumber.value,
|
||
pageSize: currentPageSize.value,
|
||
},
|
||
documentCollectionListRouteSchema,
|
||
);
|
||
}
|
||
|
||
function openKnowledgeDetail(row: {
|
||
id: string;
|
||
name?: string;
|
||
title?: string;
|
||
}) {
|
||
router.push({
|
||
path: '/ai/documentCollection/document',
|
||
query: {
|
||
id: row.id,
|
||
pageKey: '/ai/documentCollection',
|
||
navTitle: resolveNavTitle(row),
|
||
...withListReturnTo(currentListFullPath()),
|
||
},
|
||
});
|
||
}
|
||
|
||
async function shareKnowledge(row: Record<string, any>) {
|
||
if (!row?.id || sharingKnowledgeId.value === row.id) {
|
||
return;
|
||
}
|
||
sharingKnowledgeId.value = row.id;
|
||
try {
|
||
const res = await api.post('/api/v1/knowledgeShare/url/create', {
|
||
knowledgeId: row.id,
|
||
});
|
||
const shareKey = String(res.data?.shareKey || '').trim();
|
||
if (res.errorCode !== 0 || !shareKey) {
|
||
return;
|
||
}
|
||
const shareUrl = buildAbsoluteAppRouteUrl(
|
||
router.resolve({
|
||
name: 'KnowledgeShare',
|
||
query: { shareKey },
|
||
}).href,
|
||
);
|
||
await copyTextWithFeedback(
|
||
shareUrl,
|
||
$t('message.copySuccess'),
|
||
$t('message.copyFail'),
|
||
);
|
||
} finally {
|
||
sharingKnowledgeId.value = null;
|
||
}
|
||
}
|
||
|
||
interface FieldDefinition {
|
||
// 字段名称
|
||
prop: string;
|
||
// 字段标签
|
||
label: string;
|
||
// 字段类型:input, number, select, radio, checkbox, switch, date, datetime
|
||
type?: 'input' | 'number';
|
||
// 是否必填
|
||
required?: boolean;
|
||
// 占位符
|
||
placeholder?: string;
|
||
}
|
||
const primaryAction: CardPrimaryAction = {
|
||
showIndicator: false,
|
||
text: $t('documentCollection.actions.knowledge'),
|
||
permission: '/api/v1/documentCollection/query',
|
||
onClick(row) {
|
||
openKnowledgeDetail(row);
|
||
},
|
||
};
|
||
|
||
const actions: ActionButton[] = [
|
||
{
|
||
icon: Edit,
|
||
text: $t('button.edit'),
|
||
permission: '/api/v1/documentCollection/save',
|
||
placement: 'inline',
|
||
onClick(row) {
|
||
if (!ensureManageKnowledgeItem(row)) {
|
||
return;
|
||
}
|
||
void openKnowledgeModal(row);
|
||
},
|
||
},
|
||
{
|
||
icon: Search,
|
||
text: $t('documentCollection.actions.retrieve'),
|
||
placement: 'inline',
|
||
onClick(row) {
|
||
router.push({
|
||
path: '/ai/documentCollection/document',
|
||
query: {
|
||
id: row.id,
|
||
pageKey: '/ai/documentCollection',
|
||
navTitle: resolveNavTitle(row),
|
||
activeMenu: 'knowledgeSearch',
|
||
...withListReturnTo(currentListFullPath()),
|
||
},
|
||
});
|
||
},
|
||
},
|
||
{
|
||
icon: Promotion,
|
||
text: (row) =>
|
||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus)
|
||
? $t('button.republish')
|
||
: $t('button.publish'),
|
||
permission: '/api/v1/documentCollection/save',
|
||
placement: 'inline',
|
||
visible: (row) =>
|
||
canAiResourcePublish(row.displayPublishStatus, row.publishStatus) ||
|
||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus),
|
||
onClick(row) {
|
||
if (!ensureManageKnowledgeItem(row)) {
|
||
return;
|
||
}
|
||
submitPublishAction(row);
|
||
},
|
||
},
|
||
{
|
||
icon: Share,
|
||
text: $t('button.share'),
|
||
permission: '/api/v1/documentCollection/save',
|
||
placement: 'menu',
|
||
disabled: (row) => sharingKnowledgeId.value === row.id,
|
||
loading: (row) => sharingKnowledgeId.value === row.id,
|
||
onClick(row) {
|
||
if (!ensureManageKnowledgeItem(row)) {
|
||
return;
|
||
}
|
||
shareKnowledge(row);
|
||
},
|
||
},
|
||
{
|
||
icon: Promotion,
|
||
text: $t('button.offline'),
|
||
permission: '/api/v1/documentCollection/save',
|
||
placement: 'menu',
|
||
visible: (row) =>
|
||
canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
|
||
onClick(row) {
|
||
if (!ensureManageKnowledgeItem(row)) {
|
||
return;
|
||
}
|
||
submitOfflineAction(row);
|
||
},
|
||
},
|
||
{
|
||
text: $t('button.delete'),
|
||
icon: Delete,
|
||
tone: 'danger',
|
||
permission: '/api/v1/documentCollection/remove',
|
||
placement: 'menu',
|
||
visible: (row) =>
|
||
canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
|
||
onClick(row) {
|
||
if (!ensureManageKnowledgeItem(row)) {
|
||
return;
|
||
}
|
||
submitDeleteApproval(row);
|
||
},
|
||
},
|
||
];
|
||
|
||
onMounted(() => {
|
||
getCategoryList();
|
||
});
|
||
function isRepublishAction(item: any) {
|
||
return canAiResourceRepublish(item.displayPublishStatus, item.publishStatus);
|
||
}
|
||
|
||
const submitPublishAction = async (item: any) => {
|
||
if (
|
||
isAiResourceApprovalPending(item.displayPublishStatus, item.publishStatus)
|
||
) {
|
||
ElMessage.warning($t('documentCollection.publishPendingHint'));
|
||
return;
|
||
}
|
||
const confirmation = await confirmPublishSubmission({
|
||
api,
|
||
confirmMessage: isRepublishAction(item)
|
||
? $t('documentCollection.submitRepublishApprovalConfirm')
|
||
: $t('documentCollection.submitPublishApprovalConfirm'),
|
||
id: item.id,
|
||
resourcePath: '/api/v1/documentCollection',
|
||
title: $t('message.noticeTitle'),
|
||
});
|
||
if (!confirmation) {
|
||
return;
|
||
}
|
||
const res = await api.post(
|
||
'/api/v1/documentCollection/submitPublishApproval',
|
||
{
|
||
applicationReason: confirmation.applicationReason,
|
||
id: item.id,
|
||
},
|
||
);
|
||
if (res.errorCode === 0) {
|
||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||
reloadKnowledgeList();
|
||
}
|
||
};
|
||
const submitOfflineAction = async (item: any) => {
|
||
if (
|
||
isAiResourceApprovalPending(item.displayPublishStatus, item.publishStatus)
|
||
) {
|
||
ElMessage.warning($t('documentCollection.publishPendingHint'));
|
||
return;
|
||
}
|
||
const impactRes = await api.get<{
|
||
data: OfflineImpactCheck;
|
||
errorCode: number;
|
||
}>('/api/v1/documentCollection/offlineImpactCheck', {
|
||
params: { id: item.id },
|
||
});
|
||
if (impactRes.errorCode !== 0) {
|
||
return;
|
||
}
|
||
if (impactRes.data?.hasWorkflowUsages) {
|
||
await ElMessageBox.alert(
|
||
buildOfflineImpactMessage(
|
||
$t('documentCollection.offlineImpactWorkflowBlockedIntro'),
|
||
impactRes.data.workflowUsages,
|
||
$t('documentCollection.offlineImpactWorkflowBlockedFooter'),
|
||
),
|
||
$t('message.noticeTitle'),
|
||
{
|
||
confirmButtonText: $t('button.confirm'),
|
||
type: 'warning',
|
||
},
|
||
);
|
||
return;
|
||
}
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
impactRes.data?.hasAgentBindings
|
||
? buildOfflineImpactMessage(
|
||
$t('documentCollection.offlineImpactBoundAgentsIntro'),
|
||
impactRes.data.agentBindings,
|
||
$t('documentCollection.offlineImpactBoundAgentsFooter'),
|
||
)
|
||
: $t('documentCollection.submitOfflineApprovalConfirm'),
|
||
$t('message.noticeTitle'),
|
||
{
|
||
confirmButtonText: $t('button.confirm'),
|
||
cancelButtonText: $t('button.cancel'),
|
||
type: 'warning',
|
||
},
|
||
);
|
||
} catch {
|
||
return;
|
||
}
|
||
const res = await api.post(
|
||
'/api/v1/documentCollection/submitOfflineApproval',
|
||
{
|
||
id: item.id,
|
||
},
|
||
);
|
||
if (res.errorCode === 0) {
|
||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||
reloadKnowledgeList();
|
||
}
|
||
};
|
||
const submitDeleteApproval = async (item: any) => {
|
||
if (
|
||
isAiResourceApprovalPending(item.displayPublishStatus, item.publishStatus)
|
||
) {
|
||
ElMessage.warning($t('documentCollection.deletePendingHint'));
|
||
return;
|
||
}
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
$t('documentCollection.submitDeleteApprovalConfirm'),
|
||
$t('message.noticeTitle'),
|
||
{
|
||
confirmButtonText: $t('button.confirm'),
|
||
cancelButtonText: $t('button.cancel'),
|
||
type: 'warning',
|
||
},
|
||
);
|
||
} catch {
|
||
return;
|
||
}
|
||
const res = await api.post(
|
||
'/api/v1/documentCollection/submitDeleteApproval',
|
||
{
|
||
id: item.id,
|
||
},
|
||
);
|
||
if (res.errorCode === 0) {
|
||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||
reloadKnowledgeList();
|
||
}
|
||
};
|
||
function resolvePublishStatusMeta(
|
||
displayPublishStatus?: string,
|
||
publishStatus?: string,
|
||
) {
|
||
switch (resolveAiResourceDisplayStatus(displayPublishStatus, publishStatus)) {
|
||
case 'DELETE_PENDING': {
|
||
return {
|
||
label: $t('documentCollection.publishStatusDeletePending'),
|
||
tone: 'danger',
|
||
};
|
||
}
|
||
case 'OFFLINE': {
|
||
return {
|
||
label: $t('documentCollection.publishStatusOffline'),
|
||
tone: 'draft',
|
||
};
|
||
}
|
||
case 'OFFLINE_PENDING': {
|
||
return {
|
||
label: $t('documentCollection.publishStatusOfflinePending'),
|
||
tone: 'pending',
|
||
};
|
||
}
|
||
case 'PUBLISH_PENDING': {
|
||
return {
|
||
label: $t('documentCollection.publishStatusPublishPending'),
|
||
tone: 'pending',
|
||
};
|
||
}
|
||
case 'PUBLISHED': {
|
||
return {
|
||
label: $t('documentCollection.publishStatusPublished'),
|
||
tone: 'published',
|
||
};
|
||
}
|
||
default: {
|
||
return {
|
||
label: $t('documentCollection.publishStatusDraft'),
|
||
tone: 'draft',
|
||
};
|
||
}
|
||
}
|
||
}
|
||
|
||
const pageDataRef = ref();
|
||
const headerButtons = [
|
||
{
|
||
key: 'add',
|
||
text: $t('documentCollection.actions.addKnowledge'),
|
||
icon: Plus,
|
||
type: 'primary',
|
||
data: { action: 'add' },
|
||
permission: '/api/v1/documentCollection/save',
|
||
},
|
||
];
|
||
const handleButtonClick = (event: any, _item: any) => {
|
||
switch (event.key) {
|
||
case 'add': {
|
||
void openKnowledgeModal({});
|
||
break;
|
||
}
|
||
}
|
||
};
|
||
const fieldDefinitions = ref<FieldDefinition[]>([
|
||
{
|
||
prop: 'categoryName',
|
||
label: $t('aiWorkflowCategory.categoryName'),
|
||
type: 'input',
|
||
required: true,
|
||
placeholder: $t('aiWorkflowCategory.categoryName'),
|
||
},
|
||
{
|
||
prop: 'sortNo',
|
||
label: $t('aiWorkflowCategory.sortNo'),
|
||
type: 'number',
|
||
required: false,
|
||
placeholder: $t('aiWorkflowCategory.sortNo'),
|
||
},
|
||
]);
|
||
const formRules = computed(() => {
|
||
const rules: Record<string, any[]> = {};
|
||
fieldDefinitions.value.forEach((field) => {
|
||
const fieldRules = [];
|
||
if (field.required) {
|
||
fieldRules.push({
|
||
required: true,
|
||
message: `${$t('message.required')}`,
|
||
trigger: 'blur',
|
||
});
|
||
}
|
||
if (fieldRules.length > 0) {
|
||
rules[field.prop] = fieldRules;
|
||
}
|
||
});
|
||
return rules;
|
||
});
|
||
const handleSearch = (params: any) => {
|
||
searchKeyword.value = String(params || '');
|
||
pageDataRef.value.setQuery({
|
||
categoryId: selectedCategoryId.value || undefined,
|
||
keyword: searchKeyword.value || undefined,
|
||
});
|
||
};
|
||
const reloadKnowledgeList = () => {
|
||
pageDataRef.value?.reload?.();
|
||
};
|
||
const formData = ref<any>({});
|
||
const dialogVisible = ref(false);
|
||
const formRef = ref<FormInstance>();
|
||
function showControlDialog(item: any) {
|
||
formRef.value?.resetFields();
|
||
formData.value = { ...item };
|
||
dialogVisible.value = true;
|
||
}
|
||
const categoryList = ref<any[]>([]);
|
||
const getCategoryList = async () => {
|
||
const [, res] = await tryit(api.get)(
|
||
'/api/v1/documentCollectionCategory/visibleList',
|
||
{
|
||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||
},
|
||
);
|
||
|
||
if (res && res.errorCode === 0) {
|
||
categoryList.value = [
|
||
{
|
||
id: '',
|
||
categoryName: $t('common.allCategories'),
|
||
},
|
||
...res.data,
|
||
];
|
||
if (
|
||
selectedCategoryId.value &&
|
||
!categoryList.value.some(
|
||
(item) => String(item.id) === selectedCategoryId.value,
|
||
)
|
||
) {
|
||
selectedCategoryId.value = '';
|
||
initialCategoryChangePending = false;
|
||
pageDataRef.value?.setQuery?.({
|
||
keyword: searchKeyword.value || undefined,
|
||
});
|
||
}
|
||
}
|
||
};
|
||
function removeCategory(row: any) {
|
||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||
confirmButtonText: $t('message.ok'),
|
||
cancelButtonText: $t('message.cancel'),
|
||
type: 'warning',
|
||
beforeClose: (action, instance, done) => {
|
||
if (action === 'confirm') {
|
||
instance.confirmButtonLoading = true;
|
||
api
|
||
.post('/api/v1/documentCollectionCategory/remove', { id: row.id })
|
||
.then((res) => {
|
||
instance.confirmButtonLoading = false;
|
||
if (res.errorCode === 0) {
|
||
ElMessage.success(res.message);
|
||
done();
|
||
getCategoryList();
|
||
}
|
||
})
|
||
.catch(() => {
|
||
instance.confirmButtonLoading = false;
|
||
});
|
||
} else {
|
||
done();
|
||
}
|
||
},
|
||
}).catch(() => {});
|
||
}
|
||
const controlBtns = [
|
||
{
|
||
icon: Edit,
|
||
label: $t('button.edit'),
|
||
onClick(row: any) {
|
||
showControlDialog(row);
|
||
},
|
||
},
|
||
{
|
||
type: 'danger',
|
||
icon: Delete,
|
||
label: $t('button.delete'),
|
||
onClick(row: any) {
|
||
removeCategory(row);
|
||
},
|
||
},
|
||
];
|
||
|
||
const footerButton = {
|
||
icon: Plus,
|
||
label: $t('button.add'),
|
||
onClick() {
|
||
showControlDialog({});
|
||
},
|
||
};
|
||
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) {
|
||
saveLoading.value = true;
|
||
const url = formData.value.id
|
||
? '/api/v1/documentCollectionCategory/update'
|
||
: '/api/v1/documentCollectionCategory/save';
|
||
api.post(url, formData.value).then((res) => {
|
||
saveLoading.value = false;
|
||
if (res.errorCode === 0) {
|
||
getCategoryList();
|
||
ElMessage.success(res.message);
|
||
dialogVisible.value = false;
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
function changeCategory(category: any) {
|
||
const categoryId = String(category?.id || '');
|
||
if (initialCategoryChangePending && categoryId === selectedCategoryId.value) {
|
||
initialCategoryChangePending = false;
|
||
return;
|
||
}
|
||
initialCategoryChangePending = false;
|
||
selectedCategoryId.value = categoryId;
|
||
pageDataRef.value.setQuery({
|
||
categoryId: categoryId || undefined,
|
||
keyword: searchKeyword.value || undefined,
|
||
});
|
||
}
|
||
function handlePageStateChange(state: {
|
||
pageNumber: number;
|
||
pageSize: number;
|
||
}) {
|
||
currentPageNumber.value = state.pageNumber;
|
||
currentPageSize.value = state.pageSize;
|
||
const query = mergeListRouteStateQuery(
|
||
route.query,
|
||
{
|
||
categoryId: selectedCategoryId.value,
|
||
keyword: searchKeyword.value,
|
||
pageNumber: state.pageNumber,
|
||
pageSize: state.pageSize,
|
||
},
|
||
documentCollectionListRouteSchema,
|
||
);
|
||
const target = router.resolve({
|
||
path: documentCollectionListRouteSchema.path,
|
||
query,
|
||
});
|
||
if (target.fullPath !== route.fullPath) {
|
||
void router.replace(target);
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div v-loading="knowledgeModalLoading" class="flex h-full flex-col gap-6 p-6">
|
||
<div class="knowledge-header">
|
||
<HeaderSearch
|
||
:buttons="headerButtons"
|
||
:initial-value="searchKeyword"
|
||
@search="handleSearch"
|
||
@button-click="handleButtonClick"
|
||
/>
|
||
</div>
|
||
<div class="flex max-h-[calc(100vh-191px)] flex-1 gap-6">
|
||
<PageSide
|
||
label-key="categoryName"
|
||
value-key="id"
|
||
:menus="categoryList"
|
||
:control-btns="controlBtns"
|
||
:footer-button="footerButton"
|
||
:default-selected="selectedCategoryId"
|
||
@change="changeCategory"
|
||
/>
|
||
<div class="h-full flex-1 overflow-auto">
|
||
<PageData
|
||
ref="pageDataRef"
|
||
page-url="/api/v1/documentCollection/page"
|
||
:page-size="initialListState.pageSize"
|
||
:page-sizes="[12, 24, 36, 48]"
|
||
:initial-page-number="initialListState.pageNumber"
|
||
:initial-query-params="{
|
||
categoryId: initialListState.categoryId || undefined,
|
||
keyword: initialListState.keyword || undefined,
|
||
}"
|
||
@state-change="handlePageStateChange"
|
||
>
|
||
<template #default="{ pageList }">
|
||
<CardPage
|
||
:default-icon="defaultIcon"
|
||
title-field="title"
|
||
icon-field="icon"
|
||
desc-field="description"
|
||
:data="pageList"
|
||
:primary-action="primaryAction"
|
||
:actions="actions"
|
||
density="compact"
|
||
meta-layout="compact"
|
||
tag-field="collectionType"
|
||
tag-placement="details"
|
||
:tag-map="collectionTypeLabelMap"
|
||
>
|
||
<template #details="{ item }">
|
||
<AiResourceCornerMeta>
|
||
<template #publish>
|
||
<div
|
||
class="knowledge-publish-chip"
|
||
:class="`knowledge-publish-chip--${resolvePublishStatusMeta(item.displayPublishStatus, item.publishStatus).tone}`"
|
||
>
|
||
<span class="knowledge-publish-chip__dot"></span>
|
||
<span>{{
|
||
resolvePublishStatusMeta(
|
||
item.displayPublishStatus,
|
||
item.publishStatus,
|
||
).label
|
||
}}</span>
|
||
</div>
|
||
</template>
|
||
<template #scope>
|
||
<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>
|
||
</AiResourceCornerMeta>
|
||
</template>
|
||
</CardPage>
|
||
</template>
|
||
</PageData>
|
||
</div>
|
||
</div>
|
||
<EasyFlowFormModal
|
||
v-model:open="dialogVisible"
|
||
:closable="!saveLoading"
|
||
:title="formData.id ? `${$t('button.edit')}` : `${$t('button.add')}`"
|
||
:confirm-loading="saveLoading"
|
||
:confirm-text="$t('button.confirm')"
|
||
:submitting="saveLoading"
|
||
@confirm="handleSubmit"
|
||
>
|
||
<ElForm
|
||
ref="formRef"
|
||
:model="formData"
|
||
:rules="formRules"
|
||
label-position="top"
|
||
class="easyflow-modal-form easyflow-modal-form--compact"
|
||
>
|
||
<!-- 动态生成表单项 -->
|
||
<ElFormItem
|
||
v-for="field in fieldDefinitions"
|
||
:key="field.prop"
|
||
:label="field.label"
|
||
:prop="field.prop"
|
||
>
|
||
<ElInput
|
||
v-if="!field.type || field.type === 'input'"
|
||
v-model="formData[field.prop]"
|
||
:placeholder="field.placeholder"
|
||
/>
|
||
<ElInputNumber
|
||
v-else-if="field.type === 'number'"
|
||
v-model="formData[field.prop]"
|
||
:placeholder="field.placeholder"
|
||
style="width: 100%"
|
||
/>
|
||
</ElFormItem>
|
||
</ElForm>
|
||
</EasyFlowFormModal>
|
||
|
||
<!-- 新增知识库模态框-->
|
||
<component
|
||
:is="DocumentCollectionModalComponent"
|
||
v-if="DocumentCollectionModalComponent"
|
||
ref="aiKnowledgeModalRef"
|
||
@reload="reloadKnowledgeList"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.knowledge-publish-chip {
|
||
display: inline-flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 22px;
|
||
padding: 0 8px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
line-height: 1;
|
||
border: 1px solid transparent;
|
||
border-radius: 999px;
|
||
box-shadow: inset 0 1px 0 hsl(var(--card) / 46%);
|
||
}
|
||
|
||
.knowledge-publish-chip__dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
background: currentColor;
|
||
border-radius: 999px;
|
||
opacity: 0.88;
|
||
}
|
||
|
||
.knowledge-publish-chip--draft {
|
||
color: hsl(var(--muted-foreground));
|
||
background: hsl(var(--muted) / 42%);
|
||
border-color: hsl(var(--line-subtle));
|
||
}
|
||
|
||
.knowledge-publish-chip--pending {
|
||
color: hsl(var(--warning));
|
||
background: hsl(var(--warning) / 12%);
|
||
border-color: hsl(var(--warning) / 14%);
|
||
}
|
||
|
||
.knowledge-publish-chip--published {
|
||
color: hsl(var(--success));
|
||
background: hsl(var(--success) / 12%);
|
||
border-color: hsl(var(--success) / 14%);
|
||
}
|
||
|
||
.knowledge-publish-chip--danger {
|
||
color: hsl(var(--destructive));
|
||
background: hsl(var(--destructive) / 10%);
|
||
border-color: hsl(var(--destructive) / 14%);
|
||
}
|
||
|
||
h1 {
|
||
margin-bottom: 30px;
|
||
color: #303133;
|
||
text-align: center;
|
||
}
|
||
|
||
.knowledge-scope-chip {
|
||
display: inline-flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 22px;
|
||
padding: 0 8px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
line-height: 1;
|
||
color: hsl(var(--text-strong));
|
||
background: transparent;
|
||
border: 1px solid transparent;
|
||
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 {
|
||
background: hsl(var(--card) / 76%);
|
||
box-shadow: 0 8px 16px -16px hsl(var(--foreground) / 28%);
|
||
}
|
||
|
||
button.knowledge-scope-chip:focus-visible {
|
||
outline: none;
|
||
box-shadow:
|
||
0 0 0 4px hsl(var(--primary) / 12%),
|
||
0 10px 22px -18px hsl(var(--foreground) / 32%);
|
||
}
|
||
|
||
button.knowledge-scope-chip:disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.72;
|
||
}
|
||
|
||
.knowledge-scope-chip--readonly {
|
||
cursor: default;
|
||
}
|
||
|
||
.knowledge-scope-chip__icon {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.knowledge-scope-chip__label {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.knowledge-scope-chip--private {
|
||
color: hsl(var(--primary));
|
||
background: hsl(var(--primary) / 10%);
|
||
border-color: hsl(var(--primary) / 14%);
|
||
}
|
||
|
||
.knowledge-scope-chip--dept {
|
||
color: hsl(var(--warning));
|
||
background: hsl(var(--warning) / 12%);
|
||
border-color: hsl(var(--warning) / 14%);
|
||
}
|
||
|
||
.knowledge-scope-chip--public {
|
||
color: hsl(var(--success));
|
||
background: hsl(var(--success) / 12%);
|
||
border-color: hsl(var(--success) / 14%);
|
||
}
|
||
|
||
.knowledge-scope-panel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.knowledge-scope-option {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
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) / 4%);
|
||
}
|
||
|
||
.knowledge-scope-option:focus-visible {
|
||
outline: none;
|
||
box-shadow: 0 0 0 4px hsl(var(--primary) / 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;
|
||
background: transparent;
|
||
border-radius: 9px;
|
||
}
|
||
|
||
.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) / 10%);
|
||
}
|
||
|
||
.knowledge-scope-option--dept .knowledge-scope-option__icon-wrap {
|
||
color: hsl(var(--warning));
|
||
background: hsl(var(--warning) / 12%);
|
||
}
|
||
|
||
.knowledge-scope-option--public .knowledge-scope-option__icon-wrap {
|
||
color: hsl(var(--success));
|
||
background: hsl(var(--success) / 12%);
|
||
}
|
||
|
||
.knowledge-scope-option--private.knowledge-scope-option--active {
|
||
background: hsl(var(--primary) / 8%);
|
||
}
|
||
|
||
.knowledge-scope-option--dept.knowledge-scope-option--active {
|
||
background: hsl(var(--warning) / 8%);
|
||
}
|
||
|
||
.knowledge-scope-option--public.knowledge-scope-option--active {
|
||
background: hsl(var(--success) / 8%);
|
||
}
|
||
|
||
.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-color: hsl(var(--line-subtle));
|
||
border-radius: 16px;
|
||
box-shadow: 0 18px 34px -28px hsl(var(--foreground) / 20%);
|
||
}
|
||
</style>
|