Skip to content

Commit 3eadcf0

Browse files
feat: paragraph image
1 parent 83a045b commit 3eadcf0

6 files changed

Lines changed: 129 additions & 143 deletions

File tree

‎ui/src/api/API_README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,8 @@ Workspace 共享 Paragraph API 仅 getParagraphPage、getParagraphDetail,不
548548
Workspace、System 资源管理、System 共享提供完整问题 API:
549549
`postProblems` 直接提交内容字符串数组,`putProblem` 提交 `{ content }`,单项 DELETE 使用问题 ID,
550550
`putBatchDeleteProblems` 直接提交问题 ID 数组;单个问题内容最多 256 字符。
551-
`getProblemDetail` 返回 `ProblemParagraph[]`,包含分段字段及 knowledge_id、document_name。
551+
`getProblemDetail` 返回 `ProblemParagraph[]`,包含分段字段及 knowledge_id、document_name、knowledge_type、
552+
document_resource_type、document_image_id;来源元数据由后端补齐,图片详情使用 document_image_id 读取原图。
552553
取消关联复用 Paragraph API 的 `putUnassociateParagraphProblem`,不调用问题或分段删除接口。
553554
Workspace 共享仅有 `getProblemsPage`,不补造详情或写接口,不回退其他范围。
554555

‎ui/src/api/types/paragraph.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { RelatedQuestionsConfig } from './knowledge'
1+
import type { KnowledgeType, RelatedQuestionsConfig } from './knowledge'
2+
import type { DocumentResourceType } from './document'
23

