11<script setup lang="ts">
2- import { computed , nextTick , ref , useTemplateRef } from ' vue'
2+ import { computed , ref , useTemplateRef } from ' vue'
33import type MkViewLayout from ' @/components/global/mk-view-layout/index.vue'
44import MkSearchList from ' @/components/mk-search-list/index.vue'
55import type { ParamsPage } from ' @/api/admin/core/types'
66import { DOCUMENT_RESOURCE_TYPE } from ' @/api/enums'
77import type { DocumentItem , DocumentResourceType , ParagraphItem , ProblemParagraphReference } from ' @/api/types'
8+ import { KNOWLEDGE_TYPE_KEY , KNOWLEDGE_TYPE_MAP } from ' @/constants/knowledge'
89import { getFileUrl } from ' @/utils/common'
910import { getFileIconUrl } from ' @/utils/icon'
1011import { MsgSuccess } from ' @/utils/message'
1112import { useKnowledgeDetailContext } from ' @/views/knowledge-detail/context'
12- import ParagraphDialog from ' @/views/document-detail/paragraph-dialog/ParagraphDialog.vue'
1313
1414defineOptions ({ name: ' AssociateParagraphDialog' })
1515const emit = defineEmits <{ success: [] }>()
@@ -26,12 +26,9 @@ const selectedDocumentId = ref('')
2626const paragraphSearchText = ref (' ' )
2727const paragraphs = ref <ParagraphItem []>([])
2828const selectedParagraphs = ref <ProblemParagraphReference []>([])
29- const detailParagraph = ref <ParagraphItem >()
3029const infiniteScrollRef = useTemplateRef <{ reset: () => Promise <void > }>(' infiniteScrollRef' )
3130const paragraphLayoutRef = useTemplateRef <InstanceType <typeof MkViewLayout >>(' paragraphLayoutRef' )
32- const paragraphDialogRef = useTemplateRef <InstanceType <typeof ParagraphDialog >>(' paragraphDialogRef' )
3331
34- const isBatchAssociation = computed (() => problemIds .value .length > 1 )
3532const selectedDocument = computed (() => documentData .value .find (({ id }) => id === selectedDocumentId .value ))
3633const 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-
160122function 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