Skip to content

Commit 662cfe2

Browse files
feat: hit-test and log-message
1 parent 66a0999 commit 662cfe2

29 files changed

Lines changed: 755 additions & 662 deletions

File tree

‎ui/src/assets/iconfont.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎ui/src/components/COMPONENT_README.md‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -539,17 +539,18 @@ Logo 数据统一从 Theme Store 获取。
539539
### MkDragUpload
540540

541541
入口 `mk-drag-upload/index.vue`,内部按单/多文件分离实现。`v-model` 为 `DragUploadFile[]`,
542-
兼容 UploadUserFile,支持 file_id、errMsg、canRetry;类型从组件 `types.ts` 导入。
542+
兼容 UploadUserFile,支持 file_id、errMsg;类型从组件 `types.ts` 导入。
543543

544544
- `multiple` 默认 false;单文件展示卡片和更换入口,多文件保留拖拽区、文件夹选择与列表。
545545
- 多文件 `mode` 默认 `file`,使用文件图标和上传进度;`mode="image"` 按选择顺序展示 104×104 的
546546
`el-image` 方块,不使用 Card,不展示汇总、文件名或状态文案。等待时中央显示空进度条,上传时显示实际进度,
547547
成功或失败后隐藏进度条。右上角提供删除,未上传成功时
548-
在原图上叠加灰色遮罩,成功后移除;网络失败和大小超限使用红框,仅网络失败在中央提供重试。
549-
失败且有错误信息时通过 MkTooltip 展示文件名及失败原因,其他情况禁用提示。内部仍维护上传状态与 uploadingCount,本地缩略图 URL 在文件移除
548+
在原图上叠加灰色遮罩,成功后移除;网络失败和大小超限使用红框,失败时中央显示警告图标,
549+
在其右侧提供重试图标,两个图标整体居中。仅悬停警告图标且有错误信息时通过 MkTooltip
550+
展示文件名及失败原因,重试图标单独提示“重试”,两个提示均定位到图片上方,图片其他区域不提示。内部仍维护上传状态与 uploadingCount,本地缩略图 URL 在文件移除
550551
和组件卸载时释放。单文件不使用 mode。
551552
- `accept`、`limit` 控制格式/数量,多文件 `sizeLimit` 默认 100MB;格式、空文件、重复选择移除,
552-
大小超限保留不可重试 fail 文件,图片模式保留原图和红框;网络失败可重试。
553+
大小超限保留 fail 文件,图片模式保留原图和红框;所有失败均提供重试,重试仍执行原有文件校验。
553554
超量发出 exceed,DS_Store 格式不支持时静默移除。
554555
- `dragText`、`selectText`、`tipText`、`replaceText` 覆盖文案;download 作用域插槽提供下载操作。
555556
- 多文件通过 `inject('upload')` 获取 `DragUploadHandler`,返回 `{ request: Promise<string>, abort }`。
@@ -559,6 +560,16 @@ Logo 数据统一从 Theme Store 获取。
559560
- 暴露 clearFiles()、handleRemove(file)、响应式 uploadingCount;公开选择状态 ready/uploading/success/fail。
560561
页面提供具体上传请求与业务校验,不通过展示插槽控制数量限制。
561562

563+
同目录的 `FileUploadCard.vue` 和 `ImageUploadCard.vue` 可显式导入,供上传列表、对话输入与已发送附件复用。
564+
两者接收只读 `UploadCardFile` 展示数据,统一呈现进度和失败红框,通过 `retry`、`remove` 事件通知使用方,
565+
不请求接口、不管理文件或预览 URL 生命周期。`percentage` 可覆盖文件内的进度,
566+
失败时均显示重试入口,`showRemove` 控制删除入口,默认显示;`disabled` 统一禁用操作。
567+
已发送附件传入 success 状态并设置 `showRemove=false`。
568+
文件卡片通过 `description` 覆盖辅助文案,`name`/`description` 作用域插槽可自定义文字内容;
569+
图片卡片的 `size` 控制缩略图宽高(默认 104px),`iconSize` 控制状态图标大小(默认 16px),
570+
`src` 由使用方提供,`showError` 控制失败警告图标,默认显示。
571+
图片卡片可通过 `previewSrcList` 启用图片预览,`initialIndex` 指定初始索引(默认 0),预览挂载到 body。
572+
562573
### PythonCodeEditor
563574