34
/** 文档分段的查询响应与新增载荷。 */
45
export interface ParagraphItem {
@@ -29,10 +30,13 @@ export interface ProblemItem extends ParagraphProblem {
2930
update_time: string
3031
}
3132

32-
/** 从问题详情查询到的关联分段,额外携带所属知识库与文档名称。 */
33+
/** 从问题详情查询到的关联分段,包含所属知识库、文档及图片来源信息。 */
3334
export interface ProblemParagraph extends ParagraphItem {
3435
knowledge_id: string
3536
document_name: string
37+
knowledge_type: KnowledgeType | null
38+
document_resource_type: DocumentResourceType
39+
document_image_id: string | null
3640
}
3741

3842
/** 批量将问题关联到文档分段时使用的分段引用。 */

‎ui/src/views/VIEW_README.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
| `knowledge-detail/library/document/`、`tags/` | 文档及标签维护 |
4646
| `knowledge-detail/library/image/` | 图片列表,复用文档操作 |
4747
| `knowledge-detail/hit-test/` | 召回测试参数、文本/图片查询及结果展示 |
48-
| `knowledge-detail/question/` | 问题管理占位页 |
48+
| `knowledge-detail/problem/` | 问题查询、维护与分段关联 |
4949
| `knowledge-detail/termbase/` | 知识库自定义分词维护 |
5050
| `knowledge-detail/chat-user/`、`external-retrieval/` | 对话授权与外部检索配置 |
5151
| `knowledge-detail/setting/base/`、`document-strategy/`、`scheduled-sync/` | 基础设置、文档策略、定时同步与日志 |
@@ -246,14 +246,17 @@ System 资源管理或 System 共享 Knowledge API 的 `postKnowledgeHitTest`;
246246

247247
### 问题管理
248248

249-
`question/` 使用 `resourceApi.problem`,默认每页 20 条,不提供表头排序,搜索将 content 传给后端。
249+
`problem/` 使用 `resourceApi.problem`,默认每页 20 条,不提供表头排序,搜索将 content 传给后端。
250250
`ButtonCreateProblem` 内聚创建按钮和多行输入弹窗;快捷创建使用 `MkQuickCreate`,每个问题最多 256 字符。
251251
单项与批量删除直接提交,删除后末页为空时回退到最后有效页;名称编辑复用 `MkEditName`。
252252
点击数据行打开 `ProblemDetailDrawer`,勾选、名称编辑和操作按钮不触发行详情。
253253
详情支持问题名称快捷编辑、关联分段卡片和跨页上一个/下一个;卡片复用 `.mk-paragraph-card` 样式,
254254
点击打开已有 `ParagraphDialog`,悬浮取消关联按钮仅解除当前关联,成功刷新卡片与列表计数。
255-
分段详情关闭后重查关联,保持编辑与解除关联后的数据一致。
256-
单项、批量、详情的“关联分段”入口暂时禁用,待交互确认后接入,不创建关联弹窗或发送关联请求。
255+
分段详情关闭后重查关联,保持编辑与解除关联后的数据一致。关联卡片按接口来源字段显示图片、Web 或文件图标,
256+
图片原图引用同时传给分段详情;加载失败支持重试。抽屉暴露 `refresh()`,由问题页在批量关联成功后刷新当前关联分段。
257+
单项、批量、详情的“关联分段”入口统一使用 `AssociateParagraphDialog`,勾选仅维护本次选择,
258+
跨文档、图片类型及搜索保留选择;确认后通过 `putBatchAssociateProblems` 一次提交问题与分段列表,
259+
成功关闭并刷新问题列表,失败保留选择,取消不提交。弹窗不回填既有关联或执行解除关联,解除关联仍由问题详情维护。
257260
Workspace 共享只展示和搜索问题列表,不显示维护入口,也不打开缺少查询接口的问题详情。
258261

259262
## 文档分段
Lines changed: 55 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
<script setup lang="ts">
2-
import { computed, nextTick, ref, useTemplateRef } from 'vue'
2+
import { computed, ref, useTemplateRef } from 'vue'
33
import type MkViewLayout from '@/components/global/mk-view-layout/index.vue'
44
import MkSearchList from '@/components/mk-search-list/index.vue'
55
import type { ParamsPage } from '@/api/admin/core/types'
66
import { DOCUMENT_RESOURCE_TYPE } from '@/api/enums'
77
import type { DocumentItem, DocumentResourceType, ParagraphItem, ProblemParagraphReference } from '@/api/types'
8+
import { KNOWLEDGE_TYPE_KEY, KNOWLEDGE_TYPE_MAP } from '@/constants/knowledge'
89
import { getFileUrl } from '@/utils/common'
910
import { getFileIconUrl } from '@/utils/icon'
1011
import { MsgSuccess } from '@/utils/message'
1112
import { useKnowledgeDetailContext } from '@/views/knowledge-detail/context'
12-
import ParagraphDialog from '@/views/document-detail/paragraph-dialog/ParagraphDialog.vue'
1313
1414
defineOptions({ name: 'AssociateParagraphDialog' })
1515
const emit = defineEmits<{ success: [] }>()
@@ -26,12 +26,9 @@ const selectedDocumentId = ref('')
2626
const paragraphSearchText = ref('')
2727
const paragraphs = ref<ParagraphItem[]>([])
2828
const selectedParagraphs = ref<ProblemParagraphReference[]>([])
29-
const detailParagraph = ref<ParagraphItem>()
3029
const infiniteScrollRef = useTemplateRef<{ reset: () => Promise<void> }>('infiniteScrollRef')
3130
const paragraphLayoutRef = useTemplateRef<InstanceType<typeof MkViewLayout>>('paragraphLayoutRef')
32-
const paragraphDialogRef = useTemplateRef<InstanceType<typeof ParagraphDialog>>('paragraphDialogRef')
3331
34-
const isBatchAssociation = computed(() => problemIds.value.length > 1)
3532
const selectedDocument = computed(() => documentData.value.find(({ id }) => id === selectedDocumentId.value))
3633
const selectedParagraphIds = computed(() => new Set(selectedParagraphs.value.map(({ paragraph_id }) => paragraph_id)))
3734
@@ -91,85 +88,43 @@ function handleParagraphSearch() {
9188
return refreshParagraphs()
9289
}
9390
94-
function removeSelectedParagraph(paragraphId: string) {
95-
selectedParagraphs.value = selectedParagraphs.value.filter(({ paragraph_id }) => paragraph_id !== paragraphId)
96-
}
97-
98-
function addSelectedParagraph(paragraph: ParagraphItem) {
99-
selectedParagraphs.value = [...selectedParagraphs.value, { paragraph_id: paragraph.id, document_id: paragraph.document_id }]
91+
// 勾选仅维护本次批量关联的选择,确认后统一提交。
92+
function handleToggleParagraph(paragraph: ParagraphItem) {
93+
if (operationLoading.value) return
94+
if (isSelected(paragraph.id)) {
95+
selectedParagraphs.value = selectedParagraphs.value.filter(({ paragraph_id }) => paragraph_id !== paragraph.id)
96+
} else {
97+
selectedParagraphs.value.push({ paragraph_id: paragraph.id, document_id: paragraph.document_id })
98+
}
10099
}
101100
102-
function updateParagraphAssociation(paragraph: ParagraphItem, unlink: boolean) {
103-
if (operationLoading.value) return
104-
const problemId = problemIds.value[0]
105-
if (!problemId) return
101+
function handleSubmit() {
102+
if (operationLoading.value || documentLoading.value || !problemIds.value.length || !selectedParagraphs.value.length) return
106103
operationLoading.value = true
107-
// TODO(图片联调):核对图片分段的关联及取消关联是否复用现有 document_id / paragraph_id 协议,成功后重新打开确认回显。
108-
const request = unlink
109-
? resourceApi.paragraph.putUnassociateParagraphProblem(knowledgeId.value, paragraph.document_id, paragraph.id, problemId)
110-
: resourceApi.problem.putBatchAssociateProblems(knowledgeId.value, {
111-
problem_id_list: [...problemIds.value],
112-
paragraph_list: [{ paragraph_id: paragraph.id, document_id: paragraph.document_id }],
113-
})
114-
return request
104+
return resourceApi.problem
105+
.putBatchAssociateProblems(knowledgeId.value, {
106+
problem_id_list: [...problemIds.value],
107+
paragraph_list: [...selectedParagraphs.value],
108+
})
115109
.then(() => {
116-
if (unlink) {
117-
removeSelectedParagraph(paragraph.id)
118-
} else {
119-
addSelectedParagraph(paragraph)
120-
}
121-
MsgSuccess(unlink ? '取消关联成功' : '关联成功')
110+
MsgSuccess('关联成功')
111+
visible.value = false
122112
emit('success')
123113
})
124114
.catch(() => {
125-
// 请求层统一提示错误,失败时保留当前选择。
115+
// 请求层统一提示错误,失败时保留本次选择以便重试。
126116
})
127117
.finally(() => {
128118
operationLoading.value = false
129119
})
130120
}
131121
132-
function handleAssociateParagraph(paragraph: ParagraphItem) {
133-
if (isSelected(paragraph.id)) return
134-
return updateParagraphAssociation(paragraph, false)
135-
}
136-
137-
function handleUnassociateParagraph(paragraph: ParagraphItem) {
138-
// 批量关联提交后不支持在此弹窗批量解除关联,避免只解除首个问题的关联。
139-
if (isBatchAssociation.value || !isSelected(paragraph.id)) return
140-
return updateParagraphAssociation(paragraph, true)
141-
}
142-
143-
async function handleOpenParagraphDetail(paragraph: ParagraphItem) {
144-
if (operationLoading.value || detailParagraph.value) return
145-
detailParagraph.value = paragraph
146-
await nextTick()
147-
paragraphDialogRef.value?.open(paragraph)
148-
}
149-
150-
function handleParagraphDialogClosed() {
151-
detailParagraph.value = undefined
152-
const problemId = problemIds.value[0]
153-
if (isBatchAssociation.value || !problemId) return
154-
// 分段详情可修改问题关联,关闭后重新读取当前问题的关联状态。
155-
void resourceApi.problem.getProblemDetail(knowledgeId.value, problemId).then((associatedParagraphs) => {
156-
selectedParagraphs.value = associatedParagraphs.map(({ id, document_id }) => ({ paragraph_id: id, document_id }))
157-
})
158-
}
159-
160122
function open(targetProblemIds: string[]) {
161123
if (visible.value || operationLoading.value || !targetProblemIds[0]) return
162124
problemIds.value = [...targetProblemIds]
163125
visible.value = true
164126
documentLoading.value = true
165-
return Promise.all([
166-
loadDocuments(),
167-
// TODO(图片联调):确认已关联分段查询包含图片分段,并返回 id、document_id,供选中态和关联数量回显。
168-
targetProblemIds.length === 1 ? resourceApi.problem.getProblemDetail(knowledgeId.value, targetProblemIds[0]) : [],
169-
])
170-
.then(([, problemParagraphs]) => {
171-
selectedParagraphs.value = problemParagraphs.map(({ id, document_id }) => ({ paragraph_id: id, document_id }))
172-
})
127+
return loadDocuments()
173128
.catch(() => {
174129
// 请求层统一提示错误,失败时保持空列表。
175130
})
@@ -186,7 +141,6 @@ function handleClosed() {
186141
paragraphSearchText.value = ''
187142
paragraphs.value = []
188143
selectedParagraphs.value = []
189-
detailParagraph.value = undefined
190144
resourceType.value = DOCUMENT_RESOURCE_TYPE.DOCUMENT
191145
}
192146
@@ -203,6 +157,7 @@ defineExpose({ open })
203157
<div v-loading="documentLoading" class="flex-column h-full min-h-0 pt-4" :inert="operationLoading">
204158
<!-- 切换文档或图片资源 -->
205159
<el-tabs
160+
v-if="resourceType === DOCUMENT_RESOURCE_TYPE.IMAGE"
206161
v-model="resourceType"
207162
class="mx-4 mb-4"
208163
:before-leave="() => !documentLoading && !operationLoading"
@@ -212,13 +167,7 @@ defineExpose({ open })
212167
<el-tab-pane label="图片" :name="DOCUMENT_RESOURCE_TYPE.IMAGE" />
213168
</el-tabs>
214169
<!-- 搜索并切换候选分段所在资源 -->
215-
<MkSearchList
216-
v-model="documentSearchText"
217-
:data="documentData"
218-
:default-active="selectedDocumentId"
219-
:empty-text="resourceType === DOCUMENT_RESOURCE_TYPE.IMAGE ? '暂无图片' : '暂无文档'"
220-
@click="handleSelectDocument"
221-
>
170+
<MkSearchList v-model="documentSearchText" :data="documentData" :default-active="selectedDocumentId" @click="handleSelectDocument">
222171
<template #default="{ row }">
223172
<!-- TODO 图片 -->
224173
<el-image
@@ -228,6 +177,12 @@ defineExpose({ open })
228177
:alt="row.name"
229178
fit="cover"
230179
/>
180+
<img
181+
v-else-if="KNOWLEDGE_TYPE_MAP[row.type] === KNOWLEDGE_TYPE_KEY.WEB"
182+
src="@/assets/file-type/web-link-icon.svg"
183+
alt=""
184+
class="size-4"
185+
/>
231186
<img v-else :src="getFileIconUrl(row.name)" class="mr-2 size-4 shrink-0" alt="" />
232187
<span class="min-w-0 flex-1 truncate" :title="row.name">{{ row.name }}</span>
233188
<span v-if="getSelectedCount(row.id)" class="ml-1 flex-center h-4 min-w-4 shrink-0 rounded-full bg-primary px-1 text-xs text-white">
@@ -253,37 +208,29 @@ defineExpose({ open })
253208
>
254209
<div class="space-y-2">
255210
<template v-for="paragraph in paragraphs" :key="paragraph.id">
256-
<!-- 查看当前分段详情及其关联状态 -->
211+
<!-- 勾选或取消本次批量关联的分段 -->
257212
<el-card
258213
shadow="never"
259214
class="mk-paragraph-card group relative cursor-pointer"
260215
:class="{ active: isSelected(paragraph.id), disabled: operationLoading }"
261-
@click="handleOpenParagraphDetail(paragraph)"
216+
@click="handleToggleParagraph(paragraph)"
262217
>
263-
<!-- 关联当前问题与分段 -->
264-
<MkTooltip v-if="!isSelected(paragraph.id)" content="关联" placement="top">
265-
<el-button
266-
circle
267-
class="group-hover-visible absolute top-2 right-2 shadow-md"
268-
:disabled="operationLoading"
269-
@click.stop="handleAssociateParagraph(paragraph)"
270-
>
271-
<MkIcon name="icon_link-record_outlined" />
272-
</el-button>
273-
</MkTooltip>
274-
<!-- 取消当前问题与分段的关联 -->
275-
<MkTooltip v-else-if="!isBatchAssociation" content="取消关联" placement="top">
276-
<el-button
277-
circle
278-
class="group-hover-visible absolute top-2 right-2 shadow-md"
279-
:disabled="operationLoading"
280-
@click.stop="handleUnassociateParagraph(paragraph)"
281-
>
282-
<MkIcon name="icon_unlink_outlined" />
283-
</el-button>
284-
</MkTooltip>
285-
<h4 class="mb-2">{{ paragraph.title || '-' }}</h4>
218+
<!-- 勾选当前分段用于批量关联 -->
219+
<el-checkbox
220+
class="absolute! top-4 right-4"
221+
:model-value="isSelected(paragraph.id)"
222+
@click.stop
223+
@change="handleToggleParagraph(paragraph)"
224+
/>
286225

226+
<el-image
227+
v-if="resourceType === DOCUMENT_RESOURCE_TYPE.IMAGE"
228+
:src="getFileUrl(selectedDocument.meta?.source_file_id as string | undefined)"
229+
class="mr-2 size-6 shrink-0"
230+
:alt="selectedDocument.name"
231+
fit="cover"
232+
/>
233+
<h4 class="mb-2">{{ paragraph.title || '-' }}</h4>
287234
<MdPreview :model-value="paragraph.content" />
288235
<p class="mt-2 text-sm text-N600">{{ paragraph.content.length }} 字符</p>
289236
</el-card>
@@ -297,15 +244,15 @@ defineExpose({ open })
297244
</template>
298245
</MkViewLayout>
299246
<template #footer>
300-
<div class="text-left text-sm text-N600">已关联 {{ selectedParagraphs.length }}</div>
247+
<div class="flex-between">
248+
<div class="text-sm text-N600">已选择 {{ selectedParagraphs.length }}</div>
249+
<div>
250+
<!-- 取消本次批量关联 -->
251+
<el-button plain :disabled="operationLoading || documentLoading" @click="visible = false">取消</el-button>
252+
<!-- 确认批量关联所选分段 -->
253+
<el-button type="primary" :loading="operationLoading" @click="handleSubmit">确认</el-button>
254+
</div>
255+
</div>
301256
</template>
302257
</MkDialog>
303-
<ParagraphDialog
304-
v-if="detailParagraph"
305-
ref="paragraphDialogRef"
306-
title="分段详情"
307-
:knowledge-id="knowledgeId"
308-
:document-id="detailParagraph.document_id"
309-
@closed="handleParagraphDialogClosed"
310-
/>
311258
</template>

0 commit comments

Comments
 (0)