3519 lines
103 KiB
Vue
3519 lines
103 KiB
Vue
<script setup lang="ts">
|
||
import { ref, onMounted, onUnmounted, watch, reactive, computed, nextTick, h, type ComponentPublicInstance } from "vue";
|
||
import { MessagePlugin, Icon as TIcon } from "tdesign-vue-next";
|
||
import DocContent from "@/components/doc-content.vue";
|
||
import useKnowledgeBase from '@/hooks/useKnowledgeBase';
|
||
import { useRoute, useRouter } from 'vue-router';
|
||
import EmptyKnowledge from '@/components/empty-knowledge.vue';
|
||
import { getSessionsList, createSessions, generateSessionsTitle } from "@/api/chat/index";
|
||
import { useMenuStore } from '@/stores/menu';
|
||
import { useUIStore } from '@/stores/ui';
|
||
import { useOrganizationStore } from '@/stores/organization';
|
||
import { useAuthStore } from '@/stores/auth';
|
||
import KnowledgeBaseEditorModal from './KnowledgeBaseEditorModal.vue';
|
||
const usemenuStore = useMenuStore();
|
||
const uiStore = useUIStore();
|
||
const orgStore = useOrganizationStore();
|
||
const authStore = useAuthStore();
|
||
const router = useRouter();
|
||
import {
|
||
batchQueryKnowledge,
|
||
getKnowledgeBaseById,
|
||
listKnowledgeTags,
|
||
updateKnowledgeTagBatch,
|
||
createKnowledgeBaseTag,
|
||
updateKnowledgeBaseTag,
|
||
deleteKnowledgeBaseTag,
|
||
uploadKnowledgeFile,
|
||
createKnowledgeFromURL,
|
||
listKnowledgeBases,
|
||
reparseKnowledge,
|
||
} from "@/api/knowledge-base/index";
|
||
import FAQEntryManager from './components/FAQEntryManager.vue';
|
||
import { listMoveTargets, moveKnowledge, getKnowledgeMoveProgress } from '@/api/knowledge-base';
|
||
import { useI18n } from 'vue-i18n';
|
||
import { formatStringDate, kbFileTypeVerification } from '@/utils';
|
||
import { getParserEngines, type ParserEngineInfo } from '@/api/system';
|
||
const route = useRoute();
|
||
const { t } = useI18n();
|
||
const kbId = computed(() => (route.params as any).kbId as string || '');
|
||
const kbInfo = ref<any>(null);
|
||
const uploadInputRef = ref<HTMLInputElement | null>(null);
|
||
const folderUploadInputRef = ref<HTMLInputElement | null>(null);
|
||
const uploading = ref(false);
|
||
const kbLoading = ref(false);
|
||
const isFAQ = computed(() => (kbInfo.value?.type || '') === 'faq');
|
||
const missingStorageEngine = computed(() => {
|
||
if (!kbInfo.value || isFAQ.value) return false
|
||
const spc = kbInfo.value.storage_provider_config
|
||
return !spc || !spc.provider
|
||
})
|
||
const parserEngines = ref<ParserEngineInfo[]>([]);
|
||
|
||
const supportedFileTypes = computed<Set<string>>(() => {
|
||
const engines = parserEngines.value
|
||
if (!engines.length) return new Set<string>()
|
||
|
||
const rules: { file_types: string[]; engine: string }[] =
|
||
kbInfo.value?.chunking_config?.parser_engine_rules || []
|
||
|
||
const ruleMap = new Map<string, string>()
|
||
for (const r of rules) {
|
||
for (const ft of r.file_types) ruleMap.set(ft, r.engine)
|
||
}
|
||
|
||
const available = new Set<string>()
|
||
const availableEngineNames = new Set(
|
||
engines.filter(e => e.Available !== false).map(e => e.Name)
|
||
)
|
||
|
||
for (const engine of engines) {
|
||
for (const ft of engine.FileTypes || []) {
|
||
if (available.has(ft)) continue
|
||
|
||
const explicitEngine = ruleMap.get(ft)
|
||
if (explicitEngine) {
|
||
if (availableEngineNames.has(explicitEngine)) available.add(ft)
|
||
} else {
|
||
if (engine.Available !== false) available.add(ft)
|
||
}
|
||
}
|
||
}
|
||
return available
|
||
})
|
||
|
||
const acceptFileTypes = computed(() =>
|
||
[...supportedFileTypes.value].map(t => '.' + t).join(',')
|
||
)
|
||
|
||
const unsupportedFileTypes = computed<string[]>(() => {
|
||
const engines = parserEngines.value
|
||
if (!engines.length) return []
|
||
|
||
const allTypes = new Set<string>()
|
||
for (const engine of engines) {
|
||
for (const ft of engine.FileTypes || []) allTypes.add(ft)
|
||
}
|
||
|
||
const supported = supportedFileTypes.value
|
||
return [...allTypes].filter(ft => !supported.has(ft)).sort()
|
||
})
|
||
|
||
const goToParserSettings = () => {
|
||
if (kbId.value) {
|
||
uiStore.openKBSettings(kbId.value, 'parser')
|
||
}
|
||
}
|
||
|
||
// Permission control: check if current user owns this KB or has edit/manage permission
|
||
const isOwner = computed(() => {
|
||
if (!kbInfo.value) return false;
|
||
// Check if the current user's tenant ID matches the KB's tenant ID
|
||
const userTenantId = authStore.effectiveTenantId;
|
||
return kbInfo.value.tenant_id === userTenantId;
|
||
});
|
||
|
||
// Can edit: owner, admin, or editor
|
||
const canEdit = computed(() => {
|
||
return orgStore.canEditKB(kbId.value, isOwner.value);
|
||
});
|
||
|
||
// Can manage (delete, settings, etc.): owner or admin
|
||
const canManage = computed(() => {
|
||
return orgStore.canManageKB(kbId.value, isOwner.value);
|
||
});
|
||
|
||
// Current KB's shared record (when accessed via organization share)
|
||
const currentSharedKb = computed(() =>
|
||
orgStore.sharedKnowledgeBases.find((s) => s.knowledge_base?.id === kbId.value) ?? null,
|
||
);
|
||
|
||
// Effective permission: from direct org share list or from GET /knowledge-bases/:id (e.g. agent-visible KB)
|
||
const effectiveKBPermission = computed(() => orgStore.getKBPermission(kbId.value) || kbInfo.value?.my_permission || '');
|
||
|
||
// Display role label: owner or org role (admin/editor/viewer)
|
||
const accessRoleLabel = computed(() => {
|
||
if (isOwner.value) return t('knowledgeBase.accessInfo.roleOwner');
|
||
const perm = effectiveKBPermission.value;
|
||
if (perm) return t(`organization.role.${perm}`);
|
||
return '--';
|
||
});
|
||
|
||
// Permission summary text for current role
|
||
const accessPermissionSummary = computed(() => {
|
||
if (isOwner.value) return t('knowledgeBase.accessInfo.permissionOwner');
|
||
const perm = effectiveKBPermission.value;
|
||
if (perm === 'admin') return t('knowledgeBase.accessInfo.permissionAdmin');
|
||
if (perm === 'editor') return t('knowledgeBase.accessInfo.permissionEditor');
|
||
if (perm === 'viewer') return t('knowledgeBase.accessInfo.permissionViewer');
|
||
return '--';
|
||
});
|
||
|
||
// Last updated time from kbInfo
|
||
const kbLastUpdated = computed(() => {
|
||
const raw = kbInfo.value?.updated_at;
|
||
if (!raw) return null;
|
||
return formatStringDate(new Date(raw));
|
||
});
|
||
|
||
const knowledgeList = ref<Array<{ id: string; name: string; type?: string }>>([]);
|
||
let { cardList, total, moreIndex, details, getKnowled, delKnowledge, openMore, onVisibleChange: _onVisibleChange, getCardDetails, getfDetails } = useKnowledgeBase(kbId.value)
|
||
const onVisibleChange = (visible: boolean) => {
|
||
_onVisibleChange(visible);
|
||
if (!visible) {
|
||
moveMenuMode.value = 'normal';
|
||
}
|
||
};
|
||
let isCardDetails = ref(false);
|
||
let timeout: ReturnType<typeof setInterval> | null = null;
|
||
let delDialog = ref(false)
|
||
let rebuildDialog = ref(false)
|
||
let rebuildKnowledgeItem = ref<KnowledgeCard>({ id: '', parse_status: '' })
|
||
let knowledge = ref<KnowledgeCard>({ id: '', parse_status: '' })
|
||
let knowledgeIndex = ref(-1)
|
||
let knowledgeScroll = ref()
|
||
let page = 1;
|
||
let pageSize = 35;
|
||
|
||
// Move state — inline in card menu
|
||
const moveMenuMode = ref<'normal' | 'targets' | 'confirm'>('normal');
|
||
const moveKnowledgeId = ref('');
|
||
const moveTargetKbs = ref<any[]>([]);
|
||
const moveTargetsLoading = ref(false);
|
||
const moveSelectedTargetId = ref('');
|
||
const moveSelectedTargetName = ref('');
|
||
const moveMode = ref<'reuse_vectors' | 'reparse'>('reuse_vectors');
|
||
const moveSubmitting = ref(false);
|
||
let movePollTimer: ReturnType<typeof setInterval> | null = null;
|
||
|
||
const selectedTagId = ref<string>('');
|
||
const tagList = ref<any[]>([]);
|
||
const tagLoading = ref(false);
|
||
const tagSearchQuery = ref('');
|
||
const TAG_PAGE_SIZE = 50;
|
||
const tagPage = ref(1);
|
||
const tagHasMore = ref(false);
|
||
const tagLoadingMore = ref(false);
|
||
const tagTotal = ref(0);
|
||
let tagSearchDebounce: ReturnType<typeof setTimeout> | null = null;
|
||
let docSearchDebounce: ReturnType<typeof setTimeout> | null = null;
|
||
const docSearchKeyword = ref('');
|
||
const selectedFileType = ref('');
|
||
const fileTypeOptions = computed(() => [
|
||
{ content: t('knowledgeBase.allFileTypes'), value: '' },
|
||
{ content: 'PDF', value: 'pdf' },
|
||
{ content: 'DOCX', value: 'docx' },
|
||
{ content: 'DOC', value: 'doc' },
|
||
{ content: 'PPTX', value: 'pptx' },
|
||
{ content: 'PPT', value: 'ppt' },
|
||
{ content: 'TXT', value: 'txt' },
|
||
{ content: 'MD', value: 'md' },
|
||
{ content: 'URL', value: 'url' },
|
||
{ content: t('knowledgeBase.typeManual'), value: 'manual' },
|
||
{ content: 'MP4', value: 'mp4' },
|
||
{ content: 'MOV', value: 'mov' },
|
||
{ content: 'AVI', value: 'avi' },
|
||
{ content: 'MKV', value: 'mkv' },
|
||
{ content: 'WEBM', value: 'webm' },
|
||
{ content: 'WMV', value: 'wmv' },
|
||
{ content: 'FLV', value: 'flv' },
|
||
{ content: 'MP3', value: 'mp3' },
|
||
{ content: 'WAV', value: 'wav' },
|
||
{ content: 'M4A', value: 'm4a' },
|
||
{ content: 'FLAC', value: 'flac' },
|
||
{ content: 'OGG', value: 'ogg' },
|
||
]);
|
||
type TagInputInstance = ComponentPublicInstance<{ focus: () => void; select: () => void }>;
|
||
const tagDropdownOptions = computed(() =>
|
||
tagList.value.map((tag: any) => ({
|
||
content: tag.name,
|
||
value: tag.id,
|
||
})),
|
||
);
|
||
const tagMap = computed<Record<string, any>>(() => {
|
||
const map: Record<string, any> = {};
|
||
tagList.value.forEach((tag) => {
|
||
map[tag.id] = tag;
|
||
});
|
||
return map;
|
||
});
|
||
const sidebarCategoryCount = computed(() => tagList.value.length);
|
||
const filteredTags = computed(() => {
|
||
const query = tagSearchQuery.value.trim().toLowerCase();
|
||
if (!query) return tagList.value;
|
||
return tagList.value.filter((tag) => (tag.name || '').toLowerCase().includes(query));
|
||
});
|
||
|
||
const editingTagInputRefs = new Map<string, TagInputInstance | null>();
|
||
const setEditingTagInputRef = (el: TagInputInstance | null, tagId: string) => {
|
||
if (el) {
|
||
editingTagInputRefs.set(tagId, el);
|
||
} else {
|
||
editingTagInputRefs.delete(tagId);
|
||
}
|
||
};
|
||
const setEditingTagInputRefByTag = (tagId: string) => (el: TagInputInstance | null) => {
|
||
setEditingTagInputRef(el, tagId);
|
||
};
|
||
const newTagInputRef = ref<TagInputInstance | null>(null);
|
||
const creatingTag = ref(false);
|
||
const creatingTagLoading = ref(false);
|
||
const newTagName = ref('');
|
||
const editingTagId = ref<string | null>(null);
|
||
const editingTagName = ref('');
|
||
const editingTagSubmitting = ref(false);
|
||
const getPageSize = () => {
|
||
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
||
const itemHeight = 148;
|
||
let itemsInView = Math.floor(viewportHeight / itemHeight) * 5;
|
||
pageSize = Math.max(35, itemsInView);
|
||
}
|
||
getPageSize()
|
||
// 直接调用 API 获取知识库文件列表
|
||
const getTagName = (tagId?: string | number) => {
|
||
if (!tagId && tagId !== 0) return '';
|
||
const key = String(tagId);
|
||
return tagMap.value[key]?.name || '';
|
||
};
|
||
|
||
const formatDocTime = (time?: string) => {
|
||
if (!time) return '--'
|
||
const formatted = formatStringDate(new Date(time))
|
||
return formatted.slice(2, 16) // "YY-MM-DD HH:mm"
|
||
}
|
||
|
||
// 格式化文件大小,用于气泡等展示
|
||
const formatFileSize = (bytes?: number | string) => {
|
||
if (bytes == null || bytes === '') return ''
|
||
const n = typeof bytes === 'string' ? parseInt(bytes, 10) : bytes
|
||
if (Number.isNaN(n) || n <= 0) return ''
|
||
if (n < 1024) return `${n} B`
|
||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
|
||
return `${(n / (1024 * 1024)).toFixed(1)} MB`
|
||
}
|
||
|
||
const channelLabelMap: Record<string, string> = {
|
||
web: 'knowledgeBase.channelWeb',
|
||
api: 'knowledgeBase.channelApi',
|
||
browser_extension: 'knowledgeBase.channelBrowserExtension',
|
||
wechat: 'knowledgeBase.channelWechat',
|
||
wecom: 'knowledgeBase.channelWecom',
|
||
feishu: 'knowledgeBase.channelFeishu',
|
||
dingtalk: 'knowledgeBase.channelDingtalk',
|
||
slack: 'knowledgeBase.channelSlack',
|
||
im: 'knowledgeBase.channelIm',
|
||
};
|
||
|
||
const getChannelLabel = (channel: string) => {
|
||
const key = channelLabelMap[channel];
|
||
return key ? t(key) : t('knowledgeBase.channelUnknown');
|
||
};
|
||
|
||
// 获取知识条目的显示类型
|
||
const getKnowledgeType = (item: any) => {
|
||
if (item.type === 'url') {
|
||
return t('knowledgeBase.typeURL') || 'URL';
|
||
}
|
||
if (item.type === 'manual') {
|
||
return t('knowledgeBase.typeManual');
|
||
}
|
||
if (item.file_type) {
|
||
return item.file_type.toUpperCase();
|
||
}
|
||
return '--';
|
||
}
|
||
|
||
const loadKnowledgeFiles = (kbIdValue: string) => {
|
||
if (!kbIdValue) return;
|
||
getKnowled(
|
||
{
|
||
page: 1,
|
||
page_size: pageSize,
|
||
tag_id: selectedTagId.value || undefined,
|
||
keyword: docSearchKeyword.value ? docSearchKeyword.value.trim() : undefined,
|
||
file_type: selectedFileType.value || undefined,
|
||
},
|
||
kbIdValue,
|
||
);
|
||
};
|
||
|
||
const loadTags = async (kbIdValue: string, reset = false) => {
|
||
if (!kbIdValue) {
|
||
tagList.value = [];
|
||
tagTotal.value = 0;
|
||
tagHasMore.value = false;
|
||
tagPage.value = 1;
|
||
return;
|
||
}
|
||
|
||
if (reset) {
|
||
tagPage.value = 1;
|
||
tagList.value = [];
|
||
tagTotal.value = 0;
|
||
tagHasMore.value = false;
|
||
}
|
||
|
||
const currentPage = tagPage.value || 1;
|
||
tagLoading.value = currentPage === 1;
|
||
tagLoadingMore.value = currentPage > 1;
|
||
|
||
try {
|
||
const res: any = await listKnowledgeTags(kbIdValue, {
|
||
page: currentPage,
|
||
page_size: TAG_PAGE_SIZE,
|
||
keyword: tagSearchQuery.value || undefined,
|
||
});
|
||
const pageData = (res?.data || {}) as {
|
||
data?: any[];
|
||
total?: number;
|
||
};
|
||
const pageTags = (pageData.data || []).map((tag: any) => ({
|
||
...tag,
|
||
id: String(tag.id),
|
||
}));
|
||
|
||
if (currentPage === 1) {
|
||
tagList.value = pageTags;
|
||
} else {
|
||
tagList.value = [...tagList.value, ...pageTags];
|
||
}
|
||
|
||
tagTotal.value = pageData.total || tagList.value.length;
|
||
tagHasMore.value = tagList.value.length < tagTotal.value;
|
||
if (tagHasMore.value) {
|
||
tagPage.value = currentPage + 1;
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load tags', error);
|
||
} finally {
|
||
tagLoading.value = false;
|
||
tagLoadingMore.value = false;
|
||
}
|
||
};
|
||
|
||
const handleTagFilterChange = (value: string) => {
|
||
selectedTagId.value = value;
|
||
// 同步更新 store 中的 selectedTagId,供 menu.vue 上传时使用
|
||
uiStore.setSelectedTagId(value);
|
||
page = 1;
|
||
loadKnowledgeFiles(kbId.value);
|
||
};
|
||
|
||
const handleTagRowClick = (tagId: string) => {
|
||
if (creatingTag.value) {
|
||
creatingTag.value = false;
|
||
newTagName.value = '';
|
||
}
|
||
if (editingTagId.value) {
|
||
editingTagId.value = null;
|
||
editingTagName.value = '';
|
||
}
|
||
if (selectedTagId.value === tagId) {
|
||
handleTagFilterChange('');
|
||
return;
|
||
}
|
||
handleTagFilterChange(tagId);
|
||
};
|
||
|
||
const startCreateTag = () => {
|
||
if (!kbId.value) {
|
||
MessagePlugin.warning(t('knowledgeEditor.messages.missingId'));
|
||
return;
|
||
}
|
||
if (creatingTag.value) {
|
||
return;
|
||
}
|
||
editingTagId.value = null;
|
||
editingTagName.value = '';
|
||
creatingTag.value = true;
|
||
nextTick(() => {
|
||
newTagInputRef.value?.focus?.();
|
||
newTagInputRef.value?.select?.();
|
||
});
|
||
};
|
||
|
||
const cancelCreateTag = () => {
|
||
creatingTag.value = false;
|
||
newTagName.value = '';
|
||
};
|
||
|
||
const submitCreateTag = async () => {
|
||
if (!kbId.value) {
|
||
MessagePlugin.warning(t('knowledgeEditor.messages.missingId'));
|
||
return;
|
||
}
|
||
const name = newTagName.value.trim();
|
||
if (!name) {
|
||
MessagePlugin.warning(t('knowledgeBase.tagNameRequired'));
|
||
return;
|
||
}
|
||
creatingTagLoading.value = true;
|
||
try {
|
||
await createKnowledgeBaseTag(kbId.value, { name });
|
||
MessagePlugin.success(t('knowledgeBase.tagCreateSuccess'));
|
||
cancelCreateTag();
|
||
await loadTags(kbId.value);
|
||
} catch (error: any) {
|
||
MessagePlugin.error(error?.message || t('common.operationFailed'));
|
||
} finally {
|
||
creatingTagLoading.value = false;
|
||
}
|
||
};
|
||
|
||
const startEditTag = (tag: any) => {
|
||
creatingTag.value = false;
|
||
newTagName.value = '';
|
||
editingTagId.value = tag.id;
|
||
editingTagName.value = tag.name;
|
||
nextTick(() => {
|
||
const inputRef = editingTagInputRefs.get(tag.id);
|
||
inputRef?.focus?.();
|
||
inputRef?.select?.();
|
||
});
|
||
};
|
||
|
||
const cancelEditTag = () => {
|
||
editingTagId.value = null;
|
||
editingTagName.value = '';
|
||
};
|
||
|
||
const submitEditTag = async () => {
|
||
if (!kbId.value || !editingTagId.value) {
|
||
return;
|
||
}
|
||
const name = editingTagName.value.trim();
|
||
if (!name) {
|
||
MessagePlugin.warning(t('knowledgeBase.tagNameRequired'));
|
||
return;
|
||
}
|
||
if (name === tagMap.value[editingTagId.value]?.name) {
|
||
cancelEditTag();
|
||
return;
|
||
}
|
||
editingTagSubmitting.value = true;
|
||
try {
|
||
await updateKnowledgeBaseTag(kbId.value, editingTagId.value, { name });
|
||
MessagePlugin.success(t('knowledgeBase.tagEditSuccess'));
|
||
cancelEditTag();
|
||
await loadTags(kbId.value);
|
||
} catch (error: any) {
|
||
MessagePlugin.error(error?.message || t('common.operationFailed'));
|
||
} finally {
|
||
editingTagSubmitting.value = false;
|
||
}
|
||
};
|
||
|
||
const confirmDeleteTag = (tag: any) => {
|
||
if (!kbId.value) {
|
||
MessagePlugin.warning(t('knowledgeEditor.messages.missingId'));
|
||
return;
|
||
}
|
||
if (creatingTag.value) {
|
||
cancelCreateTag();
|
||
}
|
||
if (editingTagId.value) {
|
||
cancelEditTag();
|
||
}
|
||
const deleteDescKey = isFAQ.value ? 'knowledgeBase.tagDeleteDesc' : 'knowledgeBase.tagDeleteDescDoc';
|
||
const confirm = window.confirm(
|
||
t(deleteDescKey, { name: tag.name }) as string,
|
||
);
|
||
if (!confirm) return;
|
||
deleteKnowledgeBaseTag(kbId.value, tag.seq_id, { force: true })
|
||
.then(() => {
|
||
MessagePlugin.success(t('knowledgeBase.tagDeleteSuccess'));
|
||
if (selectedTagId.value === tag.id) {
|
||
// Reset to show all entries when current tag is deleted
|
||
selectedTagId.value = '';
|
||
handleTagFilterChange('');
|
||
}
|
||
loadTags(kbId.value);
|
||
// 由于后端是异步删除文档,延迟刷新以确保看到最新数据
|
||
setTimeout(() => {
|
||
page = 1; // Reset page counter when reloading files after tag deletion
|
||
loadKnowledgeFiles(kbId.value);
|
||
}, 500);
|
||
})
|
||
.catch((error: any) => {
|
||
MessagePlugin.error(error?.message || t('common.operationFailed'));
|
||
});
|
||
};
|
||
|
||
const handleKnowledgeTagChange = async (knowledgeId: string, tagValue: string) => {
|
||
try {
|
||
// Pass the tag value directly (empty string means no tag)
|
||
const tagIdToUpdate = tagValue || null;
|
||
await updateKnowledgeTagBatch({ updates: { [knowledgeId]: tagIdToUpdate } });
|
||
MessagePlugin.success(t('knowledgeBase.tagUpdateSuccess'));
|
||
page = 1; // Reset page counter to 1 when reloading files after tag change
|
||
loadKnowledgeFiles(kbId.value);
|
||
loadTags(kbId.value);
|
||
} catch (error: any) {
|
||
MessagePlugin.error(error?.message || t('common.operationFailed'));
|
||
}
|
||
};
|
||
|
||
const loadKnowledgeBaseInfo = async (targetKbId: string) => {
|
||
if (!targetKbId) {
|
||
kbInfo.value = null;
|
||
return;
|
||
}
|
||
kbLoading.value = true;
|
||
try {
|
||
const res: any = await getKnowledgeBaseById(targetKbId);
|
||
kbInfo.value = res?.data || null;
|
||
selectedTagId.value = '';
|
||
// 重置store中的标签选择状态,避免上传文档时自动带上之前选择的标签
|
||
uiStore.setSelectedTagId('');
|
||
if (!isFAQ.value) {
|
||
loadKnowledgeFiles(targetKbId);
|
||
} else {
|
||
cardList.value = [];
|
||
total.value = 0;
|
||
}
|
||
loadTags(targetKbId, true);
|
||
} catch (error) {
|
||
console.error('Failed to load knowledge base info:', error);
|
||
kbInfo.value = null;
|
||
} finally {
|
||
kbLoading.value = false;
|
||
}
|
||
};
|
||
|
||
const loadKnowledgeList = async () => {
|
||
try {
|
||
const res: any = await listKnowledgeBases();
|
||
const myKbs = (res?.data || []).map((item: any) => ({
|
||
id: String(item.id),
|
||
name: item.name,
|
||
type: item.type || 'document',
|
||
}));
|
||
|
||
// Also include shared knowledge bases from orgStore
|
||
const sharedKbs = (orgStore.sharedKnowledgeBases || [])
|
||
.filter(s => s.knowledge_base != null)
|
||
.map(s => ({
|
||
id: String(s.knowledge_base.id),
|
||
name: s.knowledge_base.name,
|
||
type: s.knowledge_base.type || 'document',
|
||
}));
|
||
|
||
// Merge and deduplicate by id (my KBs take precedence)
|
||
const myKbIds = new Set(myKbs.map(kb => kb.id));
|
||
const uniqueSharedKbs = sharedKbs.filter(kb => !myKbIds.has(kb.id));
|
||
|
||
knowledgeList.value = [...myKbs, ...uniqueSharedKbs];
|
||
} catch (error) {
|
||
console.error('Failed to load knowledge list:', error);
|
||
}
|
||
};
|
||
|
||
// 监听路由参数变化,重新获取知识库内容
|
||
watch(() => kbId.value, (newKbId, oldKbId) => {
|
||
if (newKbId && newKbId !== oldKbId) {
|
||
tagSearchQuery.value = '';
|
||
tagPage.value = 1;
|
||
// 重置标签选择状态,避免在不同知识库间保持标签选择
|
||
uiStore.setSelectedTagId('');
|
||
loadKnowledgeBaseInfo(newKbId);
|
||
}
|
||
}, { immediate: false });
|
||
|
||
watch(selectedTagId, (newVal, oldVal) => {
|
||
if (oldVal === undefined) return
|
||
if (newVal !== oldVal && kbId.value) {
|
||
loadKnowledgeFiles(kbId.value);
|
||
}
|
||
});
|
||
|
||
watch(tagSearchQuery, (newVal, oldVal) => {
|
||
if (newVal === oldVal) return;
|
||
if (tagSearchDebounce) {
|
||
clearTimeout(tagSearchDebounce);
|
||
}
|
||
tagSearchDebounce = window.setTimeout(() => {
|
||
if (kbId.value) {
|
||
loadTags(kbId.value, true);
|
||
}
|
||
}, 300);
|
||
});
|
||
|
||
// 监听文档搜索关键词变化
|
||
watch(docSearchKeyword, (newVal, oldVal) => {
|
||
if (newVal === oldVal) return;
|
||
if (docSearchDebounce) {
|
||
clearTimeout(docSearchDebounce);
|
||
}
|
||
docSearchDebounce = window.setTimeout(() => {
|
||
if (kbId.value) {
|
||
page = 1;
|
||
loadKnowledgeFiles(kbId.value);
|
||
}
|
||
}, 300);
|
||
});
|
||
|
||
// 监听文件类型筛选变化
|
||
watch(selectedFileType, (newVal, oldVal) => {
|
||
if (newVal === oldVal) return;
|
||
if (kbId.value) {
|
||
page = 1;
|
||
loadKnowledgeFiles(kbId.value);
|
||
}
|
||
});
|
||
|
||
// 监听文件上传事件
|
||
const handleFileUploaded = (event: CustomEvent) => {
|
||
const uploadedKbId = event.detail.kbId;
|
||
console.log('接收到文件上传事件,上传的知识库ID:', uploadedKbId, '当前知识库ID:', kbId.value);
|
||
if (uploadedKbId && uploadedKbId === kbId.value && !isFAQ.value) {
|
||
console.log('匹配当前知识库,开始刷新文件列表');
|
||
// 如果上传的文件属于当前知识库,使用 loadKnowledgeFiles 刷新文件列表
|
||
page = 1; // Reset page counter when reloading files after upload
|
||
loadKnowledgeFiles(uploadedKbId);
|
||
loadTags(uploadedKbId);
|
||
}
|
||
};
|
||
|
||
|
||
// 监听从菜单触发的URL导入事件
|
||
const handleOpenURLImportDialog = (event: CustomEvent) => {
|
||
const eventKbId = event.detail.kbId;
|
||
console.log('接收到URL导入对话框打开事件,知识库ID:', eventKbId, '当前知识库ID:', kbId.value);
|
||
if (eventKbId && eventKbId === kbId.value && !isFAQ.value) {
|
||
urlDialogVisible.value = true;
|
||
}
|
||
};
|
||
|
||
// Auto-open document detail when navigated with ?knowledge_id=xxx
|
||
const pendingKnowledgeId = ref<string | null>(
|
||
(route.query.knowledge_id as string) || null
|
||
);
|
||
|
||
const tryAutoOpenDocument = () => {
|
||
if (!pendingKnowledgeId.value || !cardList.value?.length) return;
|
||
const targetId = pendingKnowledgeId.value;
|
||
pendingKnowledgeId.value = null;
|
||
const card = cardList.value.find((c: KnowledgeCard) => c.id === targetId);
|
||
if (card) {
|
||
nextTick(() => openCardDetails(card));
|
||
} else {
|
||
nextTick(() => {
|
||
openCardDetails({ id: targetId } as KnowledgeCard);
|
||
});
|
||
}
|
||
};
|
||
|
||
onMounted(() => {
|
||
loadKnowledgeBaseInfo(kbId.value);
|
||
loadKnowledgeList();
|
||
orgStore.fetchSharedKnowledgeBases();
|
||
|
||
getParserEngines()
|
||
.then(res => { parserEngines.value = res?.data || [] })
|
||
.catch(() => { parserEngines.value = [] })
|
||
|
||
window.addEventListener('knowledgeFileUploaded', handleFileUploaded as EventListener);
|
||
window.addEventListener('openURLImportDialog', handleOpenURLImportDialog as EventListener);
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
window.removeEventListener('knowledgeFileUploaded', handleFileUploaded as EventListener);
|
||
window.removeEventListener('openURLImportDialog', handleOpenURLImportDialog as EventListener);
|
||
stopMovePoll();
|
||
});
|
||
watch(() => cardList.value, (newValue) => {
|
||
if (isFAQ.value) return;
|
||
|
||
// Auto-open document if navigated with ?knowledge_id=xxx
|
||
if (pendingKnowledgeId.value && newValue?.length) {
|
||
tryAutoOpenDocument();
|
||
}
|
||
|
||
let analyzeList = [];
|
||
// Filter items that need polling: parsing in progress OR summary generation in progress
|
||
analyzeList = newValue.filter(item => {
|
||
const isParsing = item.parse_status == 'pending' || item.parse_status == 'processing';
|
||
const isSummaryPending = item.parse_status == 'completed' &&
|
||
(item.summary_status == 'pending' || item.summary_status == 'processing');
|
||
return isParsing || isSummaryPending;
|
||
})
|
||
if (timeout !== null) {
|
||
clearInterval(timeout);
|
||
timeout = null;
|
||
}
|
||
if (analyzeList.length) {
|
||
updateStatus(analyzeList)
|
||
}
|
||
|
||
}, { deep: true })
|
||
type KnowledgeCard = {
|
||
id: string;
|
||
knowledge_base_id?: string;
|
||
parse_status: string;
|
||
summary_status?: string;
|
||
description?: string;
|
||
file_name?: string;
|
||
original_file_name?: string;
|
||
display_name?: string;
|
||
title?: string;
|
||
type?: string;
|
||
updated_at?: string;
|
||
file_type?: string;
|
||
isMore?: boolean;
|
||
metadata?: any;
|
||
error_message?: string;
|
||
tag_id?: string;
|
||
};
|
||
const updateStatus = (analyzeList: KnowledgeCard[]) => {
|
||
let query = ``;
|
||
for (let i = 0; i < analyzeList.length; i++) {
|
||
query += `ids=${analyzeList[i].id}&`;
|
||
}
|
||
timeout = setInterval(() => {
|
||
batchQueryKnowledge(query).then((result: any) => {
|
||
if (result.success && result.data) {
|
||
(result.data as KnowledgeCard[]).forEach((item: KnowledgeCard) => {
|
||
const index = cardList.value.findIndex(card => card.id == item.id);
|
||
if (index == -1) return;
|
||
|
||
// Always update the card data
|
||
cardList.value[index].parse_status = item.parse_status;
|
||
cardList.value[index].summary_status = item.summary_status;
|
||
cardList.value[index].description = item.description;
|
||
});
|
||
}
|
||
}).catch((_err) => {
|
||
// 错误处理
|
||
});
|
||
}, 1500);
|
||
};
|
||
|
||
|
||
// 恢复文档处理状态(用于刷新后恢复)
|
||
|
||
const closeDoc = () => {
|
||
isCardDetails.value = false;
|
||
};
|
||
const openCardDetails = (item: KnowledgeCard) => {
|
||
isCardDetails.value = true;
|
||
getCardDetails(item);
|
||
};
|
||
|
||
// 悬停知识卡片时跟随鼠标显示详情气泡
|
||
const hoveredCardItem = ref<KnowledgeCard | null>(null);
|
||
const cardPopoverPos = ref({ x: 0, y: 0 });
|
||
const CARD_POPOVER_OFFSET = 16;
|
||
const cardHoverShowDelay = 300;
|
||
let cardHoverTimer: ReturnType<typeof setTimeout> | null = null;
|
||
|
||
const onCardMouseEnter = (ev: MouseEvent, item: KnowledgeCard) => {
|
||
if (cardHoverTimer) {
|
||
clearTimeout(cardHoverTimer);
|
||
cardHoverTimer = null;
|
||
}
|
||
cardHoverTimer = setTimeout(() => {
|
||
cardHoverTimer = null;
|
||
hoveredCardItem.value = item;
|
||
cardPopoverPos.value = {
|
||
x: ev.clientX + CARD_POPOVER_OFFSET,
|
||
y: ev.clientY + CARD_POPOVER_OFFSET,
|
||
};
|
||
}, cardHoverShowDelay);
|
||
};
|
||
|
||
const onCardMouseMove = (ev: MouseEvent) => {
|
||
if (hoveredCardItem.value) {
|
||
cardPopoverPos.value = {
|
||
x: ev.clientX + CARD_POPOVER_OFFSET,
|
||
y: ev.clientY + CARD_POPOVER_OFFSET,
|
||
};
|
||
}
|
||
};
|
||
|
||
const onCardMouseLeave = () => {
|
||
if (cardHoverTimer) {
|
||
clearTimeout(cardHoverTimer);
|
||
cardHoverTimer = null;
|
||
}
|
||
hoveredCardItem.value = null;
|
||
};
|
||
|
||
const delCard = (index: number, item: KnowledgeCard) => {
|
||
knowledgeIndex.value = index;
|
||
knowledge.value = item;
|
||
delDialog.value = true;
|
||
};
|
||
|
||
const handleMoveKnowledge = async (item: KnowledgeCard) => {
|
||
moveKnowledgeId.value = item.id;
|
||
moveMenuMode.value = 'targets';
|
||
moveTargetsLoading.value = true;
|
||
moveTargetKbs.value = [];
|
||
try {
|
||
const res: any = await listMoveTargets(kbId.value);
|
||
moveTargetKbs.value = res.data || [];
|
||
} catch {
|
||
moveTargetKbs.value = [];
|
||
} finally {
|
||
moveTargetsLoading.value = false;
|
||
}
|
||
};
|
||
|
||
const handleMoveSelectTarget = (kb: any) => {
|
||
moveSelectedTargetId.value = kb.id;
|
||
moveSelectedTargetName.value = kb.name;
|
||
moveMode.value = 'reuse_vectors';
|
||
moveMenuMode.value = 'confirm';
|
||
};
|
||
|
||
const handleMoveBack = () => {
|
||
if (moveMenuMode.value === 'confirm') {
|
||
moveMenuMode.value = 'targets';
|
||
} else {
|
||
moveMenuMode.value = 'normal';
|
||
}
|
||
};
|
||
|
||
const handleMoveConfirm = async () => {
|
||
if (!moveSelectedTargetId.value || moveSubmitting.value) return;
|
||
moveSubmitting.value = true;
|
||
try {
|
||
const res: any = await moveKnowledge({
|
||
knowledge_ids: [moveKnowledgeId.value],
|
||
source_kb_id: kbId.value,
|
||
target_kb_id: moveSelectedTargetId.value,
|
||
mode: moveMode.value,
|
||
});
|
||
const taskId = res.data?.task_id;
|
||
MessagePlugin.info(t('knowledgeBase.moveStarted'));
|
||
// Close the card menu
|
||
moveMenuMode.value = 'normal';
|
||
cardList.value.forEach(c => { c.isMore = false; });
|
||
|
||
if (taskId) {
|
||
startMovePoll(taskId);
|
||
} else {
|
||
moveSubmitting.value = false;
|
||
page = 1; // Reset page counter when reloading files after move
|
||
loadKnowledgeFiles(kbId.value);
|
||
}
|
||
} catch (e: any) {
|
||
MessagePlugin.error(e?.message || t('knowledgeBase.moveFailed'));
|
||
moveSubmitting.value = false;
|
||
}
|
||
};
|
||
|
||
const startMovePoll = (taskId: string) => {
|
||
if (movePollTimer) clearInterval(movePollTimer);
|
||
movePollTimer = setInterval(async () => {
|
||
try {
|
||
const res: any = await getKnowledgeMoveProgress(taskId);
|
||
const data = res.data;
|
||
if (!data) return;
|
||
if (data.status === 'completed') {
|
||
stopMovePoll();
|
||
moveSubmitting.value = false;
|
||
const failed = data.failed || 0;
|
||
if (failed > 0) {
|
||
MessagePlugin.warning(t('knowledgeBase.moveCompletedWithErrors', { success: (data.processed || 0) - failed, failed }));
|
||
} else {
|
||
MessagePlugin.success(t('knowledgeBase.moveCompleted'));
|
||
}
|
||
page = 1; // Reset page counter when reloading files after move completion
|
||
loadKnowledgeFiles(kbId.value);
|
||
} else if (data.status === 'failed') {
|
||
stopMovePoll();
|
||
moveSubmitting.value = false;
|
||
MessagePlugin.error(t('knowledgeBase.moveFailed'));
|
||
}
|
||
} catch {
|
||
// ignore poll errors
|
||
}
|
||
}, 2000);
|
||
};
|
||
|
||
const stopMovePoll = () => {
|
||
if (movePollTimer) {
|
||
clearInterval(movePollTimer);
|
||
movePollTimer = null;
|
||
}
|
||
};
|
||
|
||
const manualEditorSuccess = ({ kbId: savedKbId }: { kbId: string; knowledgeId: string; status: 'draft' | 'publish' }) => {
|
||
if (savedKbId === kbId.value && !isFAQ.value) {
|
||
page = 1; // Reset page counter when reloading files after manual edit
|
||
loadKnowledgeFiles(savedKbId);
|
||
}
|
||
};
|
||
|
||
const documentTitle = computed(() => {
|
||
if (kbInfo.value?.name) {
|
||
return `${kbInfo.value.name} · ${t('knowledgeEditor.document.title')}`;
|
||
}
|
||
return t('knowledgeEditor.document.title');
|
||
});
|
||
|
||
// 文档操作下拉菜单选项
|
||
const documentActionOptions = computed(() => [
|
||
{ content: t('upload.uploadDocument'), value: 'upload', prefixIcon: () => h(TIcon, { name: 'upload', size: '16px' }) },
|
||
{ content: t('upload.uploadFolder'), value: 'uploadFolder', prefixIcon: () => h(TIcon, { name: 'folder-add', size: '16px' }) },
|
||
{ content: t('knowledgeBase.importURL'), value: 'importURL', prefixIcon: () => h(TIcon, { name: 'link', size: '16px' }) },
|
||
{ content: t('upload.onlineEdit'), value: 'manualCreate', prefixIcon: () => h(TIcon, { name: 'edit', size: '16px' }) },
|
||
]);
|
||
|
||
// 处理文档操作下拉菜单选择
|
||
const handleDocumentActionSelect = (data: { value: string }) => {
|
||
switch (data.value) {
|
||
case 'upload':
|
||
handleDocumentUploadClick();
|
||
break;
|
||
case 'uploadFolder':
|
||
handleFolderUploadClick();
|
||
break;
|
||
case 'importURL':
|
||
handleURLImportClick();
|
||
break;
|
||
case 'manualCreate':
|
||
handleManualCreate();
|
||
break;
|
||
}
|
||
};
|
||
|
||
const ensureDocumentKbReady = () => {
|
||
if (isFAQ.value) {
|
||
MessagePlugin.warning(t('knowledgeBase.operationNotSupportedForType'));
|
||
return false;
|
||
}
|
||
if (!kbId.value) {
|
||
MessagePlugin.warning(t('knowledgeEditor.messages.missingId'));
|
||
return false;
|
||
}
|
||
if (!kbInfo.value || !kbInfo.value.embedding_model_id || !kbInfo.value.summary_model_id) {
|
||
MessagePlugin.warning(t('knowledgeBase.notInitialized'));
|
||
return false;
|
||
}
|
||
if (missingStorageEngine.value) {
|
||
MessagePlugin.warning(t('knowledgeBase.missingStorageEngineUpload'));
|
||
return false;
|
||
}
|
||
return true;
|
||
};
|
||
|
||
|
||
const handleDocumentUploadClick = () => {
|
||
if (!ensureDocumentKbReady()) return;
|
||
uploadInputRef.value?.click();
|
||
};
|
||
|
||
const handleFolderUploadClick = () => {
|
||
if (!ensureDocumentKbReady()) return;
|
||
folderUploadInputRef.value?.click();
|
||
};
|
||
|
||
const resetUploadInput = () => {
|
||
if (uploadInputRef.value) {
|
||
uploadInputRef.value.value = '';
|
||
}
|
||
};
|
||
|
||
const handleDocumentUpload = async (event: Event) => {
|
||
const input = event.target as HTMLInputElement;
|
||
const files = input?.files;
|
||
if (!files || files.length === 0) return;
|
||
|
||
if (!kbId.value) {
|
||
MessagePlugin.error(t('error.missingKbId'));
|
||
resetUploadInput();
|
||
return;
|
||
}
|
||
|
||
const dynamicTypes = supportedFileTypes.value.size > 0 ? supportedFileTypes.value : undefined
|
||
const validFiles: File[] = [];
|
||
let skippedCount = 0;
|
||
for (let i = 0; i < files.length; i++) {
|
||
const file = files[i];
|
||
if (!kbFileTypeVerification(file, files.length > 1, dynamicTypes)) {
|
||
validFiles.push(file);
|
||
} else {
|
||
skippedCount++;
|
||
}
|
||
}
|
||
|
||
if (validFiles.length === 0) {
|
||
if (skippedCount > 0) {
|
||
MessagePlugin.warning(t('knowledgeBase.allFilesSkippedNoEngine'));
|
||
}
|
||
resetUploadInput();
|
||
return;
|
||
}
|
||
if (skippedCount > 0) {
|
||
MessagePlugin.warning(t('knowledgeBase.filesSkippedNoEngine', { count: skippedCount }));
|
||
}
|
||
|
||
let successCount = 0;
|
||
let failCount = 0;
|
||
const totalCount = validFiles.length;
|
||
|
||
// 获取当前选中的分类ID(如果不是"未分类"则传递)
|
||
const tagIdToUpload = selectedTagId.value !== '__untagged__' ? selectedTagId.value : undefined;
|
||
|
||
for (const file of validFiles) {
|
||
try {
|
||
const responseData: any = await uploadKnowledgeFile(kbId.value, { file, tag_id: tagIdToUpload });
|
||
const isSuccess = responseData?.success || responseData?.code === 200 || responseData?.status === 'success' || (!responseData?.error && responseData);
|
||
if (isSuccess) {
|
||
successCount++;
|
||
} else {
|
||
failCount++;
|
||
let errorMessage = t('knowledgeBase.uploadFailed');
|
||
if (responseData?.error?.message) {
|
||
errorMessage = responseData.error.message;
|
||
} else if (responseData?.message) {
|
||
errorMessage = responseData.message;
|
||
}
|
||
if (responseData?.code === 'duplicate_file' || responseData?.error?.code === 'duplicate_file') {
|
||
errorMessage = t('knowledgeBase.fileExists');
|
||
}
|
||
if (totalCount === 1) {
|
||
MessagePlugin.error(errorMessage);
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
failCount++;
|
||
let errorMessage = error?.error?.message || error?.message || t('knowledgeBase.uploadFailed');
|
||
if (error?.code === 'duplicate_file') {
|
||
errorMessage = t('knowledgeBase.fileExists');
|
||
}
|
||
if (totalCount === 1) {
|
||
MessagePlugin.error(errorMessage);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 显示上传结果
|
||
if (successCount > 0) {
|
||
window.dispatchEvent(new CustomEvent('knowledgeFileUploaded', {
|
||
detail: { kbId: kbId.value }
|
||
}));
|
||
}
|
||
|
||
if (totalCount === 1) {
|
||
if (successCount === 1) {
|
||
MessagePlugin.success(t('knowledgeBase.uploadSuccess'));
|
||
}
|
||
} else {
|
||
if (failCount === 0) {
|
||
MessagePlugin.success(t('knowledgeBase.allUploadSuccess', { count: successCount }));
|
||
} else if (successCount > 0) {
|
||
MessagePlugin.warning(t('knowledgeBase.partialUploadSuccess', { success: successCount, fail: failCount }));
|
||
} else {
|
||
MessagePlugin.error(t('knowledgeBase.allUploadFailed', { count: failCount }));
|
||
}
|
||
}
|
||
|
||
resetUploadInput();
|
||
};
|
||
|
||
// 处理文件夹上传
|
||
const handleFolderUpload = async (event: Event) => {
|
||
const input = event.target as HTMLInputElement;
|
||
const files = input?.files;
|
||
if (!files || files.length === 0) return;
|
||
|
||
if (!kbId.value) {
|
||
MessagePlugin.error(t('error.missingKbId'));
|
||
if (input) input.value = '';
|
||
return;
|
||
}
|
||
|
||
const vlmEnabled = kbInfo.value?.vlm_config?.enabled || false;
|
||
const asrEnabled = kbInfo.value?.asr_config?.enabled || false;
|
||
const dynamicTypes = supportedFileTypes.value.size > 0 ? supportedFileTypes.value : undefined
|
||
|
||
const validFiles: File[] = [];
|
||
let hiddenFileCount = 0;
|
||
let imageFilteredCount = 0;
|
||
let videoFilteredCount = 0;
|
||
let audioFilteredCount = 0;
|
||
|
||
for (let i = 0; i < files.length; i++) {
|
||
const file = files[i];
|
||
const relativePath = (file as any).webkitRelativePath || file.name;
|
||
|
||
const pathParts = relativePath.split('/');
|
||
const hasHiddenComponent = pathParts.some((part: string) => part.startsWith('.'));
|
||
if (hasHiddenComponent) {
|
||
hiddenFileCount++;
|
||
continue;
|
||
}
|
||
|
||
const fileExt = file.name.substring(file.name.lastIndexOf('.') + 1).toLowerCase();
|
||
const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||
const videoTypes = ['mp4', 'mov', 'avi', 'mkv', 'webm', 'wmv', 'flv'];
|
||
const audioTypes = ['mp3', 'wav', 'm4a', 'flac', 'ogg'];
|
||
|
||
if (!vlmEnabled) {
|
||
if (imageTypes.includes(fileExt)) {
|
||
imageFilteredCount++;
|
||
continue;
|
||
}
|
||
if (videoTypes.includes(fileExt)) {
|
||
videoFilteredCount++;
|
||
continue;
|
||
}
|
||
}
|
||
|
||
if (!asrEnabled && audioTypes.includes(fileExt)) {
|
||
audioFilteredCount++;
|
||
continue;
|
||
}
|
||
|
||
if (!kbFileTypeVerification(file, true, dynamicTypes)) {
|
||
validFiles.push(file);
|
||
}
|
||
}
|
||
|
||
if (validFiles.length === 0) {
|
||
MessagePlugin.warning(t('knowledgeBase.noValidFilesInFolder', { total: files.length }));
|
||
if (input) input.value = '';
|
||
return;
|
||
}
|
||
MessagePlugin.info(t('knowledgeBase.uploadingFolder', { total: validFiles.length }));
|
||
|
||
// 批量上传
|
||
let successCount = 0;
|
||
let failCount = 0;
|
||
const tagIdToUpload = selectedTagId.value !== '__untagged__' ? selectedTagId.value : undefined;
|
||
|
||
for (const file of validFiles) {
|
||
const relativePath = (file as any).webkitRelativePath;
|
||
let fileName = file.name;
|
||
if (relativePath) {
|
||
const pathParts = relativePath.split('/');
|
||
if (pathParts.length > 2) {
|
||
const subPath = pathParts.slice(1, -1).join('/');
|
||
fileName = `${subPath}/${file.name}`;
|
||
}
|
||
}
|
||
|
||
try {
|
||
await uploadKnowledgeFile(kbId.value, { file, fileName, tag_id: tagIdToUpload });
|
||
successCount++;
|
||
} catch (error: any) {
|
||
failCount++;
|
||
}
|
||
}
|
||
|
||
if (successCount > 0) {
|
||
window.dispatchEvent(new CustomEvent('knowledgeFileUploaded', {
|
||
detail: { kbId: kbId.value }
|
||
}));
|
||
}
|
||
|
||
if (failCount === 0) {
|
||
MessagePlugin.success(t('knowledgeBase.uploadAllSuccess', { count: successCount }));
|
||
} else if (successCount > 0) {
|
||
MessagePlugin.warning(t('knowledgeBase.uploadPartialSuccess', { success: successCount, fail: failCount }));
|
||
} else {
|
||
MessagePlugin.error(t('knowledgeBase.uploadAllFailed'));
|
||
}
|
||
|
||
if (input) input.value = '';
|
||
};
|
||
|
||
const handleManualCreate = () => {
|
||
if (!ensureDocumentKbReady()) return;
|
||
uiStore.openManualEditor({
|
||
mode: 'create',
|
||
kbId: kbId.value,
|
||
status: 'draft',
|
||
onSuccess: manualEditorSuccess,
|
||
});
|
||
};
|
||
|
||
// URL 导入相关
|
||
const urlDialogVisible = ref(false);
|
||
const urlInputValue = ref('');
|
||
const urlImporting = ref(false);
|
||
|
||
const handleURLImportClick = () => {
|
||
if (!ensureDocumentKbReady()) return;
|
||
urlInputValue.value = '';
|
||
urlDialogVisible.value = true;
|
||
};
|
||
|
||
const handleURLImportCancel = () => {
|
||
urlDialogVisible.value = false;
|
||
urlInputValue.value = '';
|
||
};
|
||
|
||
const handleURLImportConfirm = async () => {
|
||
const url = urlInputValue.value.trim();
|
||
if (!url) {
|
||
MessagePlugin.warning(t('knowledgeBase.urlRequired'));
|
||
return;
|
||
}
|
||
|
||
// 简单的URL格式验证
|
||
try {
|
||
new URL(url);
|
||
} catch (error) {
|
||
MessagePlugin.warning(t('knowledgeBase.invalidURL'));
|
||
return;
|
||
}
|
||
|
||
if (!kbId.value) {
|
||
MessagePlugin.error(t('error.missingKbId'));
|
||
return;
|
||
}
|
||
|
||
urlImporting.value = true;
|
||
try {
|
||
// 获取当前选中的分类ID
|
||
const tagIdToUpload = selectedTagId.value !== '__untagged__' ? selectedTagId.value : undefined;
|
||
const responseData: any = await createKnowledgeFromURL(kbId.value, { url, tag_id: tagIdToUpload });
|
||
window.dispatchEvent(new CustomEvent('knowledgeFileUploaded', {
|
||
detail: { kbId: kbId.value }
|
||
}));
|
||
const isSuccess = responseData?.success || responseData?.code === 200 || responseData?.status === 'success' || (!responseData?.error && responseData);
|
||
if (isSuccess) {
|
||
MessagePlugin.success(t('knowledgeBase.urlImportSuccess'));
|
||
urlDialogVisible.value = false;
|
||
urlInputValue.value = '';
|
||
} else {
|
||
let errorMessage = t('knowledgeBase.urlImportFailed');
|
||
if (responseData?.error?.message) {
|
||
errorMessage = responseData.error.message;
|
||
} else if (responseData?.message) {
|
||
errorMessage = responseData.message;
|
||
}
|
||
if (responseData?.code === 'duplicate_url' || responseData?.error?.code === 'duplicate_url') {
|
||
errorMessage = t('knowledgeBase.urlExists');
|
||
}
|
||
MessagePlugin.error(errorMessage);
|
||
}
|
||
} catch (error: any) {
|
||
let errorMessage = error?.error?.message || error?.message || t('knowledgeBase.urlImportFailed');
|
||
if (error?.code === 'duplicate_url') {
|
||
errorMessage = t('knowledgeBase.urlExists');
|
||
}
|
||
MessagePlugin.error(errorMessage);
|
||
} finally {
|
||
urlImporting.value = false;
|
||
}
|
||
};
|
||
|
||
const handleOpenKBSettings = () => {
|
||
if (!kbId.value) {
|
||
MessagePlugin.warning(t('knowledgeEditor.messages.missingId'));
|
||
return;
|
||
}
|
||
uiStore.openKBSettings(kbId.value);
|
||
};
|
||
|
||
const handleNavigateToKbList = () => {
|
||
router.push('/platform/knowledge-bases');
|
||
};
|
||
|
||
const handleNavigateToCurrentKB = () => {
|
||
if (!kbId.value) return;
|
||
router.push(`/platform/knowledge-bases/${kbId.value}`);
|
||
};
|
||
|
||
const knowledgeDropdownOptions = computed(() =>
|
||
knowledgeList.value.map((item) => ({
|
||
content: item.name,
|
||
value: item.id,
|
||
prefixIcon: () => h(TIcon, { name: item.type === 'faq' ? 'chat-bubble-help' : 'folder', size: '16px' }),
|
||
}))
|
||
);
|
||
|
||
const handleKnowledgeDropdownSelect = (data: { value: string }) => {
|
||
if (!data?.value) return;
|
||
if (data.value === kbId.value) return;
|
||
router.push(`/platform/knowledge-bases/${data.value}`);
|
||
};
|
||
|
||
const handleManualEdit = (index: number, item: KnowledgeCard) => {
|
||
if (isFAQ.value) return;
|
||
if (cardList.value[index]) {
|
||
cardList.value[index].isMore = false;
|
||
}
|
||
uiStore.openManualEditor({
|
||
mode: 'edit',
|
||
kbId: item.knowledge_base_id || kbId.value,
|
||
knowledgeId: item.id,
|
||
onSuccess: manualEditorSuccess,
|
||
});
|
||
};
|
||
|
||
const handleKnowledgeReparse = (index: number, item: KnowledgeCard) => {
|
||
if (isFAQ.value) return;
|
||
if (!canEdit.value) return;
|
||
if (!item?.id) {
|
||
MessagePlugin.warning(t('knowledgeEditor.messages.missingId'));
|
||
return;
|
||
}
|
||
if (item.parse_status === 'pending' || item.parse_status === 'processing') {
|
||
MessagePlugin.info(t('knowledgeBase.rebuildInProgress'));
|
||
return;
|
||
}
|
||
if (cardList.value[index]) {
|
||
cardList.value[index].isMore = false;
|
||
}
|
||
rebuildKnowledgeItem.value = item;
|
||
rebuildDialog.value = true;
|
||
};
|
||
|
||
const rebuildConfirm = async () => {
|
||
rebuildDialog.value = false;
|
||
const item = rebuildKnowledgeItem.value;
|
||
if (!item?.id) return;
|
||
try {
|
||
await reparseKnowledge(item.id);
|
||
MessagePlugin.success(t('knowledgeBase.rebuildSubmitted'));
|
||
page = 1; // Reset page counter when reloading files after reparse
|
||
loadKnowledgeFiles(kbId.value);
|
||
} catch (error: any) {
|
||
MessagePlugin.error(error?.message || t('knowledgeBase.rebuildFailed'));
|
||
}
|
||
};
|
||
|
||
const handleScroll = () => {
|
||
if (isFAQ.value) return;
|
||
const element = knowledgeScroll.value;
|
||
if (element) {
|
||
let pageNum = Math.ceil(total.value / pageSize)
|
||
const { scrollTop, scrollHeight, clientHeight } = element;
|
||
if (scrollTop + clientHeight >= scrollHeight) {
|
||
page++;
|
||
if (cardList.value.length < total.value && page <= pageNum) {
|
||
getKnowled({ page, page_size: pageSize, tag_id: selectedTagId.value, keyword: docSearchKeyword.value ? docSearchKeyword.value.trim() : undefined, file_type: selectedFileType.value || undefined });
|
||
}
|
||
}
|
||
}
|
||
};
|
||
const getDoc = (page: number) => {
|
||
getfDetails(details.id, page)
|
||
};
|
||
|
||
const delCardConfirm = () => {
|
||
delDialog.value = false;
|
||
delKnowledge(knowledgeIndex.value, knowledge.value, () => {
|
||
// 删除成功后刷新文档列表和分类数量
|
||
page = 1; // Reset page counter when reloading files after deletion
|
||
loadKnowledgeFiles(kbId.value);
|
||
loadTags(kbId.value);
|
||
});
|
||
};
|
||
|
||
// 处理知识库编辑成功后的回调
|
||
const handleKBEditorSuccess = (kbIdValue: string) => {
|
||
if (kbIdValue === kbId.value) {
|
||
loadKnowledgeBaseInfo(kbIdValue);
|
||
}
|
||
};
|
||
|
||
const getTitle = (session_id: string, value: string) => {
|
||
const now = new Date().toISOString();
|
||
let obj = {
|
||
title: t('knowledgeBase.newSession'),
|
||
path: `chat/${session_id}`,
|
||
id: session_id,
|
||
isMore: false,
|
||
isNoTitle: true,
|
||
created_at: now,
|
||
updated_at: now
|
||
};
|
||
usemenuStore.updataMenuChildren(obj);
|
||
usemenuStore.changeIsFirstSession(true);
|
||
usemenuStore.changeFirstQuery(value);
|
||
router.push(`/platform/chat/${session_id}`);
|
||
};
|
||
|
||
async function createNewSession(value: string): Promise<void> {
|
||
// Session 不再和知识库绑定,直接创建 Session
|
||
createSessions({}).then(res => {
|
||
if (res.data && res.data.id) {
|
||
getTitle(res.data.id, value);
|
||
} else {
|
||
// 错误处理
|
||
console.error(t('knowledgeBase.createSessionFailed'));
|
||
}
|
||
}).catch(error => {
|
||
console.error(t('knowledgeBase.createSessionError'), error);
|
||
});
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<template v-if="!isFAQ">
|
||
<div class="knowledge-layout">
|
||
<div class="document-header">
|
||
<div class="document-header-title">
|
||
<div class="document-title-row">
|
||
<h2 class="document-breadcrumb">
|
||
<button type="button" class="breadcrumb-link" @click="handleNavigateToKbList">
|
||
{{ $t('menu.knowledgeBase') }}
|
||
</button>
|
||
<t-icon name="chevron-right" class="breadcrumb-separator" />
|
||
<t-dropdown
|
||
v-if="knowledgeDropdownOptions.length"
|
||
:options="knowledgeDropdownOptions"
|
||
trigger="click"
|
||
placement="bottom-left"
|
||
@click="handleKnowledgeDropdownSelect"
|
||
>
|
||
<button
|
||
type="button"
|
||
class="breadcrumb-link dropdown"
|
||
:disabled="!kbId"
|
||
@click.stop="handleNavigateToCurrentKB"
|
||
>
|
||
<span>{{ kbInfo?.name || '--' }}</span>
|
||
<t-icon name="chevron-down" />
|
||
</button>
|
||
</t-dropdown>
|
||
<button
|
||
v-else
|
||
type="button"
|
||
class="breadcrumb-link"
|
||
:disabled="!kbId"
|
||
@click="handleNavigateToCurrentKB"
|
||
>
|
||
{{ kbInfo?.name || '--' }}
|
||
</button>
|
||
<t-icon name="chevron-right" class="breadcrumb-separator" />
|
||
<span class="breadcrumb-current">{{ $t('knowledgeEditor.document.title') }}</span>
|
||
</h2>
|
||
<!-- 身份与最后更新:紧凑单行,置于标题行右侧,悬停显示权限说明 -->
|
||
<div v-if="kbInfo" class="kb-access-meta">
|
||
<t-tooltip :content="accessPermissionSummary" placement="top">
|
||
<span class="kb-access-meta-inner">
|
||
<t-tag size="small" :theme="isOwner ? 'success' : (effectiveKBPermission === 'admin' ? 'primary' : effectiveKBPermission === 'editor' ? 'warning' : 'default')" class="kb-access-role-tag">
|
||
{{ accessRoleLabel }}
|
||
</t-tag>
|
||
<template v-if="currentSharedKb">
|
||
<span class="kb-access-meta-sep">·</span>
|
||
<span class="kb-access-meta-text">
|
||
{{ $t('knowledgeBase.accessInfo.fromOrg') }}「{{ currentSharedKb.org_name }}」
|
||
{{ $t('knowledgeBase.accessInfo.sharedAt') }} {{ formatStringDate(new Date(currentSharedKb.shared_at)) }}
|
||
</span>
|
||
</template>
|
||
<template v-else-if="effectiveKBPermission">
|
||
<span class="kb-access-meta-sep">·</span>
|
||
<span class="kb-access-meta-text">{{ $t('knowledgeList.detail.sourceTypeAgent') }}</span>
|
||
</template>
|
||
<template v-else-if="kbLastUpdated">
|
||
<span class="kb-access-meta-sep">·</span>
|
||
<span class="kb-access-meta-text">{{ $t('knowledgeBase.accessInfo.lastUpdated') }} {{ kbLastUpdated }}</span>
|
||
</template>
|
||
</span>
|
||
</t-tooltip>
|
||
</div>
|
||
<t-tooltip v-if="canManage" :content="$t('knowledgeBase.settings')" placement="top">
|
||
<button
|
||
type="button"
|
||
class="kb-settings-button"
|
||
:disabled="!kbId"
|
||
@click="handleOpenKBSettings"
|
||
>
|
||
<t-icon name="setting" size="16px" />
|
||
</button>
|
||
</t-tooltip>
|
||
</div>
|
||
<p class="document-subtitle">{{ $t('knowledgeEditor.document.subtitle') }}</p>
|
||
<p v-if="unsupportedFileTypes.length" class="parser-hint" @click="goToParserSettings">
|
||
<t-icon name="info-circle" class="parser-hint-icon" />
|
||
<span>{{ $t('knowledgeBase.unsupportedTypesHint', { types: unsupportedFileTypes.map(t => '.' + t).join('、') }) }}</span>
|
||
<span class="parser-hint-link">{{ $t('knowledgeBase.goToParserSettings') }} →</span>
|
||
</p>
|
||
<p v-if="missingStorageEngine" class="storage-engine-warning" @click="handleOpenKBSettings">
|
||
<t-icon name="info-circle" class="warning-icon" />
|
||
<span>{{ $t('knowledgeBase.missingStorageEngine') }}</span>
|
||
<span class="warning-link">{{ $t('knowledgeBase.goToStorageSettings') }} →</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<input
|
||
ref="uploadInputRef"
|
||
type="file"
|
||
class="document-upload-input"
|
||
:accept="acceptFileTypes || '.pdf,.docx,.doc,.txt,.md,.json,.jpg,.jpeg,.png,.csv,.xlsx,.xls,.pptx,.ppt,.mp3,.wav,.m4a,.flac,.ogg,.mp4,.mov,.avi,.mkv,.webm,.wmv,.flv'"
|
||
multiple
|
||
@change="handleDocumentUpload"
|
||
/>
|
||
<input
|
||
ref="folderUploadInputRef"
|
||
type="file"
|
||
class="document-upload-input"
|
||
webkitdirectory
|
||
@change="handleFolderUpload"
|
||
/>
|
||
<div class="knowledge-main">
|
||
<aside class="tag-sidebar">
|
||
<div class="sidebar-header">
|
||
<div class="sidebar-title">
|
||
<span>{{ $t('knowledgeBase.documentCategoryTitle') }}</span>
|
||
<span class="sidebar-count">({{ sidebarCategoryCount }})</span>
|
||
</div>
|
||
<div v-if="canEdit" class="sidebar-actions">
|
||
<t-button
|
||
size="small"
|
||
variant="text"
|
||
class="create-tag-btn"
|
||
:aria-label="$t('knowledgeBase.tagCreateAction')"
|
||
:title="$t('knowledgeBase.tagCreateAction')"
|
||
@click="startCreateTag"
|
||
>
|
||
<span class="create-tag-plus" aria-hidden="true">+</span>
|
||
</t-button>
|
||
</div>
|
||
</div>
|
||
<div class="tag-search-bar">
|
||
<t-input
|
||
v-model.trim="tagSearchQuery"
|
||
size="small"
|
||
:placeholder="$t('knowledgeBase.tagSearchPlaceholder')"
|
||
clearable
|
||
>
|
||
<template #prefix-icon>
|
||
<t-icon name="search" size="14px" />
|
||
</template>
|
||
</t-input>
|
||
</div>
|
||
<t-loading :loading="tagLoading" size="small">
|
||
<div class="tag-list">
|
||
<div v-if="creatingTag" class="tag-list-item tag-editing" @click.stop>
|
||
<div class="tag-list-left">
|
||
<t-icon name="folder" size="18px" />
|
||
<div class="tag-edit-input">
|
||
<t-input
|
||
ref="newTagInputRef"
|
||
v-model="newTagName"
|
||
size="small"
|
||
:maxlength="40"
|
||
:placeholder="$t('knowledgeBase.tagNamePlaceholder')"
|
||
@keydown.enter.stop.prevent="submitCreateTag"
|
||
@keydown.esc.stop.prevent="cancelCreateTag"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="tag-inline-actions">
|
||
<t-button
|
||
variant="text"
|
||
theme="default"
|
||
size="small"
|
||
class="tag-action-btn confirm"
|
||
:loading="creatingTagLoading"
|
||
@click.stop="submitCreateTag"
|
||
>
|
||
<t-icon name="check" size="16px" />
|
||
</t-button>
|
||
<t-button
|
||
variant="text"
|
||
theme="default"
|
||
size="small"
|
||
class="tag-action-btn cancel"
|
||
@click.stop="cancelCreateTag"
|
||
>
|
||
<t-icon name="close" size="16px" />
|
||
</t-button>
|
||
</div>
|
||
</div>
|
||
|
||
<template v-if="filteredTags.length">
|
||
<div
|
||
v-for="tag in filteredTags"
|
||
:key="tag.id"
|
||
class="tag-list-item"
|
||
:class="{ active: selectedTagId === tag.id, editing: editingTagId === tag.id }"
|
||
@click="handleTagRowClick(tag.id)"
|
||
>
|
||
<div class="tag-list-left">
|
||
<t-icon name="folder" size="18px" />
|
||
<template v-if="editingTagId === tag.id">
|
||
<div class="tag-edit-input" @click.stop>
|
||
<t-input
|
||
:ref="setEditingTagInputRefByTag(tag.id)"
|
||
v-model="editingTagName"
|
||
size="small"
|
||
:maxlength="40"
|
||
@keydown.enter.stop.prevent="submitEditTag"
|
||
@keydown.esc.stop.prevent="cancelEditTag"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<span class="tag-name" :title="tag.name">{{ tag.name }}</span>
|
||
</template>
|
||
</div>
|
||
<div class="tag-list-right">
|
||
<span class="tag-count">{{ tag.knowledge_count || 0 }}</span>
|
||
<template v-if="editingTagId === tag.id">
|
||
<div class="tag-inline-actions" @click.stop>
|
||
<t-button
|
||
variant="text"
|
||
theme="default"
|
||
size="small"
|
||
class="tag-action-btn confirm"
|
||
:loading="editingTagSubmitting"
|
||
@click.stop="submitEditTag"
|
||
>
|
||
<t-icon name="check" size="16px" />
|
||
</t-button>
|
||
<t-button
|
||
variant="text"
|
||
theme="default"
|
||
size="small"
|
||
class="tag-action-btn cancel"
|
||
@click.stop="cancelEditTag"
|
||
>
|
||
<t-icon name="close" size="16px" />
|
||
</t-button>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div v-if="canEdit" class="tag-more" @click.stop>
|
||
<t-popup trigger="click" placement="top-right" overlayClassName="tag-more-popup">
|
||
<div class="tag-more-btn">
|
||
<t-icon name="more" size="14px" />
|
||
</div>
|
||
<template #content>
|
||
<div class="tag-menu">
|
||
<div class="tag-menu-item" @click="startEditTag(tag)">
|
||
<t-icon class="menu-icon" name="edit" />
|
||
<span>{{ $t('knowledgeBase.tagEditAction') }}</span>
|
||
</div>
|
||
<div class="tag-menu-item danger" @click="confirmDeleteTag(tag)">
|
||
<t-icon class="menu-icon" name="delete" />
|
||
<span>{{ $t('knowledgeBase.tagDeleteAction') }}</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</t-popup>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<div v-else class="tag-empty-state">
|
||
{{ $t('knowledgeBase.tagEmptyResult') }}
|
||
</div>
|
||
<div v-if="tagHasMore" class="tag-load-more">
|
||
<t-button
|
||
variant="text"
|
||
size="small"
|
||
:loading="tagLoadingMore"
|
||
@click.stop="kbId && loadTags(kbId)"
|
||
>
|
||
{{ $t('tenant.loadMore') }}
|
||
</t-button>
|
||
</div>
|
||
</div>
|
||
</t-loading>
|
||
</aside>
|
||
<div class="tag-content">
|
||
<div class="doc-card-area">
|
||
<!-- 搜索栏、筛选与添加文档 -->
|
||
<div class="doc-filter-bar">
|
||
<t-input
|
||
v-model.trim="docSearchKeyword"
|
||
:placeholder="$t('knowledgeBase.docSearchPlaceholder')"
|
||
clearable
|
||
class="doc-search-input"
|
||
@clear="loadKnowledgeFiles(kbId)"
|
||
@keydown.enter="loadKnowledgeFiles(kbId)"
|
||
>
|
||
<template #prefix-icon>
|
||
<t-icon name="search" size="16px" />
|
||
</template>
|
||
</t-input>
|
||
<t-select
|
||
v-model="selectedFileType"
|
||
:options="fileTypeOptions"
|
||
:placeholder="$t('knowledgeBase.fileTypeFilter')"
|
||
class="doc-type-select"
|
||
clearable
|
||
/>
|
||
<div v-if="canEdit" class="doc-filter-actions">
|
||
<t-tooltip :content="$t('knowledgeBase.addDocument')" placement="top">
|
||
<t-dropdown
|
||
:options="documentActionOptions"
|
||
trigger="click"
|
||
placement="bottom-right"
|
||
@click="handleDocumentActionSelect"
|
||
>
|
||
<t-button variant="text" theme="default" class="content-bar-icon-btn" size="small">
|
||
<template #icon><t-icon name="file-add" size="16px" /></template>
|
||
</t-button>
|
||
</t-dropdown>
|
||
</t-tooltip>
|
||
</div>
|
||
</div>
|
||
<div
|
||
class="doc-scroll-container"
|
||
:class="{ 'is-empty': !cardList.length }"
|
||
ref="knowledgeScroll"
|
||
@scroll="handleScroll"
|
||
>
|
||
<template v-if="cardList.length">
|
||
<div class="doc-card-list">
|
||
<!-- 现有文档卡片 -->
|
||
<div
|
||
class="knowledge-card"
|
||
v-for="(item, index) in cardList"
|
||
:key="index"
|
||
@click="openCardDetails(item)"
|
||
@mouseenter="onCardMouseEnter($event, item)"
|
||
@mousemove="onCardMouseMove($event)"
|
||
@mouseleave="onCardMouseLeave"
|
||
>
|
||
<div class="card-content">
|
||
<div class="card-content-nav">
|
||
<span class="card-content-title" :title="item.file_name">{{ item.file_name }}</span>
|
||
<t-popup
|
||
v-if="canEdit"
|
||
v-model="item.isMore"
|
||
overlayClassName="card-more"
|
||
:on-visible-change="onVisibleChange"
|
||
trigger="click"
|
||
destroy-on-close
|
||
placement="bottom-right"
|
||
>
|
||
<div
|
||
variant="outline"
|
||
class="more-wrap"
|
||
@click.stop="openMore(index)"
|
||
:class="[moreIndex == index ? 'active-more' : '']"
|
||
>
|
||
<img class="more-icon" src="@/assets/img/more.png" alt="" />
|
||
</div>
|
||
<template #content>
|
||
<!-- Normal menu -->
|
||
<div v-if="moveMenuMode === 'normal'" class="card-menu">
|
||
<div
|
||
v-if="item.type === 'manual'"
|
||
class="card-menu-item"
|
||
@click.stop="handleManualEdit(index, item)"
|
||
>
|
||
<t-icon class="icon" name="edit" />
|
||
<span>{{ t('knowledgeBase.editDocument') }}</span>
|
||
</div>
|
||
<div class="card-menu-item" @click.stop="handleKnowledgeReparse(index, item)">
|
||
<t-icon class="icon" name="refresh" />
|
||
<span>{{ t('knowledgeBase.rebuildDocument') }}</span>
|
||
</div>
|
||
<div class="card-menu-item" @click.stop="handleMoveKnowledge(item)">
|
||
<t-icon class="icon" name="swap" />
|
||
<span>{{ t('knowledgeBase.moveDocument') }}</span>
|
||
</div>
|
||
<div class="card-menu-item danger" @click.stop="delCard(index, item)">
|
||
<t-icon class="icon" name="delete" />
|
||
<span>{{ t('knowledgeBase.deleteDocument') }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Move: target KB list -->
|
||
<div v-else-if="moveMenuMode === 'targets'" class="card-menu move-menu">
|
||
<div class="move-menu-header" @click.stop="handleMoveBack">
|
||
<t-icon name="chevron-left" size="16px" />
|
||
<span>{{ t('knowledgeBase.moveToKnowledgeBase') }}</span>
|
||
</div>
|
||
<div v-if="moveTargetsLoading" class="move-menu-loading">
|
||
<t-loading size="small" />
|
||
</div>
|
||
<div v-else-if="moveTargetKbs.length === 0" class="move-menu-empty">
|
||
{{ t('knowledgeBase.moveNoTargets') }}
|
||
</div>
|
||
<template v-else>
|
||
<div
|
||
v-for="kb in moveTargetKbs"
|
||
:key="kb.id"
|
||
class="card-menu-item"
|
||
@click.stop="handleMoveSelectTarget(kb)"
|
||
>
|
||
<t-icon class="icon" name="root-list" />
|
||
<span class="move-target-name">{{ kb.name }}</span>
|
||
<span v-if="kb.knowledge_count !== undefined" class="move-target-count">{{ kb.knowledge_count }}</span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- Move: confirm with mode selection -->
|
||
<div v-else-if="moveMenuMode === 'confirm'" class="card-menu move-menu">
|
||
<div class="move-menu-header" @click.stop="handleMoveBack">
|
||
<t-icon name="chevron-left" size="16px" />
|
||
<span>{{ t('knowledgeBase.moveConfirmTitle') }}</span>
|
||
</div>
|
||
<div class="move-confirm-body">
|
||
<div class="move-target-info">
|
||
<t-icon name="arrow-right" size="14px" />
|
||
<span>{{ moveSelectedTargetName }}</span>
|
||
</div>
|
||
<div
|
||
class="move-mode-item"
|
||
:class="{ active: moveMode === 'reuse_vectors' }"
|
||
@click.stop="moveMode = 'reuse_vectors'"
|
||
>
|
||
<t-radio :checked="moveMode === 'reuse_vectors'" />
|
||
<div class="move-mode-text">
|
||
<span class="move-mode-label">{{ t('knowledgeBase.moveModeReuseVectors') }}</span>
|
||
<span class="move-mode-desc">{{ t('knowledgeBase.moveModeReuseVectorsDesc') }}</span>
|
||
</div>
|
||
</div>
|
||
<div
|
||
class="move-mode-item"
|
||
:class="{ active: moveMode === 'reparse' }"
|
||
@click.stop="moveMode = 'reparse'"
|
||
>
|
||
<t-radio :checked="moveMode === 'reparse'" />
|
||
<div class="move-mode-text">
|
||
<span class="move-mode-label">{{ t('knowledgeBase.moveModeReparse') }}</span>
|
||
<span class="move-mode-desc">{{ t('knowledgeBase.moveModeReparseDesc') }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="move-confirm-actions">
|
||
<t-button size="small" variant="outline" @click.stop="handleMoveBack">{{ t('common.cancel') }}</t-button>
|
||
<t-button size="small" theme="primary" :loading="moveSubmitting" @click.stop="handleMoveConfirm">{{ t('knowledgeBase.moveConfirm') }}</t-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</t-popup>
|
||
</div>
|
||
<div
|
||
v-if="item.parse_status === 'processing' || item.parse_status === 'pending'"
|
||
class="card-analyze"
|
||
>
|
||
<t-icon name="loading" class="card-analyze-loading"></t-icon>
|
||
<span class="card-analyze-txt">{{ t('knowledgeBase.parsingInProgress') }}</span>
|
||
</div>
|
||
<div v-else-if="item.parse_status === 'failed'" class="card-analyze failure">
|
||
<t-icon name="close-circle" class="card-analyze-loading failure"></t-icon>
|
||
<span class="card-analyze-txt failure">{{ t('knowledgeBase.parsingFailed') }}</span>
|
||
</div>
|
||
<div v-else-if="item.parse_status === 'draft'" class="card-draft">
|
||
<t-tag size="small" theme="warning" variant="light-outline">{{ t('knowledgeBase.draft') }}</t-tag>
|
||
<span class="card-draft-tip">{{ t('knowledgeBase.draftTip') }}</span>
|
||
</div>
|
||
<div
|
||
v-else-if="item.parse_status === 'completed' && (item.summary_status === 'pending' || item.summary_status === 'processing')"
|
||
class="card-analyze"
|
||
>
|
||
<t-icon name="loading" class="card-analyze-loading"></t-icon>
|
||
<span class="card-analyze-txt">{{ t('knowledgeBase.generatingSummary') }}</span>
|
||
</div>
|
||
<div v-else-if="item.parse_status === 'completed'" class="card-content-txt">
|
||
{{ item.description }}
|
||
</div>
|
||
</div>
|
||
<div class="card-bottom">
|
||
<span class="card-time">{{ formatDocTime(item.updated_at) }}</span>
|
||
<div class="card-bottom-right">
|
||
<div v-if="tagList.length" class="card-tag-selector" @click.stop>
|
||
<t-dropdown
|
||
v-if="canEdit"
|
||
:options="tagDropdownOptions"
|
||
trigger="click"
|
||
@click="(data: any) => handleKnowledgeTagChange(item.id, data.value as string)"
|
||
>
|
||
<t-tag size="small" variant="light-outline">
|
||
<span class="tag-text">{{ getTagName(item.tag_id) }}</span>
|
||
</t-tag>
|
||
</t-dropdown>
|
||
<t-tag v-else size="small" variant="light-outline">
|
||
<span class="tag-text">{{ getTagName(item.tag_id) }}</span>
|
||
</t-tag>
|
||
</div>
|
||
<div class="card-type">
|
||
<span>{{ getKnowledgeType(item) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 悬停卡片时跟随鼠标的详情气泡 -->
|
||
<Teleport to="body">
|
||
<div
|
||
v-show="hoveredCardItem"
|
||
class="knowledge-card-hover-popover"
|
||
:style="{ left: cardPopoverPos.x + 'px', top: cardPopoverPos.y + 'px' }"
|
||
>
|
||
<template v-if="hoveredCardItem">
|
||
<div class="card-popover-title">{{ hoveredCardItem.file_name }}</div>
|
||
<div v-if="hoveredCardItem.parse_status === 'processing' || hoveredCardItem.parse_status === 'pending'" class="card-popover-status parsing">
|
||
<t-icon name="loading" size="14px" /> {{ t('knowledgeBase.parsingInProgress') }}
|
||
</div>
|
||
<div v-else-if="hoveredCardItem.parse_status === 'failed'" class="card-popover-status failure">
|
||
<t-icon name="close-circle" size="14px" /> {{ t('knowledgeBase.parsingFailed') }}
|
||
<span v-if="(hoveredCardItem as any).error_message" class="card-popover-error-msg">{{ (hoveredCardItem as any).error_message }}</span>
|
||
</div>
|
||
<div v-else-if="hoveredCardItem.parse_status === 'draft'" class="card-popover-status draft">
|
||
{{ t('knowledgeBase.draft') }}
|
||
</div>
|
||
<template v-else>
|
||
<div v-if="hoveredCardItem.description" class="card-popover-desc">{{ hoveredCardItem.description }}</div>
|
||
<div v-if="(hoveredCardItem as any).source" class="card-popover-source" :title="(hoveredCardItem as any).source">
|
||
<t-icon name="link" size="12px" /> {{ (hoveredCardItem as any).source }}
|
||
</div>
|
||
<div class="card-popover-extra">
|
||
<span v-if="(hoveredCardItem as any).created_at" class="card-popover-created">
|
||
{{ t('knowledgeBase.createdAt') }}:{{ formatDocTime((hoveredCardItem as any).created_at) }}
|
||
</span>
|
||
<span v-if="formatFileSize((hoveredCardItem as any).file_size)" class="card-popover-size">
|
||
{{ formatFileSize((hoveredCardItem as any).file_size) }}
|
||
</span>
|
||
</div>
|
||
</template>
|
||
<div class="card-popover-meta">
|
||
<span class="card-popover-time">{{ t('knowledgeBase.updatedAt') }}:{{ formatDocTime(hoveredCardItem.updated_at) }}</span>
|
||
<span v-if="(hoveredCardItem as any).channel && (hoveredCardItem as any).channel !== 'web'" class="card-popover-channel">{{ getChannelLabel((hoveredCardItem as any).channel) }}</span>
|
||
<span v-if="getTagName(hoveredCardItem.tag_id)" class="card-popover-tag">{{ getTagName(hoveredCardItem.tag_id) }}</span>
|
||
<span class="card-popover-type">{{ getKnowledgeType(hoveredCardItem) }}</span>
|
||
</div>
|
||
<div class="card-popover-hint">{{ t('knowledgeBase.clickToViewFull') }}</div>
|
||
</template>
|
||
</div>
|
||
</Teleport>
|
||
</template>
|
||
<template v-else>
|
||
<div class="doc-empty-state">
|
||
<EmptyKnowledge />
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<t-dialog
|
||
v-model:visible="delDialog"
|
||
dialogClassName="del-knowledge"
|
||
:closeBtn="false"
|
||
:cancelBtn="null"
|
||
:confirmBtn="null"
|
||
>
|
||
<div class="circle-wrap">
|
||
<div class="header">
|
||
<img class="circle-img" src="@/assets/img/circle.png" alt="" />
|
||
<span class="circle-title">{{ t('knowledgeBase.deleteConfirmation') }}</span>
|
||
</div>
|
||
<span class="del-circle-txt">
|
||
{{ t('knowledgeBase.confirmDeleteDocument', { fileName: knowledge.file_name || '' }) }}
|
||
</span>
|
||
<div class="circle-btn">
|
||
<span class="circle-btn-txt" @click="delDialog = false">{{ t('common.cancel') }}</span>
|
||
<span class="circle-btn-txt confirm" @click="delCardConfirm">
|
||
{{ t('knowledgeBase.confirmDelete') }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</t-dialog>
|
||
|
||
<!-- 重建知识确认弹窗 -->
|
||
<t-dialog
|
||
v-model:visible="rebuildDialog"
|
||
dialogClassName="del-knowledge"
|
||
:closeBtn="false"
|
||
:cancelBtn="null"
|
||
:confirmBtn="null"
|
||
>
|
||
<div class="circle-wrap">
|
||
<div class="header">
|
||
<img class="circle-img" src="@/assets/img/circle.png" alt="" />
|
||
<span class="circle-title">{{ t('knowledgeBase.rebuildDocument') }}</span>
|
||
</div>
|
||
<span class="del-circle-txt">
|
||
{{ t('knowledgeBase.rebuildConfirm', { fileName: rebuildKnowledgeItem.file_name || rebuildKnowledgeItem.title || '' }) }}
|
||
</span>
|
||
<div class="circle-btn">
|
||
<span class="circle-btn-txt" @click="rebuildDialog = false">{{ t('common.cancel') }}</span>
|
||
<span class="circle-btn-txt confirm" @click="rebuildConfirm">
|
||
{{ t('common.confirm') }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</t-dialog>
|
||
|
||
<!-- URL 导入对话框 -->
|
||
<t-dialog
|
||
v-model:visible="urlDialogVisible"
|
||
:header="$t('knowledgeBase.importURLTitle')"
|
||
:confirm-btn="{
|
||
content: $t('common.confirm'),
|
||
theme: 'primary',
|
||
loading: urlImporting,
|
||
}"
|
||
:cancel-btn="{ content: $t('common.cancel') }"
|
||
@confirm="handleURLImportConfirm"
|
||
@cancel="handleURLImportCancel"
|
||
width="500px"
|
||
>
|
||
<div class="url-import-form">
|
||
<div class="url-input-label">{{ $t('knowledgeBase.urlLabel') }}</div>
|
||
<t-input
|
||
v-model="urlInputValue"
|
||
:placeholder="$t('knowledgeBase.urlPlaceholder')"
|
||
clearable
|
||
autofocus
|
||
@keydown.enter="handleURLImportConfirm"
|
||
/>
|
||
<div class="url-input-tip">{{ $t('knowledgeBase.urlTip') }}</div>
|
||
</div>
|
||
</t-dialog>
|
||
|
||
<DocContent :visible="isCardDetails" :details="details" @closeDoc="closeDoc" @getDoc="getDoc"></DocContent>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div class="faq-manager-wrapper">
|
||
<FAQEntryManager v-if="kbId" :kb-id="kbId" />
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 知识库编辑器(创建/编辑统一组件) -->
|
||
<KnowledgeBaseEditorModal
|
||
:visible="uiStore.showKBEditorModal"
|
||
:mode="uiStore.kbEditorMode"
|
||
:kb-id="uiStore.currentKBId || undefined"
|
||
:initial-type="uiStore.kbEditorType"
|
||
@update:visible="(val) => val ? null : uiStore.closeKBEditor()"
|
||
@success="handleKBEditorSuccess"
|
||
/>
|
||
</template>
|
||
<style>
|
||
/* 下拉菜单容器样式已统一至 @/assets/dropdown-menu.less */
|
||
</style>
|
||
<style scoped lang="less">
|
||
.knowledge-layout {
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin: 0 16px 0 4px;
|
||
gap: 20px;
|
||
height: 100%;
|
||
flex: 1;
|
||
width: 100%;
|
||
min-width: 0;
|
||
padding: 24px 32px 32px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
// 与列表页一致:浅灰底圆角区,左侧筛选为白底卡片
|
||
.knowledge-main {
|
||
display: flex;
|
||
flex: 1;
|
||
min-height: 0;
|
||
background: var(--td-bg-color-container);
|
||
border: 1px solid var(--td-component-stroke);
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
// 与列表页筛选区一致:白底卡片感、细分界
|
||
.tag-sidebar {
|
||
width: 200px;
|
||
background: var(--td-bg-color-container);
|
||
border-right: 1px solid var(--td-component-stroke);
|
||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.04);
|
||
padding: 16px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex-shrink: 0;
|
||
max-height: 100%;
|
||
min-height: 0;
|
||
|
||
.sidebar-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 10px;
|
||
color: var(--td-text-color-primary);
|
||
|
||
.sidebar-title {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 4px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
|
||
.sidebar-count {
|
||
font-size: 12px;
|
||
color: var(--td-text-color-secondary);
|
||
}
|
||
}
|
||
|
||
.sidebar-actions {
|
||
display: flex;
|
||
gap: 6px;
|
||
color: var(--td-text-color-placeholder);
|
||
align-items: center;
|
||
|
||
.create-tag-btn {
|
||
width: 24px;
|
||
height: 24px;
|
||
padding: 0;
|
||
border-radius: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--td-success-color);
|
||
line-height: 1;
|
||
transition: background 0.2s ease, color 0.2s ease;
|
||
|
||
&:hover {
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
color: var(--td-brand-color-active);
|
||
}
|
||
}
|
||
|
||
.create-tag-plus {
|
||
line-height: 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
.tag-search-bar {
|
||
margin-bottom: 10px;
|
||
|
||
:deep(.t-input) {
|
||
font-size: 13px;
|
||
background-color: var(--td-bg-color-container);
|
||
border-color: var(--td-component-stroke);
|
||
border-radius: 6px;
|
||
}
|
||
}
|
||
|
||
.tag-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
scrollbar-width: none;
|
||
|
||
&::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
|
||
.tag-load-more {
|
||
padding: 8px 0 0;
|
||
display: flex;
|
||
justify-content: center;
|
||
|
||
:deep(.t-button) {
|
||
padding: 0;
|
||
font-size: 12px;
|
||
color: var(--td-success-color);
|
||
}
|
||
}
|
||
|
||
.tag-list-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 9px 12px;
|
||
border-radius: 6px;
|
||
color: var(--td-text-color-primary);
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
font-family: "PingFang SC", -apple-system, BlinkMacSystemFont, sans-serif;
|
||
font-size: 14px;
|
||
-webkit-font-smoothing: antialiased;
|
||
|
||
.tag-list-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
flex: 1;
|
||
|
||
.t-icon {
|
||
flex-shrink: 0;
|
||
color: var(--td-text-color-secondary);
|
||
font-size: 14px;
|
||
transition: color 0.2s ease;
|
||
}
|
||
}
|
||
|
||
.tag-name {
|
||
flex: 1;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-family: "PingFang SC", -apple-system, BlinkMacSystemFont, sans-serif;
|
||
font-size: 14px;
|
||
font-weight: 450;
|
||
line-height: 1.4;
|
||
letter-spacing: 0.01em;
|
||
}
|
||
|
||
.tag-list-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-left: 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.tag-count {
|
||
font-size: 12px;
|
||
color: var(--td-text-color-secondary);
|
||
font-weight: 500;
|
||
min-width: 28px;
|
||
padding: 3px 7px;
|
||
border-radius: 8px;
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
transition: all 0.2s ease;
|
||
text-align: center;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
&:hover {
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
color: var(--td-text-color-primary);
|
||
|
||
.tag-list-left .t-icon {
|
||
color: var(--td-text-color-primary);
|
||
}
|
||
|
||
.tag-count {
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
color: var(--td-text-color-primary);
|
||
}
|
||
}
|
||
|
||
&.active {
|
||
background: var(--td-success-color-light);
|
||
color: var(--td-brand-color);
|
||
font-weight: 500;
|
||
|
||
.tag-list-left .t-icon {
|
||
color: var(--td-brand-color);
|
||
}
|
||
|
||
.tag-name {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.tag-count {
|
||
background: var(--td-success-color-light);
|
||
color: var(--td-brand-color);
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
&.editing {
|
||
background: transparent;
|
||
border: none;
|
||
}
|
||
|
||
&.tag-editing {
|
||
cursor: default;
|
||
padding-right: 8px;
|
||
background: transparent;
|
||
border: none;
|
||
|
||
.tag-edit-input {
|
||
flex: 1;
|
||
}
|
||
}
|
||
|
||
&.tag-editing .tag-edit-input {
|
||
width: 100%;
|
||
}
|
||
|
||
.tag-inline-actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
margin-left: auto;
|
||
|
||
:deep(.t-button) {
|
||
padding: 0 4px;
|
||
height: 24px;
|
||
}
|
||
|
||
:deep(.tag-action-btn) {
|
||
border-radius: 4px;
|
||
transition: all 0.2s ease;
|
||
|
||
.t-icon {
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
:deep(.tag-action-btn.confirm) {
|
||
background: var(--td-success-color-light);
|
||
color: var(--td-brand-color-active);
|
||
|
||
&:hover {
|
||
background: var(--td-success-color-light);
|
||
color: var(--td-success-color);
|
||
}
|
||
}
|
||
|
||
:deep(.tag-action-btn.cancel) {
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
color: var(--td-text-color-secondary);
|
||
|
||
&:hover {
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
color: var(--td-text-color-secondary);
|
||
}
|
||
}
|
||
}
|
||
|
||
.tag-edit-input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
|
||
:deep(.t-input) {
|
||
font-size: 12px;
|
||
background-color: transparent;
|
||
border: none;
|
||
border-bottom: 1px solid var(--td-component-stroke);
|
||
border-radius: 0;
|
||
box-shadow: none;
|
||
padding-left: 0;
|
||
padding-right: 0;
|
||
}
|
||
|
||
:deep(.t-input__wrap) {
|
||
background-color: transparent;
|
||
border: none;
|
||
border-bottom: 1px solid var(--td-component-stroke);
|
||
border-radius: 0;
|
||
box-shadow: none;
|
||
}
|
||
|
||
:deep(.t-input__inner) {
|
||
padding-left: 0;
|
||
padding-right: 0;
|
||
color: var(--td-text-color-primary);
|
||
caret-color: var(--td-text-color-primary);
|
||
}
|
||
|
||
:deep(.t-input:hover),
|
||
:deep(.t-input.t-is-focused),
|
||
:deep(.t-input__wrap:hover),
|
||
:deep(.t-input__wrap.t-is-focused) {
|
||
border-bottom-color: var(--td-success-color);
|
||
}
|
||
}
|
||
|
||
.tag-more {
|
||
display: flex;
|
||
align-items: center;
|
||
opacity: 0;
|
||
transition: opacity 0.2s ease;
|
||
}
|
||
|
||
&:hover .tag-more {
|
||
opacity: 1;
|
||
}
|
||
|
||
.tag-more-btn {
|
||
width: 22px;
|
||
height: 22px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 4px;
|
||
color: var(--td-text-color-secondary);
|
||
transition: all 0.2s ease;
|
||
|
||
&:hover {
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
color: var(--td-text-color-secondary);
|
||
}
|
||
}
|
||
}
|
||
|
||
.tag-empty-state {
|
||
text-align: center;
|
||
padding: 10px 6px;
|
||
color: var(--td-text-color-placeholder);
|
||
font-size: 11px;
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.tag-menu) {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
:deep(.tag-menu-item) {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 8px 16px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
color: var(--td-text-color-primary);
|
||
font-family: 'PingFang SC';
|
||
font-size: 14px;
|
||
font-weight: 400;
|
||
|
||
.menu-icon {
|
||
margin-right: 8px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
&:hover {
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
color: var(--td-text-color-primary);
|
||
}
|
||
|
||
&.danger {
|
||
color: var(--td-text-color-primary);
|
||
|
||
&:hover {
|
||
background: var(--td-error-color-light);
|
||
color: var(--td-error-color);
|
||
|
||
.menu-icon {
|
||
color: var(--td-error-color);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.tag-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
padding: 12px;
|
||
overflow: hidden;
|
||
background: var(--td-bg-color-container);
|
||
}
|
||
|
||
.doc-card-area {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
}
|
||
|
||
.doc-filter-bar {
|
||
padding: 0 0 12px 0;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
|
||
.doc-search-input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.doc-type-select {
|
||
width: 140px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.doc-filter-actions {
|
||
flex-shrink: 0;
|
||
:deep(.content-bar-icon-btn) {
|
||
color: var(--td-text-color-secondary);
|
||
background: transparent;
|
||
border: none;
|
||
&:hover {
|
||
color: var(--td-brand-color);
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.t-input) {
|
||
font-size: 13px;
|
||
background-color: var(--td-bg-color-container);
|
||
border-color: var(--td-component-stroke);
|
||
border-radius: 6px;
|
||
|
||
&:hover,
|
||
&:focus,
|
||
&.t-is-focused {
|
||
border-color: var(--td-brand-color);
|
||
background-color: var(--td-bg-color-container);
|
||
}
|
||
}
|
||
|
||
:deep(.t-select) {
|
||
.t-input {
|
||
font-size: 13px;
|
||
background-color: var(--td-bg-color-container);
|
||
border-color: var(--td-component-stroke);
|
||
border-radius: 6px;
|
||
|
||
&:hover,
|
||
&.t-is-focused {
|
||
border-color: var(--td-brand-color);
|
||
background-color: var(--td-bg-color-container);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.doc-scroll-container {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
padding-right: 4px;
|
||
|
||
&.is-empty {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow-y: hidden;
|
||
}
|
||
}
|
||
|
||
// Header 样式(无底部分割线,留更多空间给下方内容区)
|
||
.document-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
flex-shrink: 0;
|
||
|
||
.document-header-title {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.document-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.kb-access-meta {
|
||
margin-left: auto;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.kb-access-meta-inner {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
color: var(--td-text-color-secondary);
|
||
cursor: default;
|
||
}
|
||
|
||
.kb-access-role-tag {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.kb-access-meta-sep {
|
||
color: var(--td-text-color-placeholder);
|
||
user-select: none;
|
||
}
|
||
|
||
.kb-access-meta-text {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.document-breadcrumb {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin: 0;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: var(--td-text-color-primary);
|
||
}
|
||
|
||
.breadcrumb-link {
|
||
border: none;
|
||
background: transparent;
|
||
padding: 4px 8px;
|
||
margin: -4px -8px;
|
||
font: inherit;
|
||
color: var(--td-text-color-secondary);
|
||
cursor: pointer;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
border-radius: 6px;
|
||
transition: all 0.12s ease;
|
||
|
||
&:hover:not(:disabled) {
|
||
color: var(--td-success-color);
|
||
background: var(--td-bg-color-container);
|
||
}
|
||
|
||
&:disabled {
|
||
cursor: not-allowed;
|
||
color: var(--td-text-color-placeholder);
|
||
}
|
||
|
||
&.dropdown {
|
||
padding-right: 6px;
|
||
|
||
:deep(.t-icon) {
|
||
font-size: 14px;
|
||
transition: transform 0.12s ease;
|
||
}
|
||
|
||
&:hover:not(:disabled) {
|
||
:deep(.t-icon) {
|
||
transform: translateY(1px);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.breadcrumb-separator {
|
||
font-size: 14px;
|
||
color: var(--td-text-color-placeholder);
|
||
}
|
||
|
||
.breadcrumb-current {
|
||
color: var(--td-text-color-primary);
|
||
font-weight: 600;
|
||
}
|
||
|
||
h2 {
|
||
margin: 0;
|
||
color: var(--td-text-color-primary);
|
||
font-family: "PingFang SC";
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
line-height: 32px;
|
||
}
|
||
|
||
.document-subtitle {
|
||
margin: 0;
|
||
color: var(--td-text-color-placeholder);
|
||
font-family: "PingFang SC";
|
||
font-size: 14px;
|
||
font-weight: 400;
|
||
line-height: 20px;
|
||
}
|
||
|
||
.parser-hint {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
margin: 2px 0 0;
|
||
color: var(--td-warning-color);
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
cursor: pointer;
|
||
transition: color 0.15s ease;
|
||
|
||
&:hover {
|
||
color: var(--td-warning-color-active);
|
||
|
||
.parser-hint-link {
|
||
text-decoration: underline;
|
||
}
|
||
}
|
||
|
||
.parser-hint-icon {
|
||
font-size: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.parser-hint-link {
|
||
color: var(--td-brand-color);
|
||
margin-left: 2px;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.storage-engine-warning {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
margin: 2px 0 0;
|
||
color: var(--td-warning-color);
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
cursor: pointer;
|
||
transition: color 0.15s ease;
|
||
|
||
&:hover {
|
||
color: var(--td-warning-color-active);
|
||
|
||
.warning-link {
|
||
text-decoration: underline;
|
||
}
|
||
}
|
||
|
||
.warning-icon {
|
||
font-size: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.warning-link {
|
||
color: var(--td-brand-color);
|
||
margin-left: 2px;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
.document-upload-input {
|
||
display: none;
|
||
}
|
||
|
||
.kb-settings-button {
|
||
width: 30px;
|
||
height: 30px;
|
||
border: none;
|
||
border-radius: 50%;
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--td-text-color-secondary);
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
padding: 0;
|
||
|
||
&:hover:not(:disabled) {
|
||
background: var(--td-success-color-light);
|
||
color: var(--td-brand-color);
|
||
box-shadow: none;
|
||
}
|
||
|
||
&:disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.4;
|
||
}
|
||
|
||
:deep(.t-icon) {
|
||
font-size: 18px;
|
||
}
|
||
}
|
||
|
||
.tag-filter-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
|
||
.tag-filter-label {
|
||
color: var(--td-text-color-placeholder);
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
.card-tag-selector {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
:deep(.t-tag) {
|
||
cursor: pointer;
|
||
max-width: 160px;
|
||
border-radius: 999px;
|
||
border-color: var(--td-component-stroke);
|
||
color: var(--td-text-color-primary);
|
||
padding: 0 10px;
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
transition: all 0.2s ease;
|
||
|
||
&:hover {
|
||
border-color: var(--td-brand-color);
|
||
color: var(--td-brand-color-active);
|
||
background: var(--td-success-color-light);
|
||
}
|
||
}
|
||
|
||
.tag-text {
|
||
display: inline-block;
|
||
max-width: 110px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
vertical-align: middle;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
.card-bottom-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.faq-manager-wrapper {
|
||
flex: 1;
|
||
padding: 24px 32px;
|
||
overflow-y: auto;
|
||
margin: 0 16px 0 4px;
|
||
}
|
||
|
||
@media (max-width: 1250px) and (min-width: 1045px) {
|
||
.answers-input {
|
||
transform: translateX(-329px);
|
||
}
|
||
|
||
:deep(.t-textarea__inner) {
|
||
width: 654px !important;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1045px) {
|
||
.answers-input {
|
||
transform: translateX(-250px);
|
||
}
|
||
|
||
:deep(.t-textarea__inner) {
|
||
width: 500px !important;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 750px) {
|
||
.answers-input {
|
||
transform: translateX(-182px);
|
||
}
|
||
|
||
:deep(.t-textarea__inner) {
|
||
width: 340px !important;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
.answers-input {
|
||
transform: translateX(-164px);
|
||
}
|
||
|
||
:deep(.t-textarea__inner) {
|
||
width: 300px !important;
|
||
}
|
||
}
|
||
|
||
.doc-card-list {
|
||
box-sizing: border-box;
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
|
||
gap: 14px;
|
||
align-content: flex-start;
|
||
width: 100%;
|
||
}
|
||
|
||
.doc-empty-state {
|
||
flex: 1;
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 60px 20px;
|
||
min-height: 100%;
|
||
}
|
||
|
||
|
||
:deep(.del-knowledge) {
|
||
padding: 0px !important;
|
||
border-radius: 6px !important;
|
||
|
||
.t-dialog__header {
|
||
display: none;
|
||
}
|
||
|
||
.t-dialog__body {
|
||
padding: 16px;
|
||
}
|
||
|
||
.t-dialog__footer {
|
||
padding: 0;
|
||
}
|
||
}
|
||
|
||
:deep(.t-dialog__position.t-dialog--top) {
|
||
padding-top: 40vh !important;
|
||
}
|
||
|
||
.circle-wrap {
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.circle-img {
|
||
width: 20px;
|
||
height: 20px;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.circle-title {
|
||
color: var(--td-text-color-primary);
|
||
font-family: "PingFang SC";
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
line-height: 24px;
|
||
}
|
||
|
||
.del-circle-txt {
|
||
color: var(--td-text-color-placeholder);
|
||
font-family: "PingFang SC";
|
||
font-size: 14px;
|
||
font-weight: 400;
|
||
line-height: 22px;
|
||
display: inline-block;
|
||
margin-left: 29px;
|
||
margin-bottom: 21px;
|
||
}
|
||
|
||
.circle-btn {
|
||
height: 22px;
|
||
width: 100%;
|
||
display: flex;
|
||
justify-content: end;
|
||
}
|
||
|
||
.circle-btn-txt {
|
||
color: var(--td-text-color-primary);
|
||
font-family: "PingFang SC";
|
||
font-size: 14px;
|
||
font-weight: 400;
|
||
line-height: 22px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.confirm {
|
||
color: var(--td-error-color);
|
||
margin-left: 40px;
|
||
}
|
||
}
|
||
|
||
.card-menu {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-width: 140px;
|
||
gap: 1px;
|
||
}
|
||
|
||
.card-menu-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 8px 12px;
|
||
cursor: pointer;
|
||
color: var(--td-text-color-primary);
|
||
transition: all 0.15s cubic-bezier(0.2, 0, 0, 1);
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
line-height: 20px;
|
||
|
||
&:hover {
|
||
background: var(--td-bg-color-container-hover);
|
||
}
|
||
|
||
&:active {
|
||
background: var(--td-bg-color-container-active);
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.icon {
|
||
font-size: 16px;
|
||
color: var(--td-text-color-secondary);
|
||
transition: all 0.15s cubic-bezier(0.2, 0, 0, 1);
|
||
}
|
||
|
||
&:hover .icon {
|
||
color: var(--td-text-color-primary);
|
||
}
|
||
|
||
&.danger {
|
||
color: var(--td-error-color-6);
|
||
margin-top: 4px;
|
||
position: relative;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: -3px;
|
||
left: 8px;
|
||
right: 8px;
|
||
height: 1px;
|
||
background: var(--td-component-stroke);
|
||
}
|
||
|
||
.icon {
|
||
color: var(--td-error-color-6);
|
||
}
|
||
|
||
&:hover {
|
||
background: var(--td-error-color-1);
|
||
color: var(--td-error-color-6);
|
||
|
||
.icon {
|
||
color: var(--td-error-color-6);
|
||
}
|
||
}
|
||
|
||
&:active {
|
||
background: var(--td-error-color-2);
|
||
}
|
||
}
|
||
}
|
||
|
||
.move-menu {
|
||
min-width: 220px;
|
||
max-width: 280px;
|
||
max-height: 360px;
|
||
overflow-y: auto;
|
||
|
||
.move-menu-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 8px 12px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--td-text-color-primary);
|
||
border-bottom: 1px solid var(--td-component-stroke);
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
background: var(--td-bg-color-container-hover);
|
||
}
|
||
}
|
||
|
||
.move-menu-loading {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px 0;
|
||
}
|
||
|
||
.move-menu-empty {
|
||
padding: 12px 16px;
|
||
font-size: 12px;
|
||
color: var(--td-text-color-placeholder);
|
||
text-align: center;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.move-target-name {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.move-target-count {
|
||
font-size: 12px;
|
||
color: var(--td-text-color-placeholder);
|
||
}
|
||
|
||
.move-confirm-body {
|
||
padding: 8px;
|
||
|
||
.move-target-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 8px;
|
||
background: var(--td-bg-color-container-hover);
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
color: var(--td-text-color-secondary);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.move-mode-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 6px;
|
||
padding: 6px 8px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
margin-bottom: 4px;
|
||
|
||
&:hover {
|
||
background: var(--td-bg-color-container-hover);
|
||
}
|
||
|
||
&.active {
|
||
background: var(--td-brand-color-light);
|
||
}
|
||
|
||
.move-mode-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
|
||
.move-mode-label {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--td-text-color-primary);
|
||
}
|
||
|
||
.move-mode-desc {
|
||
font-size: 11px;
|
||
color: var(--td-text-color-placeholder);
|
||
line-height: 1.4;
|
||
}
|
||
}
|
||
}
|
||
|
||
.move-confirm-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.card-draft {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 6px 0;
|
||
}
|
||
|
||
.card-draft-tip {
|
||
color: var(--td-warning-color);
|
||
font-size: 11px;
|
||
}
|
||
|
||
.knowledge-card {
|
||
min-width: 248px;
|
||
border: 1px solid var(--td-component-stroke);
|
||
height: 148px;
|
||
border-radius: 9px;
|
||
overflow: hidden;
|
||
box-sizing: border-box;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||
background: var(--td-bg-color-container);
|
||
position: relative;
|
||
cursor: pointer;
|
||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||
|
||
.card-content {
|
||
padding: 15px 17px 13px;
|
||
}
|
||
|
||
.card-analyze {
|
||
height: 52px;
|
||
display: flex;
|
||
}
|
||
|
||
.card-analyze-loading {
|
||
display: block;
|
||
color: var(--td-brand-color);
|
||
font-size: 14px;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.card-analyze-txt {
|
||
color: var(--td-brand-color);
|
||
font-family: "PingFang SC";
|
||
font-size: 11px;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.failure {
|
||
color: var(--td-error-color);
|
||
}
|
||
|
||
.card-content-nav {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 11px;
|
||
gap: 8px;
|
||
}
|
||
|
||
.card-content-title {
|
||
flex: 1;
|
||
min-width: 0;
|
||
height: 29px;
|
||
line-height: 29px;
|
||
display: inline-block;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
color: var(--td-text-color-primary);
|
||
font-family: "PingFang SC", -apple-system, sans-serif;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.01em;
|
||
}
|
||
|
||
.more-wrap {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
width: 25px;
|
||
height: 25px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.more-wrap:hover {
|
||
background: var(--td-component-stroke);
|
||
}
|
||
|
||
.more-icon {
|
||
width: 14px;
|
||
height: 14px;
|
||
}
|
||
|
||
.active-more {
|
||
background: var(--td-component-stroke);
|
||
}
|
||
|
||
.card-content-txt {
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 2;
|
||
line-clamp: 2;
|
||
overflow: hidden;
|
||
color: var(--td-text-color-secondary);
|
||
font-family: "PingFang SC";
|
||
font-size: 12px;
|
||
font-weight: 400;
|
||
line-height: 19px;
|
||
}
|
||
|
||
.card-bottom {
|
||
position: absolute;
|
||
bottom: 0;
|
||
padding: 0 17px;
|
||
box-sizing: border-box;
|
||
height: 34px;
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: var(--td-bg-color-container);
|
||
border-top: 1px solid var(--td-component-stroke);
|
||
}
|
||
|
||
.card-time {
|
||
color: var(--td-text-color-secondary);
|
||
font-family: "PingFang SC";
|
||
font-size: 12px;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.card-type {
|
||
color: var(--td-text-color-secondary);
|
||
font-family: "PingFang SC";
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
padding: 3px 8px;
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
border-radius: 4px;
|
||
}
|
||
}
|
||
|
||
.knowledge-card:hover {
|
||
border-color: var(--td-brand-color);
|
||
box-shadow: 0 2px 8px rgba(7, 192, 95, 0.12);
|
||
}
|
||
|
||
/* 悬停知识卡片时跟随鼠标的详情气泡 */
|
||
.knowledge-card-hover-popover {
|
||
position: fixed;
|
||
z-index: 9999;
|
||
pointer-events: none;
|
||
min-width: 220px;
|
||
max-width: 360px;
|
||
padding: 12px 14px;
|
||
background: var(--td-bg-color-container);
|
||
border: 1px solid var(--td-component-stroke);
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||
font-family: "PingFang SC", -apple-system, sans-serif;
|
||
transition: opacity 0.15s ease;
|
||
|
||
.card-popover-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--td-text-color-primary);
|
||
margin-bottom: 8px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.card-popover-status {
|
||
font-size: 12px;
|
||
margin-bottom: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
|
||
&.parsing {
|
||
color: var(--td-brand-color);
|
||
}
|
||
|
||
&.failure {
|
||
color: var(--td-error-color);
|
||
}
|
||
|
||
&.draft {
|
||
color: var(--td-warning-color);
|
||
}
|
||
}
|
||
|
||
.card-popover-desc {
|
||
font-size: 12px;
|
||
color: var(--td-text-color-secondary);
|
||
line-height: 1.5;
|
||
margin-bottom: 8px;
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 5;
|
||
line-clamp: 5;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.card-popover-error-msg {
|
||
display: block;
|
||
margin-top: 4px;
|
||
font-size: 11px;
|
||
color: var(--td-error-color);
|
||
opacity: 0.95;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
max-width: 280px;
|
||
}
|
||
|
||
.card-popover-source {
|
||
font-size: 11px;
|
||
color: var(--td-brand-color);
|
||
margin-bottom: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.card-popover-extra {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
font-size: 11px;
|
||
color: var(--td-text-color-secondary);
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.card-popover-created,
|
||
.card-popover-size {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.card-popover-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
font-size: 11px;
|
||
color: var(--td-text-color-secondary);
|
||
}
|
||
|
||
.card-popover-channel {
|
||
padding: 1px 6px;
|
||
background: var(--td-warning-color-light);
|
||
color: var(--td-warning-color);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.card-popover-tag {
|
||
padding: 1px 6px;
|
||
background: var(--td-success-color-light);
|
||
color: var(--td-brand-color);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.card-popover-type {
|
||
padding: 1px 6px;
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
color: var(--td-text-color-secondary);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.card-popover-hint {
|
||
margin-top: 8px;
|
||
padding-top: 8px;
|
||
border-top: 1px solid var(--td-component-stroke);
|
||
font-size: 11px;
|
||
color: var(--td-text-color-secondary);
|
||
}
|
||
}
|
||
|
||
.url-import-form {
|
||
padding: 8px 0;
|
||
|
||
.url-input-label {
|
||
color: var(--td-text-color-primary);
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.url-input-tip {
|
||
color: var(--td-text-color-secondary);
|
||
font-size: 12px;
|
||
margin-top: 8px;
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
|
||
.knowledge-card-upload {
|
||
color: var(--td-text-color-primary);
|
||
font-family: "PingFang SC";
|
||
font-size: 14px;
|
||
font-weight: 400;
|
||
cursor: pointer;
|
||
|
||
.btn-upload {
|
||
margin: 33px auto 0;
|
||
width: 112px;
|
||
height: 32px;
|
||
border: 1px solid var(--td-component-border);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.svg-icon-download {
|
||
margin-right: 8px;
|
||
}
|
||
}
|
||
|
||
.upload-described {
|
||
color: var(--td-text-color-disabled);
|
||
font-family: "PingFang SC";
|
||
font-size: 12px;
|
||
font-weight: 400;
|
||
text-align: center;
|
||
display: block;
|
||
width: 188px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.del-card {
|
||
vertical-align: middle;
|
||
}
|
||
</style>
|