564575
入口 `codemirror-editor/python.vue`。
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
import { getFileIconUrl } from '@/utils/icon'
4+
import { formatFileSize } from '@/utils/number'
5+
import type { UploadCardFile } from './types'
6+
7+
defineOptions({ name: 'FileUploadCard' })
8+
const props = withDefaults(
9+
defineProps<{ file: UploadCardFile; description?: string; percentage?: number; disabled?: boolean; showRemove?: boolean }>(),
10+
{
11+
disabled: false,
12+
showRemove: true,
13+
},
14+
)
15+
const emit = defineEmits<{ retry: []; remove: [] }>()
16+
defineSlots<{
17+
name?: (props: { file: UploadCardFile }) => unknown
18+
description?: (props: { file: UploadCardFile; percentage: number }) => unknown
19+
}>()
20+
21+
const percentage = computed(() => props.percentage ?? props.file.percentage ?? 0)
22+
</script>
23+
24+
<template>
25+
<el-card shadow="never" class="small relative overflow-hidden!" :class="{ 'border-danger!': file.status === 'fail' }">
26+
<div class="flex-align-center gap-3">
27+
<img :src="getFileIconUrl(file.name)" alt="" class="size-8 shrink-0" />
28+
<div class="min-w-0 flex-1">
29+
<slot name="name" :file="file">
30+
<p class="truncate" :title="file.name">{{ file.name }}</p>
31+
</slot>
32+
<div class="flex-align-center gap-2 text-sm">
33+
<span class="min-w-0 text-N500">
34+
<slot name="description" :file="file" :percentage="percentage">
35+
<template v-if="description !== undefined">{{ description }}</template>
36+
<template v-else>
37+
<template v-if="file.status === 'uploading'">{{ formatFileSize(((file.size ?? 0) * percentage) / 100) }} /</template>
38+
{{ formatFileSize(file.size) }}
39+
</template>
40+
</slot>
41+
</span>
42+
<span v-if="file.status === 'fail'" class="truncate text-danger" :title="file.errMsg">{{ file.errMsg }}</span>
43+
</div>
44+
</div>
45+
<div v-if="showRemove || file.status === 'fail'" class="flex shrink-0 gap-1">
46+
<!-- 重新上传失败附件 -->
47+
<el-button v-if="file.status === 'fail'" :disabled="disabled" text title="重新上传" @click.stop="emit('retry')">
48+
<MkIcon name="icon_refresh_outlined" />
49+
</el-button>
50+
<!-- 删除已选附件 -->
51+
<el-button v-if="showRemove" :disabled="disabled" text title="删除文件" @click.stop="emit('remove')">
52+
<MkIcon name="icon_delete-trash_outlined" />
53+
</el-button>
54+
</div>
55+
</div>
56+
<el-progress
57+
v-if="file.status === 'uploading'"
58+
:percentage="percentage"
59+
:stroke-width="4"
60+
:show-text="false"
61+
class="absolute! inset-x-0 bottom-0"
62+
/>
63+
</el-card>
64+
</template>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
import type { UploadCardFile } from './types'
4+
5+
defineOptions({ name: 'ImageUploadCard' })
6+
const props = withDefaults(
7+
defineProps<{
8+
file: UploadCardFile
9+
src?: string
10+
size?: number
11+
iconSize?: number
12+
percentage?: number
13+
disabled?: boolean
14+
showError?: boolean
15+
showRemove?: boolean
16+
previewSrcList?: string[]
17+
initialIndex?: number
18+
}>(),
19+
{ size: 104, iconSize: 16, disabled: false, showError: true, showRemove: true, initialIndex: 0 },
20+
)
21+
const emit = defineEmits<{ retry: []; remove: [] }>()
22+
const percentage = computed(() => props.percentage ?? props.file.percentage ?? 0)
23+
</script>
24+
25+
<template>
26+
<div class="group relative shrink-0 overflow-hidden rounded-md" :style="{ width: `${size}px`, height: `${size}px` }">
27+
<!-- 查看图片,未提供预览列表时仅展示缩略图。 -->
28+
<el-image
29+
:src="src"
30+
:alt="file.name"
31+
:preview-src-list="previewSrcList"
32+
:initial-index="initialIndex"
33+
:scale="0.8"
34+
fit="cover"
35+
preview-teleported
36+
class="h-full w-full border"
37+
:class="{ 'border-danger!': file.status === 'fail' }"
38+
/>
39+
<div v-if="file.status !== 'success'" class="pointer-events-none absolute inset-0 bg-N900/30" />
40+
<div v-if="file.status === 'ready' || file.status === 'uploading'" class="absolute-center w-3/5">
41+
<el-progress :percentage="file.status === 'uploading' ? percentage : 0" :stroke-width="4" :show-text="false" />
42+
</div>
43+
<div v-if="file.status === 'fail'" class="absolute-center flex-align-center gap-3">
44+
<MkTooltip v-if="showError" :content="`${file.name}:${file.errMsg}`" :disabled="!file.errMsg" placement="top" :offset="size / 2">
45+
<span class="flex-center"><MkIcon name="icon_warning_filled" class="text-danger!" :size="iconSize" /></span>
46+
</MkTooltip>
47+
<!-- 重新上传失败图片 -->
48+
<MkTooltip content="重新上传" placement="top" :offset="size / 2">
49+
<button type="button" class="flex-center cursor-pointer disabled:cursor-not-allowed" :disabled="disabled" @click.stop="emit('retry')">
50+
<MkIcon name="icon_refresh_outlined" class="text-white!" :size="iconSize" />
51+
</button>
52+
</MkTooltip>
53+
</div>
54+
<!-- 删除已选图片 -->
55+
<button
56+
v-if="showRemove"
57+
type="button"
58+
class="group-hover-visible mk-remove-button disabled:cursor-not-allowed"
59+
title="删除图片"
60+
:disabled="disabled"
61+
@click.stop="emit('remove')"
62+
>
63+
<MkIcon name="icon_close_bold_outlined" class="text-white!" :size="6" />
64+
</button>
65+
</div>
66+
</template>

