239 lines
8.2 KiB
Vue
239 lines
8.2 KiB
Vue
<template>
|
||
<div class="upload">
|
||
<el-upload
|
||
v-model:file-list="fileList"
|
||
ref="uploadRefs"
|
||
:action="action"
|
||
:multiple="multiple"
|
||
:limit="limit"
|
||
:show-file-list="false"
|
||
:headers="headers"
|
||
:data="data"
|
||
:http-request="useDirect ? httpRequest : undefined"
|
||
:on-progress="handleProgress"
|
||
:on-success="handleSuccess"
|
||
:on-exceed="handleExceed"
|
||
:on-error="handleError"
|
||
:accept="getAccept"
|
||
>
|
||
<slot />
|
||
</el-upload>
|
||
<el-dialog
|
||
v-if="showProgress && fileList.length"
|
||
v-model="visible"
|
||
title="上传进度"
|
||
:close-on-click-modal="false"
|
||
width="500px"
|
||
:modal="false"
|
||
@close="handleClose"
|
||
>
|
||
<div class="file-list p-4">
|
||
<template v-for="(item, index) in fileList" :key="index">
|
||
<div class="mb-5">
|
||
<div>{{ item.name }}</div>
|
||
<div class="flex-1">
|
||
<el-progress :percentage="parseInt(item.percentage)" />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts">
|
||
import type { ElUpload, UploadRequestOptions } from 'element-plus'
|
||
import { computed, defineComponent, ref, shallowRef } from 'vue'
|
||
|
||
import config from '@/config'
|
||
import { RequestCodeEnum } from '@/enums/requestEnums'
|
||
import useAppStore from '@/stores/modules/app'
|
||
import useUserStore from '@/stores/modules/user'
|
||
import feedback from '@/utils/feedback'
|
||
import {
|
||
DirectUploadFallbackError,
|
||
uploadVideoDirectToCos
|
||
} from '@/utils/oss-direct-upload'
|
||
|
||
export default defineComponent({
|
||
components: {},
|
||
props: {
|
||
// 上传文件类型
|
||
type: {
|
||
type: String,
|
||
default: 'image'
|
||
},
|
||
// 是否支持多选
|
||
multiple: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// 多选时最多选择几条
|
||
limit: {
|
||
type: Number,
|
||
default: 10
|
||
},
|
||
// 上传时的额外参数
|
||
data: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
// 是否显示上传进度
|
||
showProgress: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
// 视频直传到 OSS(绕开服务器中转,仅 type=video 生效)
|
||
direct: {
|
||
type: Boolean,
|
||
default: false
|
||
}
|
||
},
|
||
emits: ['change', 'error', 'success', 'allSuccess'],
|
||
setup(props, { emit }) {
|
||
const userStore = useUserStore()
|
||
const appStore = useAppStore()
|
||
const uploadRefs = shallowRef<InstanceType<typeof ElUpload>>()
|
||
const action = ref(`${config.baseUrl}${config.urlPrefix}/upload/${props.type}`)
|
||
const headers = computed(() => ({
|
||
token: userStore.token,
|
||
version: appStore.config.version
|
||
}))
|
||
const visible = ref(false)
|
||
const fileList = ref<any[]>([])
|
||
|
||
// 仅 video/voice + direct 时才接管 http-request
|
||
const useDirect = computed(() => props.direct && ['video', 'voice'].includes(props.type))
|
||
|
||
const handleProgress = () => {
|
||
visible.value = true
|
||
}
|
||
let uploadLen = 0
|
||
const handleSuccess = (response: any, file: any) => {
|
||
uploadLen++
|
||
if (uploadLen == fileList.value.length) {
|
||
uploadLen = 0
|
||
fileList.value = []
|
||
emit('allSuccess')
|
||
}
|
||
emit('change', file)
|
||
if (response.code == RequestCodeEnum.SUCCESS) {
|
||
emit('success', response)
|
||
}
|
||
if (response.code == RequestCodeEnum.FAIL && response.msg) {
|
||
feedback.msgError(response.msg)
|
||
}
|
||
}
|
||
const handleError = (event: any, file: any) => {
|
||
uploadLen++
|
||
if (uploadLen == fileList.value.length) {
|
||
uploadLen = 0
|
||
fileList.value = []
|
||
emit('allSuccess')
|
||
}
|
||
feedback.msgError(`${file.name}文件上传失败`)
|
||
uploadRefs.value?.abort(file)
|
||
visible.value = false
|
||
emit('change', file)
|
||
emit('error', file)
|
||
}
|
||
const handleExceed = () => {
|
||
feedback.msgError(`超出上传上限${props.limit},请重新上传`)
|
||
}
|
||
const handleClose = () => {
|
||
fileList.value = []
|
||
visible.value = false
|
||
}
|
||
|
||
const getAccept = computed(() => {
|
||
switch (props.type) {
|
||
case 'image':
|
||
return '.jpg,.png,.gif,.jpeg,.ico'
|
||
case 'video':
|
||
return '.wmv,.avi,.mpg,.mpeg,.3gp,.mov,.mp4,.flv,.rmvb,.mkv'
|
||
case 'voice':
|
||
return '.mp3,.wav,.wma,.m4a,.aac,.amr'
|
||
default:
|
||
return '*'
|
||
}
|
||
})
|
||
|
||
// 走 COS 直传:成功时模拟老接口的响应 envelope,失败/降级时回到默认 XHR
|
||
const httpRequest = async (options: UploadRequestOptions) => {
|
||
visible.value = true
|
||
try {
|
||
const data = await uploadVideoDirectToCos({
|
||
file: options.file,
|
||
type: props.type as any,
|
||
cid: Number((options.data as any)?.cid ?? 0),
|
||
onProgress(info) {
|
||
// 触发 ElUpload 内部进度(保持与默认上传一致的体验)
|
||
;(options as any).onProgress?.({
|
||
percent: info.percent,
|
||
loaded: info.loaded,
|
||
total: info.total
|
||
})
|
||
}
|
||
})
|
||
;(options as any).onSuccess?.({ code: RequestCodeEnum.SUCCESS, msg: 'ok', data })
|
||
} catch (err: any) {
|
||
if (err instanceof DirectUploadFallbackError) {
|
||
feedback.msgWarning('当前存储不支持直传,已切换为普通上传')
|
||
await defaultXhrUpload(options)
|
||
return
|
||
}
|
||
;(options as any).onError?.(err instanceof Error ? err : new Error(String(err?.message ?? err)))
|
||
}
|
||
}
|
||
|
||
// 当 STS 不可用时,回退到 ElUpload 原生 XHR 行为(POST /upload/{type})
|
||
const defaultXhrUpload = (options: UploadRequestOptions) =>
|
||
new Promise<void>((resolve) => {
|
||
const xhr = new XMLHttpRequest()
|
||
xhr.open('POST', action.value)
|
||
Object.entries(headers.value).forEach(([k, v]) => xhr.setRequestHeader(k, String(v ?? '')))
|
||
xhr.upload.onprogress = (e) => {
|
||
if (!e.lengthComputable) return
|
||
const percent = (e.loaded / e.total) * 100
|
||
;(options as any).onProgress?.({ percent, loaded: e.loaded, total: e.total })
|
||
}
|
||
xhr.onload = () => {
|
||
try {
|
||
const json = JSON.parse(xhr.responseText)
|
||
;(options as any).onSuccess?.(json)
|
||
} catch (e) {
|
||
;(options as any).onError?.(new Error('上传响应解析失败'))
|
||
}
|
||
resolve()
|
||
}
|
||
xhr.onerror = () => {
|
||
;(options as any).onError?.(new Error('网络错误'))
|
||
resolve()
|
||
}
|
||
const formData = new FormData()
|
||
formData.append('file', options.file)
|
||
Object.entries(options.data || {}).forEach(([k, v]) => formData.append(k, String(v)))
|
||
xhr.send(formData)
|
||
})
|
||
|
||
return {
|
||
uploadRefs,
|
||
action,
|
||
headers,
|
||
visible,
|
||
fileList,
|
||
useDirect,
|
||
httpRequest,
|
||
getAccept,
|
||
handleProgress,
|
||
handleSuccess,
|
||
handleError,
|
||
handleExceed,
|
||
handleClose
|
||
}
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss"></style>
|