Skip to content

Commit 288aa2c

Browse files
committed
feat: Document List - Tag Optimization
1 parent 580bc79 commit 288aa2c

File tree

10 files changed

+218
-88
lines changed

10 files changed

+218
-88
lines changed

apps/knowledge/serializers/document.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,16 +392,34 @@ class Query(serializers.Serializer):
392392
status = serializers.CharField(required=False, label=_('status'), allow_null=True, allow_blank=True)
393393
order_by = serializers.CharField(required=False, label=_('order by'), allow_null=True, allow_blank=True)
394394
tag = serializers.CharField(required=False, label=_('tag'), allow_null=True, allow_blank=True)
395+
tag_ids = serializers.ListField(child=serializers.UUIDField(),allow_null=True,required=False,allow_empty=True)
396+
no_tag = serializers.BooleanField(required=False,default=False, allow_null=True)
395397

396398
def get_query_set(self):
397399
query_set = QuerySet(model=Document)
398400
query_set = query_set.filter(**{'knowledge_id': self.data.get("knowledge_id")})
401+
402+
tag_ids = self.data.get('tag_ids')
403+
no_tag = self.data.get('no_tag')
399404
if 'name' in self.data and self.data.get('name') is not None:
400405
query_set = query_set.filter(**{'name__icontains': self.data.get('name')})
401406
if 'hit_handling_method' in self.data and self.data.get('hit_handling_method') not in [None, '']:
402407
query_set = query_set.filter(**{'hit_handling_method': self.data.get('hit_handling_method')})
403408
if 'is_active' in self.data and self.data.get('is_active') is not None:
404409
query_set = query_set.filter(**{'is_active': self.data.get('is_active')})
410+
if no_tag and tag_ids:
411+
matched_doc_ids = QuerySet(DocumentTag).filter(tag_id__in=tag_ids).values_list('document_id', flat=True)
412+
tagged_doc_ids = QuerySet(DocumentTag).values_list('document_id', flat=True)
413+
query_set = query_set.filter(
414+
Q(id__in=matched_doc_ids) | ~Q(id__in=tagged_doc_ids)
415+
)
416+
elif no_tag:
417+
tagged_doc_ids = QuerySet(DocumentTag).values_list('document_id', flat=True)
418+
query_set = query_set.exclude(id__in=tagged_doc_ids)
419+
elif tag_ids:
420+
matched_doc_ids = QuerySet(DocumentTag).filter(tag_id__in=tag_ids).values_list('document_id', flat=True)
421+
query_set = query_set.filter(id__in=matched_doc_ids)
422+
405423
if 'status' in self.data and self.data.get('status') is not None:
406424
task_type = self.data.get('task_type')
407425
status = self.data.get('status')
Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
11
SELECT * from (
22
SELECT
3-
"document".* ,
4-
to_json("document"."meta") as meta,
5-
to_json("document"."status_meta") as status_meta,
6-
(SELECT "count"("id") FROM "paragraph" WHERE document_id="document"."id") as "paragraph_count"
3+
"document".*,
4+
to_json("document"."meta") as meta,
5+
to_json("document"."status_meta") as status_meta,
6+
(SELECT "count"("id") FROM "paragraph" WHERE document_id = "document"."id") as "paragraph_count",
7+
tag_agg.tag_count as "tag_count",
8+
COALESCE(tag_agg.tags, '[]'::json) as "tags"
79
FROM
8-
"document" "document"
10+
"document" "document"
11+
LEFT JOIN LATERAL (
12+
SELECT
13+
COUNT(*)::int as tag_count,
14+
json_agg(
15+
json_build_object(
16+
'id', "tag"."id",
17+
'key', "tag"."key",
18+
'value', "tag"."value"
19+
)
20+
ORDER BY "tag"."key", "tag"."value"
21+
) as tags
22+
FROM "document_tag" "document_tag"
23+
INNER JOIN "tag" "tag" ON "tag"."id" = "document_tag"."tag_id"
24+
WHERE "document_tag"."document_id" = "document"."id"
25+
) tag_agg ON TRUE
926
${document_custom_sql}
1027
) temp
1128
${order_by_query}