‎ui/src/components/mk-drag-upload/MultipleFileUpload.vue‎

Lines changed: 13 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import type { UploadFile, UploadFiles, UploadInstance, UploadRawFile, UploadUser
44
import type { DragUploadFile, DragUploadHandler, DragUploadMode } from './types'
55
import FileApi from '@/api/admin/file'
66
import uploadImage from '@/assets/mk_icon_upload.svg'
7-
import { getFileIconUrl } from '@/utils/icon'
8-
import { formatFileSize } from '@/utils/number'
97
import { MsgWarning } from '@/utils/message'
8+
import FileUploadCard from './FileUploadCard.vue'
9+
import ImageUploadCard from './ImageUploadCard.vue'
1010
1111
defineOptions({ name: 'MultipleFileUpload' })
1212
@@ -94,7 +94,6 @@ function validateFile(file: DragUploadFile) {
9494
file.status = 'fail'
9595
file.percentage = 0
9696
file.errMsg = `大小超限`
97-
file.canRetry = false
9897
return false
9998
}
10099
const acceptedTypes = props.accept
@@ -127,7 +126,6 @@ function handleUpload(file: DragUploadFile) {
127126
file.status = 'uploading'
128127
file.percentage = 0
129128
file.errMsg = ''
130-
file.canRetry = false
131129
const uid = file.uid
132130
// Promise 链同时覆盖上传接口同步抛错和异步请求失败。
133131
void Promise.resolve()
@@ -149,7 +147,6 @@ function handleUpload(file: DragUploadFile) {
149147
if (file.status !== 'uploading') return
150148
file.status = 'fail'
151149
file.errMsg = '网络失败'
152-
file.canRetry = true
153150
})
154151
.finally(() => uploadRequests.delete(uid))
155152
}
@@ -204,17 +201,17 @@ const uploadSummary = computed(() => {
204201
summary.completed = summary.success + summary.error
205202
return summary
206203
})
207-
const retryFiles = computed(() => fileList.value.filter((file) => file.status === 'fail' && file.canRetry))
204+
const retryFiles = computed(() => fileList.value.filter((file) => file.status === 'fail'))
208205
const sortedFiles = computed(() => [...fileList.value].sort((first, second) => getFileStatusOrder(first) - getFileStatusOrder(second)))
209206
210207
function getFileStatusOrder(file: DragUploadFile) {
211-
if (file.status === 'fail') return file.canRetry ? 0 : 1
208+
if (file.status === 'fail') return 0
212209
if (file.status === 'uploading') return 2
213210
return 3
214211
}
215212
216213
function handleRetry(file: DragUploadFile) {
217-
if (props.disabled || file.status !== 'fail' || !file.canRetry) return
214+
if (props.disabled || file.status !== 'fail' || !validateFile(file)) return
218215
if (upload) handleUpload(file)
219216
else emit('retry', file)
220217
}
@@ -302,67 +299,18 @@ defineExpose({ clearFiles, handleRemove })
302299
</div>
303300
<div v-if="mode === 'image'" class="mt-4 flex flex-wrap gap-3">
304301
<template v-for="file in fileList" :key="file.uid ?? file.name">
305-
<MkTooltip :content="`${file.name}:${file.errMsg}`" :disabled="file.status !== 'fail' || !file.errMsg" placement="top">
306-
<div class="group relative shrink-0 overflow-hidden rounded-md">
307-
<el-image
308-
:src="(file.raw ? imagePreviewUrls.get(file.raw) : undefined) ?? file.url"
309-
fit="cover"
310-
class="size-26"
311-
:class="{ 'border-danger!': file.status === 'fail' }"
312-
></el-image>
313-
314-
<div v-if="file.status !== 'success'" class="pointer-events-none absolute inset-0 bg-N900/30" />
315-
<div v-if="file.status === 'ready' || file.status === 'uploading'" class="absolute-center w-16">
316-
<el-progress :percentage="file.status === 'uploading' ? (file.percentage ?? 0) : 0" :stroke-width="4" :show-text="false" />
317-
</div>
318-
<!-- 重试网络失败的图片 -->
319-
<div v-if="file.status === 'fail' && file.canRetry" class="absolute-center cursor-pointer" @click="handleRetry(file)">
320-
<MkIcon name="icon_refresh_outlined" class="text-white!" />
321-
</div>
322-
<!-- 删除已选图片 -->
323-
<div class="group-hover-visible mk-remove-button" @click="handleRemove(file)">
324-
<MkIcon name="icon_close_bold_outlined" class="text-white!" :size="6" />
325-
</div>
326-
</div>
327-
</MkTooltip>
302+
<ImageUploadCard
303+
:file="file"
304+
:src="(file.raw ? imagePreviewUrls.get(file.raw) : undefined) ?? file.url"
305+
:disabled="disabled"
306+
@retry="handleRetry(file)"
307+
@remove="handleRemove(file)"
308+
/>
328309
</template>
329310
</div>
330311
<div v-else class="grid grid-cols-1 gap-3 md:grid-cols-2">
331312
<template v-for="file in sortedFiles" :key="file.uid ?? file.name">
332-
<el-card class="small relative" shadow="never" :class="{ 'border-danger!': file.status === 'fail' }">
333-
<div class="flex-align-center gap-3">
334-
<img :src="getFileIconUrl(file.name)" alt="" class="size-8 shrink-0" />
335-
<div class="min-w-0 flex-1">
336-
<p class="truncate" :title="file.name">{{ file.name }}</p>
337-
<div class="flex-align-center gap-2 text-sm">
338-
<span class="shrink-0 text-N500">
339-
<template v-if="file.status === 'uploading'">{{ formatFileSize(((file.size ?? 0) * (file.percentage ?? 0)) / 100) }} /</template>
340-
{{ formatFileSize(file.size) }}
341-
</span>
342-
<span v-if="file.status === 'fail'" class="text-danger">
343-
{{ file.errMsg }}
344-
</span>
345-
</div>
346-
</div>
347-
<div class="flex shrink-0 gap-1">
348-
<!-- 重试当前失败文件 -->
349-
<el-button v-if="file.status === 'fail' && file.canRetry" :disabled="disabled" text @click="handleRetry(file)">
350-
<MkIcon name="icon_refresh_outlined" />
351-
</el-button>
352-
<!-- 删除已选文件 -->
353-
<el-button :disabled="disabled" text title="删除文件" @click="handleRemove(file)">
354-
<MkIcon name="icon_delete-trash_outlined" />
355-
</el-button>
356-
</div>
357-
</div>
358-
<el-progress
359-
v-if="file.status === 'uploading'"
360-
:percentage="file.percentage ?? 0"
361-
:stroke-width="4"
362-
:show-text="false"
363-
class="absolute inset-x-0 bottom-0"
364-
/>
365-
</el-card>
313+
<FileUploadCard :file="file" :disabled="disabled" @retry="handleRetry(file)" @remove="handleRemove(file)" />
366314
</template>
367315
</div>
368316
</template>

