feat: 改进未分类标签的处理逻辑
将未分类标签从空字符串改为特殊标识符__untagged__,统一前后端处理逻辑
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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+"%")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user