Files
zyt/admin/src/components/upload/index.vue
T
2026-09-01 17:18:42 +08:00

254 lines
8.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 {
DirectUploadApiError,
DirectUploadFallbackError,
uploadDirectToCos,
type DirectUploadType
} 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
},
// 直传到对象存储,绕开服务器中转
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[]>([])
const directTypes: DirectUploadType[] = ['video', 'voice', 'desktop_package']
const useDirect = computed(
() => props.direct && directTypes.includes(props.type as DirectUploadType)
)
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')
}
if (!(event instanceof DirectUploadApiError)) {
const message = event instanceof Error ? event.message : ''
feedback.msgError(message || `${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'
case 'desktop_package':
return '.exe,.zip'
default:
return '*'
}
})
// 走 COS 直传:成功时模拟老接口的响应 envelope
const httpRequest = async (options: UploadRequestOptions) => {
visible.value = true
try {
const data = await uploadDirectToCos({
file: options.file,
type: props.type as DirectUploadType,
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) {
if (props.type === 'desktop_package') {
;(options as any).onError?.(
new Error('当前未启用腾讯云 COS,安装包无法直传,请配置 COS 后重试')
)
return
}
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>