‎ui/src/components/mk-drag-upload/types.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import type { UploadRawFile, UploadUserFile } from 'element-plus'
44
export interface DragUploadFile extends UploadUserFile {
55
file_id?: string
66
errMsg?: string
7-
canRetry?: boolean
87
}
98

9+
/** 上传卡片的只读展示数据,不依赖具体上传接口。 */
10+
export type UploadCardFile = Pick<DragUploadFile, 'name' | 'size' | 'status' | 'percentage' | 'errMsg'>
11+
1012
/** 由调用方提供具体上传接口,组件管理进度、完成状态和取消。 */
1113
export type DragUploadHandler = (file: UploadRawFile, onProgress: (percent: number) => void) => { request: Promise<string>; abort: () => void }
1214

‎ui/src/constants/CONSTANT_README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ API 枚举值。例如角色类型和登录方式以 `src/api/enums` 为唯一
2626
`knowledge.ts` 的 `KNOWLEDGE_SEARCH_MODE_OPTIONS` 统一维护向量、全文和混合检索的值、文案及说明,
2727
召回测试页与工作流知识库检索节点直接复用,选项值引用 `KNOWLEDGE_SEARCH_MODE`。
2828

29+
`knowledge.ts` 的 `KNOWLEDGE_IMAGE_EXTENSIONS` 维护知识库上传和召回测试支持的图片扩展名,
30+
`KNOWLEDGE_IMAGE_ACCEPT` 和 `KNOWLEDGE_IMAGE_TYPE_LABEL` 由它派生,供文件选择、校验与提示使用。
31+
此配置独立于聊天、工作流等场景使用的通用文件类型配置。
32+
2933
`schedule.ts` 的 `SCHEDULE_OPTION` 统一提供触发器、长期记忆与知识库定时同步的周期级联选项,
3034
包含周期文案、执行时间和间隔数值;使用方只读,配置回填与校验仍由各自组件维护。
3135
周期类型统一使用 `api/enums/schedule.ts` 的 `SCHEDULE_TYPE`,经 `@/api/enums` 导入。

