Skip to content

Commit 4ffda00

Browse files
committed
feat: 改进未分类标签的处理逻辑
将未分类标签从空字符串改为特殊标识符__untagged__,统一前后端处理逻辑
1 parent 0fc5692 commit 4ffda00

File tree

5 files changed

+96
-35
lines changed

5 files changed

+96
-35
lines changed

frontend/src/views/knowledge/KnowledgeBase.vue

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,9 @@ let knowledgeScroll = ref()
4646
let page = 1;
4747
let pageSize = 35;
4848
49-
const selectedTagId = ref<string>("");
49+
const selectedTagId = ref<string>("__untagged__");
5050
const tagList = ref<any[]>([]);
5151
const tagLoading = ref(false);
52-
const overallKnowledgeTotal = ref(0);
5352
const tagSearchQuery = ref('');
5453
const TAG_PAGE_SIZE = 50;
5554
const tagPage = ref(1);
@@ -60,6 +59,7 @@ let tagSearchDebounce: ReturnType<typeof setTimeout> | null = null;
6059
let docSearchDebounce: ReturnType<typeof setTimeout> | null = null;
6160
const docSearchKeyword = ref('');
6261
const selectedFileType = ref('');
62+
const UNTAGGED_TAG_ID = '__untagged__';
6363
const fileTypeOptions = computed(() => [
6464
{ content: t('knowledgeBase.allFileTypes') || '全部类型', value: '' },
6565
{ content: 'PDF', value: 'pdf' },
@@ -73,11 +73,13 @@ const fileTypeOptions = computed(() => [
7373
type TagInputInstance = ComponentPublicInstance<{ focus: () => void; select: () => void }>;
7474
const tagDropdownOptions = computed(() => {
7575
const options = [
76-
{ content: t('knowledgeBase.untagged') || '未分类', value: "" },
77-
...tagList.value.map((tag: any) => ({
78-
content: tag.name,
79-
value: tag.id,
80-
})),
76+
{ content: t('knowledgeBase.untagged') || '未分类', value: UNTAGGED_TAG_ID },
77+
...tagList.value
78+
.filter((tag: any) => tag.id !== UNTAGGED_TAG_ID)
79+
.map((tag: any) => ({
80+
content: tag.name,
81+
value: tag.id,
82+
})),
8183
];
8284
return options;
8385
});
@@ -88,17 +90,15 @@ const tagMap = computed<Record<string, any>>(() => {
8890
});
8991
return map;
9092
});
91-
const sidebarCategoryCount = computed(() => tagList.value.length + 1);
92-
const assignedKnowledgeTotal = computed(() =>
93-
tagList.value.reduce((sum, tag) => sum + (tag.knowledge_count || 0), 0),
94-
);
95-
const untaggedKnowledgeCount = computed(() => {
96-
return Math.max(overallKnowledgeTotal.value - assignedKnowledgeTotal.value, 0);
97-
});
93+
const sidebarCategoryCount = computed(() => tagList.value.length);
94+
// Filter out the untagged pseudo-tag from regular tags
95+
const regularTags = computed(() => tagList.value.filter((tag) => tag.id !== UNTAGGED_TAG_ID));
96+
// Get the untagged pseudo-tag from the list
97+
const untaggedTag = computed(() => tagList.value.find((tag) => tag.id === UNTAGGED_TAG_ID));
9898
const filteredTags = computed(() => {
9999
const query = tagSearchQuery.value.trim().toLowerCase();
100-
if (!query) return tagList.value;
101-
return tagList.value.filter((tag) => (tag.name || '').toLowerCase().includes(query));
100+
if (!query) return regularTags.value;
101+
return regularTags.value.filter((tag) => (tag.name || '').toLowerCase().includes(query));
102102
});
103103
104104
const editingTagInputRefs = new Map<string, TagInputInstance | null>();
@@ -129,6 +129,7 @@ getPageSize()
129129
// 直接调用 API 获取知识库文件列表
130130
const getTagName = (tagId?: string | number) => {
131131
if (!tagId && tagId !== 0) return t('knowledgeBase.untagged') || '未分类';
132+
if (tagId === UNTAGGED_TAG_ID) return t('knowledgeBase.untagged') || '未分类';
132133
const key = String(tagId);
133134
return tagMap.value[key]?.name || (t('knowledgeBase.untagged') || '未分类');
134135
};
@@ -252,8 +253,8 @@ const handleUntaggedClick = () => {
252253
editingTagId.value = null;
253254
editingTagName.value = '';
254255
}
255-
if (selectedTagId.value === '') return;
256-
handleTagFilterChange('');
256+
if (selectedTagId.value === UNTAGGED_TAG_ID) return;
257+
handleTagFilterChange(UNTAGGED_TAG_ID);
257258
};
258259
259260
const startCreateTag = () => {
@@ -363,7 +364,7 @@ const confirmDeleteTag = (tag: any) => {
363364
.then(() => {
364365
MessagePlugin.success(t('knowledgeBase.tagDeleteSuccess'));
365366
if (selectedTagId.value === tag.id) {
366-
handleTagFilterChange('');
367+
handleTagFilterChange(UNTAGGED_TAG_ID);
367368
}
368369
loadTags(kbId.value);
369370
loadKnowledgeFiles(kbId.value);
@@ -375,7 +376,9 @@ const confirmDeleteTag = (tag: any) => {
375376
376377
const handleKnowledgeTagChange = async (knowledgeId: string, tagValue: string) => {
377378
try {
378-
await updateKnowledgeTagBatch({ updates: { [knowledgeId]: tagValue || null } });
379+
// When selecting "untagged", pass null to clear the tag
380+
const tagIdToUpdate = tagValue === UNTAGGED_TAG_ID ? null : (tagValue || null);
381+
await updateKnowledgeTagBatch({ updates: { [knowledgeId]: tagIdToUpdate } });
379382
MessagePlugin.success(t('knowledgeBase.tagUpdateSuccess') || '分类已更新');
380383
loadKnowledgeFiles(kbId.value);
381384
loadTags(kbId.value);
@@ -393,16 +396,14 @@ const loadKnowledgeBaseInfo = async (targetKbId: string) => {
393396
try {
394397
const res: any = await getKnowledgeBaseById(targetKbId);
395398
kbInfo.value = res?.data || null;
396-
selectedTagId.value = "";
399+
selectedTagId.value = UNTAGGED_TAG_ID;
397400
if (!isFAQ.value) {
398-
getKnowled({ page: 1, page_size: pageSize, tag_id: undefined }, targetKbId);
399401
loadKnowledgeFiles(targetKbId);
400402
} else {
401403
cardList.value = [];
402404
total.value = 0;
403405
}
404406
loadTags(targetKbId, true);
405-
overallKnowledgeTotal.value = total.value;
406407
} catch (error) {
407408
console.error('Failed to load knowledge base info:', error);
408409
kbInfo.value = null;
@@ -440,12 +441,6 @@ watch(selectedTagId, (newVal, oldVal) => {
440441
}
441442
});
442443
443-
watch(total, (val) => {
444-
if (selectedTagId.value === '') {
445-
overallKnowledgeTotal.value = val || 0;
446-
}
447-
});
448-
449444
watch(tagSearchQuery, (newVal, oldVal) => {
450445
if (newVal === oldVal) return;
451446
if (tagSearchDebounce) {
@@ -857,7 +852,7 @@ const handleScroll = () => {
857852
if (scrollTop + clientHeight >= scrollHeight) {
858853
page++;
859854
if (cardList.value.length < total.value && page <= pageNum) {
860-
getKnowled({ page, page_size: pageSize, tag_id: selectedTagId.value || undefined, keyword: docSearchKeyword.value ? docSearchKeyword.value.trim() : undefined, file_type: selectedFileType.value || undefined });
855+
getKnowled({ page, page_size: pageSize, tag_id: selectedTagId.value, keyword: docSearchKeyword.value ? docSearchKeyword.value.trim() : undefined, file_type: selectedFileType.value || undefined });
861856
}
862857
}
863858
}
@@ -1009,15 +1004,16 @@ async function createNewSession(value: string): Promise<void> {
10091004
<t-loading :loading="tagLoading" size="small">
10101005
<div class="tag-list">
10111006
<div
1007+
v-if="untaggedTag"
10121008
class="tag-list-item"
1013-
:class="{ active: selectedTagId === '' }"
1009+
:class="{ active: selectedTagId === UNTAGGED_TAG_ID }"
10141010
@click="handleUntaggedClick"
10151011
>
10161012
<div class="tag-list-left">
10171013
<t-icon name="folder" size="18px" />
10181014
<span>{{ $t('knowledgeBase.untagged') || '未分类' }}</span>
10191015
</div>
1020-
<span class="tag-count">{{ untaggedKnowledgeCount }}</span>
1016+
<span class="tag-count">{{ untaggedTag.knowledge_count || 0 }}</span>
10211017
</div>
10221018
<div v-if="creatingTag" class="tag-list-item tag-editing" @click.stop>
10231019
<div class="tag-list-left">

internal/application/repository/knowledge.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,12 @@ func (r *knowledgeRepository) ListPagedKnowledgeByKnowledgeBaseID(
7272
query := r.db.WithContext(ctx).Model(&types.Knowledge{}).
7373
Where("tenant_id = ? AND knowledge_base_id = ?", tenantID, kbID)
7474
if tagID != "" {
75-
query = query.Where("tag_id = ?", tagID)
75+
if tagID == types.UntaggedTagID {
76+
// Special value to filter entries without a tag
77+
query = query.Where("tag_id = '' OR tag_id IS NULL")
78+
} else {
79+
query = query.Where("tag_id = ?", tagID)
80+
}
7681
}
7782
if keyword != "" {
7883
query = query.Where("file_name LIKE ?", "%"+keyword+"%")
@@ -96,7 +101,12 @@ func (r *knowledgeRepository) ListPagedKnowledgeByKnowledgeBaseID(
96101
dataQuery := r.db.WithContext(ctx).
97102
Where("tenant_id = ? AND knowledge_base_id = ?", tenantID, kbID)
98103
if tagID != "" {
99-
dataQuery = dataQuery.Where("tag_id = ?", tagID)
104+
if tagID == types.UntaggedTagID {
105+
// Special value to filter entries without a tag
106+
dataQuery = dataQuery.Where("tag_id = '' OR tag_id IS NULL")
107+
} else {
108+
dataQuery = dataQuery.Where("tag_id = ?", tagID)
109+
}
100110
}
101111
if keyword != "" {
102112
dataQuery = dataQuery.Where("file_name LIKE ?", "%"+keyword+"%")

internal/application/repository/tag.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,24 @@ func (r *knowledgeTagRepository) CountReferences(
135135
}
136136
return
137137
}
138+
139+
// CountUntaggedReferences returns the number of knowledges and chunks without a tag
140+
func (r *knowledgeTagRepository) CountUntaggedReferences(
141+
ctx context.Context,
142+
tenantID uint64,
143+
kbID string,
144+
) (knowledgeCount int64, chunkCount int64, err error) {
145+
if err = r.db.WithContext(ctx).
146+
Model(&types.Knowledge{}).
147+
Where("tenant_id = ? AND knowledge_base_id = ? AND (tag_id = '' OR tag_id IS NULL)", tenantID, kbID).
148+
Count(&knowledgeCount).Error; err != nil {
149+
return
150+
}
151+
if err = r.db.WithContext(ctx).
152+
Model(&types.Chunk{}).
153+
Where("tenant_id = ? AND knowledge_base_id = ? AND (tag_id = '' OR tag_id IS NULL)", tenantID, kbID).
154+
Count(&chunkCount).Error; err != nil {
155+
return
156+
}
157+
return
158+
}

internal/application/service/tag.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,29 @@ func (s *knowledgeTagService) ListTags(
7272
return nil, err
7373
}
7474

75-
results := make([]*types.KnowledgeTagWithStats, 0, len(tags))
75+
results := make([]*types.KnowledgeTagWithStats, 0, len(tags)+1)
76+
77+
// Add untagged pseudo-tag at the beginning (only on first page and when no keyword filter)
78+
if page.Page <= 1 && keyword == "" {
79+
untaggedKCount, untaggedCCount, err := s.repo.CountUntaggedReferences(ctx, tenantID, kbID)
80+
if err != nil {
81+
logger.ErrorWithFields(ctx, err, map[string]interface{}{
82+
"kb_id": kbID,
83+
})
84+
return nil, err
85+
}
86+
results = append(results, &types.KnowledgeTagWithStats{
87+
KnowledgeTag: types.KnowledgeTag{
88+
ID: types.UntaggedTagID,
89+
TenantID: tenantID,
90+
KnowledgeBaseID: kbID,
91+
Name: "未分类",
92+
},
93+
KnowledgeCount: untaggedKCount,
94+
ChunkCount: untaggedCCount,
95+
})
96+
}
97+
7698
for _, tag := range tags {
7799
if tag == nil {
78100
continue
@@ -91,6 +113,12 @@ func (s *knowledgeTagService) ListTags(
91113
ChunkCount: cCount,
92114
})
93115
}
116+
117+
// Adjust total to include the untagged pseudo-tag
118+
if keyword == "" {
119+
total++
120+
}
121+
94122
return types.NewPageResult(total, page, results), nil
95123
}
96124

internal/types/interfaces/tag.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,10 @@ type KnowledgeTagRepository interface {
4848
kbID string,
4949
tagID string,
5050
) (knowledgeCount int64, chunkCount int64, err error)
51+
// CountUntaggedReferences returns number of knowledges and chunks without a tag.
52+
CountUntaggedReferences(
53+
ctx context.Context,
54+
tenantID uint64,
55+
kbID string,
56+
) (knowledgeCount int64, chunkCount int64, err error)
5157
}

0 commit comments

Comments
 (0)