diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 17398172..e878570a 100755 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -370,8 +370,13 @@ export default { publishToWeb: 'Publish to Web', documentCategoryTitle: 'Document Categories', tagUpdateSuccess: 'Tag updated successfully', + tagEditDialogHeading: 'Edit tags', tagEditDialogTitle: 'Edit Tags — {name}', tagEditSearch: 'Search tags...', + tagEditSelectedSection: 'Selected', + tagEditAvailableSection: 'Available', + tagEditNoSelected: 'None selected', + tagFilterAll: 'All documents', tagSelectedCount: '{count} selected', tagOverflowTip: 'Click to edit tags', tagNewPlaceholder: 'New tag name, press Enter to add', diff --git a/frontend/src/i18n/locales/ko-KR.ts b/frontend/src/i18n/locales/ko-KR.ts index c21cafb0..4fd149f6 100755 --- a/frontend/src/i18n/locales/ko-KR.ts +++ b/frontend/src/i18n/locales/ko-KR.ts @@ -372,8 +372,13 @@ export default { untagged: "미분류", tagClearAction: "분류 해제", tagUpdateSuccess: "태그 업데이트 성공", + tagEditDialogHeading: "태그 편집", tagEditDialogTitle: '태그 편집 — {name}', tagEditSearch: '태그 검색...', + tagEditSelectedSection: '선택된 태그', + tagEditAvailableSection: '선택 가능', + tagEditNoSelected: '선택 없음', + tagFilterAll: '전체 문서', tagSelectedCount: '{count}개 선택됨', tagOverflowTip: '클릭하여 태그 편집', tagNewPlaceholder: '새 태그 이름 입력, Enter로 추가', diff --git a/frontend/src/i18n/locales/ru-RU.ts b/frontend/src/i18n/locales/ru-RU.ts index d5d83a1e..3b37d729 100755 --- a/frontend/src/i18n/locales/ru-RU.ts +++ b/frontend/src/i18n/locales/ru-RU.ts @@ -313,8 +313,13 @@ export default { tagClearAction: 'Снять категорию', tagSearchTooltip: 'Поиск тегов', tagUpdateSuccess: 'Тег успешно обновлен', + tagEditDialogHeading: 'Редактировать теги', tagEditDialogTitle: 'Редактировать теги — {name}', tagEditSearch: 'Поиск тегов...', + tagEditSelectedSection: 'Выбранные', + tagEditAvailableSection: 'Доступные', + tagEditNoSelected: 'Ничего не выбрано', + tagFilterAll: 'Все документы', tagSelectedCount: 'Выбрано: {count}', tagOverflowTip: 'Нажмите, чтобы изменить теги', tagNewPlaceholder: 'Название нового тега, Enter для добавления', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index c27e7af3..60b9d41e 100755 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -373,8 +373,13 @@ export default { untagged: "未分类", tagClearAction: "取消分类", tagUpdateSuccess: "标签更新成功", + tagEditDialogHeading: "编辑标签", tagEditDialogTitle: '编辑标签 — {name}', tagEditSearch: "搜索标签...", + tagEditSelectedSection: "已选标签", + tagEditAvailableSection: "可选标签", + tagEditNoSelected: "暂未选择", + tagFilterAll: "全部文档", tagSelectedCount: "已选 {count} 个标签", tagOverflowTip: "点击编辑标签", tagNewPlaceholder: "输入新标签名称,回车添加", diff --git a/frontend/src/views/knowledge/KnowledgeBase.vue b/frontend/src/views/knowledge/KnowledgeBase.vue index 94a0caaa..64c995fb 100644 --- a/frontend/src/views/knowledge/KnowledgeBase.vue +++ b/frontend/src/views/knowledge/KnowledgeBase.vue @@ -46,6 +46,7 @@ import DocumentListView from './components/DocumentListView.vue'; import DocumentBatchBar from './components/DocumentBatchBar.vue'; import KbUploadSourceDropdown from './components/KbUploadSourceDropdown.vue'; import TagEditDialog from './components/TagEditDialog.vue'; +import { useTagChipsOverflow } from '@/composables/useTagChipsOverflow'; import type { KnowledgeProcessOverrides } from '@/types/knowledgeProcess'; import { useUploadConfirmStore, type UploadConfirmResult } from '@/stores/uploadConfirm'; import WikiBrowser from './wiki/WikiBrowser.vue'; @@ -567,26 +568,6 @@ const filterParams = computed(() => { }; }); type TagInputInstance = ComponentPublicInstance<{ focus: () => void; select: () => void }>; -const onPickTag = (item: any, tagId: string | number) => { - if (item) item.isTagPopup = false; - const currentTags = item?.tags || []; - const tagIdStr = tagId ? String(tagId) : ''; - let updatedTags: string[]; - if (!tagIdStr) { - // Clear all tags - updatedTags = []; - } else { - const idx = currentTags.findIndex((t: any) => t.id === tagIdStr); - if (idx >= 0) { - // Remove tag - updatedTags = currentTags.filter((t: any) => t.id !== tagIdStr).map((t: any) => t.id); - } else { - // Add tag - updatedTags = [...currentTags.map((t: any) => t.id), tagIdStr]; - } - } - handleKnowledgeTagChange(item.id, updatedTags); -}; const tagMap = computed>(() => { const map: Record = {}; tagList.value.forEach((tag) => { @@ -594,13 +575,30 @@ const tagMap = computed>(() => { }); 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 sidebarCategoryCount = computed(() => tagTotal.value || tagList.value.length); +const sidebarTags = computed(() => { + const list = tagList.value; + const selectedId = selectedTagIds.value[0]; + if (!selectedId || list.some((tag) => tag.id === selectedId)) { + return list; + } + const selected = tagMap.value[selectedId]; + return selected ? [selected, ...list] : list; }); +const tagChipMenuOptions = computed(() => [ + { content: t('knowledgeBase.tagEditAction'), value: 'edit' }, + { content: t('knowledgeBase.tagDeleteAction'), value: 'delete', theme: 'error' }, +]); + +const handleTagChipMenu = (data: { value: string }, tag: any) => { + if (data.value === 'edit') { + startEditTag(tag); + } else if (data.value === 'delete') { + confirmDeleteTag(tag); + } +}; + const editingTagInputRefs = new Map(); const setEditingTagInputRef = (el: TagInputInstance | null, tagId: string) => { if (el) { @@ -623,59 +621,12 @@ const editingTagSubmitting = ref(false); const tagEditDialogVisible = ref(false); const tagEditTarget = ref(null); -// 动态标签可见数量(根据卡片容器宽度自适应) -const TAG_EST_WIDTH = 82; // 单个标签估算宽度(含间距) -const TAG_OVERFLOW_MIN = 32; // +N 徽章预留宽度 -const tagVisibleLimit = reactive>({}); -const tagItemTotalMap = new Map(); -let tagChipsRO: ResizeObserver | null = null; - -function setupTagChipsObserver(el: Element | null, itemId: string, totalCount: number) { - if (!el) return; - const htmlEl = el as HTMLElement; - htmlEl.dataset.tagItemId = itemId; - tagItemTotalMap.set(itemId, totalCount); - if (!tagChipsRO) { - tagChipsRO = new ResizeObserver((entries) => { - for (const entry of entries) { - const target = entry.target as HTMLElement; - const id = target.dataset.tagItemId; - if (!id) continue; - const w = entry.contentRect.width; - const total = tagItemTotalMap.get(id) ?? 0; - if (total <= 0) { tagVisibleLimit[id] = 99; continue; } - // 计算能容纳几个标签:总宽 - 溢出徽章预留,除以单标签宽 - const maxFit = Math.floor((w - TAG_OVERFLOW_MIN) / TAG_EST_WIDTH); - const limit = Math.max(1, Math.min(maxFit, total)); - tagVisibleLimit[id] = limit >= total ? 99 : limit; - } - }); - } - tagChipsRO.observe(htmlEl); - // 初始计算 - requestAnimationFrame(() => { - const w = htmlEl.clientWidth; - const total = totalCount; - if (total <= 0) { tagVisibleLimit[itemId] = 99; return; } - const maxFit = Math.floor((w - TAG_OVERFLOW_MIN) / TAG_EST_WIDTH); - const limit = Math.max(1, Math.min(maxFit, total)); - tagVisibleLimit[itemId] = limit >= total ? 99 : limit; - }); -} - -function getTagLimit(itemId: string): number { - return tagVisibleLimit[itemId] ?? 99; -} - -function hasTagOverflow(itemId: string, total: number): boolean { - const limit = tagVisibleLimit[itemId] ?? 99; - return total > limit; -} - -function getOverflowCount(itemId: string, total: number): number { - const limit = tagVisibleLimit[itemId] ?? 99; - return Math.max(0, total - limit); -} +const { + setupTagChipsObserver, + getTagLimit, + hasTagOverflow, + getOverflowCount, +} = useTagChipsOverflow('tagItemId'); function openTagEditDialog(item: KnowledgeCard) { tagEditTarget.value = item; @@ -819,7 +770,6 @@ const handleTagFilterChange = (tagIds: string[]) => { uiStore.clearSelectedTagIds(); tagIds.forEach(id => uiStore.toggleSelectedTagId(id)); resetPage(); - loadKnowledgeFiles(kbId.value); }; const handleTagRowClick = (tagId: string) => { @@ -831,14 +781,22 @@ const handleTagRowClick = (tagId: string) => { editingTagId.value = null; editingTagName.value = ''; } - // Toggle selection - const idx = selectedTagIds.value.indexOf(tagId); - if (idx >= 0) { - selectedTagIds.value.splice(idx, 1); - } else { - selectedTagIds.value.push(tagId); + const next = selectedTagIds.value.length === 1 && selectedTagIds.value[0] === tagId + ? [] + : [tagId]; + handleTagFilterChange(next); +}; + +const clearTagFilter = () => { + if (creatingTag.value) { + creatingTag.value = false; + newTagName.value = ''; } - handleTagFilterChange([...selectedTagIds.value]); + if (editingTagId.value) { + editingTagId.value = null; + editingTagName.value = ''; + } + handleTagFilterChange([]); }; const startCreateTag = () => { @@ -954,15 +912,13 @@ const confirmDeleteTag = (tag: any) => { handleTagFilterChange([...selectedTagIds.value]); } loadTags(kbId.value, true); - // 由于后端是异步删除文档,延迟刷新文档列表 - setTimeout(() => { - resetPage(); // Reset page counter when reloading files after tag deletion - loadKnowledgeFiles(kbId.value); - }, 800); - // 再次延迟刷新标签列表,确保异步删除完成后其他标签的数量正确更新 - setTimeout(() => { - loadTags(kbId.value, true); - }, 2500); + void (async () => { + await new Promise((resolve) => setTimeout(resolve, 800)); + if (!kbId.value) return; + resetPage(); + await loadKnowledgeFiles(kbId.value); + await loadTags(kbId.value, true); + })(); }) .catch((error: any) => { MessagePlugin.error(error?.message || t('common.operationFailed')); @@ -1083,11 +1039,11 @@ watch(() => kbId.value, (newKbId, oldKbId) => { }, { immediate: true }); watch(selectedTagIds, (newVal, oldVal) => { - if (oldVal === undefined) return + if (oldVal === undefined) return; if (kbId.value) { loadKnowledgeFiles(kbId.value); } -}); +}, { deep: true }); watch(tagSearchQuery, (newVal, oldVal) => { if (newVal === oldVal) return; @@ -2279,93 +2235,74 @@ async function createNewSession(value: string): Promise { -
-