‎ui/src/constants/knowledge.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { KNOWLEDGE_TYPE, KNOWLEDGE_SYNC_TYPE, KNOWLEDGE_SEARCH_MODE } from '@/api/enums'
22
import type { KnowledgeType } from '@/api/types'
33

4+
/** 知识库图片上传与召回测试共用的格式、文件选择范围和展示文案。 */
5+
export const KNOWLEDGE_IMAGE_EXTENSIONS: readonly string[] = ['JPG', 'JPEG', 'PNG', 'WEBP', 'BMP']
6+
export const KNOWLEDGE_IMAGE_ACCEPT = KNOWLEDGE_IMAGE_EXTENSIONS.map((extension) => `.${extension.toLowerCase()}`).join(',')
7+
export const KNOWLEDGE_IMAGE_TYPE_LABEL = KNOWLEDGE_IMAGE_EXTENSIONS.join('、')
8+
49
/** 前端知识库类型标识。 */
510
export const KNOWLEDGE_TYPE_KEY = {
611
BASE: 'BASE',
Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
<template>
2-
<component v-if="typeRenderers[content.type]" :is="typeRenderers[content.type]" :content="content" />
3-
<TextItem v-else :content="content" />
4-
</template>
5-
61
<script setup lang="ts">
2+
import { computed, type Component } from 'vue'
73
import TextItem from './items/text.vue'
84
import ReasoningItem from './items/reasoning.vue'
95
import ToolItem from './items/tool.vue'
106
import FormItem from './items/form.vue'
117
import FailureItem from './items/failure.vue'
128
import QuestionItem from './items/question.vue'
139
14-
const typeRenderers: Record<string, any> = {
10+
defineOptions({ name: 'ConversationContent' })
11+
12+
const props = defineProps<{
13+
content: { type: string; [key: string]: unknown }
14+
}>()
15+
16+
const typeRenderers: Partial<Record<string, Component>> = {
1517
TEXT: TextItem,
1618
REASONING: ReasoningItem,
1719
TOOL: ToolItem,
@@ -20,5 +22,10 @@ const typeRenderers: Record<string, any> = {
2022
QUESTION: QuestionItem,
2123
}
2224
23-
defineProps<{ content: any }>()
25+
// 未识别的内容类型继续按正文展示。
26+
const contentRenderer = computed(() => typeRenderers[props.content.type] ?? TextItem)
2427
</script>
28+
29+
<template>
30+
<component :is="contentRenderer" :content="content" />
31+
</template>

0 commit comments

Comments
 (0)