apps/knowledge/views/document.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,13 +639,17 @@ class Page(APIView):
639639
[PermissionConstants.KNOWLEDGE.get_workspace_knowledge_permission()], CompareConstants.AND),
640640
)
641641
def get(self, request: Request, workspace_id: str, knowledge_id: str, current_page: int, page_size: int):
642+
raw_tags = request.query_params.getlist("tags[]")
643+
642644
return result.success(DocumentSerializers.Query(
643645
data={
644646
'workspace_id': workspace_id,
645647
'knowledge_id': knowledge_id,
646648
'folder_id': request.query_params.get('folder_id'),
647649
'name': request.query_params.get('name'),
648650
'tag': request.query_params.get('tag'),
651+
'tag_ids': [tag for tag in raw_tags if tag != 'NO_TAG'],
652+
'no_tag': 'NO_TAG' in raw_tags,
649653
'desc': request.query_params.get("desc"),
650654
'user_id': request.query_params.get('user_id'),
651655
'status': request.query_params.get('status'),

ui/src/locales/lang/en-US/views/document.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export default {
103103
key: 'Tag',
104104
value: 'Value',
105105
addTag: 'Add Tag',
106+
noTag: 'No Tag',
106107
setting: 'Tag Settings',
107108
create: 'Create Tag',
108109
createValue: 'Create Tag Value',

ui/src/locales/lang/zh-CN/views/document.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export default {
9898
key: '标签',
9999
value: '标签值',
100100
addTag: '添加标签',
101+
noTag: '无标签',
101102
addValue: '添加标签值',
102103
setting: '标签设置',
103104
create: '创建标签',

ui/src/locales/lang/zh-Hant/views/document.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export default {
9999
label: '標籤管理',
100100
key: '標籤',
101101
value: '標籤值',
102+
noTag: '無標籤',
102103
addTag: '添加標籤',
103104
setting: '標籤設置',
104105
create: '創建標籤',

ui/src/views/document/index.vue

Lines changed: 143 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -210,74 +210,6 @@
210210
:label="$t('views.document.fileStatus.label')"
211211
width="130"
212212
>
213-
<template #header>
214-
<div>
215-
<span>{{ $t('views.document.fileStatus.label') }}</span>
216-
<el-dropdown trigger="click" @command="dropdownHandle">
217-
<el-button
218-
style="margin-top: 1px"
219-
link
220-
:type="filterMethod['status'] ? 'primary' : ''"
221-
>
222-
<el-icon>
223-
<Filter />
224-
</el-icon>
225-
</el-button>
226-
<template #dropdown>
227-
<el-dropdown-menu style="width: 100px">
228-
<el-dropdown-item
229-
:class="filterMethod['status'] ? '' : 'is-active'"
230-
:command="beforeCommand('status', '')"
231-
class="justify-center"
232-
>{{ $t('common.status.all') }}
233-
</el-dropdown-item>
234-
<el-dropdown-item
235-
:class="filterMethod['status'] === State.SUCCESS ? 'is-active' : ''"
236-
class="justify-center"
237-
:command="beforeCommand('status', State.SUCCESS)"
238-
>{{ $t('common.status.success') }}
239-
</el-dropdown-item>
240-
<el-dropdown-item
241-
:class="filterMethod['status'] === State.FAILURE ? 'is-active' : ''"
242-
class="justify-center"
243-
:command="beforeCommand('status', State.FAILURE)"
244-
>{{ $t('common.status.fail') }}
245-
</el-dropdown-item>
246-
<el-dropdown-item
247-
:class="
248-
filterMethod['status'] === State.STARTED &&
249-
filterMethod['task_type'] == TaskType.EMBEDDING
250-
? 'is-active'
251-
: ''
252-
"
253-
class="justify-center"
254-
:command="beforeCommand('status', State.STARTED, TaskType.EMBEDDING)"
255-
>{{ $t('views.document.fileStatus.EMBEDDING') }}
256-
</el-dropdown-item>
257-
<el-dropdown-item
258-
:class="filterMethod['status'] === State.PENDING ? 'is-active' : ''"
259-
class="justify-center"
260-
:command="beforeCommand('status', State.PENDING)"
261-
>{{ $t('views.document.fileStatus.PENDING') }}
262-
</el-dropdown-item>
263-
<el-dropdown-item
264-
:class="
265-
filterMethod['status'] === State.STARTED &&
266-
filterMethod['task_type'] === TaskType.GENERATE_PROBLEM
267-
? 'is-active'
268-
: ''
269-
"
270-
class="justify-center"
271-
:command="
272-
beforeCommand('status', State.STARTED, TaskType.GENERATE_PROBLEM)
273-
"
274-
>{{ $t('views.document.fileStatus.GENERATE') }}
275-
</el-dropdown-item>
276-
</el-dropdown-menu>
277-
</template>
278-
</el-dropdown>
279-
</div>
280-
</template>
281213
<template #default="{ row }">
282214
<StatusValue :status="row.status" :status-meta="row.status_meta"></StatusValue>
283215
</template>
@@ -357,6 +289,76 @@
357289
</div>
358290
</template>
359291
</el-table-column>
292+
<el-table-column width="160">
293+
<template #header>
294+
<div>
295+
<span>{{ $t('views.document.tag.label') }}</span>
296+
<el-dropdown trigger="click" @visible-change="handleTagVisibleChange">
297+
<el-button
298+
style="margin-top: 1px"
299+
link
300+
:type="filterMethod['tags']?.length > 0 ? 'primary' : ''"
301+
>
302+
<el-icon>
303+
<Filter />
304+
</el-icon>
305+
</el-button>
306+
<template #dropdown>
307+
<div>
308+
<el-cascader-panel
309+
v-model="tagFilterValue"
310+
:options="tagFilterOptions"
311+
:props="{
312+
multiple: true,
313+
checkStrictly: true,
314+
emitPath: false,
315+
showPrefix: false,
316+
}"
317+
@change="(val: any) => dropdownHandle({ attr: 'tags', command: val })"
318+
/>
319+
</div>
320+
</template>
321+
</el-dropdown>
322+
</div>
323+
</template>
324+
<template #default="{ row }">
325+
<el-popover
326+
trigger="hover"
327+
placement="bottom"
328+
:disabled="!row.tag_count"
329+
:width="160"
330+
>
331+
<div v-for="tag in row.tags" :key="tag.id" flex class="pt-4">
332+
<span class="mr-8 color-input-placeholder">{{ tag.key }}</span
333+
>{{ tag.value }}
334+
</div>
335+
336+
<template #reference>
337+
<el-space :size="4">
338+
<el-button
339+
size="small"
340+
style="padding: 1px 6px"
341+
@click.stop="openTagSettingDrawer(row)"
342+
:disabled="!permissionPrecise.doc_tag(id)"
343+
>
344+
<AppIcon iconName="app-tag"></AppIcon>
345+
<span>{{ row.tag_count || 0 }}</span>
346+
</el-button>
347+
<el-button
348+
size="small"
349+
plain
350+
style="padding: 1px 6px; border-style: dashed"
351+
:disabled="!permissionPrecise.doc_tag(id)"
352+
@click.stop="openAddTagDialog(row.id)"
353+
>
354+
<el-icon class="color-secondary"><Plus /></el-icon>
355+
<span class="color-secondary">{{ $t('views.document.tag.key') }}</span>
356+
</el-button>
357+
</el-space>
358+
</template>
359+
</el-popover>
360+
</template>
361+
</el-table-column>
360362
<el-table-column width="170">
361363
<template #header>
362364
<div>
@@ -734,15 +736,23 @@
734736
:workspaceId="knowledgeDetail?.workspace_id"
735737
/>
736738
<GenerateRelatedDialog ref="GenerateRelatedDialogRef" @refresh="getList" :apiType="apiType" />
737-
<TagDrawer ref="tagDrawerRef" />
738-
<TagSettingDrawer ref="tagSettingDrawerRef" />
739+
<TagDrawer ref="tagDrawerRef" @tag-changed="onTagChanged" />
740+
<TagSettingDrawer
741+
ref="tagSettingDrawerRef"
742+
@refresh="
743+
() => {
744+
onTagChanged()
745+
getList()
746+
}
747+
"
748+
/>
739749
<AddTagDialog ref="addTagDialogRef" @addTags="addTags" :apiType="apiType" />
740750
<!-- 执行详情 -->
741751
<ExecutionRecord ref="ListActionRef"></ExecutionRecord>
742752
</div>
743753
</template>
744754
<script setup lang="ts">
745-
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
755+
import { ref, onMounted, onBeforeUnmount, computed, reactive } from 'vue'
746756
import { useRouter, useRoute, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
747757
import type { ElTable } from 'element-plus'
748758
import ImportDocumentDialog from './component/ImportDocumentDialog.vue'
@@ -1359,6 +1369,61 @@ function openGenerateDialog(row?: any) {
13591369
GenerateRelatedDialogRef.value.open(arr, 'document')
13601370
}
13611371
1372+
const tagFilterValue = ref<string[]>([])
1373+
const tagFilterDirty = ref(false)
1374+
const tagFilterOptions = ref<any[]>([])
1375+
const tagFilterLoaded = ref(false)
1376+
const tagFilterLoading = ref(false)
1377+
1378+
function buildTagCascaderOptions(tags: any[]) {
1379+
const options = tags.map((group: any) => ({
1380+
label: group.key,
1381+
value: group.key,
1382+
children: (group.values || []).map((item: any) => ({
1383+
label: item.value,
1384+
value: item.id, // 叶子节点 tag.id
1385+
})),
1386+
}))
1387+
1388+
options.push({
1389+
label: t('views.document.tag.noTag'),
1390+
value: 'NO_TAG',
1391+
children: [],
1392+
})
1393+
1394+
return options
1395+
}
1396+
1397+
async function ensureTagFilterOptions(needRefresh = false) {
1398+
// 非刷新 && 已加载 && 非脏数据
1399+
if (!needRefresh && tagFilterLoaded.value && !tagFilterDirty.value) return
1400+
1401+
try {
1402+
tagFilterLoading.value = true
1403+
const params = {}
1404+
const res: any = await loadSharedApi({
1405+
type: 'knowledge',
1406+
systemType: apiType.value,
1407+
isShared: isShared.value,
1408+
}).getTags(id, params, tagFilterLoading)
1409+
1410+
tagFilterOptions.value = buildTagCascaderOptions(res?.data || [])
1411+
tagFilterLoaded.value = true
1412+
tagFilterDirty.value = false
1413+
} finally {
1414+
tagFilterLoading.value = false
1415+
}
1416+
}
1417+
1418+
async function handleTagVisibleChange(visible: boolean) {
1419+
if (!visible) return
1420+
await ensureTagFilterOptions()
1421+
}
1422+
1423+
function onTagChanged() {
1424+
tagFilterDirty.value = true
1425+
}
1426+
13621427
const tagDrawerRef = ref()
13631428
function openTagDrawer() {
13641429
tagDrawerRef.value.open()
@@ -1371,13 +1436,14 @@ function openTagSettingDrawer(doc: any) {
13711436
13721437
const addTagDialogRef = ref()
13731438
1374-
function openAddTagDialog() {
1375-
addTagDialogRef.value?.open()
1439+
function openAddTagDialog(rowId?: string) {
1440+
addTagDialogRef.value?.open(rowId)
13761441
}
13771442
1378-
function addTags(tags: any) {
1379-
const arr: string[] = multipleSelection.value.map((v) => v.id)
1380-
1443+
function addTags(tags: any, rowId?: string) {
1444+
const arr: string[] = multipleSelection.value.length
1445+
? multipleSelection.value.map((v) => v.id)
1446+
: [rowId]
13811447
loadSharedApi({ type: 'document', systemType: apiType.value })
13821448
.postMulDocumentTags(id, { tag_ids: tags, document_ids: arr }, loading)
13831449
.then(() => {
@@ -1399,7 +1465,7 @@ onMounted(() => {
13991465
}
14001466
getList()
14011467
// 初始化定时任务
1402-
initInterval()
1468+
// initInterval()
14031469
})
14041470
14051471
onBeforeUnmount(() => {

0 commit comments

Comments
 (0)