feat: 改进未分类标签的处理逻辑

将未分类标签从空字符串改为特殊标识符__untagged__,统一前后端处理逻辑
This commit is contained in:
wizardchen
2026-01-04 19:35:12 +08:00
committed by lyingbug
parent 0fc56920d3
commit 4ffda00a86
5 changed files with 96 additions and 35 deletions
+28 -32
View File
@@ -46,10 +46,9 @@ let knowledgeScroll = ref()
let page = 1;
let pageSize = 35;
const selectedTagId = ref<string>("");
const selectedTagId = ref<string>("__untagged__");
const tagList = ref<any[]>([]);
const tagLoading = ref(false);
const overallKnowledgeTotal = ref(0);
const tagSearchQuery = ref('');
const TAG_PAGE_SIZE = 50;
const tagPage = ref(1);
@@ -60,6 +59,7 @@ let tagSearchDebounce: ReturnType<typeof setTimeout> | null = null;
let docSearchDebounce: ReturnType<typeof setTimeout> | null = null;
const docSearchKeyword = ref('');
const selectedFileType = ref('');
const UNTAGGED_TAG_ID = '__untagged__';
const fileTypeOptions = computed(() => [
{ content: t('knowledgeBase.allFileTypes') || '全部类型', value: '' },
{ content: 'PDF', value: 'pdf' },
@@ -73,11 +73,13 @@ const fileTypeOptions = computed(() => [
type TagInputInstance = ComponentPublicInstance<{ focus: () => void; select: () => void }>;
const tagDropdownOptions = computed(() => {
const options = [
{ content: t('knowledgeBase.untagged') || '未分类', value: "" },
...tagList.value.map((tag: any) => ({
content: tag.name,
value: tag.id,
})),
{ content: t('knowledgeBase.untagged') || '未分类', value: UNTAGGED_TAG_ID },
...tagList.value
.filter((tag: any) => tag.id !== UNTAGGED_TAG_ID)
.map((tag: any) => ({
content: tag.name,
value: tag.id,
})),
];
return options;
});
@@ -88,17 +90,15 @@ const tagMap = computed<Record<string, any>>(() => {
});
return map;
});
const sidebarCategoryCount = computed(() => tagList.value.length + 1);
const assignedKnowledgeTotal = computed(() =>
tagList.value.reduce((sum, tag) => sum + (tag.knowledge_count || 0), 0),
);
const untaggedKnowledgeCount = computed(() => {
return Math.max(overallKnowledgeTotal.value - assignedKnowledgeTotal.value, 0);
});
const sidebarCategoryCount = computed(() => tagList.value.length);
// Filter out the untagged pseudo-tag from regular tags
const regularTags = computed(() => tagList.value.filter((tag) => tag.id !== UNTAGGED_TAG_ID));
// Get the untagged pseudo-tag from the list
const untaggedTag = computed(() => tagList.value.find((tag) => tag.id === UNTAGGED_TAG_ID));
const filteredTags = computed(() => {
const query = tagSearchQuery.value.trim().toLowerCase();
if (!query) return tagList.value;
return tagList.value.filter((tag) => (tag.name || '').toLowerCase().includes(query));
if (!query) return regularTags.value;
return regularTags.value.filter((tag) => (tag.name || '').toLowerCase().includes(query));
});
const editingTagInputRefs = new Map<string, TagInputInstance | null>();
@@ -129,6 +129,7 @@ getPageSize()
// 直接调用 API 获取知识库文件列表
const getTagName = (tagId?: string | number) => {
if (!tagId && tagId !== 0) return t('knowledgeBase.untagged') || '未分类';
if (tagId === UNTAGGED_TAG_ID) return t('knowledgeBase.untagged') || '未分类';
const key = String(tagId);
return tagMap.value[key]?.name || (t('knowledgeBase.untagged') || '未分类');
};
@@ -252,8 +253,8 @@ const handleUntaggedClick = () => {
editingTagId.value = null;
editingTagName.value = '';
}
if (selectedTagId.value === '') return;
handleTagFilterChange('');
if (selectedTagId.value === UNTAGGED_TAG_ID) return;
handleTagFilterChange(UNTAGGED_TAG_ID);
};
const startCreateTag = () => {
@@ -363,7 +364,7 @@ const confirmDeleteTag = (tag: any) => {
.then(() => {
MessagePlugin.success(t('knowledgeBase.tagDeleteSuccess'));
if (selectedTagId.value === tag.id) {
handleTagFilterChange('');
handleTagFilterChange(UNTAGGED_TAG_ID);
}
loadTags(kbId.value);
loadKnowledgeFiles(kbId.value);
@@ -375,7 +376,9 @@ const confirmDeleteTag = (tag: any) => {
const handleKnowledgeTagChange = async (knowledgeId: string, tagValue: string) => {
try {
await updateKnowledgeTagBatch({ updates: { [knowledgeId]: tagValue || null } });
// When selecting "untagged", pass null to clear the tag
const tagIdToUpdate = tagValue === UNTAGGED_TAG_ID ? null : (tagValue || null);
await updateKnowledgeTagBatch({ updates: { [knowledgeId]: tagIdToUpdate } });
MessagePlugin.success(t('knowledgeBase.tagUpdateSuccess') || '分类已更新');
loadKnowledgeFiles(kbId.value);
loadTags(kbId.value);
@@ -393,16 +396,14 @@ const loadKnowledgeBaseInfo = async (targetKbId: string) => {
try {
const res: any = await getKnowledgeBaseById(targetKbId);
kbInfo.value = res?.data || null;
selectedTagId.value = "";
selectedTagId.value = UNTAGGED_TAG_ID;
if (!isFAQ.value) {
getKnowled({ page: 1, page_size: pageSize, tag_id: undefined }, targetKbId);
loadKnowledgeFiles(targetKbId);
} else {
cardList.value = [];
total.value = 0;
}
loadTags(targetKbId, true);
overallKnowledgeTotal.value = total.value;
} catch (error) {
console.error('Failed to load knowledge base info:', error);
kbInfo.value = null;
@@ -440,12 +441,6 @@ watch(selectedTagId, (newVal, oldVal) => {
}
});
watch(total, (val) => {
if (selectedTagId.value === '') {
overallKnowledgeTotal.value = val || 0;
}
});
watch(tagSearchQuery, (newVal, oldVal) => {
if (newVal === oldVal) return;
if (tagSearchDebounce) {
@@ -857,7 +852,7 @@ const handleScroll = () => {
if (scrollTop + clientHeight >= scrollHeight) {
page++;
if (cardList.value.length < total.value && page <= pageNum) {
getKnowled({ page, page_size: pageSize, tag_id: selectedTagId.value || undefined, keyword: docSearchKeyword.value ? docSearchKeyword.value.trim() : undefined, file_type: selectedFileType.value || undefined });
getKnowled({ page, page_size: pageSize, tag_id: selectedTagId.value, keyword: docSearchKeyword.value ? docSearchKeyword.value.trim() : undefined, file_type: selectedFileType.value || undefined });
}
}
}
@@ -1009,15 +1004,16 @@ async function createNewSession(value: string): Promise<void> {
<t-loading :loading="tagLoading" size="small">
<div class="tag-list">
<div
v-if="untaggedTag"
class="tag-list-item"
:class="{ active: selectedTagId === '' }"
:class="{ active: selectedTagId === UNTAGGED_TAG_ID }"
@click="handleUntaggedClick"
>
<div class="tag-list-left">
<t-icon name="folder" size="18px" />
<span>{{ $t('knowledgeBase.untagged') || '未分类' }}</span>
</div>
<span class="tag-count">{{ untaggedKnowledgeCount }}</span>
<span class="tag-count">{{ untaggedTag.knowledge_count || 0 }}</span>
</div>
<div v-if="creatingTag" class="tag-list-item tag-editing" @click.stop>
<div class="tag-list-left">
+12 -2
View File
@@ -72,7 +72,12 @@ func (r *knowledgeRepository) ListPagedKnowledgeByKnowledgeBaseID(
query := r.db.WithContext(ctx).Model(&types.Knowledge{}).
Where("tenant_id = ? AND knowledge_base_id = ?", tenantID, kbID)
if tagID != "" {
query = query.Where("tag_id = ?", tagID)
if tagID == types.UntaggedTagID {
// Special value to filter entries without a tag
query = query.Where("tag_id = '' OR tag_id IS NULL")
} else {
query = query.Where("tag_id = ?", tagID)
}
}
if keyword != "" {
query = query.Where("file_name LIKE ?", "%"+keyword+"%")
@@ -96,7 +101,12 @@ func (r *knowledgeRepository) ListPagedKnowledgeByKnowledgeBaseID(
dataQuery := r.db.WithContext(ctx).
Where("tenant_id = ? AND knowledge_base_id = ?", tenantID, kbID)
if tagID != "" {
dataQuery = dataQuery.Where("tag_id = ?", tagID)
if tagID == types.UntaggedTagID {
// Special value to filter entries without a tag
dataQuery = dataQuery.Where("tag_id = '' OR tag_id IS NULL")
} else {
dataQuery = dataQuery.Where("tag_id = ?", tagID)
}
}
if keyword != "" {
dataQuery = dataQuery.Where("file_name LIKE ?", "%"+keyword+"%")
+21
View File
@@ -135,3 +135,24 @@ func (r *knowledgeTagRepository) CountReferences(
}
return
}
// CountUntaggedReferences returns the number of knowledges and chunks without a tag
func (r *knowledgeTagRepository) CountUntaggedReferences(
ctx context.Context,
tenantID uint64,
kbID string,
) (knowledgeCount int64, chunkCount int64, err error) {
if err = r.db.WithContext(ctx).
Model(&types.Knowledge{}).
Where("tenant_id = ? AND knowledge_base_id = ? AND (tag_id = '' OR tag_id IS NULL)", tenantID, kbID).
Count(&knowledgeCount).Error; err != nil {
return
}
if err = r.db.WithContext(ctx).
Model(&types.Chunk{}).
Where("tenant_id = ? AND knowledge_base_id = ? AND (tag_id = '' OR tag_id IS NULL)", tenantID, kbID).
Count(&chunkCount).Error; err != nil {
return
}
return
}
+29 -1
View File
@@ -72,7 +72,29 @@ func (s *knowledgeTagService) ListTags(
return nil, err
}
results := make([]*types.KnowledgeTagWithStats, 0, len(tags))
results := make([]*types.KnowledgeTagWithStats, 0, len(tags)+1)
// Add untagged pseudo-tag at the beginning (only on first page and when no keyword filter)
if page.Page <= 1 && keyword == "" {
untaggedKCount, untaggedCCount, err := s.repo.CountUntaggedReferences(ctx, tenantID, kbID)
if err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{
"kb_id": kbID,
})
return nil, err
}
results = append(results, &types.KnowledgeTagWithStats{
KnowledgeTag: types.KnowledgeTag{
ID: types.UntaggedTagID,
TenantID: tenantID,
KnowledgeBaseID: kbID,
Name: "未分类",
},
KnowledgeCount: untaggedKCount,
ChunkCount: untaggedCCount,
})
}
for _, tag := range tags {
if tag == nil {
continue
@@ -91,6 +113,12 @@ func (s *knowledgeTagService) ListTags(
ChunkCount: cCount,
})
}
// Adjust total to include the untagged pseudo-tag
if keyword == "" {
total++
}
return types.NewPageResult(total, page, results), nil
}
+6
View File
@@ -48,4 +48,10 @@ type KnowledgeTagRepository interface {
kbID string,
tagID string,
) (knowledgeCount int64, chunkCount int64, err error)
// CountUntaggedReferences returns number of knowledges and chunks without a tag.
CountUntaggedReferences(
ctx context.Context,
tenantID uint64,
kbID string,
) (knowledgeCount int64, chunkCount int64, err error)
}