Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ui/src/styles/STYLE_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ Admin 和 Chat 共用。组件不单独恢复焦点 `outline`,包括通过隐
单元格最小宽度为 120px、最大宽度为 400px,正文正常换行并允许长链接折行;表头不换行,
避免多列时标题和正文被压缩成逐字纵向排列。滚动条沿用 Markdown 主题已有样式。

## 数量徽标

`app.scss` 的 `.mk-badge` 用于行内数量展示,统一提供 16px 高度与最小宽度、水平居中、圆角、
主题色背景和白色小字号文字;是否显示及数量内容由使用方控制。

## 右上角移除按钮

`app.scss` 的 button 分组提供 `.mk-remove-button`,用于图片、附件等内容的右上角移除入口。
Expand Down
17 changes: 17 additions & 0 deletions ui/src/styles/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,23 @@ h6 {
line-height: var(--mk-line-height-base);
}

/* badge */
.mk-badge {
align-items: center;
background-color: var(--mk-primary);
border-radius: 9999px;
color: white;
display: flex;
flex-shrink: 0;
font-size: 12px;
height: 16px;
justify-content: center;
line-height: 20px;
font-weight: 500;
min-width: 16px;
padding: 0 4px;
}

/* button */
// 资源菜单的导入按钮
.mk-import-button {
Expand Down
7 changes: 4 additions & 3 deletions ui/src/views/VIEW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,10 +253,11 @@ System 资源管理或 System 共享 Knowledge API 的 `postKnowledgeHitTest`;
详情支持问题名称快捷编辑、关联分段卡片和跨页上一个/下一个;卡片复用 `.mk-paragraph-card` 样式,
点击打开已有 `ParagraphDialog`,悬浮取消关联按钮仅解除当前关联,成功刷新卡片与列表计数。
分段详情关闭后重查关联,保持编辑与解除关联后的数据一致。关联卡片按接口来源字段显示图片、Web 或文件图标,
图片原图引用同时传给分段详情;加载失败支持重试。抽屉暴露 `refresh()`,由问题页在批量关联成功后刷新当前关联分段。
图片原图引用同时传给分段详情;请求失败由请求层统一提示,保留当前关联内容。抽屉暴露 `refresh()`,由问题页在批量关联成功后刷新当前关联分段。
单项、批量、详情的“关联分段”入口统一使用 `AssociateParagraphDialog`,勾选仅维护本次选择,
跨文档、图片类型及搜索保留选择;确认后通过 `putBatchAssociateProblems` 一次提交问题与分段列表,
成功关闭并刷新问题列表,失败保留选择,取消不提交。弹窗不回填既有关联或执行解除关联,解除关联仍由问题详情维护。
跨文档、图片类型及搜索保留选择;单个问题打开时查询已有关系并回填勾选,所有选项均可切换,批量问题打开时只维护新增选择。
确认后通过 `putBatchAssociateProblems` 一次提交问题与新增分段列表;没有新增选择时直接关闭。
成功关闭并刷新问题列表,失败保留选择,取消不提交。解除关联仍由问题详情维护。
Workspace 共享只展示和搜索问题列表,不显示维护入口,也不打开缺少查询接口的问题详情。

## 文档分段
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const selectedDocumentId = ref('')
const paragraphSearchText = ref('')
const paragraphs = ref<ParagraphItem[]>([])
const selectedParagraphs = ref<ProblemParagraphReference[]>([])
const associatedParagraphIds = ref(new Set<string>())
const infiniteScrollRef = useTemplateRef<{ reset: () => Promise<void> }>('infiniteScrollRef')
const paragraphLayoutRef = useTemplateRef<InstanceType<typeof MkViewLayout>>('paragraphLayoutRef')

Expand Down Expand Up @@ -88,7 +89,7 @@ function handleParagraphSearch() {
return refreshParagraphs()
}

// 勾选仅维护本次批量关联的选择,确认后统一提交。
// 已关联项回显后也可切换勾选,确认时只提交新增关联。
function handleToggleParagraph(paragraph: ParagraphItem) {
if (operationLoading.value) return
if (isSelected(paragraph.id)) {
Expand All @@ -99,12 +100,17 @@ function handleToggleParagraph(paragraph: ParagraphItem) {
}

function handleSubmit() {
if (operationLoading.value || documentLoading.value || !problemIds.value.length || !selectedParagraphs.value.length) return
if (operationLoading.value || documentLoading.value || !problemIds.value.length) return
const paragraphReferences = selectedParagraphs.value.filter(({ paragraph_id }) => !associatedParagraphIds.value.has(paragraph_id))
if (!paragraphReferences.length) {
visible.value = false
return
}
operationLoading.value = true
return resourceApi.problem
.putBatchAssociateProblems(knowledgeId.value, {
problem_id_list: [...problemIds.value],
paragraph_list: [...selectedParagraphs.value],
paragraph_list: paragraphReferences,
})
.then(() => {
MsgSuccess('关联成功')
Expand All @@ -124,7 +130,14 @@ function open(targetProblemIds: string[]) {
problemIds.value = [...targetProblemIds]
visible.value = true
documentLoading.value = true
return loadDocuments()
return Promise.all([
loadDocuments(),
targetProblemIds.length === 1 ? resourceApi.problem.getProblemDetail(knowledgeId.value, targetProblemIds[0]) : [],
])
.then(([, problemParagraphs]) => {
associatedParagraphIds.value = new Set(problemParagraphs.map(({ id }) => id))
selectedParagraphs.value = problemParagraphs.map(({ id, document_id }) => ({ paragraph_id: id, document_id }))
})
.catch(() => {
// 请求层统一提示错误,失败时保持空列表。
})
Expand All @@ -141,6 +154,7 @@ function handleClosed() {
paragraphSearchText.value = ''
paragraphs.value = []
selectedParagraphs.value = []
associatedParagraphIds.value.clear()
resourceType.value = DOCUMENT_RESOURCE_TYPE.DOCUMENT
}

Expand All @@ -157,7 +171,6 @@ defineExpose({ open })
<div v-loading="documentLoading" class="flex-column h-full min-h-0 pt-4" :inert="operationLoading">
<!-- 切换文档或图片资源 -->
<el-tabs
v-if="resourceType === DOCUMENT_RESOURCE_TYPE.IMAGE"
v-model="resourceType"
class="mx-4 mb-4"
:before-leave="() => !documentLoading && !operationLoading"
Expand All @@ -169,25 +182,27 @@ defineExpose({ open })
<!-- 搜索并切换候选分段所在资源 -->
<MkSearchList v-model="documentSearchText" :data="documentData" :default-active="selectedDocumentId" @click="handleSelectDocument">
<template #default="{ row }">
<!-- TODO 图片 -->
<el-image
v-if="resourceType === DOCUMENT_RESOURCE_TYPE.IMAGE"
:src="getFileUrl(row.meta?.source_file_id as string | undefined)"
class="mr-2 size-6 shrink-0"
:alt="row.name"
fit="cover"
/>
<img
v-else-if="KNOWLEDGE_TYPE_MAP[row.type] === KNOWLEDGE_TYPE_KEY.WEB"
src="@/assets/file-type/web-link-icon.svg"
alt=""
class="size-4"
/>
<img v-else :src="getFileIconUrl(row.name)" class="mr-2 size-4 shrink-0" alt="" />
<span class="min-w-0 flex-1 truncate" :title="row.name">{{ row.name }}</span>
<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">
{{ getSelectedCount(row.id) }}
</span>
<div class="flex-align-center gap-2 min-w-0">
<el-image
v-if="resourceType === DOCUMENT_RESOURCE_TYPE.IMAGE"
:src="getFileUrl(row.meta?.source_file_id as string | undefined)"
class="size-6 shrink-0"
:alt="row.name"
fit="cover"
/>
<img
v-else-if="KNOWLEDGE_TYPE_MAP[row.type] === KNOWLEDGE_TYPE_KEY.WEB"
src="@/assets/file-type/web-link-icon.svg"
alt=""
class="size-4"
/>
<img v-else :src="getFileIconUrl(row.name)" class="size-4 shrink-0" alt="" />
<span class="min-w-0 flex-1 truncate" :title="row.name">{{ row.name }}</span>
<!-- 关联数字 -->
<span v-if="getSelectedCount(row.id)" class="mk-badge">
{{ getSelectedCount(row.id) }}
</span>
</div>
</template>
</MkSearchList>
</div>
Expand Down Expand Up @@ -225,11 +240,12 @@ defineExpose({ open })

<el-image
v-if="resourceType === DOCUMENT_RESOURCE_TYPE.IMAGE"
class="w-fit [&_img]:size-auto! [&_img]:max-h-50 mb-2"
:src="getFileUrl(selectedDocument.meta?.source_file_id as string | undefined)"
class="mr-2 size-6 shrink-0"
:alt="selectedDocument.name"
fit="cover"
fit="contain"
/>

<h4 class="mb-2">{{ paragraph.title || '-' }}</h4>
<MdPreview :model-value="paragraph.content" />
<p class="mt-2 text-sm text-N600">{{ paragraph.content.length }} 字符</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,20 @@ const emit = defineEmits<{ previous: []; next: []; refresh: []; closed: []; asso
const { knowledgeId, resourceApi } = useKnowledgeDetailContext({ resourceScopes: ['workspace', 'system-resource', 'system-shared'] as const })
const visible = ref(true)
const loading = ref(false)
const loadFailed = ref(false)
const paragraphs = ref<ProblemParagraph[]>([])
const selectedParagraph = ref<ProblemParagraph>()
const paragraphDialogRef = ref<InstanceType<typeof ParagraphDialog>>()

// 关联分段查询:同步数量,失败时保留当前内容并支持重试。
// 关联分段查询:同步数量,失败时保留当前内容。
function loadParagraphs() {
loadFailed.value = false
return resourceApi.problem
.getProblemDetail(knowledgeId.value, problem.value.id)
.then((records) => {
paragraphs.value = records
problem.value.paragraph_count = records.length
})
.catch(() => {
loadFailed.value = true
// 请求层统一提示错误,失败时保留当前关联。
})
}

Expand Down Expand Up @@ -121,34 +119,36 @@ defineExpose({ refresh: handleLoadParagraphs })
<div v-if="paragraphs.length" class="flex-column gap-3">
<template v-for="paragraph in paragraphs" :key="paragraph.id">
<!-- 查看关联分段详情 -->
<el-card class="mk-paragraph-card group relative cursor-pointer" shadow="never" @click="handleOpenParagraph(paragraph)">
<el-card
class="mk-paragraph-card group relative cursor-pointer overflow-visible!"
body-class="overflow-visible!"
shadow="never"
@click="handleOpenParagraph(paragraph)"
>
<!-- 取消当前问题与分段的关联,不删除分段 -->
<MkTooltip content="取消关联" placement="top">
<el-button
circle
class="group-hover-visible absolute top-3 right-3 shadow-md"
:disabled="loading"
@click.stop="handleUnlinkParagraph(paragraph)"
>
<MkIcon name="icon_unlink_outlined" />
</el-button>
</MkTooltip>
<div class="group-hover-visible sticky top-3 z-20 h-0" @click.stop>
<MkTooltip content="取消关联" placement="top">
<el-button circle class="absolute top-0 right-0 shadow-md" :disabled="loading" @click.stop="handleUnlinkParagraph(paragraph)">
<MkIcon name="icon_unlink_outlined" />
</el-button>
</MkTooltip>
</div>

<!-- 图片文档显示图片 -->
<el-image
v-if="paragraph.document_resource_type === DOCUMENT_RESOURCE_TYPE.IMAGE && paragraph.document_image_id"
:src="getFileUrl(paragraph.document_image_id)"
:alt="paragraph.title || ''"
fit="contain"
class="mb-2"
class="w-fit [&_img]:size-auto! [&_img]:max-h-50 mb-2"
/>

<h4 class="mb-2">{{ paragraph.title || '-' }}</h4>
<MdPreview :model-value="paragraph.content" />
<div class="flex-align-center gap-2 text-sm text-N600">
<span class="shrink-0">{{ paragraph.content.length }} 字符</span>
<el-divider direction="vertical" />
<el-image v-if="paragraph.document_resource_type === DOCUMENT_RESOURCE_TYPE.IMAGE" :src="fileImageIcon" class="size-4 shrink-0" />
<img v-if="paragraph.document_resource_type === DOCUMENT_RESOURCE_TYPE.IMAGE" :src="fileImageIcon" class="size-4 shrink-0" />
<img
v-else-if="paragraph.knowledge_type != null && KNOWLEDGE_TYPE_MAP[paragraph.knowledge_type] === KNOWLEDGE_TYPE_KEY.WEB"
src="@/assets/file-type/web-link-icon.svg"
Expand All @@ -161,11 +161,7 @@ defineExpose({ refresh: handleLoadParagraphs })
</el-card>
</template>
</div>
<MkEmpty v-else-if="!loading && !loadFailed" description="暂无关联分段" />
<MkEmpty v-if="loadFailed" description="关联分段加载失败">
<!-- 重新加载当前问题的关联分段 -->
<el-button type="primary" :loading="loading" @click="handleLoadParagraphs">重新加载</el-button>
</MkEmpty>
<MkEmpty v-else-if="!loading" description="暂无关联分段" />
</div>
<template #footer>
<!-- 关联当前问题的分段 -->
Expand Down
Loading