Files
EasyFlow/easyflow-ui-admin/app/src/views/ai/documentCollection/DocumentCollection.vue
陈子默 64a85c6a5b feat: 统一列表模糊搜索行为
- 统一管理端和用户中心搜索参数及多字段包含匹配

- 修复聊天搜索竞态并优化部门重名路径展示

- 补充部门展开、模型空白词和聊天查询回归测试
2026-08-13 22:29:59 +08:00

1262 lines
35 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>