Skip to content

Commit 0413dc6

Browse files
feat: Workflow file upload adds progress bar
1 parent 35cbd57 commit 0413dc6

12 files changed

Lines changed: 392 additions & 41 deletions

File tree

ui/src/api/application/application.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import { Result } from '@/request/Result'
2-
import { get, post, postStream, del, put, request, download, exportFile } from '@/request/index'
2+
import {
3+
get,
4+
post,
5+
postUpload,
6+
postStream,
7+
del,
8+
put,
9+
request,
10+
download,
11+
exportFile,
12+
} from '@/request/index'
313
import type { pageRequest } from '@/api/type/common'
414
import type { ApplicationFormType } from '@/api/type/application'
515
import { type Ref } from 'vue'
@@ -417,6 +427,47 @@ const postUploadFile: (
417427
return post(`/oss/file`, fd, undefined, loading)
418428
}
419429

430+
/**
431+
* 上传文件(支持上传进度回调与中断)
432+
* @param file
433+
* @param sourceId
434+
* @param resourceType
435+
* @param onProgress 上传进度回调,参数为百分比(0-100)
436+
* @param loading
437+
* @returns 返回 { request, abort },request 为异步 promise 对象,abort 用于中断上传
438+
*/
439+
const postUploadFileProgress: (
440+
file: any,
441+
sourceId: string,
442+
resourceType:
443+
| 'KNOWLEDGE'
444+
| 'APPLICATION'
445+
| 'TOOL'
446+
| 'DOCUMENT'
447+
| 'CHAT'
448+
| 'TEMPORARY_30_MINUTE'
449+
| 'TEMPORARY_120_MINUTE'
450+
| 'TEMPORARY_1_DAY',
451+
onProgress?: (percent: number, event: any) => void,
452+
loading?: Ref<boolean>,
453+
) => { request: Promise<Result<any>>; abort: () => void } = (
454+
file,
455+
sourceId,
456+
resourceType,
457+
onProgress,
458+
loading,
459+
) => {
460+
const fd = new FormData()
461+
fd.append('file', file)
462+
fd.append('source_id', sourceId)
463+
fd.append('source_type', resourceType)
464+
return postUpload(`/oss/file`, fd, onProgress, undefined, loading)
465+
}
466+
467+
const deleteFile: (file_id: string) => Promise<Result<any>> = (file_id) => {
468+
return del(`/oss/file/${file_id}`)
469+
}
470+
420471
const getFile: (application_id: string, params: any) => Promise<Result<any>> = (
421472
application_id,
422473
params,
@@ -495,6 +546,7 @@ export default {
495546
speechToText,
496547
getMcpTools,
497548
postUploadFile,
549+
postUploadFileProgress,
498550
generate_prompt,
499551
getTokenUsage,
500552
topQuestions,
@@ -503,4 +555,5 @@ export default {
503555
delMulApplication,
504556
putMulMoveApplication,
505557
putMulCleanTime,
558+
deleteFile,
506559
}

ui/src/api/chat/chat.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Result } from '@/request/Result'
22
import {
33
get,
44
post,
5+
postUpload,
56
postStream,
67
del,
78
put,
@@ -342,6 +343,43 @@ const postUploadFile: (
342343
return post(`/oss/file`, fd, undefined, loading)
343344
}
344345

346+
/**
347+
* 上传文件(支持上传进度回调与中断)
348+
* @param file
349+
* @param sourceId 资源id
350+
* @param resourceType 资源类型
351+
* @param onProgress 上传进度回调,参数为百分比(0-100)
352+
* @param loading
353+
* @returns 返回 { request, abort },request 为异步 promise 对象,abort 用于中断上传
354+
*/
355+
const postUploadFileProgress: (
356+
file: any,
357+
sourceId: string,
358+
resourceType:
359+
| 'KNOWLEDGE'
360+
| 'APPLICATION'
361+
| 'TOOL'
362+
| 'DOCUMENT'
363+
| 'CHAT'
364+
| 'TEMPORARY_30_MINUTE'
365+
| 'TEMPORARY_120_MINUTE'
366+
| 'TEMPORARY_1_DAY',
367+
onProgress?: (percent: number, event: any) => void,
368+
loading?: Ref<boolean>,
369+
) => { request: Promise<Result<any>>; abort: () => void } = (
370+
file,
371+
sourceId,
372+
sourceType,
373+
onProgress,
374+
loading,
375+
) => {
376+
const fd = new FormData()
377+
fd.append('file', file)
378+
fd.append('source_id', sourceId)
379+
fd.append('source_type', sourceType)
380+
return postUpload(`/oss/file`, fd, onProgress, undefined, loading)
381+
}
382+
345383
const getFile: (application_id: string, params: any) => Promise<Result<any>> = (
346384
application_id,
347385
params,
@@ -398,6 +436,7 @@ export default {
398436
clearChat,
399437
modifyChat,
400438
postUploadFile,
439+
postUploadFileProgress,
401440
getFile,
402441
postShareChat,
403442
getShareLink,

ui/src/components/dynamics-form/items/upload/LocalFileUpload.vue

Lines changed: 150 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div v-loading="loading" class="w-full">
2+
<div class="w-full">
33
<el-upload
44
ref="UploadRef"
55
:webkitdirectory="false"
@@ -37,36 +37,89 @@
3737
</div>
3838
</div>
3939
</el-upload>
40-
<el-row :gutter="8" v-if="modelValue?.length" class="mt-16">
41-
<template v-for="(item, index) in modelValue" :key="index">
40+
<div v-if="fileArray?.length" class="flex-between w-full mt-16">
41+
<span>
42+
{{
43+
$t('dynamicsForm.UploadInput.uploadStatus', {
44+
success: successCount,
45+
total: fileArray.length,
46+
})
47+
}}
48+
</span>
49+
<span v-if="uploadingCount" class="flex align-center">
50+
<el-icon class="is-loading color-primary" size="18"><Loading /></el-icon>
51+
<span class="ml-4">{{ $t('dynamicsForm.UploadInput.uploading') }}</span>
52+
</span>
53+
<span v-else-if="errorCount" class="flex align-center">
54+
<el-icon class="color-danger ml-4" size="18"><WarningFilled /></el-icon>
55+
<span class="ml-4">
56+
{{ $t('dynamicsForm.UploadInput.failedStatus', { count: errorCount }) }}
57+
</span>
58+
<el-button v-if="retryList.length" text @click="retryAll">
59+
<AppIcon iconName="app-refresh"></AppIcon>
60+
{{ $t('dynamicsForm.UploadInput.reUpload') }}
61+
</el-button>
62+
</span>
63+
</div>
64+
<el-row :gutter="8" v-if="fileArray?.length" class="mt-8">
65+
<template v-for="(item, index) in fileArray" :key="index">
4266
<el-col :span="12" class="mb-8">
43-
<el-card shadow="never" style="--el-card-padding: 8px 12px; line-height: normal">
67+
<el-card
68+
shadow="never"
69+
style="
70+
--el-card-padding: 8px 12px;
71+
line-height: normal;
72+
position: relative;
73+
overflow: hidden;
74+
"
75+
:class="item.status === 'error' ? 'border-danger' : ''"
76+
>
4477
<div class="flex-between">
4578
<div class="flex">
4679
<img :src="getImgUrl(item && item?.name)" alt="" width="40" />
4780
<div class="ml-8">
4881
<p class="ellipsis-1" :title="item && item?.name">{{ item && item?.name }}</p>
49-
<el-text type="info" size="small">{{
50-
filesize(item && item?.size) || '0K'
51-
}}</el-text>
82+
<el-text type="info" size="small">
83+
<template v-if="item.status === 'uploading'">
84+
{{ filesize((item.size * item.percentage) / 100) }} /
85+
{{ filesize(item.size) || '0K' }}
86+
</template>
87+
<template v-else>{{ filesize(item && item?.size) || '0K' }}</template>
88+
</el-text>
89+
<el-text class="ml-8" v-if="item.status === 'error'" type="danger" size="small">
90+
{{ item.errMsg }}
91+
</el-text>
5292
</div>
5393
</div>
54-
<el-button text @click="deleteFile(index)">
55-
<AppIcon iconName="app-delete"></AppIcon>
56-
</el-button>
94+
<div class="flex align-center">
95+
<el-button v-if="item.canRetry" text @click="uploadFile(item)">
96+
<AppIcon iconName="app-refresh"></AppIcon>
97+
</el-button>
98+
<el-button text @click="deleteFile(index, item)">
99+
<AppIcon iconName="app-delete"></AppIcon>
100+
</el-button>
101+
</div>
57102
</div>
103+
<el-progress
104+
v-if="item.status === 'uploading'"
105+
class="card-progress"
106+
:percentage="item.percentage"
107+
:stroke-width="4"
108+
:show-text="false"
109+
/>
58110
</el-card>
59111
</el-col>
60112
</template>
61113
</el-row>
62114
</div>
63115
</template>
64116
<script setup lang="ts">
65-
import { computed, useAttrs, nextTick, inject, ref } from 'vue'
117+
import { computed, useAttrs, nextTick, inject, ref, reactive } from 'vue'
66118
import type { FormField } from '@/components/dynamics-form/type'
67119
import { MsgError } from '@/utils/message'
68120
import type { UploadFiles } from 'element-plus'
69121
import { filesize, getImgUrl, fileType } from '@/utils/common'
122+
import applicationApi from '@/api/application/application'
70123
import { t } from '@/locales'
71124
const upload = inject('upload') as any
72125
const attrs = useAttrs() as any
@@ -81,48 +134,111 @@ const onExceed = () => {
81134
)
82135
}
83136
const emit = defineEmits(['update:modelValue'])
137+
84138
const fileArray = ref<any>([])
85-
const loading = ref<boolean>(false)
86-
const UploadRef = ref()
139+
const loading = ref(false)
140+
// 上传成功数量
141+
const successCount = computed(
142+
() => fileArray.value.filter((i: any) => i.status !== 'uploading').length,
143+
)
144+
// 上传失败数量
145+
const errorCount = computed(() => fileArray.value.filter((i: any) => i.status === 'error').length)
146+
// 上传中数量
147+
const uploadingCount = computed(
148+
() => fileArray.value.filter((i: any) => i.status === 'uploading').length,
149+
)
150+
// 可重新上传的失败项(网络错误等)
151+
const retryList = computed(() =>
152+
fileArray.value.filter((i: any) => i.status === 'error' && i.canRetry),
153+
)
154+
// 重新上传所有可重试的失败文件
155+
const retryAll = () => {
156+
retryList.value.forEach((i: any) => uploadFile(i))
157+
}
87158
// 上传on-change事件
88159
const fileHandleChange = (file: any, fileList: UploadFiles) => {
89-
// 按文件唯一标识精确定位并移除当前文件
90-
// 注意:不能使用 splice(-1, 1) 盲删末尾元素,文件夹上传时会误删正常文件而放走超限文件
91-
const removeCurrentFile = () => {
92-
const index = fileList.findIndex((item: any) => item.uid === file.uid)
93-
if (index !== -1) {
94-
fileList.splice(index, 1)
95-
}
96-
}
160+
const item = reactive({
161+
name: file.name,
162+
size: file.size,
163+
file_id: '',
164+
percentage: 0,
165+
status: 'uploading' as 'uploading' | 'success' | 'error',
166+
errMsg: '',
167+
canRetry: false,
168+
raw: file.raw,
169+
abort: null as null | (() => void),
170+
aborted: false,
171+
})
172+
97173
//1、判断文件大小是否合法,文件限制不能大于100M
98174
const isLimit = file?.size / 1024 / 1024 < file_size_limit.value
99175
if (!isLimit) {
100-
MsgError(t('views.document.tip.fileLimitSizeTip1') + file_size_limit.value + 'MB')
101-
removeCurrentFile() //移除当前超出大小的文件
176+
item.status = 'error'
177+
item.errMsg = t('dynamicsForm.UploadInput.errorTip.sizeError')
178+
// MsgError(t('views.document.tip.fileLimitSizeTip1') + file_size_limit.value + 'MB')
179+
// fileList.splice(-1, 1) //移除当前超出大小的文件
180+
fileArray.value?.push(item)
102181
return false
103182
}
104183
if (!file_type_list.value.includes(fileType(file.name).toLocaleUpperCase())) {
105184
if (file?.name !== '.DS_Store') {
106185
MsgError(t('views.document.upload.errorMessage2'))
107186
}
108-
removeCurrentFile()
109187
return false
110188
}
111189
112190
if (file?.size === 0) {
113191
MsgError(t('views.document.upload.errorMessage3'))
114-
removeCurrentFile()
115192
return false
116193
}
117-
upload(file.raw, loading).then((ok: any) => {
118-
const split_path = ok.data.split('/')
119-
const file_id = split_path[split_path.length - 1]
120-
fileArray.value?.push({ name: file.name, file_id, size: file.size })
121-
emit('update:modelValue', fileArray.value)
122-
})
194+
195+
fileArray.value?.push(item)
196+
uploadFile(item)
123197
}
124-
function deleteFile(index: number) {
125-
props.modelValue.splice(index, 1)
198+
// 执行上传
199+
const uploadFile = (item: any) => {
200+
item.status = 'uploading'
201+
item.percentage = 0
202+
item.errMsg = ''
203+
item.canRetry = false
204+
item.aborted = false
205+
const res: any = upload(
206+
item.raw,
207+
(percent: number) => {
208+
item.percentage = percent
209+
},
210+
loading,
211+
)
212+
// provider 返回 { request, abort } 时保存中断方法,删除时可中断上传
213+
item.abort = typeof res?.abort === 'function' ? res.abort : null
214+
const request: Promise<any> = res?.then ? res : res?.request
215+
request
216+
.then((ok: any) => {
217+
const split_path = ok.data.split('/')
218+
item.file_id = split_path[split_path.length - 1]
219+
item.percentage = 100
220+
item.status = 'success'
221+
emit('update:modelValue', fileArray.value)
222+
})
223+
.catch(() => {
224+
// 主动中断(删除)导致的失败不再标记错误
225+
if (item.aborted) return
226+
item.status = 'error'
227+
item.errMsg = t('dynamicsForm.UploadInput.errorTip.networkError')
228+
item.canRetry = true
229+
})
230+
}
231+
function deleteFile(index: any, item?: any) {
232+
// 上传过程中删除则中断上传请求
233+
if (item?.status === 'uploading' && typeof item.abort === 'function') {
234+
item.aborted = true
235+
item.abort()
236+
}
237+
fileArray.value.splice(index, 1)
238+
// if (item?.file_id) {
239+
// applicationApi.deleteFile(item.file_id)
240+
// }
241+
emit('update:modelValue', fileArray.value)
126242
}
127243
128244
const handlePreview = (bool: boolean) => {
@@ -150,5 +266,4 @@ const file_count_limit = computed(() => {
150266
return attrs.file_count_limit || 100
151267
})
152268
</script>
153-
<style lang="scss" scoped>
154-
</style>
269+
<style lang="scss" scoped></style>

0 commit comments

Comments
 (0)